mirror of
https://github.com/jeertmans/manim-slides.git
synced 2025-07-04 15:37:58 +08:00
Compare commits
473 Commits
v4.6.0
...
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 | |||
14df6eb55a | |||
ef014ebac6 | |||
d5d1513d94 | |||
bd04dae2bf | |||
d5c20d1791 | |||
527ce2767e | |||
04bca458f1 | |||
4e7abe8706 | |||
49e2c31d9a | |||
5920a843f5 | |||
59dd365291 | |||
3e2e64b09f | |||
8a3bf87db8 | |||
498e9af2bf | |||
24ee23af11 | |||
a775c4989b | |||
04f6ee7f9b | |||
bbc539b461 | |||
2d9d263c9c | |||
cfa9c082ab | |||
67533c460e | |||
a85f1c4036 | |||
b3fe6f17b9 | |||
e7182a445d | |||
1dbd2fdde5 | |||
07fd2bdcf1 | |||
d586dab102 | |||
0ff1f37475 | |||
92c569950c | |||
648d7ff921 | |||
b47068ede5 | |||
973522a2ac | |||
2f82ca3409 | |||
e208cced03 | |||
abbe577aae | |||
38ef91d30c | |||
b17fd5409f | |||
186badba03 | |||
39816d4994 | |||
9cb1dae990 | |||
9e4c1d76ee | |||
0316adf69e | |||
fd9b06b955 | |||
16f740d2ad | |||
f260d0d310 | |||
39ff80ed1e | |||
b84a22d0f9 | |||
7ba47728ff | |||
02a8173ed1 | |||
07dff32be3 | |||
e60edcc960 | |||
c1d2dc26b2 | |||
0db4a8c260 | |||
cb4f6f552c | |||
9cd50e73b2 | |||
cea3d0b0c3 | |||
75126d8bab | |||
be227bbdea | |||
62560ea41f | |||
4b8f90c1fa | |||
8b1c45b84e | |||
34cc66d8be | |||
57e11a0ea7 | |||
de342b32bb | |||
6bd431d748 | |||
2e996c03a7 | |||
d9715ccb96 | |||
e3b3dd677f | |||
db91de1412 | |||
bf67a7b695 | |||
ea2d352fc1 | |||
74ddefe519 | |||
20b7ef4110 | |||
19a31a9136 | |||
d2925340aa | |||
afeaa0d793 | |||
af3c4971ae | |||
b3ed127e31 | |||
fc200f22f5 | |||
050ee0ae78 | |||
a9b8081167 | |||
dc58d498a8 | |||
f898dd3054 | |||
b09a000c17 | |||
eb8efa8e3d | |||
1f0c94dc5c | |||
2771aac4d0 | |||
ce799aeded | |||
891273b2fc | |||
2b25b6a89d | |||
fc594533e9 | |||
422102524f | |||
739cbcee0a | |||
3b3e3109a3 | |||
8921c3b8f9 | |||
5eb23dc5c1 | |||
a890832a4d | |||
106c7d4c06 | |||
6c52906037 | |||
2853ed08e1 | |||
760ceb8ce1 | |||
2fa0301935 | |||
61b983db3a | |||
56b1ffe430 | |||
541bf96945 | |||
626764146a | |||
0322dae743 | |||
7928f6020c | |||
f3dfa782b0 | |||
e13ca7e0dc | |||
6c9505b98a | |||
7b3a5c4824 | |||
5daa94b823 | |||
498e33ad8d | |||
860ab231b5 | |||
c075904a27 | |||
387d0f76b5 | |||
1126a20785 | |||
802f6406ae | |||
685f871186 | |||
da14b5f24a | |||
18a9906ae5 | |||
a0ee723c89 | |||
2b7bd0a68d | |||
497e4e964f | |||
86fc774a3d | |||
ce14c79230 | |||
6272d3f7ec | |||
bbe8b96030 | |||
1bc8423381 | |||
67147442f3 | |||
859d48ad2e | |||
9a23296fa2 | |||
0f5b374bce | |||
2dc4c1ab99 | |||
f2ee29ad58 | |||
d127af9dd2 | |||
05ebf40543 | |||
9cc7957e35 | |||
b72b7bc256 | |||
4a1b8aea87 | |||
c2b12d16eb | |||
bb5b294f40 | |||
9a3a343231 | |||
0f07d36f52 | |||
806b7d00f6 | |||
3d9522cbb0 | |||
28c5336b83 | |||
48614105bd | |||
933afdd465 | |||
599f9f22ae | |||
5490a0a5ef | |||
c875363b40 | |||
bd9bf06876 | |||
4d76f2ccc1 | |||
8cf05ea44d | |||
638616c94f | |||
b321161717 | |||
7363281ff0 | |||
d056d8d8b1 | |||
c95929dc7d | |||
e08edb6fe1 | |||
efc3017df8 | |||
788727ea22 | |||
0bb0285b18 | |||
ce878bece2 | |||
1275e9119f | |||
5555ac2e54 | |||
838de83c75 | |||
f1f7146c7e | |||
fb02764bb7 | |||
f6f2e4090f | |||
146a2f7839 | |||
d282766f2d | |||
dec2f5e724 | |||
4d44166677 | |||
455f104a11 | |||
0f9048a27b | |||
9e1e0f2367 | |||
097ae8ffdd | |||
aace5dea11 | |||
738c54f9a6 | |||
f01f811639 | |||
94265f6842 | |||
6ed76ffd01 | |||
71df62d79b | |||
fc36909688 | |||
580da4a885 | |||
0de877e43a | |||
7313e3e0d4 | |||
1e967894db | |||
98fa5349d3 | |||
0e3ed3f9eb | |||
a45242236d | |||
66451473b8 | |||
72152bd625 | |||
88bffe0f0b | |||
a7719dbb8b | |||
529a6c534f | |||
2b6240c4d3 | |||
d98d41aaa8 | |||
d892a4e77d | |||
8069ab5405 | |||
f9e22fe63c | |||
b195b823ba | |||
fc8717fa9c | |||
dbbd6813ec | |||
a10902eaaa | |||
f4c1c34994 | |||
540c7034c8 | |||
86aeeb861b | |||
979e2c549a | |||
4e4e29380b | |||
caa4c48fe7 | |||
a353de270e | |||
60f284e748 | |||
501af3b658 | |||
f820819896 | |||
9279d2a22a | |||
e1d5fb732c | |||
384af332d8 | |||
c231f4d100 | |||
751eae74e9 | |||
97fe80caa2 | |||
a7eea6fbea | |||
04e2f265f6 | |||
8096636cf1 | |||
c7e38bfb38 | |||
421cad3038 | |||
9edf23856c | |||
62236f5796 | |||
1e28d70c0e | |||
6a96b3ab8c | |||
a1c041db80 | |||
4fd3452f95 | |||
ff2be6851b | |||
95289ee7a5 | |||
f1a026208a | |||
b3fd1d209e | |||
8c38db0989 | |||
6da0c36c96 | |||
3b01efa601 | |||
c9ef5e9a75 | |||
bfad43bd38 | |||
6f2cbc9b19 | |||
5bd88c2fd5 | |||
f0c17b1e2a | |||
fce9546a9b | |||
d6ad56120e | |||
5db0261b01 | |||
8ab33ef71f | |||
4da0e2cc2d | |||
0e82e28313 | |||
8b13106fcc | |||
bce4d8188f | |||
c420b47ad2 | |||
fad13f33dc | |||
d42a7f5ff1 | |||
88d598709a | |||
2ba9b734a3 | |||
49c4a10453 | |||
8c578d2577 | |||
2a327c470b | |||
04dcf530f5 | |||
9a573f29f1 | |||
02f425f536 | |||
149b12fd01 | |||
e01be300a0 | |||
940916d4aa | |||
3da8fab145 | |||
f0c5d48107 | |||
426470ef3c | |||
700584cbcc | |||
a440da9468 | |||
6486ce147c | |||
b258deeb31 | |||
a32773c50f | |||
a16aa93ee6 | |||
e809e64f9a | |||
5967760dc3 | |||
7f824be682 | |||
9346f199d7 | |||
5c40dc69d8 | |||
bf10068cfc | |||
2f307225d1 | |||
8b5db4b2fd | |||
855c74de34 | |||
a70876d696 | |||
3cb0085f24 | |||
42d70380b0 | |||
dc1be25e6e | |||
4cd433b35a | |||
e83df48c5d | |||
ed30e2136a | |||
a9f5355595 | |||
1ef42ec82a | |||
b5f6a165db | |||
7b9c9b0c39 | |||
ac23949043 | |||
71564a4c2e | |||
b06250056d | |||
43c24d7ae1 | |||
35195f89e4 | |||
2d7a67addf | |||
5b212ecccb | |||
f15a3e9b59 | |||
205972125c | |||
e9d28dc0a8 | |||
70b5ee39c3 | |||
616e025867 | |||
0ce4c18519 | |||
68ff5269eb | |||
753f4e788b | |||
f1f98bf241 | |||
4b413c1528 | |||
478e1d7d76 | |||
2b224530ab | |||
cd7a054cf1 | |||
1ff2330ff2 | |||
1e150bbb84 | |||
13f19649aa | |||
4c97bdd3a3 | |||
777ff444a3 | |||
9cb1c35f00 | |||
1fed193cb3 | |||
9f227936f7 | |||
2fe6139d18 | |||
54f2c60c4e | |||
9810425ff2 | |||
3dc543e3a6 | |||
c0c73ad4d4 | |||
a82ca81dc5 | |||
a68a4e1517 | |||
519dd47ac6 | |||
0565a99639 | |||
8dfe600656 | |||
03107867ab | |||
bf64962c46 | |||
97e7bf8cb0 | |||
1bca2683e1 | |||
d6bb82261c | |||
0c682e4ec9 | |||
2f0453c9a6 | |||
85ea9f3096 | |||
1ae8db7966 | |||
82cccc3fc2 | |||
726b0abf5a | |||
80f4f4e3f7 | |||
82eebae686 | |||
7367cc2cb5 |
205
.github/ISSUE_TEMPLATE/bug.yml
vendored
205
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -1,40 +1,177 @@
|
||||
name: Bug
|
||||
description: Report an issue to help improve the project.
|
||||
labels: "bug"
|
||||
title: '[BUG] <description>'
|
||||
title: '[BUG] <short-description-here>'
|
||||
labels: [bug]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A brief description of the question or issue, also include what you tried and what didn't work
|
||||
validations:
|
||||
- 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
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: Which version of Manim Slides are you using? You can use `manim-slides --version` to get that information.
|
||||
validations:
|
||||
- label: Checked the [frequently asked questions](https://manim-slides.eertmans.be/latest/faq.html);
|
||||
required: true
|
||||
- type: textarea
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: What is your platform. Linux, macOS, or Windows?
|
||||
validations:
|
||||
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Please add screenshots if applicable
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: extrainfo
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Is there anything else we should know about this bug?
|
||||
validations:
|
||||
required: false
|
||||
- label: Created a virtual environment in which I can reproduce my bug;
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the issue
|
||||
description: A description of the issue, also include what you tried and what didn't work.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: command
|
||||
attributes:
|
||||
label: Command
|
||||
description: |
|
||||
Enter the command that failed.
|
||||
This will be automatically formatted into code, so no need for backticks.
|
||||
placeholder: manim-slides render mwe.py MWE
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: Issue Type
|
||||
description: >
|
||||
Please select the option in the drop-down.
|
||||
options:
|
||||
- Installation issue
|
||||
- Visual bug when presenting (`manim-slides present`)
|
||||
- Bug when presenting with HTML/PowerPoint/... format (`manim-slides convert`)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: py-version
|
||||
attributes:
|
||||
label: Python version
|
||||
description: |
|
||||
Please copy and paste the output of `python --version`.
|
||||
Make sure to activate your virtual environment first (if any).
|
||||
placeholder: Python 3.11.8
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: venv
|
||||
attributes:
|
||||
label: Python environment
|
||||
description: |
|
||||
Please copy and paste the output of `manim-slides checkhealth`.
|
||||
Make sure to activate your virtual environment first (if any).
|
||||
This will be automatically formatted into code, so no need for backticks.
|
||||
If Manim Slides installation failed, enter 'N/A' instead.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: What is your platform?
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
- Other (please precise below)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: platform-other
|
||||
attributes:
|
||||
label: Other platform
|
||||
description: Please answer if you have replied *Other* above.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: code
|
||||
attributes:
|
||||
label: Manim Slides Python code
|
||||
description: |
|
||||
Please copy and paste a minimal working example (MWE) of your Python code that can reproduce your bug.
|
||||
This will be automatically formatted into code, so no need for backticks.
|
||||
placeholder: |
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
|
||||
class MWE(Slide):
|
||||
def construct(self):
|
||||
circle = Circle(radius=2, color=RED)
|
||||
dot = Dot()
|
||||
|
||||
self.play(GrowFromCenter(circle))
|
||||
|
||||
self.next_slide(loop=True)
|
||||
self.play(MoveAlongPath(dot, circle), run_time=0.5)
|
||||
self.next_slide()
|
||||
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
render: python
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: |
|
||||
Please copy and paste any relevant log output.
|
||||
This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Please add screenshots if applicable.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: extra-info
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Is there anything else we should know about this bug?
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: suggested-fix
|
||||
attributes:
|
||||
label: Recommended fix or suggestions
|
||||
description: A clear and concise description of how you want to update it.
|
||||
validations:
|
||||
required: false
|
||||
|
100
.github/ISSUE_TEMPLATE/documentation.yml
vendored
100
.github/ISSUE_TEMPLATE/documentation.yml
vendored
@ -1,59 +1,63 @@
|
||||
name: Documentation
|
||||
description: Ask / Report an issue related to the documentation.
|
||||
title: "DOC: <description>"
|
||||
labels: ['bug', 'docs']
|
||||
title: '[DOC] <short-description-here>'
|
||||
labels: [documentation]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
**Thank you for wanting to report a problem with manim-slides docs!**
|
||||
- type: markdown
|
||||
id: preamble
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for reporting a problem about Manim Slides' documentation!**
|
||||
|
||||
If you know how to solve your problem, feel free to submit a PR too!
|
||||
|
||||
If the problem seems straightforward, feel free to submit a PR instead!
|
||||
|
||||
|
||||
⚠
|
||||
Verify first that your issue is not already reported on GitHub [Issues].
|
||||
|
||||
|
||||
[Issues]:
|
||||
https://github.com/jeertmans/manim-slides/issues
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Issue
|
||||
description: A clear and concise description of the issue you encountered.
|
||||
validations:
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Terms
|
||||
description: 'By submitting this issue, I have:'
|
||||
options:
|
||||
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Adocumentation+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Affected Page
|
||||
description: Add a link to page with the problem.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the issue
|
||||
description: A clear and concise description of the issue you encountered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Issue Type
|
||||
description: >
|
||||
Please select the option in the drop-down.
|
||||
- type: textarea
|
||||
id: pages
|
||||
attributes:
|
||||
label: Affected page(s)
|
||||
description: Link to page(s) with the problem.
|
||||
placeholder: |
|
||||
+ https://manim-slides.eertmans.be/latest/installation.html
|
||||
+ https://manim-slides.eertmans.be/latest/features_table.html
|
||||
validations:
|
||||
required: true
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<em>Issue?</em>
|
||||
</summary>
|
||||
</details>
|
||||
options:
|
||||
- Documentation Enhancement
|
||||
- Documentation Report
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: Issue type
|
||||
description: >
|
||||
Please select the option in the drop-down.
|
||||
options:
|
||||
- Typo, spelling mistake, broken link, etc.
|
||||
- Something is missing
|
||||
- Documentation enhancement
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Recommended fix or suggestions
|
||||
description: A clear and concise description of how you want to update it.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: suggested-fix
|
||||
attributes:
|
||||
label: Recommended fix or suggestions
|
||||
description: A clear and concise description of how you want to update it.
|
||||
validations:
|
||||
required: false
|
||||
|
66
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
66
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,26 +1,46 @@
|
||||
name: Feature Request
|
||||
name: Feature request
|
||||
description: Have a new idea/feature? Please suggest!
|
||||
labels: "enhancement"
|
||||
title: '[FEATURE] <description>'
|
||||
title: '[FEATURE] <short-description-here>'
|
||||
labels: [enhancement]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A brief description of the enhancement you propose, also include what you tried and what worked.
|
||||
validations:
|
||||
- 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
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Please add screenshots if applicable
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: extrainfo
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Is there anything else we should know about this idea?
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A brief description of the enhancement you propose, also include what you tried and what worked.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Please add screenshots if applicable
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: extrainfo
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Is there anything else we should know about this idea?
|
||||
validations:
|
||||
required: false
|
||||
|
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
|
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
labels:
|
||||
- dependencies
|
10
.github/pull_request_template.md
vendored
10
.github/pull_request_template.md
vendored
@ -10,11 +10,13 @@
|
||||
|
||||
<!-- Describe all the proposed changes in your PR -->
|
||||
|
||||
## Check List (Check all the applicable boxes)
|
||||
## Check List
|
||||
|
||||
- [ ] I understand that my contributions needs to pass the checks.
|
||||
- [ ] If I created new functions / methods, I documented them and add type hints.
|
||||
- [ ] If I modified already existing code, I updated the documentation accordingly.
|
||||
Check all the applicable boxes:
|
||||
|
||||
- [ ] I understand that my contributions needs to pass the checks;
|
||||
- [ ] If I created new functions / methods, I documented them and add type hints;
|
||||
- [ ] If I modified already existing code, I updated the documentation accordingly;
|
||||
- [ ] The title of my pull request is a short description of the requested changes.
|
||||
|
||||
## Screenshots
|
||||
|
27
.github/scripts/check_github_issues.py
vendored
Normal file
27
.github/scripts/check_github_issues.py
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
"""Check that GitHub issues (and PR) links match the number in Markdown link."""
|
||||
|
||||
import glob
|
||||
import re
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = re.compile(
|
||||
r"\[#(?P<number1>[0-9]+)\]"
|
||||
r"\(https://github\.com/"
|
||||
r"(?:[a-zA-Z0-9_-]+)/(?:[a-zA-Z0-9_-]+)/"
|
||||
r"(?:(?:issues)|(?:pull))/(?P<number2>[0-9]+)\)"
|
||||
)
|
||||
|
||||
ret_code = 0
|
||||
|
||||
for glob_pattern in sys.argv[1:]:
|
||||
for file in glob.glob(glob_pattern, recursive=True):
|
||||
with open(file) as f:
|
||||
for i, line in enumerate(f):
|
||||
for m in p.finditer(line):
|
||||
if m.group("number1") != m.group("number2"):
|
||||
start, end = m.span()
|
||||
print(f"{file}:{i}: ", line[start:end], file=sys.stderr) # noqa: T201
|
||||
ret_code = 1
|
||||
|
||||
sys.exit(ret_code)
|
34
.github/workflows/clearcache.yml
vendored
Normal file
34
.github/workflows/clearcache.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
# From: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||
name: Cleanup caches by a branch
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
|
||||
REPO=${{ github.repository }}
|
||||
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
18
.github/workflows/codeql-analysis.yml
vendored
18
.github/workflows/codeql-analysis.yml
vendored
@ -9,16 +9,16 @@
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '45 3 * * 2'
|
||||
- cron: 45 3 * * 2
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@ -32,17 +32,17 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
language: [python]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@ -56,7 +56,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@ -69,4 +69,4 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
33
.github/workflows/draft-pdf.yml
vendored
Normal file
33
.github/workflows/draft-pdf.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Create JOSE Paper
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
paths:
|
||||
- paper/*
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
paper:
|
||||
runs-on: ubuntu-latest
|
||||
name: Paper Draft
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Build draft PDF
|
||||
uses: openjournals/openjournals-draft-action@master
|
||||
with:
|
||||
journal: jose
|
||||
# This should be the path to the paper within your repo.
|
||||
paper-path: paper/paper.md
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: paper
|
||||
# This is the output path where Pandoc will write the compiled
|
||||
# PDF. Note, this should be the same directory as the input
|
||||
# paper.md
|
||||
path: paper/paper.pdf
|
10
.github/workflows/languagetool.yml
vendored
10
.github/workflows/languagetool.yml
vendored
@ -8,8 +8,8 @@ jobs:
|
||||
languagetool_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: reviewdog/action-languagetool@v1
|
||||
with:
|
||||
reporter: github-pr-review
|
||||
level: warning
|
||||
- uses: actions/checkout@v4
|
||||
- uses: reviewdog/action-languagetool@v1
|
||||
with:
|
||||
reporter: github-pr-review
|
||||
level: warning
|
||||
|
13
.github/workflows/latest_tag.yml
vendored
Normal file
13
.github/workflows/latest_tag.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Keep the versions up-to-date
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published, edited]
|
||||
|
||||
jobs:
|
||||
actions-tagger:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: Actions-R-Us/actions-tagger@latest
|
||||
with:
|
||||
publish_latest_tag: true
|
58
.github/workflows/pages.yml
vendored
58
.github/workflows/pages.yml
vendored
@ -1,58 +0,0 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy static content to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
- name: Install Linux Dependencies
|
||||
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
|
||||
- name: Install Python dependencies
|
||||
run: pip install manim sphinx sphinx_click furo
|
||||
- name: Install local Python package
|
||||
run: pip install -e .
|
||||
- name: Build animation and convert it into HTML slides
|
||||
run: |
|
||||
manim example.py ConvertExample
|
||||
manim-slides convert ConvertExample docs/source/_static/slides.html -cembedded=true -ccontrols=true
|
||||
- name: Build docs
|
||||
run: cd docs && make html
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
# Upload docs/build/html dir
|
||||
path: 'docs/build/html/'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
72
.github/workflows/publish.yml
vendored
Normal file
72
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
name: Upload Packages
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-python:
|
||||
name: Publish Python package
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
# IMPORTANT: this permission is mandatory for trusted publishing
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Build package
|
||||
run: uv build
|
||||
|
||||
- name: Publish to PyPI
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
publish-docker:
|
||||
name: Publish Docker image
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get Version
|
||||
id: create_release
|
||||
shell: python
|
||||
env:
|
||||
tag_act: ${{ github.ref }}
|
||||
run: |
|
||||
import os
|
||||
ref_tag = os.getenv('tag_act').split('/')[-1]
|
||||
with open(os.getenv('GITHUB_OUTPUT'), 'w') as f:
|
||||
print(f"tag_name={ref_tag}", file=f)
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
file: docker/Dockerfile
|
||||
push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }}
|
||||
tags: |
|
||||
ghcr.io/jeertmans/manim-slides:latest
|
||||
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}
|
55
.github/workflows/python-publish.yml
vendored
55
.github/workflows/python-publish.yml
vendored
@ -1,55 +0,0 @@
|
||||
# From: https://github.com/pypa/cibuildwheel
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build_wheels:
|
||||
name: Build wheels on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Used to host cibuildwheel
|
||||
- uses: actions/setup-python@v2
|
||||
|
||||
- name: Install deps
|
||||
run: sudo apt-get install libsdl-pango-dev
|
||||
|
||||
- name: Install packages
|
||||
run: python -m pip install -U manim tqdm
|
||||
|
||||
- name: Install cibuildwheel
|
||||
run: python -m pip install -U setuptools wheel pip
|
||||
|
||||
- name: Build wheels
|
||||
run: python setup.py sdist
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist/*.tar.*
|
||||
|
||||
release:
|
||||
name: Release
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build_wheels ]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Upload to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
104
.github/workflows/test_examples.yml
vendored
104
.github/workflows/test_examples.yml
vendored
@ -1,104 +0,0 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.py'
|
||||
workflow_dispatch:
|
||||
|
||||
name: Test Examples
|
||||
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
MANIM_SLIDES_VERBOSITY: debug
|
||||
|
||||
jobs:
|
||||
build-examples:
|
||||
strategy:
|
||||
matrix:
|
||||
manim: [manim, manimgl]
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
pyversion: ['3.7', '3.8', '3.9', '3.10']
|
||||
exclude:
|
||||
# excludes manimgl on Windows because if throws errors
|
||||
# related to OpenGL, which seems hard to fix:
|
||||
# Your graphics drivers do not support OpenGL 2.0.
|
||||
- os: windows-latest
|
||||
manim: manimgl
|
||||
# manimgl actually requires Python >= 3.8, see:
|
||||
# https://github.com/3b1b/manim/issues/1808
|
||||
- manim: manimgl
|
||||
pyversion: '3.7'
|
||||
# We only test Python 3.10 on Windows and MacOS
|
||||
- os: windows-latest
|
||||
pyversion: '3.7'
|
||||
- os: windows-latest
|
||||
pyversion: '3.8'
|
||||
- os: windows-latest
|
||||
pyversion: '3.9'
|
||||
- os: macos-latest
|
||||
pyversion: '3.7'
|
||||
- os: macos-latest
|
||||
pyversion: '3.8'
|
||||
- os: macos-latest
|
||||
pyversion: '3.9'
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.pyversion }}
|
||||
- name: Append to Path on MacOS and Ubuntu
|
||||
if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest'
|
||||
run: echo "${HOME}/.local/bin" >> $GITHUB_PATH
|
||||
- name: Append to Path on Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: echo "${HOME}/.local/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
- name: Install MacOS dependencies (manim only)
|
||||
if: matrix.os == 'macos-latest' && matrix.manim == 'manim'
|
||||
run: brew install py3cairo
|
||||
- name: Install MacOS dependencies
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: brew install ffmpeg
|
||||
- name: Install Ubuntu dependencies
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: sudo apt install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
|
||||
- name: Install Windows dependencies
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: choco install ffmpeg
|
||||
- name: Install manim on MacOs
|
||||
if: matrix.manim == 'manim' && matrix.os == 'macos-latest'
|
||||
run: pip3 install --user manim
|
||||
- name: Install manim on Ubuntu and Windows
|
||||
if: matrix.manim == 'manim' && (matrix.os == 'ubuntu-latest' || matrix.os == 'windows-latest')
|
||||
run: python -m pip install --user manim
|
||||
- name: Install manimgl on MacOs
|
||||
if: matrix.manim == 'manimgl' && matrix.os == 'macos-latest'
|
||||
run: pip3 install --user manimgl
|
||||
- name: Install manimgl on Ubuntu and Windows
|
||||
if: matrix.manim == 'manimgl' && matrix.os != 'macos-latest'
|
||||
run: python -m pip install --user manimgl
|
||||
- name: Install manim-slides on MacOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: pip3 install --user .
|
||||
- name: Install manim-slides on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: xvfb-run -a -s "-screen 0 1400x900x24" python -m pip install --user .
|
||||
- name: Install manim-slides on Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: pip3 install -e .
|
||||
- name: Build slides with manim
|
||||
if: matrix.manim == 'manim'
|
||||
run: python -m manim -ql example.py Example ThreeDExample ConvertExample
|
||||
- name: Build slides with manimgl on Ubuntu
|
||||
if: matrix.manim == 'manimgl' && matrix.os == 'ubuntu-latest'
|
||||
run: xvfb-run -a -s "-screen 0 1400x900x24" manim-render -l example.py Example ThreeDExample ConvertExample
|
||||
- name: Build slides with manimgl on MacOS or Windows
|
||||
if: matrix.manim == 'manimgl' && (matrix.os == 'macos-latest' || matrix.os == 'windows-latest')
|
||||
run: manimgl -l example.py Example ThreeDExample ConvertExample
|
||||
- name: Test slides on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: xvfb-run -a -s "-screen 0 1400x900x24" manim-slides Example ThreeDExample ConvertExample --skip-all
|
||||
- name: Test slides on MacOS or Windows
|
||||
if: matrix.os == 'macos-latest' || matrix.os == 'windows-latest'
|
||||
run: manim-slides Example ThreeDExample ConvertExample --skip-all
|
119
.github/workflows/tests.yml
vendored
Normal file
119
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,119 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
name: Tests
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pip-install:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-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:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
pyversion: ['3.9', '3.10', '3.11', '3.12']
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
MANIM_SLIDES_VERBOSITY: error
|
||||
DISPLAY: :99
|
||||
GITHUB_WORKFLOWS: 1
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Install manim dependencies on MacOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: brew install ffmpeg py3cairo pango pkg-config scipy
|
||||
|
||||
- name: Install manim dependencies on Ubuntu
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
|
||||
nohup Xvfb $DISPLAY &
|
||||
|
||||
- name: Install Windows dependencies
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: choco install ffmpeg
|
||||
|
||||
- name: Install Mesa
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: ssciwr/setup-mesa-dist-win@v2
|
||||
|
||||
- name: Run pytest
|
||||
run: uv run --python ${{ matrix.pyversion }} --frozen --group tests --no-dev pytest
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
markdown-link-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check links
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
with:
|
||||
use-quiet-mode: yes
|
||||
use-verbose-mode: yes
|
||||
config-file: .mdlc.json
|
47
.gitignore
vendored
47
.gitignore
vendored
@ -1,25 +1,50 @@
|
||||
# Python files
|
||||
__pycache__/
|
||||
/env
|
||||
/tests
|
||||
/build
|
||||
/dist
|
||||
*.egg-info/
|
||||
|
||||
# Manim files
|
||||
images/
|
||||
/media
|
||||
/presentation
|
||||
|
||||
/.vscode
|
||||
|
||||
slides/
|
||||
|
||||
.manim-slides.json
|
||||
tests/data/media/
|
||||
docs/source/media/
|
||||
|
||||
# ManimGL files
|
||||
videos/
|
||||
|
||||
images/
|
||||
# Manim Slides files
|
||||
.manim-slides.toml
|
||||
|
||||
slides/
|
||||
!tests/data/slides/
|
||||
|
||||
slides_assets/
|
||||
|
||||
# Docs
|
||||
docs/build/
|
||||
|
||||
docs/source/_static/slides_assets/
|
||||
slides.html
|
||||
|
||||
docs/source/_static/slides.html
|
||||
docs/source/reference/.ipynb_checkpoints/
|
||||
|
||||
docs/source/_static/basic_example_assets/
|
||||
|
||||
docs/source/_static/basic_example.html
|
||||
|
||||
docs/source/_static/three_d_example.html
|
||||
|
||||
docs/source/_static/three_d_example_assets/
|
||||
|
||||
docs/source/reference/media/
|
||||
|
||||
# JOSE Paper
|
||||
paper/paper.pdf
|
||||
paper/media/
|
||||
|
||||
# Others
|
||||
.coverage*
|
||||
coverage.xml
|
||||
|
||||
rendering_times.csv
|
||||
|
8
.mdlc.json
Normal file
8
.mdlc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"replacementPatterns": [
|
||||
{
|
||||
"pattern": "^/(?<path>.*)$",
|
||||
"replacement": "https://eertmans.be/manim-slides/latest/$<path>.html"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,62 +1,47 @@
|
||||
ci:
|
||||
autofix_commit_msg: |
|
||||
chore(fmt): auto fixes from pre-commit.com hooks
|
||||
|
||||
for more information, see https://pre-commit.ci
|
||||
autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate'
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: ["--profile", "black"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-bugbear
|
||||
- flake8-comprehensions
|
||||
- flake8-tidy-imports
|
||||
- flake8-typing-imports
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: 'v0.982'
|
||||
hooks:
|
||||
- id: mypy
|
||||
args:
|
||||
- --install-types
|
||||
- --non-interactive
|
||||
- --ignore-missing-imports
|
||||
# Disallow dynamic typing
|
||||
- --disallow-any-unimported
|
||||
- --disallow-any-generics
|
||||
- --disallow-subclassing-any
|
||||
|
||||
# Disallow untyped definitions and calls
|
||||
- --disallow-untyped-defs
|
||||
- --disallow-incomplete-defs
|
||||
- --check-untyped-defs
|
||||
|
||||
# None and optional handling
|
||||
- --no-implicit-optional
|
||||
|
||||
# Configuring warnings
|
||||
- --warn-unused-ignores
|
||||
- --warn-no-return
|
||||
- --warn-return-any
|
||||
- --warn-redundant-casts
|
||||
|
||||
# Strict equality
|
||||
- --strict-equality
|
||||
|
||||
# Config file
|
||||
- --warn-unused-configs
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||
rev: v2.14.0
|
||||
hooks:
|
||||
- id: pretty-format-yaml
|
||||
args: [--autofix]
|
||||
- id: pretty-format-toml
|
||||
exclude: poetry.lock
|
||||
args: [--autofix, --trailing-commas]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.7
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.15.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-requests, types-setuptools]
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
- tomli
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: github-issues
|
||||
name: GitHub issues link check
|
||||
description: Check issues (and PR) links are matching number.
|
||||
entry: python .github/scripts/check_github_issues.py
|
||||
language: system
|
||||
types: [markdown]
|
||||
|
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.11
|
18
.readthedocs.yaml
Normal file
18
.readthedocs.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
version: 2
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: '3.11'
|
||||
apt_packages:
|
||||
- libpango1.0-dev
|
||||
- ffmpeg
|
||||
jobs:
|
||||
post_create_environment:
|
||||
- asdf plugin add uv
|
||||
- asdf install uv latest
|
||||
- asdf global uv latest
|
||||
- UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --group docs --no-dev --no-cache
|
||||
sphinx:
|
||||
builder: html
|
||||
configuration: docs/source/conf.py
|
||||
fail_on_warning: true
|
546
CHANGELOG.md
Normal file
546
CHANGELOG.md
Normal file
@ -0,0 +1,546 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
<!-- start changelog -->
|
||||
|
||||
(unreleased)=
|
||||
## [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](https://github.com/jeertmans/manim-slides/compare/v5.1.4...v5.1.5)
|
||||
|
||||
(v5.1.5-chore)=
|
||||
### Chore
|
||||
|
||||
- Added CI for broken HTML links and fixed, plus spell checking.
|
||||
[#417](https://github.com/jeertmans/manim-slides/pull/417)
|
||||
- Create FAQ page and clear FAQ from README.md.
|
||||
[#418](https://github.com/jeertmans/manim-slides/pull/418)
|
||||
- Used Rye instead of PDM for faster development.
|
||||
[#420](https://github.com/jeertmans/manim-slides/pull/420)
|
||||
|
||||
(v5.1.5-fixed)=
|
||||
### Fixed
|
||||
|
||||
- Fixed broken `--show-config` command.
|
||||
[#419](https://github.com/jeertmans/manim-slides/pull/419)
|
||||
|
||||
(v5.1.4)=
|
||||
## [v5.1.4](https://github.com/jeertmans/manim-slides/compare/v5.1.3...v5.1.4)
|
||||
|
||||
(v5.1.4-added)=
|
||||
### Added
|
||||
|
||||
- Added audio output to `manim-slides present`.
|
||||
[#382](https://github.com/jeertmans/manim-slides/pull/382)
|
||||
|
||||
(v5.1.4-changed)=
|
||||
### Changed
|
||||
|
||||
- Added `--info-window-screen` option and change `--screen-number`
|
||||
to not move the info window.
|
||||
[#389](https://github.com/jeertmans/manim-slides/pull/389)
|
||||
|
||||
(v5.1.4-chore)=
|
||||
### Chore
|
||||
|
||||
- Created a favicon for the website/documentation.
|
||||
[#399](https://github.com/jeertmans/manim-slides/pull/399)
|
||||
- Documented the Nixpkg installation.
|
||||
[#404](https://github.com/jeertmans/manim-slides/pull/404 )
|
||||
- Updated the default RevealJS version to 5.1.0.
|
||||
[#412](https://github.com/jeertmans/manim-slides/pull/412)
|
||||
- Removed the `opencv-python` dependency.
|
||||
[#415](https://github.com/jeertmans/manim-slides/pull/415)
|
||||
|
||||
(v5.1.4-fixed)=
|
||||
### Fixed
|
||||
|
||||
- Fixed the retrieval of `background_color` with ManimCE.
|
||||
[#414](https://github.com/jeertmans/manim-slides/pull/414)
|
||||
- Fixed #390 issue caused by empty media created by ManimCE.
|
||||
[#416](https://github.com/jeertmans/manim-slides/pull/416)
|
||||
|
||||
(v5.1.3)=
|
||||
## [v5.1.3](https://github.com/jeertmans/manim-slides/compare/v5.1.2...v5.1.3)
|
||||
|
||||
(v5.1.3-chore)=
|
||||
### Chore
|
||||
|
||||
- Fix link in documentation.
|
||||
[#368](https://github.com/jeertmans/manim-slides/pull/368)
|
||||
|
||||
- Warn users if not using recommended Qt bindings.
|
||||
[#373](https://github.com/jeertmans/manim-slides/pull/373)
|
||||
|
||||
(v5.1.2)=
|
||||
## [v5.1.2](https://github.com/jeertmans/manim-slides/compare/v5.1.1...v5.1.2)
|
||||
|
||||
(v5.1.2-chore)=
|
||||
### Chore
|
||||
|
||||
- Fix ReadTheDocs version flyout in iframes.
|
||||
[#367](https://github.com/jeertmans/manim-slides/pull/367)
|
||||
|
||||
(v5.1.1)=
|
||||
## [v5.1.1](https://github.com/jeertmans/manim-slides/compare/v5.1.0...v5.1.1)
|
||||
|
||||
(v5.1.1-chore)=
|
||||
### Chore
|
||||
|
||||
- Move documentation to ReadTheDocs for better versioning.
|
||||
[#365](https://github.com/jeertmans/manim-slides/pull/365)
|
||||
|
||||
(v5.1)=
|
||||
## [v5.1](https://github.com/jeertmans/manim-slides/compare/v5.0.0...v5.1.0)
|
||||
|
||||
(v5.1-added)=
|
||||
### Added
|
||||
|
||||
- Added the `--hide-info-window` option to `manim-slides present`.
|
||||
[#313](https://github.com/jeertmans/manim-slides/pull/313)
|
||||
- Added the `manim-slides render` command
|
||||
to render slides using correct Manim installation.
|
||||
[#317](https://github.com/jeertmans/manim-slides/pull/317)
|
||||
- Added the `playback-rate` and `reversed-playback-rate` options
|
||||
to slide config.
|
||||
[#320](https://github.com/jeertmans/manim-slides/pull/320)
|
||||
- Added the speaker notes option.
|
||||
[#322](https://github.com/jeertmans/manim-slides/pull/322)
|
||||
- Added `auto` option for conversion format, which is the default.
|
||||
This is somewhat a **breaking change**, but changes to the CLI
|
||||
API are not considered to be very important.
|
||||
[#325](https://github.com/jeertmans/manim-slides/pull/325)
|
||||
- Added `return_animation` option to slide animations `self.wipe`
|
||||
and `self.zoom`.
|
||||
[#331](https://github.com/jeertmans/manim-slides/pull/331)
|
||||
- Created a Docker image, published on GitHub.
|
||||
[#355](https://github.com/jeertmans/manim-slides/pull/355)
|
||||
- Added `:template:` and `:config_options` options to
|
||||
the Sphinx directive.
|
||||
[#357](https://github.com/jeertmans/manim-slides/pull/357)
|
||||
|
||||
(v5.1-modified)=
|
||||
### Modified
|
||||
|
||||
- Modified the internal logic to simplify adding configuration options.
|
||||
[#321](https://github.com/jeertmans/manim-slides/pull/321)
|
||||
- Remove `reversed` file assets when exporting to HTML, as it was not used.
|
||||
[#336](https://github.com/jeertmans/manim-slides/pull/336)
|
||||
|
||||
(v5.1-chore)=
|
||||
### Chore
|
||||
|
||||
- Removed subrocess calls to FFmpeg with direct `libav` bindings using
|
||||
the `av` Python module. This should enhance rendering speed and security.
|
||||
[#335](https://github.com/jeertmans/manim-slides/pull/335)
|
||||
- Changed build backend to PDM and reflected on docs.
|
||||
[#354](https://github.com/jeertmans/manim-slides/pull/354)
|
||||
- Dropped Python 3.8 support.
|
||||
[#350](https://github.com/jeertmans/manim-slides/pull/350)
|
||||
- Made Qt backend optional and support PyQt6 too.
|
||||
[#350](https://github.com/jeertmans/manim-slides/pull/350)
|
||||
- Documentated how to create and use a custom HTML template.
|
||||
[#357](https://github.com/jeertmans/manim-slides/pull/357)
|
||||
|
||||
## [v5](https://github.com/jeertmans/manim-slides/compare/v4.16.0...v5.0.0)
|
||||
|
||||
Prior to v5, there was no real CHANGELOG other than the GitHub releases,
|
||||
with most of the content automatically generated by GitHub from merged
|
||||
pull requests.
|
||||
|
||||
In an effort to better document changes, this CHANGELOG document is now created.
|
||||
|
||||
(v5-added)=
|
||||
### Added
|
||||
|
||||
- Added the following option aliases to `manim-slides present`:
|
||||
`-F` and `--full-screen` for `fullscreen`,
|
||||
`-H` for `--hide-mouse`,
|
||||
and `-S` for `--screen-number`.
|
||||
[#243](https://github.com/jeertmans/manim-slides/pull/243)
|
||||
- Added a full screen key binding (defaults to <kbd>F</kbd>) in the
|
||||
presenter.
|
||||
[#243](https://github.com/jeertmans/manim-slides/pull/243)
|
||||
- Added support for including code from a file in Manim Slides
|
||||
Sphinx directive.
|
||||
[#261](https://github.com/jeertmans/manim-slides/pull/261)
|
||||
- Added the `manim_slides.slide.animation` module and created the
|
||||
`Wipe` and `Zoom` classes, that return a new animation.
|
||||
[#285](https://github.com/jeertmans/manim-slides/pull/285)
|
||||
- Added two environ variables, `MANIM_API` and `FORCE_MANIM_API`,
|
||||
to specify the `MANIM_API` to be used: `manim` and `manimce` will
|
||||
import `manim`, while `manimgl` and `manimlib` will import `manimlib`.
|
||||
If one of the two APIs is already imported, use `FORCE_MANIM_API=1` to
|
||||
override this.
|
||||
[#285](https://github.com/jeertmans/manim-slides/pull/285)
|
||||
- Added a working `ThreeDSlide` class compatible with `manimlib`.
|
||||
[#285](https://github.com/jeertmans/manim-slides/pull/285)
|
||||
- Added `loop` option to `Slide`'s `next_slide` method.
|
||||
Calling `next_slide` will never fail anymore.
|
||||
[#294](https://github.com/jeertmans/manim-slides/pull/294)
|
||||
- Added `Slide.next_section` for compatibility with `manim`'s
|
||||
`Scene.next_section` method.
|
||||
[#295](https://github.com/jeertmans/manim-slides/pull/295)
|
||||
- Added `--next-terminates-loop` option to `manim-slides present` for turn a
|
||||
looping slide into a normal one, so that it ends nicely. This is useful to
|
||||
have a smooth transition with the next slide.
|
||||
[#299](https://github.com/jeertmans/manim-slides/pull/299)
|
||||
- Added `--playback-rate` option to `manim-slides present` for testing purposes.
|
||||
[#300](https://github.com/jeertmans/manim-slides/pull/300)
|
||||
- Added `auto_next` option to `Slide`'s `next_slide` method to automatically
|
||||
play the next slide upon terminating. Supported by `present` and
|
||||
`convert --to=html` commands.
|
||||
[#304](https://github.com/jeertmans/manim-slides/pull/304)
|
||||
|
||||
(v5-changed)=
|
||||
### Changed
|
||||
|
||||
- Automatically concatenate all animations from a slide into one.
|
||||
This is a **breaking change** because the config file format is
|
||||
different from the previous one. For migration help, see associated PR.
|
||||
[#242](https://github.com/jeertmans/manim-slides/pull/242)
|
||||
- Changed the player interface to only use PySide6, and not a combination of
|
||||
PySide6 and OpenCV. A few features have been removed (see removed section),
|
||||
but the new player should be much easier to maintain and more performant,
|
||||
than its predecessor.
|
||||
[#243](https://github.com/jeertmans/manim-slides/pull/243)
|
||||
- Changed the slide config format to exclude unnecessary information.
|
||||
`StypeType` is removed in favor to one boolean `loop` field. This is
|
||||
a **breaking change** and one should re-render the slides to apply changes.
|
||||
[#243](https://github.com/jeertmans/manim-slides/pull/243)
|
||||
- Renamed key bindings in the config. This is a **breaking change** and one
|
||||
should either manually rename them (see list below) or re-init a config.
|
||||
List of changes: `CONTINUE` to `NEXT`, `BACK` to `PREVIOUS`, and
|
||||
`REWIND` to `REPLAY`.
|
||||
[#243](https://github.com/jeertmans/manim-slides/pull/243)
|
||||
- Conversion to HTML now uses Jinja2 templating. The template file has
|
||||
been modified accordingly, and old templates will not work anymore.
|
||||
This is a **breaking change**.
|
||||
[#271](https://github.com/jeertmans/manim-slides/pull/271)
|
||||
- Bumped RevealJS' default version to v4.6.1, and added three new themes.
|
||||
[#272](https://github.com/jeertmans/manim-slides/pull/272)
|
||||
- Changed the logger such that `make_logger` is called at module import,
|
||||
and we do not use Manim's logger anymore.
|
||||
[#285](https://github.com/jeertmans/manim-slides/pull/285)
|
||||
- Changed `Slide.wipe` and `Slide.zoom` to automatically call `self.play`.
|
||||
This is a **breaking change** as calling `self.play(self.wipe(...))` now
|
||||
raises an error (because `None` is not an animation).
|
||||
[#285](https://github.com/jeertmans/manim-slides/pull/285)
|
||||
- Changed the `manim_slides.slide` module to contain submodules, i.e.,
|
||||
`slide.manim`, `slide.manimlib`, `slide.animation`.
|
||||
Only `slide.animation` is part of the public API.
|
||||
Rules for choosing the Manim API (either `manim` or `manimlib`) has changed,
|
||||
and defaults to the currently imported module, with a preference for `manim`.
|
||||
[#285](https://github.com/jeertmans/manim-slides/pull/285)
|
||||
|
||||
(v5-fixed)=
|
||||
### Fixed
|
||||
|
||||
- Patched enums in `manim_slides/convert.py` to correctly call `str`'s
|
||||
`__str__` method, and not the `Enum` one.
|
||||
This bug was discovered by
|
||||
[@alexanderskulikov](https://github.com/alexanderskulikov) in
|
||||
[#253](https://github.com/jeertmans/manim-slides/discussions/253), caused by
|
||||
Python 3.11's change in how `Enum` work.
|
||||
[#257](https://github.com/jeertmans/manim-slides/pull/257).
|
||||
- Fixed potential non-existing parent path issue in
|
||||
`manim convert`'s destination path.
|
||||
[#262](https://github.com/jeertmans/manim-slides/pull/262)
|
||||
|
||||
(v5-removed)=
|
||||
### Removed
|
||||
|
||||
- Removed `--start-at-animation-number` option from `manim-slides present`.
|
||||
[#242](https://github.com/jeertmans/manim-slides/pull/242)
|
||||
- Removed the following options from `manim-slides present`:
|
||||
`--resolution`, `--record-to`, `--resize-mode`, and `--background-color`.
|
||||
[#243](https://github.com/jeertmans/manim-slides/pull/243)
|
||||
- Removed `PERF` verbosity level because not used anymore.
|
||||
[#245](https://github.com/jeertmans/manim-slides/pull/245)
|
||||
- Remove `Slide`'s method `start_loop` and `self.end_loop`
|
||||
in favor to `self.next_slide(loop=True)`.
|
||||
This is a **breaking change**.
|
||||
[#294](https://github.com/jeertmans/manim-slides/pull/294)
|
||||
|
||||
<!-- end changelog -->
|
45
CITATION.cff
Normal file
45
CITATION.cff
Normal file
@ -0,0 +1,45 @@
|
||||
# This CITATION.cff file was generated with cffinit.
|
||||
# Visit https://bit.ly/cffinit to generate yours today!
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: Manim Slides
|
||||
message: >-
|
||||
If you use this software, please cite it using our article
|
||||
in the Journal of Open Source Education.
|
||||
type: software
|
||||
authors:
|
||||
- name: Jérome Eertmans
|
||||
orcid: 'https://orcid.org/0000-0002-5579-5360'
|
||||
website: 'https://eertmans.be'
|
||||
doi: 10.5281/zenodo.7971360
|
||||
repository-code: 'https://github.com/jeertmans/manim-slides'
|
||||
url: 'https://eertmans.be/manim-slides'
|
||||
abstract: >-
|
||||
Manim Slides is a Python package that makes presenting
|
||||
Manim animations straightforward. With minimal changes
|
||||
required to pre-existing code, one can slide through
|
||||
animations in a PowerPoint-like manner, or share its
|
||||
slides online using ReavealJS' power.
|
||||
keywords:
|
||||
- Education
|
||||
- Math Animations
|
||||
- Presentation Tool
|
||||
- PowerPoint
|
||||
- Python
|
||||
license: MIT
|
||||
version: v5.5.1
|
||||
preferred-citation:
|
||||
publisher:
|
||||
name: The Open Journal
|
||||
type: article
|
||||
authors:
|
||||
- name: Jérome Eertmans
|
||||
orcid: 'https://orcid.org/0000-0002-5579-5360'
|
||||
doi: 10.21105/jose.00206
|
||||
journal: Journal of Open Source Education
|
||||
month: 8
|
||||
year: 2023
|
||||
title: 'Manim Slides: A Python package for presenting Manim content anywhere'
|
||||
volume: 6
|
||||
number: 66
|
||||
pages: 206
|
695
LICENSE.md
695
LICENSE.md
@ -1,674 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2024 Jérome Eertmans
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
295
README.md
295
README.md
@ -1,185 +1,238 @@
|
||||

|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_dark_transparent.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo_light_transparent.png">
|
||||
<img alt="Manim Slides Logo" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png">
|
||||
</picture>
|
||||
|
||||
<!-- start pypi -->
|
||||
|
||||
[![Latest Release][pypi-version-badge]][pypi-version-url]
|
||||
[![Python version][pypi-python-version-badge]][pypi-version-url]
|
||||

|
||||
[![PyPI - Downloads][pypi-download-badge]][pypi-version-url]
|
||||
[![Documentation][documentation-badge]][documentation-url]
|
||||
[![DOI][doi-badge]][doi-url]
|
||||
[![JOSE Paper][jose-badge]][jose-url]
|
||||
[![codecov][codecov-badge]][codecov-url]
|
||||
[![Binder][binder-badge]][binder-url]
|
||||
|
||||
# Manim Slides
|
||||
|
||||
Tool for live presentations using either [Manim (community edition)](https://www.manim.community/) or [ManimGL](https://3b1b.github.io/manim/). Manim Slides will *automatically* detect the one you are using!
|
||||
Tool for live presentations using either
|
||||
[Manim (community edition)](https://www.manim.community/)
|
||||
or [ManimGL](https://3b1b.github.io/manim/).
|
||||
Manim Slides will *automatically* detect the one you are using!
|
||||
|
||||
> **_NOTE:_** This project extends the work of [`manim-presentation`](https://github.com/galatolofederico/manim-presentation), with a lot more features!
|
||||
> [!NOTE]
|
||||
> This project extends the work of
|
||||
> [`manim-presentation`](https://github.com/galatolofederico/manim-presentation),
|
||||
> with a lot more features!
|
||||
|
||||
- [Installation](#installation)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Pip install](#pip-install)
|
||||
* [Install From Repository](#install-from-repository)
|
||||
- [Usage](#usage)
|
||||
* [Basic Example](#basic-example)
|
||||
* [Key Bindings](#key-bindings)
|
||||
* [Other Examples](#other-examples)
|
||||
- [Features and Comparison with Original manim-presentation](#features-and-comparison-with-original-manim-presentation)
|
||||
- [F.A.Q](#faq)
|
||||
* [How to increase quality on Windows](#how-to-increase-quality-on-windows)
|
||||
- [Comparison with Similar Tools](#comparison-with-similar-tools)
|
||||
- [F.A.Q](https://eertmans.be/manim-slides/latest/faq.html)
|
||||
- [Citing](#citing)
|
||||
- [Contributing](#contributing)
|
||||
* [Reporting an Issue](#reporting-an-issue)
|
||||
* [Seeking for Help](#seeking-for-help)
|
||||
* [Contact](#contact)
|
||||
|
||||
## Installation
|
||||
|
||||
While installing Manim Slides and its dependencies on your global Python is fine, I recommend using a [virtualenv](https://docs.python.org/3/tutorial/venv.html) for a local installation.
|
||||
|
||||
### Dependencies
|
||||
|
||||
Manim Slides requires either Manim or ManimGL to be installed. Having both packages installed is fine too.
|
||||
|
||||
If none of those packages are installed, please refer to their specific installation guidelines:
|
||||
- [Manim](https://docs.manim.community/en/stable/installation.html)
|
||||
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
|
||||
|
||||
### Pip Install
|
||||
|
||||
The recommended way to install the latest release is to use pip:
|
||||
|
||||
```bash
|
||||
pip install manim-slides
|
||||
```
|
||||
|
||||
### Install From Repository
|
||||
|
||||
An alternative way to install Manim Slides is to clone the git repository, and install from there:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/jeertmans/manim-slides
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
> *Note:* the `-e` flag allows you to edit the files, and observe the changes directly when using Manim Slides
|
||||
Manim Slides requires either Manim or ManimGL to be installed, along
|
||||
with their dependencies. Please checkout the
|
||||
[documentation](https://eertmans.be/manim-slides/latest/installation.html)
|
||||
for detailed install instructions.
|
||||
|
||||
## Usage
|
||||
|
||||
Using Manim Slides is a two-step process:
|
||||
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead of `Scene` (resp. `ThreeDScene`), and add calls to `self.pause()` everytime you want to create a new slide.
|
||||
2. Run `manim-slides` on rendered animations and display them like a *Power Point* presentation.
|
||||
<!-- start usage -->
|
||||
|
||||
The command-line documentation is available [online](https://eertmans.be/manim-slides/).
|
||||
Using Manim Slides is a two-step process:
|
||||
1. Render animations using `Slide` (resp. `ThreeDSlide`) as a base class instead
|
||||
of `Scene` (resp. `ThreeDScene`), and add calls to `self.next_slide()`
|
||||
every time you want to create a new slide.
|
||||
2. Run `manim-slides` on rendered animations and display them like a
|
||||
*PowerPoint* presentation.
|
||||
|
||||
The documentation is available [online](https://eertmans.be/manim-slides/).
|
||||
|
||||
### Basic Example
|
||||
|
||||
|
||||
Wrap a series of animations between `self.start_loop()` and `self.stop_loop()` when you want to loop them (until input to continue):
|
||||
Call `self.next_slide()` every time you want to create a pause between
|
||||
animations, and `self.next_slide(loop=True)` if you want the next slide to loop
|
||||
over animations until the user presses continue:
|
||||
|
||||
```python
|
||||
# example.py
|
||||
from manim import * # or: from manimlib import *
|
||||
|
||||
from manim import *
|
||||
# or: from manimlib import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Slide):
|
||||
|
||||
class BasicExample(Slide):
|
||||
def construct(self):
|
||||
circle = Circle(radius=3, color=BLUE)
|
||||
dot = Dot()
|
||||
|
||||
self.play(GrowFromCenter(circle))
|
||||
self.pause() # Waits user to press continue to go to the next slide
|
||||
self.next_slide() # Waits user to press continue to go to the next slide
|
||||
|
||||
self.start_loop() # Start loop
|
||||
self.next_slide(loop=True) # Start loop
|
||||
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
||||
self.end_loop() # This will loop until user inputs a key
|
||||
self.next_slide() # This will start a new non-looping slide
|
||||
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
self.pause() # Waits user to press continue to go to the next slide
|
||||
|
||||
self.wait()
|
||||
```
|
||||
|
||||
You **must** end your `Slide` with a `self.play(...)` or a `self.wait(...)`.
|
||||
|
||||
First, render the animation files:
|
||||
|
||||
```bash
|
||||
manim example.py
|
||||
# or
|
||||
manimgl example.py
|
||||
manim-slides render example.py BasicExample
|
||||
# or use ManimGL
|
||||
manim-slides render --GL example.py BasicExample
|
||||
```
|
||||
<!-- end usage -->
|
||||
|
||||
To start the presentation using `Scene1`, `Scene2` and so on simply run:
|
||||
> [!NOTE]
|
||||
> Using `manim-slides render` makes sure to use the `manim`
|
||||
> (or `manimlib`) library that was installed in the same Python environment.
|
||||
> Put simply, this is a wrapper around
|
||||
> `manim render [ARGS]...` (or `manimgl [ARGS]...`).
|
||||
|
||||
<!-- start more-usage -->
|
||||
|
||||
To start the presentation using `Scene1`, `Scene2` and so on, run:
|
||||
|
||||
```bash
|
||||
manim-slides [OPTIONS] Scene1 Scene2...
|
||||
```
|
||||
|
||||
Or in this example:
|
||||
In our example:
|
||||
|
||||
```bash
|
||||
manim-slides Example
|
||||
manim-slides BasicExample
|
||||
```
|
||||
|
||||
## Key Bindings
|
||||
<!-- end more-usage -->
|
||||
|
||||
The default key bindings to control the presentation are:
|
||||
<p align="center">
|
||||
<img alt="Example GIF" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/example.gif">
|
||||
</p>
|
||||
|
||||

|
||||
For detailed usage documentation, run `manim-slides --help`, or go to the
|
||||
[documentation](https://eertmans.be/manim-slides/latest/reference/cli.html).
|
||||
|
||||
## Interactive Tutorial
|
||||
|
||||
You can run the **configuration wizard** to change those key bindings:
|
||||
Click on the image to watch a slides presentation that explains to you how
|
||||
to use Manim Slides.
|
||||
|
||||
```bash
|
||||
manim-slides wizard
|
||||
[](https://eertmans.be/manim-slides/)
|
||||
|
||||
## More Examples
|
||||
|
||||
More examples are available in the
|
||||
[`example.py`](https://github.com/jeertmans/manim-slides/blob/main/example.py)
|
||||
file, if you downloaded the git repository.
|
||||
|
||||
## Comparison with Similar Tools
|
||||
|
||||
There exists a variety of tools that allows to create slides presentations
|
||||
containing Manim animations.
|
||||
|
||||
Below is a comparison of the most used ones with Manim Slides:
|
||||
|
||||
| Project name | Manim Slides | Manim Presentation | Manim Editor | Jupyter Notebooks |
|
||||
|:------------:|:------------:|:------------------:|:------------:|:-----------------:|
|
||||
| Link | [](https://github.com/jeertmans/manim-slides) | [](https://github.com/galatolofederico/manim-presentation) | [](https://github.com/ManimCommunity/manim_editor) | [](https://github.com/jupyter/notebook) |
|
||||
| Activity | [](https://github.com/jeertmans/manim-slides) | [](https://github.com/galatolofederico/manim-presentation) | [](https://github.com/ManimCommunity/manim_editor) | [](https://github.com/jupyter/notebook) |
|
||||
| Usage | Command-line | Command-line | Web Browser | Notebook |
|
||||
| Note | Requires minimal modif. in scenes files | Requires minimal modif. in scenes files | Requires the usage of sections, and configuration through graphical interface | Relies on `nbconvert` to create slides from a Notebook |
|
||||
| Support for ManimGL | Yes | No | No | No |
|
||||
| Web Browser presentations | Yes | No | Yes | 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}
|
||||
}
|
||||
```
|
||||
|
||||
A default file can be created with:
|
||||
|
||||
```bash
|
||||
manim-slides init
|
||||
```
|
||||
|
||||
> **_NOTE:_** `manim-slides` uses key codes, which are platform dependent. Using the configuration wizard is therefore highly recommended.
|
||||
|
||||
## Other Examples
|
||||
|
||||
Other examples are available in the [`example.py`](https://github.com/jeertmans/manim-slides/blob/main/example.py) file, if you downloaded the git repository.
|
||||
|
||||
Below is a small recording of me playing with the slides back and forth.
|
||||
|
||||

|
||||
|
||||
|
||||
## Features and Comparison with original manim-presentation
|
||||
|
||||
Below is a non-exhaustive list of features:
|
||||
|
||||
| Feature | `manim-slides` | `manim-presentation` |
|
||||
|:--------|:--------------:|:--------------------:|
|
||||
| Support for Manim | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Support for ManimGL | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| Configurable key bindings | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Configurable paths | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| Play / Pause slides | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Next / Previous slide | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Replay slide | :heavy_check_mark: | :heavy_check_mark: |
|
||||
| Reverse slide | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| Multiple key per actions | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| One command line tool | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| Robust config file parsing | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| Support for 3D Scenes | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| Documented code | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| Tested on Unix, macOS, and Windows | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
| Hide mouse cursor | :heavy_check_mark: | :heavy_multiplication_x: |
|
||||
|
||||
## F.A.Q
|
||||
|
||||
### How to increase quality on Windows
|
||||
|
||||
On Windows platform, one may encounter a lower image resolution than expected. Usually, this is observed because Windows rescales every application to fit the screen.
|
||||
As found by [@arashash](https://github.com/arashash), in [#20](https://github.com/jeertmans/manim-slides/issues/20), the problem can be addressed by changing the scaling factor to 100%:
|
||||
|
||||

|
||||
|
||||
in *Settings*->*Display*.
|
||||
or by linking this GitHub repository at the end of the presentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are more than welcome!
|
||||
Contributions are more than welcome! Please read through
|
||||
[our contributing section](https://eertmans.be/manim-slides/latest/contributing/index.html).
|
||||
|
||||
### Reporting an Issue
|
||||
|
||||
<!-- start reporting-an-issue -->
|
||||
|
||||
If you think you found a bug,
|
||||
an error in the documentation,
|
||||
or wish there was some feature that is currently missing,
|
||||
we would love to hear from you!
|
||||
|
||||
The best way to reach us is via the
|
||||
[GitHub issues](https://github.com/jeertmans/manim-slides/issues).
|
||||
If your problem is not covered by an already existing (closed or open) issue,
|
||||
then we suggest you create a
|
||||
[new issue](https://github.com/jeertmans/manim-slides/issues/new/choose).
|
||||
You can choose from a list of templates, or open a
|
||||
[blank issue](https://github.com/jeertmans/manim-slides/issues/new)
|
||||
if your issue does not fit one of the proposed topics.
|
||||
|
||||
The more precise you are in the description of your problem, the faster we will
|
||||
be able to help you!
|
||||
|
||||
<!-- end reporting-an-issue -->
|
||||
|
||||
### Seeking for help
|
||||
|
||||
<!-- start seeking-for-help -->
|
||||
|
||||
Sometimes, you may have a question about Manim Slides,
|
||||
not necessarily an issue.
|
||||
|
||||
First, make sure to read the
|
||||
[F.A.Q](https://eertmans.be/manim-slides/latest/faq.html) to see if
|
||||
your question has already been answered. If not, please follow the
|
||||
recommendation (from that page) to reach us for questions.
|
||||
|
||||
<!-- end seeking-for-help -->
|
||||
|
||||
### Contact
|
||||
|
||||
<!-- start contact -->
|
||||
|
||||
Finally, if you do not have any GitHub account,
|
||||
or just wish to contact the author of Manim Slides,
|
||||
you can do so at: [jeertmans@icloud.com](mailto:jeertmans@icloud.com).
|
||||
|
||||
<!-- end contact -->
|
||||
|
||||
[pypi-version-badge]: https://img.shields.io/pypi/v/manim-slides?label=manim-slides
|
||||
[pypi-version-url]: https://pypi.org/project/manim-slides/
|
||||
[pypi-python-version-badge]: https://img.shields.io/pypi/pyversions/manim-slides
|
||||
[pypi-download-badge]: https://img.shields.io/pypi/dm/manim-slides
|
||||
[documentation-badge]: https://readthedocs.org/projects/manim-slides/badge/?version=latest
|
||||
[documentation-url]: https://manim-slides.readthedocs.io/
|
||||
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.7971360.svg
|
||||
[doi-url]: https://doi.org/10.5281/zenodo.7971360
|
||||
[jose-badge]: https://jose.theoj.org/papers/10.21105/jose.00206/status.svg
|
||||
[jose-url]: https://doi.org/10.21105/jose.00206
|
||||
[codecov-badge]: https://codecov.io/gh/jeertmans/manim-slides/branch/main/graph/badge.svg?token=8P4DY9JCE4
|
||||
[codecov-url]: https://codecov.io/gh/jeertmans/manim-slides
|
||||
[binder-badge]: https://mybinder.org/badge_logo.svg
|
||||
[binder-url]: https://mybinder.org/v2/gh/jeertmans/manim-slides-binder/HEAD?filepath=getting_started.ipynb
|
||||
|
2
custom_config.yml
Normal file
2
custom_config.yml
Normal file
@ -0,0 +1,2 @@
|
||||
camera:
|
||||
background_color: '#000000'
|
55
docker/Dockerfile
Normal file
55
docker/Dockerfile
Normal file
@ -0,0 +1,55 @@
|
||||
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/v0.18.1/docker/Dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
build-essential \
|
||||
gcc \
|
||||
cmake \
|
||||
libcairo2-dev \
|
||||
libffi-dev \
|
||||
libpango1.0-dev \
|
||||
freeglut3-dev \
|
||||
pkg-config \
|
||||
make \
|
||||
wget \
|
||||
ghostscript
|
||||
|
||||
# setup a minimal texlive installation
|
||||
COPY docker/texlive-profile.txt /tmp/
|
||||
ENV PATH=/usr/local/texlive/bin/armhf-linux:/usr/local/texlive/bin/aarch64-linux:/usr/local/texlive/bin/x86_64-linux:$PATH
|
||||
RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz && \
|
||||
mkdir /tmp/install-tl && \
|
||||
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 \
|
||||
&& tlmgr install \
|
||||
amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
|
||||
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
||||
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
|
||||
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
|
||||
|
||||
# clone and build manim-slides
|
||||
COPY . /opt/manim-slides
|
||||
WORKDIR /opt/manim-slides
|
||||
|
||||
RUN pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
|
||||
|
||||
ARG NB_USER=manimslidesuser
|
||||
ARG NB_UID=1000
|
||||
ENV USER=${NB_USER}
|
||||
ENV NB_UID=${NB_UID}
|
||||
ENV HOME=/manim-slides
|
||||
|
||||
RUN adduser --disabled-password \
|
||||
--gecos "Default user" \
|
||||
--uid ${NB_UID} \
|
||||
${NB_USER}
|
||||
|
||||
# create working directory for user to mount local directory into
|
||||
WORKDIR ${HOME}
|
||||
USER root
|
||||
RUN chown -R ${NB_USER}:${NB_USER} ${HOME}
|
||||
RUN chmod 777 ${HOME}
|
||||
USER ${NB_USER}
|
||||
|
||||
CMD [ "/bin/bash" ]
|
15
docker/README.md
Normal file
15
docker/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Docker Image
|
||||
|
||||
Manim Slides Docker image, highly inspired from the Manim Community Docker image.
|
||||
|
||||
Building the image can be done with:
|
||||
|
||||
```bash
|
||||
docker build -t manim-slide/manin-slide:TAG -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
from the root directory of the repository.
|
||||
|
||||
> [!WARNING]
|
||||
> If you run the command above from another place,
|
||||
> Docker will not be able to find expected files.
|
10
docker/texlive-profile.txt
Normal file
10
docker/texlive-profile.txt
Normal file
@ -0,0 +1,10 @@
|
||||
selected_scheme scheme-minimal
|
||||
TEXDIR /usr/local/texlive
|
||||
TEXMFCONFIG ~/.texlive/texmf-config
|
||||
TEXMFHOME ~/texmf
|
||||
TEXMFLOCAL /usr/local/texlive/texmf-local
|
||||
TEXMFSYSCONFIG /usr/local/texlive/texmf-config
|
||||
TEXMFSYSVAR /usr/local/texlive/texmf-var
|
||||
TEXMFVAR ~/.texlive/texmf-var
|
||||
option_doc 0
|
||||
option_src 0
|
1
docs/source/_static/favicon.png
Symbolic link
1
docs/source/_static/favicon.png
Symbolic link
@ -0,0 +1 @@
|
||||
../../../static/favicon.png
|
Binary file not shown.
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 24 B |
1
docs/source/_static/logo.png
Symbolic link
1
docs/source/_static/logo.png
Symbolic link
@ -0,0 +1 @@
|
||||
../../../static/logo.png
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 24 B |
1
docs/source/_static/logo_dark_docs.png
Symbolic link
1
docs/source/_static/logo_dark_docs.png
Symbolic link
@ -0,0 +1 @@
|
||||
../../../static/logo_dark_docs.png
|
1
docs/source/_static/logo_dark_github.png
Symbolic link
1
docs/source/_static/logo_dark_github.png
Symbolic link
@ -0,0 +1 @@
|
||||
../../../static/logo_dark_github.png
|
1
docs/source/_static/logo_dark_transparent.png
Symbolic link
1
docs/source/_static/logo_dark_transparent.png
Symbolic link
@ -0,0 +1 @@
|
||||
../../../static/logo_dark_transparent.png
|
1
docs/source/_static/logo_light_transparent.png
Symbolic link
1
docs/source/_static/logo_light_transparent.png
Symbolic link
@ -0,0 +1 @@
|
||||
../../../static/logo_light_transparent.png
|
101
docs/source/_static/template.diff
Normal file
101
docs/source/_static/template.diff
Normal file
@ -0,0 +1,101 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Head stuff -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Slides stuff -->
|
||||
|
||||
<script>
|
||||
<!-- RevealJS stuff -->
|
||||
</script>
|
||||
|
||||
<!-- Add a clock to each section dynamically using JavaScript -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var revealContainer = document.querySelector('.reveal');
|
||||
|
||||
// Append dynamic content to each section
|
||||
var sections = revealContainer.querySelectorAll('.slides > section');
|
||||
sections.forEach(function (section) {
|
||||
// Create a new clock container
|
||||
var clockContainer = document.createElement('div');
|
||||
clockContainer.className = 'clock';
|
||||
|
||||
// Append the new clock container to the section
|
||||
section.appendChild(clockContainer);
|
||||
});
|
||||
|
||||
// Function to update the clock content
|
||||
function updateClock() {
|
||||
var now = new Date();
|
||||
var hours = now.getHours();
|
||||
var minutes = now.getMinutes();
|
||||
var seconds = now.getSeconds();
|
||||
|
||||
// Format the time as HH:MM:SS
|
||||
var timeString = pad(hours) + ":" + pad(minutes) + ":" + pad(seconds);
|
||||
|
||||
// Update the content of all clock containers
|
||||
var clockContainers = document.querySelectorAll('.clock');
|
||||
clockContainers.forEach(function (container) {
|
||||
container.innerText = timeString;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to pad zero for single-digit numbers
|
||||
function pad(number) {
|
||||
return String(number).padStart(2, "0");
|
||||
}
|
||||
|
||||
// Update the clock every second
|
||||
setInterval(updateClock, 1000);
|
||||
|
||||
// Register a reveal.js event to update the clock on each slide change
|
||||
Reveal.addEventListener('slidechanged', function (event) {
|
||||
updateClock();
|
||||
});
|
||||
|
||||
// Initial update
|
||||
updateClock();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- define the style of the clock -->
|
||||
<style>
|
||||
.clock {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
font-size: 24px;
|
||||
font-family: "Arial", sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* control the relative position of the clock to the slides */
|
||||
.reveal .slides > section.present, .reveal .slides > section > section.present {
|
||||
min-height: 100% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
justify-content: center !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
section > h1 {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
.print-pdf .reveal .slides > section.present, .print-pdf .reveal .slides > section > section.present {
|
||||
min-height: 770px !important;
|
||||
position: relative !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
</body>
|
||||
</html>
|
397
docs/source/_static/template.html
Normal file
397
docs/source/_static/template.html
Normal file
@ -0,0 +1,397 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
|
||||
<title>{{ title }}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/theme/{{ reveal_theme }}.min.css">
|
||||
|
||||
<!-- Theme used for syntax highlighting of code -->
|
||||
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
|
||||
<!-- <link rel="stylesheet" href="index.css"> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="reveal">
|
||||
<div class="slides">
|
||||
{% for presentation_config in presentation_configs -%}
|
||||
{%- set outer_loop = loop %}
|
||||
{% for slide_config in presentation_config.slides %}
|
||||
{% if one_file %}
|
||||
{% set file = file_to_data_uri(slide_config.file) %}
|
||||
{% else %}
|
||||
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
|
||||
{% endif %}
|
||||
<section
|
||||
data-background-size={{ background_size }}
|
||||
data-background-color="{{ presentation_config.background_color }}"
|
||||
data-background-video="{{ file }}"
|
||||
{% if loop.index == 1 and outer_loop.index == 1 %}
|
||||
data-background-video-muted
|
||||
{% endif %}
|
||||
{% if slide_config.loop %}
|
||||
data-background-video-loop
|
||||
{% endif %}
|
||||
{% if slide_config.auto_next %}
|
||||
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
||||
{% endif %}
|
||||
>
|
||||
{% if slide_config.notes != "" %}
|
||||
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.js"></script>
|
||||
|
||||
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
|
||||
{% if has_notes %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
|
||||
{% endif %}
|
||||
|
||||
<!-- <script src="index.js"></script> -->
|
||||
<script>
|
||||
Reveal.initialize({
|
||||
{% if has_notes %}
|
||||
/// The list of RevealJS plugins.
|
||||
plugins: [ RevealMarkdown, RevealNotes ],
|
||||
{% endif %}
|
||||
// The "normal" size of the presentation, aspect ratio will
|
||||
// be preserved when the presentation is scaled to fit different
|
||||
// resolutions. Can be specified using percentage units.
|
||||
width: {{ width }},
|
||||
height: {{ height }},
|
||||
// Factor of the display size that should remain empty around
|
||||
// the content
|
||||
margin: {{ margin }},
|
||||
// Bounds for smallest/largest possible scale to apply to content
|
||||
minScale: {{ min_scale }},
|
||||
maxScale: {{ max_scale }},
|
||||
// Display presentation control arrows
|
||||
controls: {{ controls }},
|
||||
// Help the user learn the controls by providing hints, for example by
|
||||
// bouncing the down arrow when they first encounter a vertical slide
|
||||
controlsTutorial: {{ controls_tutorial }},
|
||||
// Determines where controls appear, "edges" or "bottom-right"
|
||||
controlsLayout: {{ controls_layout }},
|
||||
// Visibility rule for backwards navigation arrows; "faded", "hidden"
|
||||
// or "visible"
|
||||
controlsBackArrows: {{ controls_back_arrows }},
|
||||
// Display a presentation progress bar
|
||||
progress: {{ progress }},
|
||||
// Display the page number of the current slide
|
||||
// - true: Show slide number
|
||||
// - false: Hide slide number
|
||||
//
|
||||
// Can optionally be set as a string that specifies the number formatting:
|
||||
// - "h.v": Horizontal . vertical slide number (default)
|
||||
// - "h/v": Horizontal / vertical slide number
|
||||
// - "c": Flattened slide number
|
||||
// - "c/t": Flattened slide number / total slides
|
||||
//
|
||||
// Alternatively, you can provide a function that returns the slide
|
||||
// number for the current slide. The function should take in a slide
|
||||
// object and return an array with one string [slideNumber] or
|
||||
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
|
||||
slideNumber: {{ slide_number }},
|
||||
// Can be used to limit the contexts in which the slide number appears
|
||||
// - "all": Always show the slide number
|
||||
// - "print": Only when printing to PDF
|
||||
// - "speaker": Only in the speaker view
|
||||
showSlideNumber: {{ show_slide_number }},
|
||||
// Use 1 based indexing for # links to match slide number (default is zero
|
||||
// based)
|
||||
hashOneBasedIndex: {{ hash_one_based_index }},
|
||||
// Add the current slide number to the URL hash so that reloading the
|
||||
// page/copying the URL will return you to the same slide
|
||||
hash: {{ hash }},
|
||||
// Flags if we should monitor the hash and change slides accordingly
|
||||
respondToHashChanges: {{ respond_to_hash_changes }},
|
||||
// Enable support for jump-to-slide navigation shortcuts
|
||||
jumpToSlide: {{ jump_to_slide }},
|
||||
// Push each slide change to the browser history. Implies `hash: true`
|
||||
history: {{ history }},
|
||||
// Enable keyboard shortcuts for navigation
|
||||
keyboard: {{ keyboard }},
|
||||
// Optional function that blocks keyboard events when retuning false
|
||||
//
|
||||
// If you set this to 'focused', we will only capture keyboard events
|
||||
// for embedded decks when they are in focus
|
||||
keyboardCondition: {{ keyboard_condition }},
|
||||
// Disables the default reveal.js slide layout (scaling and centering)
|
||||
// so that you can use custom CSS layout
|
||||
disableLayout: {{ disable_layout }},
|
||||
// Enable the slide overview mode
|
||||
overview: {{ overview }},
|
||||
// Vertical centering of slides
|
||||
center: {{ center }},
|
||||
// Enables touch navigation on devices with touch input
|
||||
touch: {{ touch }},
|
||||
// Loop the presentation
|
||||
loop: {{ loop }},
|
||||
// Change the presentation direction to be RTL
|
||||
rtl: {{ rtl }},
|
||||
// Changes the behavior of our navigation directions.
|
||||
//
|
||||
// "default"
|
||||
// Left/right arrow keys step between horizontal slides, up/down
|
||||
// arrow keys step between vertical slides. Space key steps through
|
||||
// all slides (both horizontal and vertical).
|
||||
//
|
||||
// "linear"
|
||||
// Removes the up/down arrows. Left/right arrows step through all
|
||||
// slides (both horizontal and vertical).
|
||||
//
|
||||
// "grid"
|
||||
// When this is enabled, stepping left/right from a vertical stack
|
||||
// to an adjacent vertical stack will land you at the same vertical
|
||||
// index.
|
||||
//
|
||||
// Consider a deck with six slides ordered in two vertical stacks:
|
||||
// 1.1 2.1
|
||||
// 1.2 2.2
|
||||
// 1.3 2.3
|
||||
//
|
||||
// If you're on slide 1.3 and navigate right, you will normally move
|
||||
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
|
||||
// from 1.3 -> 2.3.
|
||||
navigationMode: {{ navigation_mode }},
|
||||
// Randomizes the order of slides each time the presentation loads
|
||||
shuffle: {{ shuffle }},
|
||||
// Turns fragments on and off globally
|
||||
fragments: {{ fragments }},
|
||||
// Flags whether to include the current fragment in the URL,
|
||||
// so that reloading brings you to the same fragment position
|
||||
fragmentInURL: {{ fragment_in_url }},
|
||||
// Flags if the presentation is running in an embedded mode,
|
||||
// i.e. contained within a limited portion of the screen
|
||||
embedded: {{ embedded }},
|
||||
// Flags if we should show a help overlay when the question-mark
|
||||
// key is pressed
|
||||
help: {{ help }},
|
||||
// Flags if it should be possible to pause the presentation (blackout)
|
||||
pause: {{ pause }},
|
||||
// Flags if speaker notes should be visible to all viewers
|
||||
showNotes: {{ show_notes }},
|
||||
// Global override for autolaying embedded media (video/audio/iframe)
|
||||
// - null: Media will only autoplay if data-autoplay is present
|
||||
// - true: All media will autoplay, regardless of individual setting
|
||||
// - false: No media will autoplay, regardless of individual setting
|
||||
autoPlayMedia: {{ auto_play_media }},
|
||||
// Global override for preloading lazy-loaded iframes
|
||||
// - null: Iframes with data-src AND data-preload will be loaded when within
|
||||
// the viewDistance, iframes with only data-src will be loaded when visible
|
||||
// - true: All iframes with data-src will be loaded when within the viewDistance
|
||||
// - false: All iframes with data-src will be loaded only when visible
|
||||
preloadIframes: {{ preload_iframes }},
|
||||
// Can be used to globally disable auto-animation
|
||||
autoAnimate: {{ auto_animate }},
|
||||
// Optionally provide a custom element matcher that will be
|
||||
// used to dictate which elements we can animate between.
|
||||
autoAnimateMatcher: {{ auto_animate_matcher }},
|
||||
// Default settings for our auto-animate transitions, can be
|
||||
// overridden per-slide or per-element via data arguments
|
||||
autoAnimateEasing: {{ auto_animate_easing }},
|
||||
autoAnimateDuration: {{ auto_animate_duration }},
|
||||
autoAnimateUnmatched: {{ auto_animate_unmatched }},
|
||||
// CSS properties that can be auto-animated. Position & scale
|
||||
// is matched separately so there's no need to include styles
|
||||
// like top/right/bottom/left, width/height or margin.
|
||||
autoAnimateStyles: {{ auto_animate_styles }},
|
||||
// Controls automatic progression to the next slide
|
||||
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
|
||||
// is present on the current slide or fragment
|
||||
// - 1+: All slides will progress automatically at the given interval
|
||||
// - false: No auto-sliding, even if data-autoslide is present
|
||||
autoSlide: {{ auto_slide }},
|
||||
// Stop auto-sliding after user input
|
||||
autoSlideStoppable: {{ auto_slide_stoppable }},
|
||||
// Use this method for navigation when auto-sliding (defaults to navigateNext)
|
||||
autoSlideMethod: {{ auto_slide_method }},
|
||||
// Specify the average time in seconds that you think you will spend
|
||||
// presenting each slide. This is used to show a pacing timer in the
|
||||
// speaker view
|
||||
defaultTiming: {{ default_timing }},
|
||||
// Enable slide navigation via mouse wheel
|
||||
mouseWheel: {{ mouse_wheel }},
|
||||
// Opens links in an iframe preview overlay
|
||||
// Add `data-preview-link` and `data-preview-link="false"` to customize each link
|
||||
// individually
|
||||
previewLinks: {{ preview_links }},
|
||||
// Exposes the reveal.js API through window.postMessage
|
||||
postMessage: {{ post_message }},
|
||||
// Dispatches all reveal.js events to the parent window through postMessage
|
||||
postMessageEvents: {{ post_message_events }},
|
||||
// Focuses body when page changes visibility to ensure keyboard shortcuts work
|
||||
focusBodyOnPageVisibilityChange: {{ focus_body_on_page_visibility_change }},
|
||||
// Transition style
|
||||
transition: {{ transition }}, // none/fade/slide/convex/concave/zoom
|
||||
// Transition speed
|
||||
transitionSpeed: {{ transition_speed }}, // default/fast/slow
|
||||
// Transition style for full page slide backgrounds
|
||||
backgroundTransition: {{ background_transition }}, // none/fade/slide/convex/concave/zoom
|
||||
// The maximum number of pages a single slide can expand onto when printing
|
||||
// to PDF, unlimited by default
|
||||
pdfMaxPagesPerSlide: {{ pdf_max_pages_per_slide }},
|
||||
// Prints each fragment on a separate slide
|
||||
pdfSeparateFragments: {{ pdf_separate_fragments }},
|
||||
// Offset used to reduce the height of content within exported PDF pages.
|
||||
// This exists to account for environment differences based on how you
|
||||
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
|
||||
// on precisely the total height of the document whereas in-browser
|
||||
// printing has to end one pixel before.
|
||||
pdfPageHeightOffset: {{ pdf_page_height_offset }},
|
||||
// Number of slides away from the current that are visible
|
||||
viewDistance: {{ view_distance }},
|
||||
// Number of slides away from the current that are visible on mobile
|
||||
// devices. It is advisable to set this to a lower number than
|
||||
// viewDistance in order to save resources.
|
||||
mobileViewDistance: {{ mobile_view_distance }},
|
||||
// The display mode that will be used to show slides
|
||||
display: {{ display }},
|
||||
// Hide cursor if inactive
|
||||
hideInactiveCursor: {{ hide_inactive_cursor }},
|
||||
// Time before the cursor is hidden (in ms)
|
||||
hideCursorTime: {{ hide_cursor_time }}
|
||||
});
|
||||
{% if one_file %}
|
||||
// Fix found by @t-fritsch and @Rapsssito on GitHub
|
||||
// see: https://github.com/hakimel/reveal.js/discussions/3362#discussioncomment-11733074.
|
||||
function setVideoBase64(video) {
|
||||
const sources = video.querySelectorAll('source');
|
||||
// Update the source of the video
|
||||
sources.forEach((source, i) => {
|
||||
const src = source.getAttribute('src');
|
||||
if(src.match(/^data:video.*;base64$/)) {
|
||||
const nextSrc = sources[i+1]?.getAttribute('src');
|
||||
video.setAttribute('src', `${src},${nextSrc}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fixBase64VideoBackground(event) {
|
||||
// Analyze all slides backgrounds
|
||||
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
|
||||
// Get the slide video and its sources for each background
|
||||
const video = slide.querySelector('video');
|
||||
if (video) {
|
||||
setVideoBase64(video);
|
||||
} else {
|
||||
// Listen to the creation of the video element
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
for (const addedNode of mutation.addedNodes) {
|
||||
if (addedNode.tagName === 'VIDEO') {
|
||||
setVideoBase64(addedNode);
|
||||
observer.disconnect(); // Stop observing once the video is handled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(slide, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Setup base64 videos
|
||||
Reveal.on( 'ready', fixBase64VideoBackground );
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
<!-- Add a clock to each section dynamically using JavaScript -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var revealContainer = document.querySelector('.reveal');
|
||||
|
||||
// Append dynamic content to each section
|
||||
var sections = revealContainer.querySelectorAll('.slides > section');
|
||||
sections.forEach(function (section) {
|
||||
// Create a new clock container
|
||||
var clockContainer = document.createElement('div');
|
||||
clockContainer.className = 'clock';
|
||||
|
||||
// Append the new clock container to the section
|
||||
section.appendChild(clockContainer);
|
||||
});
|
||||
|
||||
// Function to update the clock content
|
||||
function updateClock() {
|
||||
var now = new Date();
|
||||
var hours = now.getHours();
|
||||
var minutes = now.getMinutes();
|
||||
var seconds = now.getSeconds();
|
||||
|
||||
// Format the time as HH:MM:SS
|
||||
var timeString = pad(hours) + ":" + pad(minutes) + ":" + pad(seconds);
|
||||
|
||||
// Update the content of all clock containers
|
||||
var clockContainers = document.querySelectorAll('.clock');
|
||||
clockContainers.forEach(function (container) {
|
||||
container.innerText = timeString;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to pad zero for single-digit numbers
|
||||
function pad(number) {
|
||||
return String(number).padStart(2, "0");
|
||||
}
|
||||
|
||||
// Update the clock every second
|
||||
setInterval(updateClock, 1000);
|
||||
|
||||
// Register a reveal.js event to update the clock on each slide change
|
||||
Reveal.addEventListener('slidechanged', function (event) {
|
||||
updateClock();
|
||||
});
|
||||
|
||||
// Initial update
|
||||
updateClock();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- define the style of the clock -->
|
||||
<style>
|
||||
.clock {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
font-size: 24px;
|
||||
font-family: "Arial", sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* control the relative position of the clock to the slides */
|
||||
.reveal .slides > section.present, .reveal .slides > section > section.present {
|
||||
min-height: 100% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
justify-content: center !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
section > h1 {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
.print-pdf .reveal .slides > section.present, .print-pdf .reveal .slides > section > section.present {
|
||||
min-height: 770px !important;
|
||||
position: relative !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
</body>
|
||||
</html>
|
1
docs/source/_static/wizard_dark.png
Symbolic link
1
docs/source/_static/wizard_dark.png
Symbolic link
@ -0,0 +1 @@
|
||||
../../../static/wizard_dark.png
|
1
docs/source/_static/wizard_light.png
Symbolic link
1
docs/source/_static/wizard_light.png
Symbolic link
@ -0,0 +1 @@
|
||||
../../../static/wizard_light.png
|
6
docs/source/changelog.md
Normal file
6
docs/source/changelog.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
```{include} ../../CHANGELOG.md
|
||||
:start-after: <!-- start changelog -->
|
||||
:end-before: <!-- end changelog -->
|
||||
```
|
@ -4,24 +4,93 @@
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import date
|
||||
|
||||
from manim_slides import __version__
|
||||
|
||||
assert sys.version_info >= (3, 10), "Building docs requires Python 3.10"
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = "Manim Slides"
|
||||
copyright = "2022, Jérome Eertmans"
|
||||
copyright = f"2024-{date.today().year}, Jérome Eertmans"
|
||||
author = "Jérome Eertmans"
|
||||
version = __version__
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = ["sphinx.ext.autodoc", "sphinx_click"]
|
||||
extensions = [
|
||||
# Built-in
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.viewcode",
|
||||
# Additional
|
||||
"nbsphinx",
|
||||
"myst_parser",
|
||||
"sphinxcontrib.programoutput",
|
||||
"sphinxext.opengraph",
|
||||
"sphinx_click",
|
||||
"sphinx_copybutton",
|
||||
"sphinx_design",
|
||||
# Custom
|
||||
"manim_slides.docs.manim_slides_directive",
|
||||
]
|
||||
|
||||
autodoc_typehints = "both"
|
||||
|
||||
myst_enable_extensions = [
|
||||
"colon_fence",
|
||||
"html_admonition",
|
||||
]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = []
|
||||
|
||||
# Removes the 'package.module' part from package.module.Class
|
||||
add_module_names = False
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/")
|
||||
html_theme = "furo"
|
||||
html_static_path = ["_static"]
|
||||
html_favicon = "_static/favicon.png"
|
||||
|
||||
html_theme_options = {
|
||||
"light_logo": "logo_light_transparent.png",
|
||||
"dark_logo": "logo_dark_transparent.png",
|
||||
"footer_icons": [
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/jeertmans/manim-slides",
|
||||
"html": """
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
|
||||
</svg>
|
||||
""",
|
||||
"class": "",
|
||||
},
|
||||
],
|
||||
"source_repository": "https://github.com/jeertmans/manim-slides/",
|
||||
"source_branch": "main",
|
||||
"source_directory": "docs/source/",
|
||||
}
|
||||
|
||||
# -- Intersphinx mapping
|
||||
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
"manim": ("https://docs.manim.community/en/stable/", None),
|
||||
"manimlib": ("https://3b1b.github.io/manim/", None),
|
||||
"numpy": ("https://numpy.org/doc/stable/", None),
|
||||
}
|
||||
|
||||
# -- OpenGraph settings
|
||||
|
||||
ogp_site_url = "https://eertmans.be/manim-slides/"
|
||||
ogp_use_first_image = True
|
||||
|
46
docs/source/contributing/index.md
Normal file
46
docs/source/contributing/index.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest in Manim Slides! ✨
|
||||
|
||||
Manim Slides is an open source project, first created as a fork of
|
||||
[manim-presentation](https://github.com/galatolofederico/manim-presentation)
|
||||
(now deprecated in favor to Manim Slides),
|
||||
and we welcome contributions of all forms.
|
||||
|
||||
This section is here to help fist-time contributors know how they can help this
|
||||
project grow. Whether you are already familiar with Manim or GitHub,
|
||||
it is worth taking a few minutes to read those documents!
|
||||
|
||||
```{toctree}
|
||||
:hidden:
|
||||
|
||||
workflow
|
||||
internals
|
||||
```
|
||||
|
||||
[Workflow](/contributing/workflow)
|
||||
: how to work on this project. Start here if you're a new contributor.
|
||||
|
||||
[Internals](/contributing/internals)
|
||||
: how Manim Slides is built and how the various parts of it work.
|
||||
|
||||
## Reporting an Issue
|
||||
|
||||
```{include} ../../../README.md
|
||||
:start-after: <!-- start reporting-an-issue -->
|
||||
:end-before: <!-- end reporting-an-issue -->
|
||||
```
|
||||
|
||||
## Seeking for Help
|
||||
|
||||
```{include} ../../../README.md
|
||||
:start-after: <!-- start seeking-for-help -->
|
||||
:end-before: <!-- end seeking-for-help -->
|
||||
```
|
||||
|
||||
## Contact
|
||||
|
||||
```{include} ../../../README.md
|
||||
:start-after: <!-- start contact -->
|
||||
:end-before: <!-- end contact -->
|
||||
```
|
11
docs/source/contributing/internals.md
Normal file
11
docs/source/contributing/internals.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Internals
|
||||
|
||||
Manim-Slides' work in split in two steps: first, when rendering animation, and, second, when converting multiple animations into one slides presentation.
|
||||
|
||||
## Rendering
|
||||
|
||||
To render animations, Manim Slides simply uses Manim or ManimGL, and creates some additional output files that it needs for the presentation.
|
||||
|
||||
## Slides presentation
|
||||
|
||||
Manim Slides searches for the local artifacts it generated previously, and concatenates them into one presentation. For the graphical interface, it uses `PySide6`.
|
100
docs/source/contributing/workflow.md
Normal file
100
docs/source/contributing/workflow.md
Normal file
@ -0,0 +1,100 @@
|
||||
# Workflow
|
||||
|
||||
This document is there to help you recreate a working environment for Manim Slides.
|
||||
|
||||
## Dependencies
|
||||
|
||||
```{include} ../installation.md
|
||||
:start-after: <!-- start deps -->
|
||||
:end-before: <!-- end deps -->
|
||||
```
|
||||
|
||||
## Forking the repository and cloning it locally
|
||||
|
||||
We use GitHub to host Manim Slides' repository, and we encourage contributors to use git.
|
||||
|
||||
Useful links:
|
||||
|
||||
* [GitHub's Hello World](https://docs.github.com/en/get-started/quickstart/hello-world).
|
||||
* [GitHub Pull Request in 100 Seconds](https://www.youtube.com/watch?v=8lGpZkjnkt4&ab_channel=Fireship).
|
||||
|
||||
Once you feel comfortable with git and GitHub,
|
||||
[fork](https://github.com/jeertmans/manim-slides/fork)
|
||||
the repository, and clone it locally.
|
||||
|
||||
As for every Python project, using virtual environment is recommended to avoid
|
||||
conflicts between modules.
|
||||
For this project, we use [uv](https://github.com/astral-sh/uv) to easily manage project
|
||||
and development dependencies. If not already, please install this tool.
|
||||
|
||||
## Installing Python modules
|
||||
|
||||
With uv, installation becomes straightforward:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
:::{note}
|
||||
You still need the same dependencies as to install Manim and ManimGL,
|
||||
so please check their respective installation guides.
|
||||
:::
|
||||
|
||||
## Running commands
|
||||
|
||||
Because modules are installed in a new Python environment,
|
||||
you cannot use them directly in the shell.
|
||||
Instead, you either need to prepend `uv run` to any command, e.g.:
|
||||
|
||||
```bash
|
||||
uv run manim-slides wizard
|
||||
```
|
||||
|
||||
## Testing your code
|
||||
|
||||
Most of the tests are done with GitHub actions, thus not on your computer.
|
||||
The only command you should run locally is:
|
||||
|
||||
```bash
|
||||
uv run pre-commit run --all-files
|
||||
```
|
||||
|
||||
This runs a few linter and formatter to make sure the code quality and style stay
|
||||
constant across time.
|
||||
If a warning or an error is displayed, please fix it before going to next step.
|
||||
|
||||
For testing your code, simply run:
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## Building the documentation
|
||||
|
||||
The documentation is generated using Sphinx, based on the content
|
||||
in `docs/source` and in the `manim_slides` Python package.
|
||||
|
||||
To generate the documentation, run the following:
|
||||
|
||||
```bash
|
||||
cd docs
|
||||
uv run make html
|
||||
```
|
||||
|
||||
Then, the output index file is located at `docs/build/html/index.html` and
|
||||
can be opened with any modern browser.
|
||||
|
||||
:::{warning}
|
||||
Building the documentation can take quite some time, especially
|
||||
the first time as it needs to render all the animations.
|
||||
|
||||
Further builds should run faster.
|
||||
:::
|
||||
|
||||
## Proposing changes
|
||||
|
||||
Once you feel ready and think your contribution is ready to be reviewed,
|
||||
create a [pull request](https://github.com/jeertmans/manim-slides/pulls)
|
||||
and wait for a reviewer to check your work!
|
||||
|
||||
Many thanks to you!
|
2
docs/source/docutils.conf
Normal file
2
docs/source/docutils.conf
Normal file
@ -0,0 +1,2 @@
|
||||
[restructuredtext parser]
|
||||
syntax_highlight = short
|
124
docs/source/faq.md
Normal file
124
docs/source/faq.md
Normal file
@ -0,0 +1,124 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
This page summarizes a few of the most frequently asked questions
|
||||
when using Manim Slides.
|
||||
|
||||
They are organized by topic.
|
||||
|
||||
If your question is not here, please first look through the
|
||||
[open **and closed** issues on GitHub](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue)
|
||||
or within the [discussions](https://github.com/jeertmans/manim-slides/discussions).
|
||||
|
||||
If you still cannot find help after that, do not hesitate to create
|
||||
your own issue or discussion on GitHub!
|
||||
|
||||
## Installing
|
||||
|
||||
Everything related to installing Manim-Slides.
|
||||
|
||||
Please do not forget the carefully read through
|
||||
the [installation](/installation) page!
|
||||
|
||||
## Rendering
|
||||
|
||||
Questions related to `manim-slides render [SCENES]...`,
|
||||
|
||||
### I cannot render with ManimGL
|
||||
|
||||
ManimGL support is only guaranteed to work
|
||||
on a very minimal set of versions, because it differs quite a lot from ManimCE,
|
||||
and its development is not very active.
|
||||
|
||||
The typical issue is that ManimGL `<1.7.1` needs an outdated NumPy version, but
|
||||
can be resolved by manually downgrading NumPy, or upgrading ManimGL (**recommended**).
|
||||
|
||||
### Presenting
|
||||
|
||||
Questions related to `manim-slides present [SCENES]...`,
|
||||
or `manim-slides [SCENES]...` for short.
|
||||
|
||||
### Can I have interactive slides
|
||||
|
||||
No. Slides are pre-rendered static videos files
|
||||
and cannot be modified on the fly.
|
||||
|
||||
If you need new to have some kind of interactive, look
|
||||
at the preview feature coupled with the OpenGL renderer
|
||||
with ManimCE or ManimGL.
|
||||
|
||||
### Slides go black when video finishes
|
||||
|
||||
This is an issue with Qt,
|
||||
which cannot be solved on all platforms and Python versions,
|
||||
see [#293](https://github.com/jeertmans/manim-slides/issues/293).
|
||||
|
||||
Recent version of Manim Slides, i.e., `manim-slides>5.1.7`, come
|
||||
with a fix that should work fine.
|
||||
|
||||
### How to increase quality on Windows
|
||||
|
||||
On Windows platform, one may encounter a lower image resolution than expected.
|
||||
Usually, this is observed because Windows rescales every application to
|
||||
fit the screen.
|
||||
As found by [@arashash](https://github.com/arashash),
|
||||
in [#20](https://github.com/jeertmans/manim-slides/issues/20),
|
||||
the problem can be addressed by changing the scaling factor to 100%:
|
||||
|
||||
<p align="center">
|
||||
<img
|
||||
alt="Windows Fix Scaling"
|
||||
src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/windows_quality_fix.png"
|
||||
>
|
||||
</p>
|
||||
|
||||
in *Settings*->*Display*.
|
||||
|
||||
## Converting to any format
|
||||
|
||||
Questions that apply to all output formats when using
|
||||
`manim-slides convert [SCENES]...`.
|
||||
|
||||
### What are all possible configuration options
|
||||
|
||||
Configuration options can be specified with the syntax
|
||||
`-c<option_name>=<option_value>`.
|
||||
|
||||
To list all accepted options, use `manim-slides convert --to=FORMAT --show-config`,
|
||||
where `FORMAT` is one of the supported formats.
|
||||
This will also show the default value for each option.
|
||||
|
||||
### How to retrieve the current template
|
||||
|
||||
If you want to create your own template, the best is to start from the default one.
|
||||
|
||||
You can either download it from the
|
||||
[template folder](https://github.com/jeertmans/manim-slides/tree/main/manim_slides/templates)
|
||||
or use the `manim-slides convert --to=FORMAT --show-template` command,
|
||||
where `FORMAT` is one of the supported formats.
|
||||
|
||||
## Converting to HTML
|
||||
|
||||
Questions related to `manim-slides convert [SCENES]... output.html`.
|
||||
|
||||
### I moved my `.html` file and it stopped working
|
||||
|
||||
If you did not specify `--one-file` (or `-cone_file=true`) when converting,
|
||||
then Manim Slides generated a folder containing all
|
||||
the video files, in the same folder as the HTML
|
||||
output. As the path to video files is a relative path,
|
||||
you need to move the HTML **and its assets** altogether.
|
||||
|
||||
## Converting to PPTX
|
||||
|
||||
Questions related to `manim-slides convert [SCENES]... output.pptx`.
|
||||
|
||||
### My media stop playing after a few slides
|
||||
|
||||
This issue is (probably) caused by PowerPoint never freeing
|
||||
memory, causing memory allocation errors, and can be partially
|
||||
solved by reducing the video quality or the number of slides,
|
||||
see [#392](https://github.com/jeertmans/manim-slides/issues/392).
|
||||
|
||||
Another solution, suggested by [@Azercoco](https://github.com/Azercoco) in
|
||||
[#392 (comment)](https://github.com/jeertmans/manim-slides/issues/392#issuecomment-2368198106),
|
||||
is to disable hardware/GPU acceleration.
|
31
docs/source/features_table.md
Normal file
31
docs/source/features_table.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Features Table
|
||||
|
||||
The following summarizes the different presentation features Manim Slides offers.
|
||||
|
||||
:::{table} Comparison of the different presentation methods.
|
||||
:widths: auto
|
||||
:align: center
|
||||
|
||||
| Feature / Constraint | [`present`](reference/cli.md) | [`convert --to=html`](reference/cli.md) | [`convert --to=pptx`](reference/cli.md) | [`convert --to=pdf`](reference/cli.md)
|
||||
| :--- | :---: | :---: | :---: | :---: |
|
||||
| Basic navigation through slides | Yes | Yes | Yes | Yes (static image) |
|
||||
| Replay slide | Yes | No | No | N/A |
|
||||
| Pause animation | Yes | Yes | No | N/A |
|
||||
| Play slide in reverse | Yes | No | No | N/A |
|
||||
| Slide count | Yes | Yes (optional) | Yes (optional) | N/A |
|
||||
| Needs Python with Manim Slides installed | Yes | No | No | No
|
||||
| Requires internet access | No | Depends[^1] | No | No |
|
||||
| Auto. play slides | Yes | Yes | Yes | N/A |
|
||||
| Loops support | Yes | Yes | Yes | N/A |
|
||||
| Fully customizable | No | Yes (`--use-template` option) | No | No |
|
||||
| Other dependencies | None | A modern web browser | PowerPoint or LibreOffice Impress[^2] | None |
|
||||
| Works cross-platforms | Yes | Yes | Partly[^2][^3] | Yes |
|
||||
:::
|
||||
|
||||
[^1]: By default, HTML assets are loaded from the internet, but they can be
|
||||
pre-downloaded and embedded in the HTML file at conversion time.
|
||||
[^2]: If you encounter a problem where slides do not automatically play or loops do not work,
|
||||
please
|
||||
[file an issue on GitHub](https://github.com/jeertmans/manim-slides/issues/new/choose).
|
||||
[^3]: PowerPoint online does not seem to support automatic playing of videos,
|
||||
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!*
|
57
docs/source/index.md
Normal file
57
docs/source/index.md
Normal file
@ -0,0 +1,57 @@
|
||||
---
|
||||
hide-toc: true
|
||||
og:description: Manim Slides makes creating slides with Manim super easy!
|
||||
---
|
||||
|
||||
```{eval-rst}
|
||||
.. image:: _static/logo_light_transparent.png
|
||||
:width: 600px
|
||||
:align: center
|
||||
:class: only-light
|
||||
:alt: Manim Slide logo
|
||||
```
|
||||
|
||||
```{eval-rst}
|
||||
.. image:: _static/logo_dark_transparent.png
|
||||
:width: 600px
|
||||
:align: center
|
||||
:class: only-dark
|
||||
:alt: Manim Slide logo
|
||||
```
|
||||
|
||||
# Welcome to Manim Slide's documentation
|
||||
|
||||
Manim Slides makes creating slides with Manim super easy!
|
||||
|
||||
In a [very few steps](/quickstart),
|
||||
you can create slides and present them either using the GUI, or your browser.
|
||||
|
||||
Slide through the demo below to get a quick glimpse on what you can do with
|
||||
Manim Slides.
|
||||
|
||||
```{eval-rst}
|
||||
.. manim-slides:: ../../example.py:ConvertExample
|
||||
:hide_source:
|
||||
:quality: high
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
:hidden:
|
||||
|
||||
quickstart
|
||||
installation
|
||||
reference/index
|
||||
features_table
|
||||
manim_or_manimgl
|
||||
gallery
|
||||
faq
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
:caption: Development
|
||||
:hidden:
|
||||
|
||||
contributing/index
|
||||
changelog
|
||||
license
|
||||
```
|
@ -1,28 +0,0 @@
|
||||
.. manim-slides documentation master file, created by
|
||||
sphinx-quickstart on Wed Sep 21 15:07:28 2022.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
.. image:: _static/logo.png
|
||||
:width: 600px
|
||||
:align: center
|
||||
:alt: Manim Slide logo
|
||||
|
||||
Welcome to Manim Slide's CLI documentation!
|
||||
===========================================
|
||||
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<!-- From: https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page -->
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;"> <iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="_static/slides.html"></iframe></div>
|
||||
|
||||
|
||||
This page contains an exhaustive list of all the commands available with `manim-slides`.
|
||||
|
||||
If you need help installing or using Manim Slide, please refer to the `GitHub README <https://github.com/jeertmans/manim-slides>`_.
|
||||
|
||||
.. click:: manim_slides.main:cli
|
||||
:prog: manim-slides
|
||||
:nested: full
|
173
docs/source/installation.md
Normal file
173
docs/source/installation.md
Normal file
@ -0,0 +1,173 @@
|
||||
# Installation
|
||||
|
||||
While installing Manim Slides and its dependencies on your global Python is fine,
|
||||
we recommend using a virtual environment
|
||||
(e.g., [venv](https://docs.python.org/3/tutorial/venv.html)) for a local installation.
|
||||
|
||||
Therefore, the following documentation will install Manim Slides using
|
||||
[pipx](https://pipx.pypa.io/). This tool is a drop-in replacement
|
||||
for installing Python packages that ship with one or more executable.
|
||||
|
||||
The benefit of using pipx is that it will automatically create a new virtual
|
||||
environment for every package you install.
|
||||
|
||||
:::{note}
|
||||
Every time you read `pipx install`, you can use `pip install` instead,
|
||||
if you are working in a virtual environment or else.
|
||||
:::
|
||||
|
||||
## Dependencies
|
||||
|
||||
<!-- start deps -->
|
||||
|
||||
Manim Slides requires either Manim or ManimGL to be installed, along
|
||||
with their dependencies.
|
||||
Having both packages installed is fine too.
|
||||
|
||||
If none of those packages are installed,
|
||||
please refer to their specific installation guidelines:
|
||||
- [Manim](https://docs.manim.community/en/stable/installation.html)
|
||||
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
|
||||
|
||||
<!-- end deps -->
|
||||
|
||||
## Pip Install
|
||||
|
||||
The recommended way to install the latest release
|
||||
with all features is to use pipx:
|
||||
|
||||
```bash
|
||||
pipx install -U "manim-slides[pyside6-full]"
|
||||
```
|
||||
|
||||
:::{tip}
|
||||
While not necessary, the `-U` indicates that we would
|
||||
like to upgrade to the latest version available,
|
||||
if Manim Slides is already installed.
|
||||
:::
|
||||
|
||||
:::{note}
|
||||
The quotes `"` are added because not all shell support unquoted
|
||||
brackets (e.g., zsh) or commas (e.g., Windows).
|
||||
:::
|
||||
|
||||
You can check that Manim Slides was correctly installed with:
|
||||
|
||||
```bash
|
||||
manim-slides --version
|
||||
```
|
||||
|
||||
## Custom install
|
||||
|
||||
If you want more control on what dependencies are installed,
|
||||
you can always install the bare minimal dependencies with:
|
||||
|
||||
```bash
|
||||
pipx install -U manim-slides
|
||||
```
|
||||
|
||||
And install additional dependencies later.
|
||||
|
||||
Optionally, you can also install Manim or ManimGL using extras[^1]:
|
||||
|
||||
```bash
|
||||
pipx install -U "manim-slides[manim]" # For Manim
|
||||
# or
|
||||
pipx install -U "manim-slides[manimgl]" # For ManimGL
|
||||
```
|
||||
|
||||
For optional dependencies documentation, see
|
||||
[next section](#optional-dependencies).
|
||||
|
||||
:::{warning}
|
||||
If you are installing with pipx, this is mandatory to at least include
|
||||
either `manim` or `manimgl`.
|
||||
:::
|
||||
|
||||
[^1]: You still need to have Manim or ManimGL platform-specific dependencies
|
||||
installed on your computer.
|
||||
|
||||
## Optional dependencies
|
||||
|
||||
Along with the optional dependencies for Manim and ManimGL,
|
||||
Manim Slides offers additional *extras*, that can be activated
|
||||
using optional dependencies:
|
||||
|
||||
- `full`, to include `magic`, `manim`, and
|
||||
`sphinx-directive` extras (see below);
|
||||
- `magic`, to include a Jupyter magic to render
|
||||
animations inside notebooks. This automatically installs `manim`,
|
||||
and does not work with ManimGL;
|
||||
- `manim` and `manimgl`, for installing the corresponding
|
||||
dependencies;
|
||||
- `pyqt6` to include PyQt6 Qt bindings;
|
||||
- `pyqt6-full` to include `full` and `pyqt6`;
|
||||
- `pyside6` to include PySide6 Qt bindings. Those bindings are available
|
||||
on most platforms and Python version, except on Python 3.12[^2];
|
||||
- `pyside6-full` to include `full` and `pyside6`;
|
||||
- `sphinx-directive`, to generate presentation inside your Sphinx
|
||||
documentation. This automatically installs `manim`,
|
||||
and does not work with ManimGL.
|
||||
|
||||
Installing those extras can be done with the following syntax:
|
||||
|
||||
```bash
|
||||
pipx install -U "manim-slides[extra1,extra2]"
|
||||
```
|
||||
|
||||
[^2]: Actually, PySide6 can be installed on Python 3.12, but you will then
|
||||
observe the same visual bug as with PyQt6.
|
||||
|
||||
## Nixpkgs installation
|
||||
|
||||
Manim Slides is distributed under Nixpkgs >=24.05.
|
||||
If you are using Nix or NixOS, you can find Manim Slides under:
|
||||
|
||||
- `nixpkgs.manim-slides`, which is meant to be a stand alone application and
|
||||
includes PyQt6 (see above);
|
||||
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
|
||||
module (for notebook magics), and includes IPython but does not include
|
||||
any Qt binding.
|
||||
|
||||
You can try out the Manim Slides package with:
|
||||
|
||||
```sh
|
||||
nix-shell -p manim ffmpeg manim-slides
|
||||
```
|
||||
|
||||
or by adding it to your
|
||||
[configuration file](https://nixos.org/manual/nixos/stable/#sec-package-management).
|
||||
|
||||
Alternatively, you can try Manim Slides in a Python environment with:
|
||||
|
||||
```sh
|
||||
nix-shell -p manim ffmpeg "python3.withPackages(ps: with ps; [ manim-slides, ...])"
|
||||
```
|
||||
|
||||
or bundle this into [your Nix environment](https://wiki.nixos.org/wiki/Python).
|
||||
|
||||
:::{note}
|
||||
Nix does not currently support `manimgl`.
|
||||
:::
|
||||
|
||||
## When you need a Qt backend
|
||||
|
||||
Before `v5.1`, Manim Slides automatically included PySide6 as
|
||||
a Qt backend. As only `manim-slides present` and `manim-slides wizard`
|
||||
command need a graphical library, and installing PySide6 on all platforms
|
||||
and Python version can be sometimes complicated, Manim Slides chooses
|
||||
**not to include** any Qt backend.
|
||||
|
||||
The use can choose between PySide6 (best) and PyQt6, depending on their
|
||||
availability and licensing rules.
|
||||
|
||||
As of `v5.1`, you **need** to have Qt bindings installed to use
|
||||
`manim-slides present` or `manim-slides wizard`. The recommended way to
|
||||
install those are via optional dependencies, as explained above.
|
||||
|
||||
## Install from source
|
||||
|
||||
An alternative way to install Manim Slides is to clone the git repository,
|
||||
and build the package from source. Read the
|
||||
[contributing guide](/contributing/workflow)
|
||||
to know how to process.
|
5
docs/source/license.md
Normal file
5
docs/source/license.md
Normal file
@ -0,0 +1,5 @@
|
||||
# License
|
||||
|
||||
|
||||
```{include} ../../LICENSE.md
|
||||
```
|
70
docs/source/manim_or_manimgl.md
Normal file
70
docs/source/manim_or_manimgl.md
Normal file
@ -0,0 +1,70 @@
|
||||
# Manim or ManimGL
|
||||
|
||||
Manim Slides supports both Manim (Community Edition) and ManimGL (by 3b1b).
|
||||
|
||||
Because both modules have slightly different APIs, Manim Slides needs to know
|
||||
which Manim API you are using, to import the correct module.
|
||||
|
||||
## Default Behavior
|
||||
|
||||
By default, Manim Slides looks at {py:data}`sys.modules` and chooses the first
|
||||
Manim package that is already imported: `manim` for Manim,
|
||||
`manimlib` for ManimGL. This works pretty well when rendering
|
||||
the slides.
|
||||
|
||||
If both modules are present in {py:data}`sys.modules`, then Manim Slides will
|
||||
prefer using `manim`.
|
||||
|
||||
### Usage
|
||||
|
||||
The simplest way to use Manim Slides with the correct Manim API is to:
|
||||
|
||||
1. first import the Manim API;
|
||||
2. and, then, import `manim_slides`.
|
||||
|
||||
Example for `manim`:
|
||||
|
||||
```python
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
```
|
||||
|
||||
Example for `manimlib`:
|
||||
|
||||
```python
|
||||
from manimlib import *
|
||||
from manim_slides import Slide
|
||||
```
|
||||
|
||||
### Example of Default Import
|
||||
|
||||
The following code shows how Manim Slides detected that `manimlib`
|
||||
was imported, so the {py:class}`Slide<manim_slides.slide.Slide>`
|
||||
automatically subclasses the class from ManimGL, not Manim.
|
||||
|
||||
```python
|
||||
from manimlib import Scene
|
||||
from manim_slides import Slide
|
||||
|
||||
assert issubclass(Slide, Scene) # Slide subclasses Scene from ManimGL
|
||||
|
||||
from manim import Scene
|
||||
|
||||
assert not issubclass(Slide, Scene) # but not Scene from Manim
|
||||
```
|
||||
|
||||
## Custom Manim API
|
||||
|
||||
If you want to override the default Manim API, you can set the `MANIM_API`
|
||||
environment variable to:
|
||||
|
||||
- `manim` or `manimce` to import `manim`;
|
||||
- `manimlib` or `manimgl` to import `manimlib`;
|
||||
|
||||
prior to importing `manim_slides`.
|
||||
|
||||
Note that Manim Slides will still first look at {py:data}`sys.modules` to check
|
||||
if any of the two modules is already imported.
|
||||
|
||||
If you want to force Manim Slides to obey the `MANIM_API` environment variable,
|
||||
you must also set `FORCE_MANIM_API=1`.
|
35
docs/source/quickstart.md
Normal file
35
docs/source/quickstart.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Quickstart
|
||||
|
||||
If not already, install Manim Slides, along with either Manim or ManimGL,
|
||||
see [installation](/installation).
|
||||
|
||||
## Creating your first slides
|
||||
|
||||
```{include} ../../README.md
|
||||
:start-after: <!-- start usage -->
|
||||
:end-before: <!-- end usage -->
|
||||
```
|
||||
|
||||
:::{note}
|
||||
Using `manim-slides render` makes sure to use the `manim`
|
||||
(or `manimlib`) library that was installed in the same Python environment.
|
||||
Put simply, this is a wrapper around
|
||||
`manim render [ARGS]...` (or `manimgl [ARGS]...`).
|
||||
:::
|
||||
|
||||
|
||||
```{include} ../../README.md
|
||||
:start-after: <!-- start more-usage -->
|
||||
:end-before: <!-- end more-usage -->
|
||||
```
|
||||
|
||||
The output slides should look this this:
|
||||
|
||||
```{eval-rst}
|
||||
.. manim-slides:: ../../example.py:BasicExample
|
||||
:hide_source:
|
||||
:quality: high
|
||||
```
|
||||
|
||||
For more advanced examples,
|
||||
see the [Examples](/reference/examples) section.
|
42
docs/source/reference/api.md
Normal file
42
docs/source/reference/api.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Application Programming Interface
|
||||
|
||||
Manim Slides' API is very limited: it simply consists of two classes, `Slide`
|
||||
and `ThreeDSlide`, which are subclasses of `Scene` and `ThreeDScene` from Manim.
|
||||
|
||||
Therefore, we only document here the methods we think the end-user will ever
|
||||
use, not the methods used internally when rendering.
|
||||
|
||||
## Slide
|
||||
|
||||
```{eval-rst}
|
||||
.. autoclass:: manim_slides.slide.Slide
|
||||
:members:
|
||||
add_to_canvas,
|
||||
canvas,
|
||||
canvas_mobjects,
|
||||
mobjects_without_canvas,
|
||||
next_section,
|
||||
next_slide,
|
||||
remove_from_canvas,
|
||||
start_skip_animations,
|
||||
stop_skip_animations,
|
||||
wait_time_between_slides,
|
||||
wipe,
|
||||
zoom,
|
||||
```
|
||||
|
||||
## 3D Slide
|
||||
|
||||
```{eval-rst}
|
||||
.. autoclass:: manim_slides.slide.ThreeDSlide
|
||||
:members:
|
||||
```
|
||||
|
||||
## Animations
|
||||
|
||||
```{eval-rst}
|
||||
.. automodule:: manim_slides.slide.animation
|
||||
:members:
|
||||
Wipe,
|
||||
Zoom,
|
||||
```
|
37
docs/source/reference/cli.md
Normal file
37
docs/source/reference/cli.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Command Line Interface
|
||||
|
||||
This page contains an exhaustive list of all the commands available with `manim-slides`.
|
||||
|
||||
|
||||
```{eval-rst}
|
||||
.. click:: manim_slides.__main__:cli
|
||||
:prog: manim-slides
|
||||
: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
|
||||
```
|
||||
::::
|
71
docs/source/reference/customize_html.md
Normal file
71
docs/source/reference/customize_html.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Customize your RevealJS slides
|
||||
|
||||
One of the benefits of the `convert` command is the use of template files.
|
||||
|
||||
Currently, only the HTML export uses one. If not specified, the template
|
||||
will be the one shipped with Manim Slides, see
|
||||
[`manim_slides/templates/revealjs.html`](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html).
|
||||
|
||||
Because you can actually use your own template with the `--use-template`
|
||||
option, possibilities are infinite!
|
||||
|
||||
:::{warning}
|
||||
Currently, the `PresentationConfig` class and its components
|
||||
are not part of the public API. You can still use them, e.g.,
|
||||
in the templates, but you may expect breaking changes between
|
||||
releases.
|
||||
|
||||
Eventually, this will become part of the public API too,
|
||||
and we will document its usage.
|
||||
:::
|
||||
|
||||
## Adding a clock to each slide
|
||||
|
||||
In this example, we show how to add a self-updating clock
|
||||
to the bottom left corner of every slide.
|
||||
|
||||
:::{note}
|
||||
This example is inspired from
|
||||
[@gsong-math's comment](https://github.com/jeertmans/manim-slides/issues/356#issuecomment-1902626943)
|
||||
on Manim Slides' repository.
|
||||
:::
|
||||
|
||||
### What to add
|
||||
|
||||
Whenever you want to create a template, it is best practice
|
||||
to start from the default one (see link above).
|
||||
|
||||
Modifying it needs very basic HTML/JavaScript/CSS skills.
|
||||
To add a clock, you can simply add the following to the
|
||||
default template file:
|
||||
|
||||
```{eval-rst}
|
||||
.. literalinclude:: ../_static/template.diff
|
||||
:language: html
|
||||
```
|
||||
|
||||
:::{tip}
|
||||
Because we use RevealJS to generate HTML slides,
|
||||
we recommend you to take a look at
|
||||
[RevealJS' documentation](https://revealjs.com/).
|
||||
:::
|
||||
|
||||
### How it renders
|
||||
|
||||
Then, using the `:template: <path/to/custom_template.html>`
|
||||
option, the basic example renders as follows:
|
||||
|
||||
```{eval-rst}
|
||||
.. manim-slides:: ../../../example.py:BasicExample
|
||||
:hide_source:
|
||||
:template: ../_static/template.html
|
||||
```
|
||||
|
||||
### Full code
|
||||
|
||||
Below, you can read the full content of the template file.
|
||||
|
||||
```{eval-rst}
|
||||
.. literalinclude:: ../_static/template.html
|
||||
:language: html+jinja
|
||||
```
|
172
docs/source/reference/examples.md
Normal file
172
docs/source/reference/examples.md
Normal file
@ -0,0 +1,172 @@
|
||||
# Examples
|
||||
|
||||
Contents of `example.py`.
|
||||
|
||||
Do not forget to import Manim Slides and Manim or ManimGL:
|
||||
|
||||
```python
|
||||
from manim import *
|
||||
from manim_slides import Slide, ThreeDSlide
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```python
|
||||
from manimlib import *
|
||||
from manim_slides import Slide, ThreeDSlide
|
||||
```
|
||||
|
||||
Then, each presentation, named `SCENE`, was generated with those two commands:
|
||||
|
||||
```bash
|
||||
manim example.py SCENE # or manimgl example SCENE
|
||||
manim-slides convert SCENE scene.html -ccontrols=true
|
||||
```
|
||||
|
||||
where `-ccontrols=true` indicates that we want to display the blue navigation arrows.
|
||||
|
||||
## Basic Example
|
||||
|
||||
Basic example from quickstart.
|
||||
|
||||
```{eval-rst}
|
||||
.. manim-slides:: ../../../example.py:BasicExample
|
||||
:hide_source:
|
||||
:quality: high
|
||||
|
||||
.. literalinclude:: ../../../example.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:pyobject: BasicExample
|
||||
```
|
||||
|
||||
## 3D Example
|
||||
|
||||
Example using 3D camera. As Manim and ManimGL handle 3D differently,
|
||||
definitions are slightly different.
|
||||
|
||||
### With Manim
|
||||
|
||||
```{eval-rst}
|
||||
.. manim-slides:: ../../../example.py:ThreeDExample
|
||||
:hide_source:
|
||||
:quality: high
|
||||
|
||||
.. literalinclude:: ../../../example.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:dedent: 4
|
||||
:start-after: [manim-3d]
|
||||
:end-before: [manim-3d]
|
||||
```
|
||||
|
||||
### With ManimGL
|
||||
|
||||
```{eval-rst}
|
||||
.. literalinclude:: ../../../example.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:dedent: 4
|
||||
:start-after: [manimgl-3d]
|
||||
:end-before: [manimgl-3d]
|
||||
```
|
||||
|
||||
## Subclass Custom Scenes
|
||||
|
||||
For compatibility reasons, Manim Slides only provides subclasses for
|
||||
`Scene` and `ThreeDScene`.
|
||||
However, subclassing other scene classes is totally possible,
|
||||
and very simple to do actually!
|
||||
|
||||
[For example](https://github.com/jeertmans/manim-slides/discussions/185),
|
||||
you can subclass the `MovingCameraScene` class from `manim`
|
||||
with the following code:
|
||||
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
|
||||
class MovingCameraSlide(Slide, MovingCameraScene):
|
||||
pass
|
||||
```
|
||||
|
||||
And later use this class anywhere in your code:
|
||||
|
||||
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
|
||||
class SubclassExample(MovingCameraSlide):
|
||||
"""Example taken from ManimCE's docs."""
|
||||
|
||||
def construct(self):
|
||||
self.camera.frame.save_state()
|
||||
|
||||
ax = Axes(x_range=[-1, 10], y_range=[-1, 10])
|
||||
graph = ax.plot(lambda x: np.sin(x), color=WHITE, x_range=[0, 3 * PI])
|
||||
|
||||
dot_1 = Dot(ax.i2gp(graph.t_min, graph))
|
||||
dot_2 = Dot(ax.i2gp(graph.t_max, graph))
|
||||
self.add(ax, graph, dot_1, dot_2)
|
||||
|
||||
self.play(self.camera.frame.animate.scale(0.5).move_to(dot_1))
|
||||
self.next_slide()
|
||||
self.play(self.camera.frame.animate.move_to(dot_2))
|
||||
self.next_slide()
|
||||
self.play(Restore(self.camera.frame))
|
||||
self.wait()
|
||||
```
|
||||
|
||||
:::{note}
|
||||
If you do not plan to reuse `MovingCameraSlide` more than once, then you can
|
||||
directly write the `construct` method in the body of `MovingCameraSlide`.
|
||||
:::
|
||||
|
||||
```{eval-rst}
|
||||
.. manim-slides:: SubclassExample
|
||||
:hide_source:
|
||||
:quality: high
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
|
||||
class MovingCameraSlide(Slide, MovingCameraScene):
|
||||
pass
|
||||
|
||||
class SubclassExample(MovingCameraSlide):
|
||||
def construct(self):
|
||||
self.camera.frame.save_state()
|
||||
|
||||
ax = Axes(x_range=[-1, 10], y_range=[-1, 10])
|
||||
graph = ax.plot(lambda x: np.sin(x), color=WHITE, x_range=[0, 3 * PI])
|
||||
|
||||
dot_1 = Dot(ax.i2gp(graph.t_min, graph))
|
||||
dot_2 = Dot(ax.i2gp(graph.t_max, graph))
|
||||
self.add(ax, graph, dot_1, dot_2)
|
||||
|
||||
self.play(self.camera.frame.animate.scale(0.5).move_to(dot_1))
|
||||
self.next_slide()
|
||||
self.play(self.camera.frame.animate.move_to(dot_2))
|
||||
self.next_slide()
|
||||
self.play(Restore(self.camera.frame))
|
||||
self.wait()
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
A more advanced example is `ConvertExample`, which is used as demo slide and tutorial.
|
||||
|
||||
```{eval-rst}
|
||||
.. manim-slides:: ../../../example.py:ConvertExample
|
||||
:hide_source:
|
||||
:quality: high
|
||||
|
||||
.. literalinclude:: ../../../example.py
|
||||
:language: python
|
||||
:linenos:
|
||||
:pyobject: ConvertExample
|
||||
```
|
72
docs/source/reference/gui.md
Normal file
72
docs/source/reference/gui.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Graphical User Interface
|
||||
|
||||
Manim Slides' graphical user interface (GUI) is the *de facto* way to present slides.
|
||||
|
||||
If you do not specify one of the commands listed in the
|
||||
[CLI reference](/reference/cli),
|
||||
Manim Slides will use **present** by default, which launches a GUI window,
|
||||
playing your scene(s) like so:
|
||||
|
||||
```bash
|
||||
manim-slides [present] [SCENES]...
|
||||
```
|
||||
|
||||
Some optional parameters can be specified and can be listed with:
|
||||
|
||||
```bash
|
||||
manim-slides present --help
|
||||
```
|
||||
|
||||
:::{note}
|
||||
All the `SCENES` must be in the same folder (`--folder DIRECTORY`), which
|
||||
defaults to `./slides`. If you rendered your animations without changing
|
||||
directory, you should not worry about that :-)
|
||||
:::
|
||||
|
||||
## Configuration File
|
||||
|
||||
It is possible to configure Manim Slides via a configuration file, even though
|
||||
this feature is currently limited. You may initialize the default configuration
|
||||
file with:
|
||||
|
||||
```bash
|
||||
manim-slides init
|
||||
```
|
||||
|
||||
:::{warning}
|
||||
Note that, by default, Manim Slides will use default key bindings that are
|
||||
platform-dependent. If you decide to overwrite those with a config file, you may
|
||||
encounter some problems from platform to platform.
|
||||
:::
|
||||
|
||||
## Configuring Key Bindings
|
||||
|
||||
If you wish to use other key bindings than the defaults, you can run the
|
||||
configuration wizard with:
|
||||
|
||||
```bash
|
||||
manim-slides wizard
|
||||
```
|
||||
|
||||
A similar window to the image below will pop up and prompt to change keys.
|
||||
|
||||
```{eval-rst}
|
||||
.. image:: ../_static/wizard_light.png
|
||||
:width: 300px
|
||||
:align: center
|
||||
:class: only-light
|
||||
:alt: Manim Slide Wizard
|
||||
```
|
||||
|
||||
```{eval-rst}
|
||||
.. image:: ../_static/wizard_dark.png
|
||||
:width: 300px
|
||||
:align: center
|
||||
:class: only-dark
|
||||
:alt: Manim Slide Wizard
|
||||
```
|
||||
|
||||
:::{note}
|
||||
Even though it is not currently supported through the GUI, you can select
|
||||
multiple key binding for the same action by modifying the config file.
|
||||
:::
|
40
docs/source/reference/html.md
Normal file
40
docs/source/reference/html.md
Normal file
@ -0,0 +1,40 @@
|
||||
# HTML Presentations
|
||||
|
||||
Manim Slides allows you to convert presentations into one HTML file, with
|
||||
[RevealJS](https://revealjs.com/). This file can then be opened with any modern
|
||||
web browser, allowing for a nice portability of your presentations.
|
||||
|
||||
As with every command with Manim Slides, converting slides' fragments into one
|
||||
HTML file (and its assets) can be done in one command:
|
||||
|
||||
```bash
|
||||
manim-slides convert [SCENES]... DEST
|
||||
```
|
||||
|
||||
where `DEST` is the `.html` destination file.
|
||||
|
||||
## Configuring the Template
|
||||
|
||||
Many configuration options are available through the `-c<option>=<value>` syntax.
|
||||
Most, if not all, RevealJS options should be available by default. If that is
|
||||
not the case, please
|
||||
[fill an issue](https://github.com/jeertmans/manim-slides/issues/new/choose)
|
||||
on GitHub.
|
||||
|
||||
You can print the list of available options with:
|
||||
|
||||
```bash
|
||||
manim-slides convert --show-config
|
||||
```
|
||||
|
||||
## Using a Custom Template
|
||||
|
||||
The default template used for HTML conversion can be found on
|
||||
[GitHub](https://github.com/jeertmans/manim-slides/blob/main/manim_slides/templates/revealjs.html)
|
||||
or printed with the `--show-template` option.
|
||||
If you wish to use another template, you can do so with the
|
||||
`--use-template FILE` option.
|
||||
|
||||
## More about HTML Slides
|
||||
|
||||
You can read more about HTML slides in the [sharing](/reference/sharing) section.
|
38
docs/source/reference/index.md
Normal file
38
docs/source/reference/index.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Reference Documentation
|
||||
|
||||
Automatically generated reference for Manim Slides.
|
||||
|
||||
```{toctree}
|
||||
:hidden:
|
||||
|
||||
api
|
||||
cli
|
||||
customize_html
|
||||
examples
|
||||
gui
|
||||
html
|
||||
IPython magic <ipython_magic>
|
||||
sharing
|
||||
Sphinx Extension <sphinx_extension>
|
||||
```
|
||||
|
||||
[Application Programming Interface](/reference/api): list of classes and methods that may
|
||||
be useful to the end-user.
|
||||
|
||||
[Command Line Interface](/reference/cli): list of all commands available using Manim
|
||||
Slides' executable.
|
||||
|
||||
[Examples](/reference/examples): curated list of examples and their output.
|
||||
|
||||
[Graphical User Interface](/reference/gui): details about the main Manim Slide' feature.
|
||||
|
||||
[HTML Presentation](/reference/html): an alternative way of presenting your animations.
|
||||
|
||||
[IPython Magic](/reference/ipython_magic): a magic to render and display Manim Slides inside notebooks.
|
||||
|
||||
+ [Example](/reference/magic_example): example notebook using the magics.
|
||||
|
||||
[Sharing](/reference/sharing): how to share your presentation with others.
|
||||
|
||||
|
||||
[Sphinx Extension](/reference/sphinx_extension): a Sphinx extension for displaying Manim Slides animations within your documentation.
|
6
docs/source/reference/ipython_magic.md
Normal file
6
docs/source/reference/ipython_magic.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Manim Slides' IPython magic
|
||||
|
||||
```{eval-rst}
|
||||
.. automodule:: manim_slides.ipython.ipython_magic
|
||||
:members: ManimSlidesMagic
|
||||
```
|
100
docs/source/reference/magic_example.ipynb
Normal file
100
docs/source/reference/magic_example.ipynb
Normal file
@ -0,0 +1,100 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6896875b-34ce-4fc5-809c-669c295067e7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Jupyter Magic Example\n",
|
||||
"\n",
|
||||
"This small example shows how to use the Manim Slides cell (`%%manim_slides`) and line (`%manim_slides`) magics:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a73f1c06-c7f8-4f19-a90e-e283bfb8c7c5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from manim import *\n",
|
||||
"\n",
|
||||
"from manim_slides import *"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "68dda1a0-74ff-4d9e-9575-5b25a98f21e7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true\n",
|
||||
"\n",
|
||||
"config.media_embed = True\n",
|
||||
"\n",
|
||||
"class MySlide(Slide):\n",
|
||||
" def construct(self):\n",
|
||||
" square = Square()\n",
|
||||
" circle = Circle()\n",
|
||||
" \n",
|
||||
" self.play(Create(square))\n",
|
||||
" self.next_slide()\n",
|
||||
" self.play(Transform(square, circle))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "54fa2d3e-bfee-417d-b64b-f3f30a8749ea",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class MyOtherSlide(Slide):\n",
|
||||
" def construct(self):\n",
|
||||
" text = VGroup(\n",
|
||||
" Text(\"Press\"),\n",
|
||||
" Text(\"and\"),\n",
|
||||
" Text(\"loop\"),\n",
|
||||
" ).arrange(DOWN, buff=1.0)\n",
|
||||
"\n",
|
||||
" self.play(Write(text))\n",
|
||||
" self.next_slide(loop=True)\n",
|
||||
" self.play(Indicate(text[-1], scale_factor=2.0, run_time=0.5))\n",
|
||||
" self.next_slide()\n",
|
||||
" self.play(FadeOut(text))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "7d8ad450-1487-4ca7-8d89-bf8ac344e1fa",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%manim_slides -v WARNING --progress_bar None MyOtherSlide --manim-slides controls=true"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.8"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
201
docs/source/reference/sharing.md
Normal file
201
docs/source/reference/sharing.md
Normal file
@ -0,0 +1,201 @@
|
||||
# Sharing your slides
|
||||
|
||||
Maybe one of the most important features is the ability to share your
|
||||
presentation with other people, or even with yourself but on another computer!
|
||||
|
||||
There exists a variety of solutions, and all of them are exposed here.
|
||||
|
||||
We will go from the *most restrictive* method, to the least restrictive one.
|
||||
If you need to present on a computer without prior knowledge on what will be
|
||||
installed on it, please directly refer to the last sections.
|
||||
|
||||
:::{note}
|
||||
In the next sections, we will assume your animations are described
|
||||
in `example.py`, and you have one presentation called `BasicExample`.
|
||||
:::
|
||||
|
||||
## With Manim Slides installed on the target machine
|
||||
|
||||
If Manim Slides, Manim (or ManimGL), and their dependencies are installed, then
|
||||
using `manim-slides present` allows for the best presentations, with the most
|
||||
options available.
|
||||
|
||||
### Sharing your Python file(s)
|
||||
|
||||
The lightest way to share your presentation is with the Python files that
|
||||
describe the slides.
|
||||
|
||||
If you have such files, you can recompile the animations locally, and use
|
||||
`manim-slides present` for your presentation. You may want to copy / paste
|
||||
you own `.manim-slides.json` config file, but it is **not recommended** if
|
||||
you are sharing from one platform (e.g., Linux) to another (e.g., Windows) as
|
||||
the key bindings might not be the same.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
# If you use ManimGl, add `--GL` after `render`
|
||||
manim-slides render example.py BasicExample
|
||||
|
||||
# This or `manim-slides BasicExample` works since
|
||||
# `present` is implied by default
|
||||
manim-slides present BasicExample
|
||||
```
|
||||
|
||||
### Sharing your animations files
|
||||
|
||||
If you do not want to recompile all the animations, you can simply share the
|
||||
slides folder (defaults to `./slides`). Then, Manim Slides will be able to read
|
||||
the animations from this folder and its subdirectories.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
# Make sure that the slides directory is in the current
|
||||
# working directory, or specify with `--folder <FOLDER>`
|
||||
manim-slides present BasicExample
|
||||
```
|
||||
|
||||
and the corresponding tree:
|
||||
|
||||
```
|
||||
.
|
||||
└── slides
|
||||
├── BasicExample.json
|
||||
└── files
|
||||
└── BasicExample (files not shown)
|
||||
```
|
||||
|
||||
## Without Manim Slides installed on the target machine
|
||||
|
||||
An alternative to `manim-slides present` is `manim-slides convert`.
|
||||
Currently, HTML and PPTX conversion are available, but do not hesitate to propose
|
||||
other formats by creating a
|
||||
[Feature Request](https://github.com/jeertmans/manim-slides/issues/new/choose),
|
||||
or directly proposing a
|
||||
[Pull Request](https://github.com/jeertmans/manim-slides/compare).
|
||||
|
||||
A major advantage of HTML files is that they can be opened cross-platform,
|
||||
granted one has a modern web browser (which is pretty standard).
|
||||
|
||||
### Sharing HTML and animation files
|
||||
|
||||
First, you need to create the HTML file and its assets directory.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
manim-slides convert BasicExample basic_example.html
|
||||
```
|
||||
|
||||
Then, you need to copy the HTML files and its assets directory to target location,
|
||||
while keeping the relative path between the HTML and the assets the same. The
|
||||
easiest solution is to compress both the file and the directory into one ZIP,
|
||||
and to extract it to the desired location.
|
||||
|
||||
By default, the assets directory will be named after the main HTML file, using `{basename}_assets`.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
.
|
||||
├── basic_example_assets
|
||||
│ ├── 1413466013_2261824125_223132457.mp4
|
||||
│ ├── 1672018281_2145352439_3942561600.mp4
|
||||
│ └── 1672018281_3136302242_2191168284.mp4
|
||||
└── basic_example.html
|
||||
```
|
||||
|
||||
Then, you can simply open the HTML file with any web browser application.
|
||||
|
||||
If you want to embed the presentation inside an HTML web page, a possibility is
|
||||
to use an `iframe`:
|
||||
|
||||
```html
|
||||
<div style="position:relative;padding-bottom:56.25%;">
|
||||
<!-- 56.25 comes from aspect ratio of 16:9, change this accordingly -->
|
||||
<iframe
|
||||
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
|
||||
frameborder="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
allowfullscreen
|
||||
allow="autoplay"
|
||||
src="basic_example.html">
|
||||
</iframe>
|
||||
</div>
|
||||
```
|
||||
|
||||
<!-- markdown-link-check-disable -->
|
||||
<!-- see why: https://github.com/tcort/markdown-link-check/discussions/189 -->
|
||||
|
||||
The additional code comes from
|
||||
[this article](https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page)
|
||||
and it there to preserve the original aspect ratio (16:9).
|
||||
|
||||
<!-- markdown-link-check-enable -->
|
||||
|
||||
### Sharing ONE HTML file
|
||||
|
||||
If you set the `--one-file` flag, all animations will be data URI encoded,
|
||||
making the HTML a self-contained presentation file that can be shared
|
||||
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
|
||||
|
||||
HTML conversion makes it convenient to play your presentation on a
|
||||
remote server.
|
||||
|
||||
This is how your are able to watch all the examples on this website. If you want
|
||||
to know how to share your slide with GitHub pages, check out the
|
||||
[Manim Slides Starter GitHub repository template](https://github.com/jeertmans/manim-slides-starter).
|
||||
|
||||
:::{warning}
|
||||
Keep in mind that playing large video files over the internet network
|
||||
can take some time, and *glitches* may occur between slide transitions for this
|
||||
reason.
|
||||
:::
|
||||
|
||||
### Using the Github starter template
|
||||
|
||||
A [starter template](https://github.com/jeertmans/manim-slides-starter) is
|
||||
available which allows to quickly get going with a new Manim slides
|
||||
presentation on your Github account. The template comes ready with
|
||||
functionality to automate tasks using Github actions and publish to Github
|
||||
Pages. Please refer to the template page for usage instructions.
|
||||
|
||||
### With PowerPoint (*EXPERIMENTAL*)
|
||||
|
||||
A convenient conversion feature is to the PowerPoint format, thanks to the
|
||||
`python-pptx` package. Even though it is fully working,
|
||||
it is still considered in an *EXPERIMENTAL* status because we do not
|
||||
exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.
|
||||
|
||||
Basically, you can create a PowerPoint in a single command:
|
||||
|
||||
```bash
|
||||
manim-slides convert --to=pptx BasicExample basic_example.pptx
|
||||
```
|
||||
|
||||
All the videos and necessary files will be contained inside the `.pptx` file, so
|
||||
you can safely share it with anyone. By default, the `poster_frame_image`, i.e.,
|
||||
what is displayed by PowerPoint when the video is not playing, is the first
|
||||
frame of each slide. This allows for smooth transitions.
|
||||
|
||||
In the future, we hope to provide more features to this format,
|
||||
so feel free to suggest new features too!
|
||||
|
||||
### Static PDF presentation
|
||||
|
||||
If you ever need backup slides, that are only made of PDF pages
|
||||
with static images, you can generate such a PDF with the following command:
|
||||
|
||||
```bash
|
||||
manim-slides convert --to=pdf BasicExample basic_example.pdf
|
||||
```
|
||||
|
||||
Note that you will lose all the benefits from animated slides. Therefore,
|
||||
this is only recommended to be used as a backup plan. By default, the last frame
|
||||
of each slide will be printed. This can be changed to be the first one with
|
||||
`-cframe_index=first`.
|
6
docs/source/reference/sphinx_extension.md
Normal file
6
docs/source/reference/sphinx_extension.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Manim Slides' Sphinx directive
|
||||
|
||||
```{eval-rst}
|
||||
.. automodule:: manim_slides.docs.manim_slides_directive
|
||||
:members: ManimSlidesDirective
|
||||
```
|
159
example.py
159
example.py
@ -1,52 +1,34 @@
|
||||
# flake8: noqa: F403, F405
|
||||
# type: ignore
|
||||
import sys
|
||||
|
||||
if "manim" in sys.modules:
|
||||
from manim import *
|
||||
|
||||
MANIMGL = False
|
||||
elif "manimlib" in sys.modules:
|
||||
from manimlib import *
|
||||
|
||||
MANIMGL = True
|
||||
else:
|
||||
raise ImportError("This script must be run with either `manim` or `manimgl`")
|
||||
|
||||
from manim_slides import Slide, ThreeDSlide
|
||||
from manim_slides.slide import MANIM, MANIMGL
|
||||
|
||||
if MANIM:
|
||||
from manim import *
|
||||
elif MANIMGL:
|
||||
from manimlib import *
|
||||
|
||||
|
||||
class Example(Slide):
|
||||
class BasicExample(Slide):
|
||||
def construct(self):
|
||||
circle = Circle(radius=3, color=BLUE)
|
||||
dot = Dot()
|
||||
|
||||
self.play(GrowFromCenter(circle))
|
||||
self.pause()
|
||||
|
||||
self.start_loop()
|
||||
self.next_slide(loop=True)
|
||||
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
||||
self.end_loop()
|
||||
self.next_slide()
|
||||
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
self.pause()
|
||||
|
||||
self.play(dot.animate.move_to(RIGHT * 3))
|
||||
self.pause()
|
||||
|
||||
self.start_loop()
|
||||
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
||||
self.end_loop()
|
||||
|
||||
# Each slide MUST end with an animation (a self.wait is considered an animation)
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
|
||||
|
||||
class ConvertExample(Slide):
|
||||
def tinywait(self):
|
||||
self.wait(0.1)
|
||||
"""WARNING: this example does not seem to work with ManimGL."""
|
||||
|
||||
def construct(self):
|
||||
self.wait_time_between_slides = 0.1
|
||||
|
||||
title = VGroup(
|
||||
Text("From Manim animations", t2c={"From": BLUE}),
|
||||
@ -58,7 +40,7 @@ class ConvertExample(Slide):
|
||||
step_2 = Text("2. Replace Scene with Slide")
|
||||
step_3 = Text("3. In construct, add pauses where you need")
|
||||
step_4 = Text("4. You can also create loops")
|
||||
step_5 = Text("5. Render you scene with Manim")
|
||||
step_5 = Text("5. Render your scene with Manim")
|
||||
step_6 = Text("6. Open your presentation with Manim Slides")
|
||||
|
||||
for step in [step_1, step_2, step_3, step_4, step_5, step_6]:
|
||||
@ -68,10 +50,10 @@ class ConvertExample(Slide):
|
||||
|
||||
self.play(FadeIn(title))
|
||||
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
code = Code(
|
||||
code="""from manim import *
|
||||
code_string="""from manim import *
|
||||
|
||||
|
||||
class Example(Scene):
|
||||
@ -90,7 +72,7 @@ class Example(Scene):
|
||||
)
|
||||
|
||||
code_step_1 = Code(
|
||||
code="""from manim import *
|
||||
code_string="""from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Scene):
|
||||
@ -109,7 +91,7 @@ class Example(Scene):
|
||||
)
|
||||
|
||||
code_step_2 = Code(
|
||||
code="""from manim import *
|
||||
code_string="""from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Slide):
|
||||
@ -128,7 +110,7 @@ class Example(Slide):
|
||||
)
|
||||
|
||||
code_step_3 = Code(
|
||||
code="""from manim import *
|
||||
code_string="""from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Slide):
|
||||
@ -137,110 +119,98 @@ class Example(Slide):
|
||||
self.add(dot)
|
||||
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
square = Square()
|
||||
self.play(Transform(dot, square))
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
self.play(Rotate(square, angle=PI/2))
|
||||
""",
|
||||
language="python",
|
||||
)
|
||||
|
||||
code_step_4 = Code(
|
||||
code="""from manim import *
|
||||
code_string="""from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class Example(Slide):
|
||||
def construct(self):
|
||||
dot = Dot()
|
||||
self.add(dot)
|
||||
self.start_loop()
|
||||
self.next_slide(loop=True)
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
self.end_loop()
|
||||
self.next_slide()
|
||||
square = Square()
|
||||
self.play(Transform(dot, square))
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
self.play(Rotate(square, angle=PI/2))
|
||||
""",
|
||||
language="python",
|
||||
)
|
||||
|
||||
code_step_5 = Code(
|
||||
code="manim example.py Example",
|
||||
code_string="manim-slide render example.py Example",
|
||||
language="console",
|
||||
)
|
||||
|
||||
code_step_6 = Code(
|
||||
code="manim-slides Example",
|
||||
code_string="manim-slides Example",
|
||||
language="console",
|
||||
)
|
||||
|
||||
or_text = Text("or generate HTML presentation").scale(0.5)
|
||||
|
||||
code_step_7 = Code(
|
||||
code="manim-slides convert Example slides.html --open",
|
||||
code_string="manim-slides convert Example slides.html --open",
|
||||
language="console",
|
||||
).shift(DOWN)
|
||||
|
||||
self.clear()
|
||||
|
||||
self.play(FadeIn(code))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
self.wipe(title, code)
|
||||
self.next_slide()
|
||||
|
||||
self.play(FadeIn(step, shift=RIGHT))
|
||||
self.play(Transform(code, code_step_1))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.play(Transform(step, step_2))
|
||||
self.play(Transform(code, code_step_2))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.play(Transform(step, step_3))
|
||||
self.play(Transform(code, code_step_3))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.play(Transform(step, step_4))
|
||||
self.play(Transform(code, code_step_4))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.play(Transform(step, step_5))
|
||||
self.play(Transform(code, code_step_5))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
self.next_slide(auto_next=True)
|
||||
|
||||
self.play(Transform(step, step_6))
|
||||
self.play(Transform(code, code_step_6))
|
||||
self.play(code.animate.shift(UP), FadeIn(code_step_7), FadeIn(or_text))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
|
||||
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.start_loop()
|
||||
self.next_slide(loop=True)
|
||||
self.play(FadeIn(watch_text))
|
||||
self.play(FadeOut(watch_text))
|
||||
self.end_loop()
|
||||
self.next_slide()
|
||||
self.clear()
|
||||
|
||||
dot = Dot()
|
||||
self.add(dot)
|
||||
self.start_loop()
|
||||
self.next_slide(loop=True)
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
self.end_loop()
|
||||
self.next_slide()
|
||||
square = Square()
|
||||
self.play(Transform(dot, square))
|
||||
self.remove(dot)
|
||||
self.add(square)
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
self.play(Rotate(square, angle=PI / 4))
|
||||
self.tinywait()
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
learn_more_text = (
|
||||
VGroup(
|
||||
@ -252,13 +222,12 @@ class Example(Slide):
|
||||
)
|
||||
|
||||
self.play(Transform(square, learn_more_text))
|
||||
self.tinywait()
|
||||
|
||||
|
||||
# For ThreeDExample, things are different
|
||||
|
||||
if not MANIMGL:
|
||||
|
||||
# [manim-3d]
|
||||
class ThreeDExample(ThreeDSlide):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
@ -272,36 +241,33 @@ if not MANIMGL:
|
||||
self.play(GrowFromCenter(circle))
|
||||
self.begin_ambient_camera_rotation(rate=75 * DEGREES / 4)
|
||||
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.start_loop()
|
||||
self.next_slide(loop=True)
|
||||
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
|
||||
self.end_loop()
|
||||
self.next_slide()
|
||||
|
||||
self.stop_ambient_camera_rotation()
|
||||
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.play(dot.animate.move_to(RIGHT * 3))
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.start_loop()
|
||||
self.next_slide(loop=True)
|
||||
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
||||
self.end_loop()
|
||||
self.next_slide()
|
||||
|
||||
# Each slide MUST end with an animation (a self.wait is considered an animation)
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
|
||||
# [manim-3d]
|
||||
else:
|
||||
# [manimgl-3d]
|
||||
# WARNING: 3b1b's manim change how ThreeDScene work,
|
||||
# this is why things have to be managed differently.
|
||||
class ThreeDExample(Slide):
|
||||
CONFIG = {
|
||||
"camera_class": ThreeDCamera,
|
||||
}
|
||||
|
||||
class ThreeDExample(ThreeDSlide):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
circle = Circle(radius=3, color=BLUE)
|
||||
@ -313,30 +279,33 @@ else:
|
||||
frame.set_euler_angles(
|
||||
theta=30 * DEGREES,
|
||||
phi=75 * DEGREES,
|
||||
gamma=0,
|
||||
)
|
||||
|
||||
self.play(GrowFromCenter(circle))
|
||||
updater = lambda m, dt: m.increment_theta((75 * DEGREES / 4) * dt)
|
||||
|
||||
def updater(m, dt):
|
||||
return m.increment_theta((75 * DEGREES / 4) * dt)
|
||||
|
||||
frame.add_updater(updater)
|
||||
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.start_loop()
|
||||
self.next_slide(loop=True)
|
||||
self.play(MoveAlongPath(dot, circle), run_time=4, rate_func=linear)
|
||||
self.end_loop()
|
||||
self.next_slide()
|
||||
|
||||
frame.remove_updater(updater)
|
||||
self.play(frame.animate.set_theta(30 * DEGREES))
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.play(dot.animate.move_to(RIGHT * 3))
|
||||
self.pause()
|
||||
self.next_slide()
|
||||
|
||||
self.start_loop()
|
||||
self.next_slide(loop=True)
|
||||
self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear)
|
||||
self.end_loop()
|
||||
self.next_slide()
|
||||
|
||||
# Each slide MUST end with an animation (a self.wait is considered an animation)
|
||||
self.play(dot.animate.move_to(ORIGIN))
|
||||
|
||||
# [manimgl-3d]
|
||||
|
@ -1,3 +1,61 @@
|
||||
# flake8: noqa: F401
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
from .__version__ import __version__
|
||||
from .slide import Slide, ThreeDSlide
|
||||
|
||||
|
||||
class Module(ModuleType):
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name == "Slide" or name == "ThreeDSlide":
|
||||
module = __import__(
|
||||
"manim_slides.slide", None, None, ["Slide", "ThreeDSlide"]
|
||||
)
|
||||
return getattr(module, name)
|
||||
elif name == "ManimSlidesMagic":
|
||||
module = __import__(
|
||||
"manim_slides.ipython.ipython_magic", None, None, ["ManimSlidesMagic"]
|
||||
)
|
||||
magic = getattr(module, name)
|
||||
|
||||
from IPython import get_ipython
|
||||
|
||||
ipy = get_ipython()
|
||||
|
||||
if ipy is not None:
|
||||
ipy.register_magics(magic)
|
||||
|
||||
return magic
|
||||
|
||||
return ModuleType.__getattribute__(self, name)
|
||||
|
||||
def __dir__(self) -> list[str]:
|
||||
result = list(new_module.__all__)
|
||||
result.extend(
|
||||
(
|
||||
"__file__",
|
||||
"__doc__",
|
||||
"__all__",
|
||||
"__docformat__",
|
||||
"__name__",
|
||||
"__path__",
|
||||
"__package__",
|
||||
"__version__",
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
old_module = sys.modules["manim_slides"]
|
||||
new_module = sys.modules["manim_slides"] = Module("manim_slides")
|
||||
|
||||
new_module.__dict__.update(
|
||||
{
|
||||
"__file__": __file__,
|
||||
"__package__": "manim_slides",
|
||||
"__path__": __path__,
|
||||
"__doc__": __doc__,
|
||||
"__version__": __version__,
|
||||
"__all__": ("__version__", "ManimSlidesMagic", "Slide", "ThreeDSlide"),
|
||||
}
|
||||
)
|
||||
|
75
manim_slides/__main__.py
Normal file
75
manim_slides/__main__.py
Normal file
@ -0,0 +1,75 @@
|
||||
import json
|
||||
|
||||
import click
|
||||
import requests
|
||||
from click_default_group import DefaultGroup
|
||||
|
||||
from .__version__ import __version__
|
||||
from .checkhealth import checkhealth
|
||||
from .convert import convert
|
||||
from .logger import logger
|
||||
from .present import list_scenes, present
|
||||
from .render import render
|
||||
from .wizard import init, wizard
|
||||
|
||||
|
||||
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
||||
@click.option(
|
||||
"--notify-outdated-version/--silent",
|
||||
" /-S",
|
||||
is_flag=True,
|
||||
default=True,
|
||||
help="Check if a new version of Manim Slides is available.",
|
||||
)
|
||||
@click.version_option(__version__, "-v", "--version")
|
||||
@click.help_option("-h", "--help")
|
||||
def cli(notify_outdated_version: bool) -> None:
|
||||
"""
|
||||
Manim Slides command-line utilities.
|
||||
|
||||
If no command is specified, defaults to `present`.
|
||||
"""
|
||||
# Code below is mostly a copy from:
|
||||
# https://github.com/ManimCommunity/manim/blob/main/manim/cli/render/commands.py
|
||||
if notify_outdated_version:
|
||||
manim_info_url = "https://pypi.org/pypi/manim-slides/json"
|
||||
warn_prompt = "Cannot check if latest release of Manim Slides is installed"
|
||||
try:
|
||||
req_info: requests.models.Response = requests.get(manim_info_url, timeout=2)
|
||||
req_info.raise_for_status()
|
||||
stable = req_info.json()["info"]["version"]
|
||||
if stable != __version__:
|
||||
click.echo(
|
||||
"You are using Manim Slides version "
|
||||
+ click.style(f"v{__version__}", fg="red")
|
||||
+ ", but version "
|
||||
+ click.style(f"v{stable}", fg="green")
|
||||
+ " is available."
|
||||
)
|
||||
click.echo(
|
||||
"You should consider upgrading via "
|
||||
+ click.style("pip install -U manim-slides", fg="yellow")
|
||||
)
|
||||
except requests.exceptions.HTTPError:
|
||||
logger.debug(f"HTTP Error: {warn_prompt}")
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.debug(f"Connection Error: {warn_prompt}")
|
||||
except requests.exceptions.Timeout:
|
||||
logger.debug(f"Timed Out: {warn_prompt}")
|
||||
except json.JSONDecodeError:
|
||||
logger.debug(warn_prompt)
|
||||
logger.debug(f"Error decoding JSON from {manim_info_url}")
|
||||
except Exception:
|
||||
logger.debug(f"Something went wrong: {warn_prompt}")
|
||||
|
||||
|
||||
cli.add_command(convert)
|
||||
cli.add_command(checkhealth)
|
||||
cli.add_command(init)
|
||||
cli.add_command(list_scenes)
|
||||
cli.add_command(present)
|
||||
cli.add_command(render)
|
||||
cli.add_command(wizard)
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
@ -1 +1 @@
|
||||
__version__ = "4.6.0"
|
||||
__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,
|
||||
)
|
@ -1,28 +1,33 @@
|
||||
from typing import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
import click
|
||||
from click import Context, Parameter
|
||||
|
||||
from .defaults import CONFIG_PATH, FOLDER_PATH
|
||||
from .manim import logger
|
||||
from .logger import logger
|
||||
|
||||
F = Callable[..., Any]
|
||||
Wrapper = Callable[[F], F]
|
||||
|
||||
|
||||
def config_path_option(function: Callable) -> Callable:
|
||||
"""Wraps a function to add configuration path option."""
|
||||
return click.option(
|
||||
def config_path_option(function: F) -> F:
|
||||
"""Wrap a function to add configuration path option."""
|
||||
wrapper: Wrapper = click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_path",
|
||||
metavar="FILE",
|
||||
default=CONFIG_PATH,
|
||||
type=click.Path(dir_okay=False),
|
||||
type=click.Path(dir_okay=False, path_type=Path),
|
||||
help="Set path to configuration file.",
|
||||
show_default=True,
|
||||
)(function)
|
||||
)
|
||||
return wrapper(function)
|
||||
|
||||
|
||||
def config_options(function: Callable) -> Callable:
|
||||
"""Wraps a function to add configuration options."""
|
||||
def config_options(function: F) -> F:
|
||||
"""Wrap a function to add configuration options."""
|
||||
function = config_path_option(function)
|
||||
function = click.option(
|
||||
"-f", "--force", is_flag=True, help="Overwrite any existing configuration file."
|
||||
@ -36,39 +41,53 @@ def config_options(function: Callable) -> Callable:
|
||||
return function
|
||||
|
||||
|
||||
def verbosity_option(function: Callable) -> Callable:
|
||||
"""Wraps a function to add verbosity option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: bool) -> None:
|
||||
def verbosity_option(function: F) -> F:
|
||||
"""Wrap a function to add verbosity option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: str) -> None:
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
|
||||
logger.setLevel(value)
|
||||
|
||||
return click.option(
|
||||
wrapper: Wrapper = click.option(
|
||||
"-v",
|
||||
"--verbosity",
|
||||
type=click.Choice(
|
||||
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
help="Verbosity of CLI output",
|
||||
help="Verbosity of CLI output.",
|
||||
default=None,
|
||||
expose_value=False,
|
||||
envvar="MANIM_SLIDES_VERBOSITY",
|
||||
show_envvar=True,
|
||||
callback=callback,
|
||||
)(function)
|
||||
)
|
||||
|
||||
return wrapper(function)
|
||||
|
||||
|
||||
def folder_path_option(function: Callable) -> Callable:
|
||||
"""Wraps a function to add folder path option."""
|
||||
return click.option(
|
||||
def folder_path_option(function: F) -> F:
|
||||
"""Wrap a function to add folder path option."""
|
||||
|
||||
def callback(ctx: Context, param: Parameter, value: Path) -> Path:
|
||||
if not value.exists():
|
||||
raise click.UsageError(
|
||||
f"Invalid value for '--folder': Directory '{value}' does not exist. "
|
||||
"Did you render the animations first?",
|
||||
ctx=ctx,
|
||||
)
|
||||
return value
|
||||
|
||||
wrapper: Wrapper = click.option(
|
||||
"--folder",
|
||||
metavar="DIRECTORY",
|
||||
default=FOLDER_PATH,
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
type=click.Path(file_okay=False, path_type=Path),
|
||||
callback=callback,
|
||||
help="Set slides folder.",
|
||||
show_default=True,
|
||||
)(function)
|
||||
)
|
||||
|
||||
return wrapper(function)
|
||||
|
@ -1,44 +1,62 @@
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Set
|
||||
from functools import wraps
|
||||
from inspect import Parameter, signature
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from pydantic import BaseModel, root_validator, validator
|
||||
from PySide6.QtCore import Qt
|
||||
import rtoml
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
FilePath,
|
||||
PositiveInt,
|
||||
PrivateAttr,
|
||||
conset,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic_extra_types.color import Color
|
||||
|
||||
from .manim import FFMPEG_BIN, logger
|
||||
from .logger import logger
|
||||
|
||||
Receiver = Callable[..., Any]
|
||||
|
||||
|
||||
def merge_basenames(files: List[str]) -> str:
|
||||
"""
|
||||
Merge multiple filenames by concatenating basenames.
|
||||
"""
|
||||
class Signal(BaseModel): # type: ignore[misc]
|
||||
__receivers: list[Receiver] = PrivateAttr(default_factory=list)
|
||||
|
||||
dirname = os.path.dirname(files[0])
|
||||
_, ext = os.path.splitext(files[0])
|
||||
def connect(self, receiver: Receiver) -> None:
|
||||
self.__receivers.append(receiver)
|
||||
|
||||
basename = "_".join(os.path.splitext(os.path.basename(file))[0] for file in files)
|
||||
def disconnect(self, receiver: Receiver) -> None:
|
||||
self.__receivers.remove(receiver)
|
||||
|
||||
return os.path.join(dirname, basename + ext)
|
||||
def emit(self, *args: Any) -> None:
|
||||
for receiver in self.__receivers:
|
||||
receiver(*args)
|
||||
|
||||
|
||||
class Key(BaseModel):
|
||||
def key_id(name: str) -> PositiveInt:
|
||||
"""Avoid importing Qt too early."""
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
return getattr(Qt, f"Key_{name}")
|
||||
|
||||
|
||||
class Key(BaseModel): # type: ignore[misc]
|
||||
"""Represents a list of key codes, with optionally a name."""
|
||||
|
||||
ids: Set[int]
|
||||
ids: conset(PositiveInt, min_length=1) # type: ignore[valid-type]
|
||||
name: Optional[str] = None
|
||||
|
||||
__signal: Signal = PrivateAttr(default_factory=Signal)
|
||||
|
||||
def set_ids(self, *ids: int) -> None:
|
||||
self.ids = set(ids)
|
||||
|
||||
@validator("ids", each_item=True)
|
||||
def id_is_posint(cls, v: int) -> int:
|
||||
if v < 0:
|
||||
raise ValueError("Key ids cannot be negative integers")
|
||||
return v
|
||||
|
||||
def match(self, key_id: int) -> bool:
|
||||
m = key_id in self.ids
|
||||
|
||||
@ -47,201 +65,274 @@ class Key(BaseModel):
|
||||
|
||||
return m
|
||||
|
||||
@property
|
||||
def signal(self) -> Signal:
|
||||
return self.__signal
|
||||
|
||||
class Config(BaseModel):
|
||||
"""General Manim Slides config"""
|
||||
def connect(self, function: Receiver) -> None:
|
||||
self.__signal.connect(function)
|
||||
|
||||
QUIT: Key = Key(ids=[Qt.Key_Q], name="QUIT")
|
||||
CONTINUE: Key = Key(ids=[Qt.Key_Right], name="CONTINUE / NEXT")
|
||||
BACK: Key = Key(ids=[Qt.Key_Left], name="BACK")
|
||||
REVERSE: Key = Key(ids=[Qt.Key_V], name="REVERSE")
|
||||
REWIND: Key = Key(ids=[Qt.Key_R], name="REWIND")
|
||||
PLAY_PAUSE: Key = Key(ids=[Qt.Key_Space], name="PLAY / PAUSE")
|
||||
HIDE_MOUSE: Key = Key(ids=[Qt.Key_H], name="HIDE / SHOW MOUSE")
|
||||
@field_serializer("ids")
|
||||
def serialize_dt(self, ids: set[int]) -> list[int]:
|
||||
return list(self.ids)
|
||||
|
||||
@root_validator
|
||||
def ids_are_unique_across_keys(cls, values):
|
||||
ids = set()
|
||||
|
||||
class Keys(BaseModel): # type: ignore[misc]
|
||||
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
|
||||
PLAY_PAUSE: Key = Field(
|
||||
default_factory=lambda: Key(ids=[key_id("Space")], name="PLAY / PAUSE")
|
||||
)
|
||||
NEXT: Key = Field(default_factory=lambda: Key(ids=[key_id("Right")], name="NEXT"))
|
||||
PREVIOUS: Key = Field(
|
||||
default_factory=lambda: Key(ids=[key_id("Left")], name="PREVIOUS")
|
||||
)
|
||||
REVERSE: Key = Field(default_factory=lambda: Key(ids=[key_id("V")], name="REVERSE"))
|
||||
REPLAY: Key = Field(default_factory=lambda: Key(ids=[key_id("R")], name="REPLAY"))
|
||||
FULL_SCREEN: Key = Field(
|
||||
default_factory=lambda: Key(ids=[key_id("F")], name="TOGGLE FULL SCREEN")
|
||||
)
|
||||
HIDE_MOUSE: Key = Field(
|
||||
default_factory=lambda: Key(ids=[key_id("H")], name="HIDE / SHOW MOUSE")
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def ids_are_unique_across_keys(cls, values: dict[str, Key]) -> dict[str, Key]:
|
||||
ids: set[int] = set()
|
||||
|
||||
for key in values.values():
|
||||
if len(ids.intersection(key.ids)) != 0:
|
||||
if len(ids.intersection(key["ids"])) != 0:
|
||||
raise ValueError(
|
||||
"Two or more keys share a common key code: please make sure each key has distinct key codes"
|
||||
)
|
||||
ids.update(key.ids)
|
||||
ids.update(key["ids"])
|
||||
|
||||
return values
|
||||
|
||||
def merge_with(self, other: "Config") -> "Config":
|
||||
def merge_with(self, other: "Keys") -> "Keys":
|
||||
for key_name, key in self:
|
||||
other_key = getattr(other, key_name)
|
||||
key.ids.update(other_key.ids)
|
||||
key.ids = list(set(key.ids).union(other_key.ids))
|
||||
key.name = other_key.name or key.name
|
||||
|
||||
return self
|
||||
|
||||
def dispatch_key_function(self) -> Callable[[PositiveInt], None]:
|
||||
_dispatch = {}
|
||||
|
||||
class SlideType(str, Enum):
|
||||
slide = "slide"
|
||||
loop = "loop"
|
||||
last = "last"
|
||||
for _, key in self:
|
||||
for _id in key.ids:
|
||||
_dispatch[_id] = key.signal
|
||||
|
||||
def dispatch(key: PositiveInt) -> None:
|
||||
if signal := _dispatch.get(key, None):
|
||||
signal.emit()
|
||||
|
||||
return dispatch
|
||||
|
||||
|
||||
class SlideConfig(BaseModel):
|
||||
type: SlideType
|
||||
class Config(BaseModel): # type: ignore[misc]
|
||||
"""General Manim Slides config."""
|
||||
|
||||
keys: Keys = Field(default_factory=Keys)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> "Config":
|
||||
"""Read a configuration from a file."""
|
||||
return cls.model_validate(rtoml.load(path)) # type: ignore
|
||||
|
||||
def to_file(self, path: Path) -> None:
|
||||
"""Dump the configuration to a file."""
|
||||
rtoml.dump(self.model_dump(), path, pretty=True)
|
||||
|
||||
def merge_with(self, other: "Config") -> "Config":
|
||||
"""Merge with another config."""
|
||||
self.keys = self.keys.merge_with(other.keys)
|
||||
return self
|
||||
|
||||
|
||||
class BaseSlideConfig(BaseModel): # type: ignore
|
||||
"""Base class for slide config."""
|
||||
|
||||
loop: bool = False
|
||||
auto_next: bool = False
|
||||
playback_rate: float = 1.0
|
||||
reversed_playback_rate: float = 1.0
|
||||
notes: str = ""
|
||||
dedent_notes: bool = True
|
||||
skip_animations: bool = False
|
||||
src: Optional[FilePath] = None
|
||||
|
||||
@classmethod
|
||||
def wrapper(cls, arg_name: str) -> Callable[..., Any]:
|
||||
"""
|
||||
Wrap a function to transform keyword argument into an instance of this class.
|
||||
|
||||
The function signature is updated to reflect the new keyword-only arguments.
|
||||
|
||||
The wrapped function must follow two criteria:
|
||||
- its last parameter must be ``**kwargs`` (or equivalent);
|
||||
- and its second last parameter must be ``<arg_name>``.
|
||||
"""
|
||||
|
||||
def _wrapper_(fun: Callable[..., Any]) -> Callable[..., Any]:
|
||||
@wraps(fun)
|
||||
def __wrapper__(*args: Any, **kwargs: Any) -> Any: # noqa: N807
|
||||
fun_kwargs = {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if key not in cls.model_fields
|
||||
}
|
||||
fun_kwargs[arg_name] = cls(**kwargs)
|
||||
return fun(*args, **fun_kwargs)
|
||||
|
||||
sig = signature(fun)
|
||||
parameters = list(sig.parameters.values())
|
||||
parameters[-2:-1] = [
|
||||
Parameter(
|
||||
field_name,
|
||||
Parameter.KEYWORD_ONLY,
|
||||
default=field_info.default,
|
||||
annotation=field_info.annotation,
|
||||
)
|
||||
for field_name, field_info in cls.model_fields.items()
|
||||
]
|
||||
|
||||
sig = sig.replace(parameters=parameters)
|
||||
__wrapper__.__signature__ = sig # type: ignore[attr-defined]
|
||||
|
||||
return __wrapper__
|
||||
|
||||
return _wrapper_
|
||||
|
||||
@model_validator(mode="after")
|
||||
def apply_dedent_notes(
|
||||
self,
|
||||
) -> "BaseSlideConfig":
|
||||
if self.dedent_notes:
|
||||
self.notes = dedent(self.notes)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class PreSlideConfig(BaseSlideConfig):
|
||||
"""Slide config to be used prior to rendering."""
|
||||
|
||||
start_animation: int
|
||||
end_animation: int
|
||||
number: int
|
||||
terminated: bool = False
|
||||
|
||||
@validator("start_animation", "end_animation")
|
||||
def index_is_posint(cls, v: int):
|
||||
@classmethod
|
||||
def from_base_slide_config_and_animation_indices(
|
||||
cls,
|
||||
base_slide_config: BaseSlideConfig,
|
||||
start_animation: int,
|
||||
end_animation: int,
|
||||
) -> "PreSlideConfig":
|
||||
return cls(
|
||||
start_animation=start_animation,
|
||||
end_animation=end_animation,
|
||||
**base_slide_config.model_dump(),
|
||||
)
|
||||
|
||||
@field_validator("start_animation", "end_animation")
|
||||
@classmethod
|
||||
def index_is_posint(cls, v: int) -> int:
|
||||
if v < 0:
|
||||
raise ValueError("Animation index (start or end) cannot be negative")
|
||||
return v
|
||||
|
||||
@validator("number")
|
||||
def number_is_strictly_posint(cls, v: int):
|
||||
if v <= 0:
|
||||
raise ValueError("Slide number cannot be negative or zero")
|
||||
return v
|
||||
|
||||
@root_validator
|
||||
def start_animation_is_before_end(cls, values):
|
||||
if values["start_animation"] >= values["end_animation"]:
|
||||
|
||||
if values["start_animation"] == values["end_animation"] == 0:
|
||||
raise ValueError(
|
||||
"You have to play at least one animation (e.g., `self.wait()`) before pausing. If you want to start paused, use the approriate command-line option when presenting. IMPORTANT: when using ManimGL, `self.wait()` is not considered to be an animation, so prefer to directly use `self.play(...)`."
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def start_animation_is_before_end(
|
||||
self,
|
||||
) -> "PreSlideConfig":
|
||||
if self.start_animation > self.end_animation:
|
||||
raise ValueError(
|
||||
"Start animation index must be strictly lower than end animation index"
|
||||
)
|
||||
return self
|
||||
|
||||
return values
|
||||
@model_validator(mode="after")
|
||||
def has_src_or_more_than_zero_animations(
|
||||
self,
|
||||
) -> "PreSlideConfig":
|
||||
if self.src is not None and self.start_animation != self.end_animation:
|
||||
raise ValueError(
|
||||
"A slide cannot have 'src=...' and more than zero animations at the same time."
|
||||
)
|
||||
elif self.src is None and self.start_animation == self.end_animation:
|
||||
raise ValueError(
|
||||
"You have to play at least one animation (e.g., 'self.wait()') "
|
||||
"before pausing. If you want to start paused, use the appropriate "
|
||||
"command-line option when presenting. "
|
||||
"IMPORTANT: when using ManimGL, 'self.wait()' is not considered "
|
||||
"to be an animation, so prefer to directly use 'self.play(...)'."
|
||||
)
|
||||
|
||||
def is_slide(self) -> bool:
|
||||
return self.type == SlideType.slide
|
||||
|
||||
def is_loop(self) -> bool:
|
||||
return self.type == SlideType.loop
|
||||
|
||||
def is_last(self) -> bool:
|
||||
return self.type == SlideType.last
|
||||
return self
|
||||
|
||||
@property
|
||||
def slides_slice(self) -> slice:
|
||||
return slice(self.start_animation, self.end_animation)
|
||||
|
||||
|
||||
class PresentationConfig(BaseModel):
|
||||
slides: List[SlideConfig]
|
||||
files: List[str]
|
||||
class SlideConfig(BaseSlideConfig):
|
||||
"""Slide config to be used after rendering."""
|
||||
|
||||
@validator("files", pre=True, each_item=True)
|
||||
def is_file_and_exists(cls, v):
|
||||
if not os.path.exists(v):
|
||||
raise ValueError(
|
||||
f"Animation file {v} does not exist. Are you in the right directory?"
|
||||
)
|
||||
file: FilePath
|
||||
rev_file: FilePath
|
||||
|
||||
if not os.path.isfile(v):
|
||||
raise ValueError(f"Animation file {v} is not a file")
|
||||
|
||||
return v
|
||||
|
||||
@root_validator
|
||||
def animation_indices_match_files(cls, values):
|
||||
files = values.get("files")
|
||||
slides = values.get("slides")
|
||||
|
||||
if files is None or slides is None:
|
||||
return values
|
||||
|
||||
n_files = len(files)
|
||||
|
||||
for slide in slides:
|
||||
if slide.end_animation > n_files:
|
||||
raise ValueError(
|
||||
f"The following slide's contains animations not listed in files {files}: {slide}"
|
||||
)
|
||||
|
||||
return values
|
||||
|
||||
def move_to(self, dest: str, copy=True) -> "PresentationConfig":
|
||||
"""
|
||||
Moves (or copy) the files to a given directory.
|
||||
"""
|
||||
move = shutil.copy if copy else shutil.move
|
||||
|
||||
n = len(self.files)
|
||||
for i in range(n):
|
||||
file = self.files[i]
|
||||
basename = os.path.basename(file)
|
||||
dest_path = os.path.join(dest, basename)
|
||||
logger.debug(f"Moving / copying {file} to {dest_path}")
|
||||
move(file, dest_path)
|
||||
self.files[i] = dest_path
|
||||
|
||||
return self
|
||||
|
||||
def concat_animations(self, dest: Optional[str] = None) -> "PresentationConfig":
|
||||
"""
|
||||
Concatenate animations such that each slide contains one animation.
|
||||
"""
|
||||
|
||||
dest_paths = []
|
||||
|
||||
for i, slide_config in enumerate(self.slides):
|
||||
files = self.files[slide_config.slides_slice]
|
||||
|
||||
if len(files) > 1:
|
||||
dest_path = merge_basenames(files)
|
||||
|
||||
f = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
||||
f.writelines(f"file {os.path.abspath(path)}\n" for path in files)
|
||||
f.close()
|
||||
|
||||
command = [
|
||||
FFMPEG_BIN,
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
f.name,
|
||||
"-c",
|
||||
"copy",
|
||||
dest_path,
|
||||
"-y",
|
||||
]
|
||||
logger.debug(" ".join(command))
|
||||
process = subprocess.Popen(
|
||||
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
output, error = process.communicate()
|
||||
|
||||
if output:
|
||||
logger.debug(output.decode())
|
||||
|
||||
if error:
|
||||
logger.debug(error.decode())
|
||||
|
||||
dest_paths.append(dest_path)
|
||||
|
||||
else:
|
||||
dest_paths.append(files[0])
|
||||
|
||||
slide_config.start_animation = i
|
||||
slide_config.end_animation = i + 1
|
||||
|
||||
self.files = dest_paths
|
||||
|
||||
if dest:
|
||||
return self.move_to(dest)
|
||||
|
||||
return self
|
||||
@classmethod
|
||||
def from_pre_slide_config_and_files(
|
||||
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
|
||||
) -> "SlideConfig":
|
||||
return cls(file=file, rev_file=rev_file, **pre_slide_config.model_dump())
|
||||
|
||||
|
||||
DEFAULT_CONFIG = Config()
|
||||
class PresentationConfig(BaseModel): # type: ignore[misc]
|
||||
slides: list[SlideConfig] = Field(min_length=1)
|
||||
resolution: tuple[PositiveInt, PositiveInt] = (1920, 1080)
|
||||
background_color: Color = "black"
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> "PresentationConfig":
|
||||
"""Read a presentation configuration from a file."""
|
||||
with open(path) as f:
|
||||
obj = json.load(f)
|
||||
|
||||
slides = obj.setdefault("slides", [])
|
||||
parent = path.parent.parent # Never fails, but parents[1] can fail
|
||||
|
||||
for slide in slides:
|
||||
if file := slide.get("file", None):
|
||||
slide["file"] = parent / file
|
||||
|
||||
if rev_file := slide.get("rev_file", None):
|
||||
slide["rev_file"] = parent / rev_file
|
||||
|
||||
return cls.model_validate(obj) # type: ignore
|
||||
|
||||
def to_file(self, path: Path) -> None:
|
||||
"""Dump the presentation configuration to a file."""
|
||||
with open(path, "w") as f:
|
||||
f.write(self.model_dump_json(indent=2))
|
||||
|
||||
def copy_to(
|
||||
self,
|
||||
folder: Path,
|
||||
use_cached: bool = True,
|
||||
include_reversed: bool = True,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
"""Copy the files to a given directory."""
|
||||
for slide_config in self.slides:
|
||||
file = slide_config.file
|
||||
rev_file = slide_config.rev_file
|
||||
|
||||
dest = folder / f"{prefix}{file.name}"
|
||||
rev_dest = folder / f"{prefix}{rev_file.name}"
|
||||
|
||||
if not use_cached or not dest.exists():
|
||||
shutil.copy(file, dest)
|
||||
|
||||
if include_reversed and (not use_cached or not rev_dest.exists()):
|
||||
shutil.copy(rev_file, rev_dest)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,190 +1,4 @@
|
||||
FOLDER_PATH: str = "./slides"
|
||||
CONFIG_PATH: str = ".manim-slides.json"
|
||||
from pathlib import Path
|
||||
|
||||
REVEALJS_TEMPLATE: str = """
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
|
||||
<title>{title}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/css/reveal.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/css/theme/{reveal_theme}.css">
|
||||
|
||||
<!-- Theme used for syntax highlighting of code -->
|
||||
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
|
||||
|
||||
<!-- Printing and PDF exports -->
|
||||
<script>
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = window.location.search.match(/print-pdf/gi) ? 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/css/print/pdf.css' : 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/css/print/paper.css';
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
</script>
|
||||
<!-- <link rel="stylesheet" href="index.css"> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="reveal">
|
||||
<div class="slides">
|
||||
{sections}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--<script src="lib/js/head.min.js"></script>-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/headjs@1.0.3/dist/1.0.0/head.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/js/reveal.min.js"></script>
|
||||
|
||||
<!-- <script src="index.js"></script> -->
|
||||
<script>
|
||||
// More info about config & dependencies:
|
||||
// - https://github.com/hakimel/reveal.js#configuration
|
||||
// - https://github.com/hakimel/reveal.js#dependencies
|
||||
Reveal.initialize({{
|
||||
// Display controls in the bottom right corner
|
||||
controls: {controls},
|
||||
|
||||
width: '{width}',
|
||||
height: '{height}',
|
||||
|
||||
// Display a presentation progress bar
|
||||
progress: {progress},
|
||||
|
||||
// Set default timing of 2 minutes per slide
|
||||
defaultTiming: 120,
|
||||
|
||||
// Display the page number of the current slide
|
||||
slideNumber: true,
|
||||
|
||||
// Push each slide change to the browser history
|
||||
history: false,
|
||||
|
||||
// Enable keyboard shortcuts for navigation
|
||||
keyboard: true,
|
||||
|
||||
// Enable the slide overview mode
|
||||
overview: true,
|
||||
|
||||
// Vertical centering of slides
|
||||
center: true,
|
||||
|
||||
// Enables touch navigation on devices with touch input
|
||||
touch: true,
|
||||
|
||||
// Loop the presentation
|
||||
loop: {loop},
|
||||
|
||||
// Change the presentation direction to be RTL
|
||||
rtl: false,
|
||||
|
||||
// Randomizes the order of slides each time the presentation loads
|
||||
shuffle: {shuffle},
|
||||
|
||||
// Turns fragments on and off globally
|
||||
fragments: {fragments},
|
||||
|
||||
// Flags if the presentation is running in an embedded mode,
|
||||
// i.e. contained within a limited portion of the screen
|
||||
embedded: {embedded},
|
||||
|
||||
// Flags if we should show a help overlay when the questionmark
|
||||
// key is pressed
|
||||
help: true,
|
||||
|
||||
// Flags if speaker notes should be visible to all viewers
|
||||
showNotes: false,
|
||||
|
||||
// Global override for autolaying embedded media (video/audio/iframe)
|
||||
// - null: Media will only autoplay if data-autoplay is present
|
||||
// - true: All media will autoplay, regardless of individual setting
|
||||
// - false: No media will autoplay, regardless of individual setting
|
||||
autoPlayMedia: null,
|
||||
|
||||
// Number of milliseconds between automatically proceeding to the
|
||||
// next slide, disabled when set to 0, this value can be overwritten
|
||||
// by using a data-autoslide attribute on your slides
|
||||
autoSlide: 0,
|
||||
|
||||
// Stop auto-sliding after user input
|
||||
autoSlideStoppable: true,
|
||||
|
||||
// Use this method for navigation when auto-sliding
|
||||
autoSlideMethod: Reveal.navigateNext,
|
||||
|
||||
// Enable slide navigation via mouse wheel
|
||||
mouseWheel: false,
|
||||
|
||||
// Hides the address bar on mobile devices
|
||||
hideAddressBar: true,
|
||||
|
||||
// Opens links in an iframe preview overlay
|
||||
previewLinks: true,
|
||||
|
||||
// Transition style
|
||||
transition: 'none', // none/fade/slide/convex/concave/zoom
|
||||
|
||||
// Transition speed
|
||||
transitionSpeed: 'default', // default/fast/slow
|
||||
|
||||
// Transition style for full page slide backgrounds
|
||||
backgroundTransition: 'none', // none/fade/slide/convex/concave/zoom
|
||||
|
||||
// Number of slides away from the current that are visible
|
||||
viewDistance: 3,
|
||||
|
||||
// Parallax background image
|
||||
parallaxBackgroundImage: '', // e.g. "'https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg'"
|
||||
|
||||
// Parallax background size
|
||||
parallaxBackgroundSize: '', // CSS syntax, e.g. "2100px 900px"
|
||||
|
||||
// Number of pixels to move the parallax background per slide
|
||||
// - Calculated automatically unless specified
|
||||
// - Set to 0 to disable movement along an axis
|
||||
parallaxBackgroundHorizontal: null,
|
||||
parallaxBackgroundVertical: null,
|
||||
|
||||
|
||||
// The display mode that will be used to show slides
|
||||
display: 'block',
|
||||
|
||||
/*
|
||||
multiplex: {{
|
||||
// Example values. To generate your own, see the socket.io server instructions.
|
||||
secret: '13652805320794272084', // Obtained from the socket.io server. Gives this (the master) control of the presentation
|
||||
id: '1ea875674b17ca76', // Obtained from socket.io server
|
||||
url: 'https://reveal-js-multiplex-ccjbegmaii.now.sh' // Location of socket.io server
|
||||
}},
|
||||
*/
|
||||
|
||||
dependencies: [
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/markdown/marked.js' }},
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/markdown/markdown.js' }},
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/notes/notes.js', async: true }},
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/highlight/highlight.js', async: true, callback: function () {{ hljs.initHighlightingOnLoad(); }} }},
|
||||
//{{ src: '//cdn.socket.io/socket.io-1.3.5.js', async: true }},
|
||||
//{{ src: 'plugin/multiplex/master.js', async: true }},
|
||||
// and if you want speaker notes
|
||||
{{ src: 'https://cdn.jsdelivr.net/npm/reveal.js@{reveal_version}/plugin/notes-server/client.js', async: true }}
|
||||
|
||||
],
|
||||
markdown: {{
|
||||
// renderer: myrenderer,
|
||||
smartypants: true
|
||||
}}
|
||||
}});
|
||||
Reveal.configure({{
|
||||
// PDF Configurations
|
||||
pdfMaxPagesPerSlide: 1
|
||||
|
||||
}});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
"""
|
||||
FOLDER_PATH: Path = Path("./slides")
|
||||
CONFIG_PATH: Path = Path(".manim-slides.toml")
|
||||
|
0
manim_slides/docs/__init__.py
Normal file
0
manim_slides/docs/__init__.py
Normal file
548
manim_slides/docs/manim_slides_directive.py
Normal file
548
manim_slides/docs/manim_slides_directive.py
Normal file
@ -0,0 +1,548 @@
|
||||
# type: ignore
|
||||
r"""
|
||||
A directive for including Manim Slides in a Sphinx document
|
||||
===========================================================
|
||||
|
||||
.. warning::
|
||||
|
||||
This Sphinx extension requires Manim to be installed,
|
||||
and won't probably work on ManimGL examples.
|
||||
|
||||
.. note::
|
||||
|
||||
The current implementation is highly inspired from Manim's own
|
||||
sphinx directive, from v0.17.3.
|
||||
|
||||
When rendering the HTML documentation, the ``.. manim-slides::``
|
||||
directive implemented here allows to include rendered videos.
|
||||
|
||||
This directive requires three additional dependencies:
|
||||
``manim``, ``docutils`` and ``jinja2``. The last two are usually bundled
|
||||
with Sphinx.
|
||||
You can install them manually, or with the extra keyword:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install "manim-slides[sphinx-directive]"
|
||||
|
||||
Note that you will still need to install Manim's platform-specific dependencies,
|
||||
see
|
||||
`their installation page <https://docs.manim.community/en/stable/installation.html>`_.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
First, you must include the directive in the Sphinx configuration file:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: Sphinx configuration file (usually :code:`docs/source/conf.py`).
|
||||
:emphasize-lines: 3
|
||||
|
||||
extensions = [
|
||||
# ...
|
||||
"manim_slides.docs.manim_slides_directive",
|
||||
]
|
||||
|
||||
Its basic usage that allows processing **inline content**
|
||||
looks as follows::
|
||||
|
||||
.. manim-slides:: MySlide
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class MySlide(Slide):
|
||||
def construct(self):
|
||||
...
|
||||
|
||||
It is required to pass the name of the class representing the
|
||||
scene to be rendered to the directive.
|
||||
|
||||
As a second application, the directive can also be used to
|
||||
render scenes that are defined within doctests, for example::
|
||||
|
||||
.. manim-slides:: DirectiveDoctestExample
|
||||
:ref_classes: Dot
|
||||
|
||||
>>> from manim import Create, Dot, RED
|
||||
>>> from manim_slides import Slide
|
||||
>>> dot = Dot(color=RED)
|
||||
>>> dot.color
|
||||
<Color #fc6255>
|
||||
>>> class DirectiveDoctestExample(Slide):
|
||||
... def construct(self):
|
||||
... self.play(Create(dot))
|
||||
...
|
||||
|
||||
A third application is to render scenes from another specific file::
|
||||
|
||||
.. manim-slides:: file.py:FileExample
|
||||
:hide_source:
|
||||
:quality: high
|
||||
|
||||
.. warning::
|
||||
|
||||
The code will be executed with the current working directory
|
||||
being the same as the one containing the source file. This being said,
|
||||
you should probably not include examples that rely on external files, since
|
||||
relative paths risk to be broken.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to skip rendering the slides (e.g., for testing)
|
||||
you can either set the ``SKIP_MANIM_SLIDES`` environ
|
||||
variable (to any value) or pass the ``skip-manim-slides``
|
||||
tag to ``sphinx``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sphinx-build -t skip-manim-slides <OTHER_SPHINX_OPTIONS>
|
||||
# or if you use a Makefile
|
||||
make html O=-tskip-manim-slides
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
Options can be passed as follows::
|
||||
|
||||
.. manim-slides:: <file>:<Class name>
|
||||
:<option name>: <value>
|
||||
|
||||
The following configuration options are supported by the
|
||||
directive:
|
||||
|
||||
hide_source
|
||||
If this flag is present without argument,
|
||||
the source code is not displayed above the rendered video.
|
||||
|
||||
quality : {'low', 'medium', 'high', 'fourk'}
|
||||
Controls render quality of the video, in analogy to
|
||||
the corresponding command line flags.
|
||||
|
||||
ref_classes
|
||||
A list of classes, separated by spaces, that is
|
||||
rendered in a reference block after the source code.
|
||||
|
||||
ref_functions
|
||||
A list of functions, separated by spaces,
|
||||
that is rendered in a reference block after the source code.
|
||||
|
||||
ref_methods
|
||||
A list of methods, separated by spaces,
|
||||
that is rendered in a reference block after the source code.
|
||||
|
||||
template
|
||||
A path to the template file to use.
|
||||
|
||||
config_options
|
||||
An unprocessed string of options to pass to ``manim-slides convert``.
|
||||
Options must be separated with a space, and each option must be
|
||||
a key, value pair using an equal sign as a separator.
|
||||
|
||||
Unlike for the CLI version, you don't need to prepend each option with
|
||||
``-c``.
|
||||
|
||||
E.g., pass ``slide_number=true controls=false``.
|
||||
|
||||
By default, ``controls=true`` is set.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following code::
|
||||
|
||||
.. manim-slides:: MySlide
|
||||
:hide_source:
|
||||
:config_options: slide_number=true controls=false
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class MySlide(Slide):
|
||||
def construct(self):
|
||||
text = Text("Hello")
|
||||
self.wipe([], text)
|
||||
|
||||
self.next_slide()
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
self.next_slide()
|
||||
self.zoom(text)
|
||||
|
||||
Renders as follows:
|
||||
|
||||
.. manim-slides:: MySlide
|
||||
:hide_source:
|
||||
:config_options: slide_number=true controls=false
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class MySlide(Slide):
|
||||
def construct(self):
|
||||
text = Text("Hello")
|
||||
self.wipe([], text)
|
||||
|
||||
self.next_slide()
|
||||
self.play(text.animate.scale(2))
|
||||
|
||||
self.next_slide()
|
||||
self.zoom(text)
|
||||
|
||||
|
||||
""" # noqa: D400, D415
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import itertools as it
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from timeit import timeit
|
||||
|
||||
import jinja2
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst import Directive, directives
|
||||
from docutils.statemachine import StringList
|
||||
from manim import QUALITIES
|
||||
|
||||
from ..convert import RevealJS
|
||||
from ..present import get_scenes_presentation_config
|
||||
|
||||
classnamedict = {}
|
||||
|
||||
|
||||
class SkipManimNode(nodes.Admonition, nodes.Element):
|
||||
"""
|
||||
Auxiliary node class that is used when the ``skip-manim-slides`` tag is present or
|
||||
``.pot`` files are being built.
|
||||
|
||||
Skips rendering the manim-slides directive and outputs a placeholder instead.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def visit(self, node, name=""):
|
||||
self.visit_admonition(node, name)
|
||||
if not isinstance(node[0], nodes.title):
|
||||
node.insert(0, nodes.title("skip-manim-slides", "Example Placeholder"))
|
||||
|
||||
|
||||
def depart(self, node):
|
||||
self.depart_admonition(node)
|
||||
|
||||
|
||||
def process_name_list(option_input: str, reference_type: str) -> list[str]:
|
||||
r"""
|
||||
Reformats a string of space separated class names as a list of strings containing
|
||||
valid Sphinx references.
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
::
|
||||
|
||||
>>> process_name_list("Tex TexTemplate", "class")
|
||||
[':class:`~.Tex`', ':class:`~.TexTemplate`']
|
||||
>>> process_name_list("Scene.play Mobject.rotate", "func")
|
||||
[':func:`~.Scene.play`', ':func:`~.Mobject.rotate`']
|
||||
"""
|
||||
return [f":{reference_type}:`~.{name}`" for name in option_input.split()]
|
||||
|
||||
|
||||
class ManimSlidesDirective(Directive):
|
||||
r"""
|
||||
The manim-slides directive, rendering videos while building the documentation.
|
||||
|
||||
See the module docstring for documentation.
|
||||
"""
|
||||
|
||||
has_content = True
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
option_spec = { # noqa: RUF012
|
||||
"hide_source": bool,
|
||||
"quality": lambda arg: directives.choice(
|
||||
arg,
|
||||
("low", "medium", "high", "fourk"),
|
||||
),
|
||||
"ref_modules": lambda arg: process_name_list(arg, "mod"),
|
||||
"ref_classes": lambda arg: process_name_list(arg, "class"),
|
||||
"ref_functions": lambda arg: process_name_list(arg, "func"),
|
||||
"ref_methods": lambda arg: process_name_list(arg, "meth"),
|
||||
"template": lambda arg: Path(arg),
|
||||
"config_options": lambda arg: dict(
|
||||
option.split("=") for option in shlex.split(arg)
|
||||
),
|
||||
}
|
||||
final_argument_whitespace = True
|
||||
|
||||
def run(self): # noqa: C901
|
||||
# Rendering is skipped if the tag skip-manim is present,
|
||||
# or if we are making the pot-files
|
||||
should_skip = (
|
||||
self.state.document.settings.env.app.builder.tags.has("skip-manim-slides")
|
||||
or self.state.document.settings.env.app.builder.name == "gettext"
|
||||
or "SKIP_MANIM_SLIDES" in os.environ
|
||||
)
|
||||
if should_skip:
|
||||
node = SkipManimNode()
|
||||
self.state.nested_parse(
|
||||
StringList(
|
||||
[
|
||||
f"Placeholder block for ``{self.arguments[0]}``.",
|
||||
"",
|
||||
".. code-block:: python",
|
||||
"",
|
||||
]
|
||||
+ [" " + line for line in self.content]
|
||||
),
|
||||
self.content_offset,
|
||||
node,
|
||||
)
|
||||
return [node]
|
||||
|
||||
from manim import config, tempconfig
|
||||
|
||||
global classnamedict
|
||||
|
||||
def split_file_cls(arg: str) -> tuple[Path, str]:
|
||||
if ":" in arg:
|
||||
file, cls = arg.split(":", maxsplit=1)
|
||||
_, file = self.state.document.settings.env.relfn2path(file)
|
||||
return Path(file), cls
|
||||
else:
|
||||
return None, arg
|
||||
|
||||
arguments = [split_file_cls(arg) for arg in self.arguments]
|
||||
|
||||
clsname = arguments[0][1]
|
||||
if clsname not in classnamedict:
|
||||
classnamedict[clsname] = 1
|
||||
else:
|
||||
classnamedict[clsname] += 1
|
||||
|
||||
hide_source = "hide_source" in self.options
|
||||
|
||||
ref_content = (
|
||||
self.options.get("ref_modules", [])
|
||||
+ self.options.get("ref_classes", [])
|
||||
+ self.options.get("ref_functions", [])
|
||||
+ self.options.get("ref_methods", [])
|
||||
)
|
||||
if ref_content:
|
||||
ref_block = "References: " + " ".join(ref_content)
|
||||
|
||||
else:
|
||||
ref_block = ""
|
||||
|
||||
if "quality" in self.options:
|
||||
quality = f"{self.options['quality']}_quality"
|
||||
else:
|
||||
quality = "example_quality"
|
||||
frame_rate = QUALITIES[quality]["frame_rate"]
|
||||
pixel_height = QUALITIES[quality]["pixel_height"]
|
||||
pixel_width = QUALITIES[quality]["pixel_width"]
|
||||
|
||||
state_machine = self.state_machine
|
||||
document = state_machine.document
|
||||
|
||||
source_file_name = Path(document.attributes["source"])
|
||||
source_rel_name = source_file_name.relative_to(setup.confdir)
|
||||
source_rel_dir = source_rel_name.parents[0]
|
||||
dest_dir = Path(setup.app.builder.outdir, source_rel_dir).absolute()
|
||||
if not dest_dir.exists():
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
source_block = [
|
||||
".. code-block:: python",
|
||||
"",
|
||||
*(" " + line for line in self.content),
|
||||
]
|
||||
source_block = "\n".join(source_block)
|
||||
|
||||
config.media_dir = (Path(setup.confdir) / "media").absolute()
|
||||
config.images_dir = "{media_dir}/images"
|
||||
config.video_dir = "{media_dir}/videos/{quality}"
|
||||
output_file = f"{clsname}-{classnamedict[clsname]}"
|
||||
config.assets_dir = Path("_static")
|
||||
config.progress_bar = "none"
|
||||
config.verbosity = "WARNING"
|
||||
|
||||
example_config = {
|
||||
"frame_rate": frame_rate,
|
||||
"pixel_height": pixel_height,
|
||||
"pixel_width": pixel_width,
|
||||
"output_file": output_file,
|
||||
}
|
||||
|
||||
if file := arguments[0][0]:
|
||||
user_code = file.absolute().read_text().splitlines()
|
||||
else:
|
||||
user_code = self.content
|
||||
|
||||
if user_code[0].startswith(">>> "): # check whether block comes from doctest
|
||||
user_code = [
|
||||
line[4:] for line in user_code if line.startswith((">>> ", "... "))
|
||||
]
|
||||
|
||||
code = [
|
||||
*user_code,
|
||||
f"{clsname}().render()",
|
||||
]
|
||||
|
||||
try:
|
||||
with tempconfig(example_config):
|
||||
print(f"Rendering {clsname}...") # noqa: T201
|
||||
run_time = timeit(lambda: exec("\n".join(code), globals()), number=1)
|
||||
video_dir = config.get_dir("video_dir")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error while rendering example {clsname}") from e
|
||||
|
||||
_write_rendering_stats(
|
||||
clsname,
|
||||
run_time,
|
||||
self.state.document.settings.env.docname,
|
||||
)
|
||||
|
||||
# copy video file to output directory
|
||||
filename = f"{output_file}.html"
|
||||
filesrc = video_dir / filename
|
||||
destfile = Path(dest_dir, filename)
|
||||
presentation_configs = get_scenes_presentation_config(
|
||||
[clsname], Path("./slides")
|
||||
)
|
||||
|
||||
template = self.options.get("template", None)
|
||||
|
||||
if template:
|
||||
template = source_file_name.parents[0].joinpath(template)
|
||||
|
||||
config_options = self.options.get("config_options", {})
|
||||
config_options.setdefault("controls", "true")
|
||||
|
||||
RevealJS(
|
||||
presentation_configs=presentation_configs,
|
||||
template=template,
|
||||
**config_options,
|
||||
).convert_to(destfile)
|
||||
|
||||
rendered_template = jinja2.Template(TEMPLATE).render(
|
||||
clsname=clsname,
|
||||
clsname_lowercase=clsname.lower(),
|
||||
hide_source=hide_source,
|
||||
filesrc_rel=Path(filesrc).relative_to(setup.confdir).as_posix(),
|
||||
output_file=output_file,
|
||||
source_block=source_block,
|
||||
ref_block=ref_block,
|
||||
)
|
||||
state_machine.insert_input(
|
||||
rendered_template.split("\n"),
|
||||
source=document.attributes["source"],
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
rendering_times_file_path = Path("../rendering_times.csv")
|
||||
|
||||
|
||||
def _write_rendering_stats(scene_name, run_time, file_name):
|
||||
with rendering_times_file_path.open("a") as file:
|
||||
csv.writer(file).writerow(
|
||||
[
|
||||
re.sub(r"^(reference\/)|(manim\.)", "", file_name),
|
||||
scene_name,
|
||||
f"{run_time:.3f}",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _log_rendering_times(*args):
|
||||
if rendering_times_file_path.exists():
|
||||
with rendering_times_file_path.open() as file:
|
||||
data = list(csv.reader(file))
|
||||
if len(data) == 0:
|
||||
sys.exit()
|
||||
|
||||
print("\nRendering Summary\n-----------------\n") # noqa: T201
|
||||
|
||||
max_file_length = max(len(row[0]) for row in data)
|
||||
for key, group in it.groupby(data, key=lambda row: row[0]):
|
||||
key = key.ljust(max_file_length + 1, ".")
|
||||
group = list(group)
|
||||
if len(group) == 1:
|
||||
row = group[0]
|
||||
print(f"{key}{row[2].rjust(7, '.')}s {row[1]}") # noqa: T201
|
||||
continue
|
||||
time_sum = sum(float(row[2]) for row in group)
|
||||
print( # noqa: T201
|
||||
f"{key}{f'{time_sum:.3f}'.rjust(7, '.')}s => {len(group)} EXAMPLES",
|
||||
)
|
||||
for row in group:
|
||||
print( # noqa: T201
|
||||
f"{' ' * (max_file_length)} {row[2].rjust(7)}s {row[1]}"
|
||||
)
|
||||
print("") # noqa: T201
|
||||
|
||||
|
||||
def _delete_rendering_times(*args):
|
||||
if rendering_times_file_path.exists():
|
||||
rendering_times_file_path.unlink()
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_node(SkipManimNode, html=(visit, depart))
|
||||
|
||||
setup.app = app
|
||||
setup.config = app.config
|
||||
setup.confdir = app.confdir
|
||||
|
||||
app.add_directive("manim-slides", ManimSlidesDirective)
|
||||
|
||||
app.connect("builder-inited", _delete_rendering_times)
|
||||
app.connect("build-finished", _log_rendering_times)
|
||||
|
||||
metadata = {"parallel_read_safe": False, "parallel_write_safe": True}
|
||||
return metadata
|
||||
|
||||
|
||||
TEMPLATE = r"""
|
||||
{% if not hide_source %}
|
||||
.. raw:: html
|
||||
|
||||
<div id="{{ clsname_lowercase }}" class="admonition admonition-manim-example">
|
||||
<p class="admonition-title">Example: {{ clsname }} <a class="headerlink" href="#{{ clsname_lowercase }}">¶</a></p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<!-- From: https://faq.dailymotion.com/hc/en-us/articles/360022841393-How-to-preserve-the-player-aspect-ratio-on-a-responsive-page -->
|
||||
|
||||
<div style="position:relative;padding-bottom:56.25%;">
|
||||
<iframe
|
||||
style="width:100%;height:100%;position:absolute;left:0px;top:0px;"
|
||||
frameborder="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
allowfullscreen
|
||||
allow="autoplay"
|
||||
src="./{{ output_file }}.html">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
{% if not hide_source %}
|
||||
{{ source_block }}
|
||||
|
||||
{{ ref_block }}
|
||||
|
||||
.. raw:: html
|
||||
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
"""
|
278
manim_slides/ipython/ipython_magic.py
Normal file
278
manim_slides/ipython/ipython_magic.py
Normal file
@ -0,0 +1,278 @@
|
||||
"""
|
||||
Utilities for using Manim Slides with IPython (in particular: Jupyter notebooks).
|
||||
=================================================================================
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
magic_example
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The current implementation is highly inspired from Manim's own
|
||||
IPython magics, from v0.17.3.
|
||||
|
||||
This magic requires two additional dependencies: ``manim`` and ``IPython``.
|
||||
You can install them manually, or with the extra keyword:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install "manim-slides[magic]"
|
||||
|
||||
Note that you will still need to install Manim's platform-specific dependencies,
|
||||
see
|
||||
`their installation page <https://docs.manim.community/en/stable/installation.html>`_.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from IPython import get_ipython
|
||||
from IPython.core.interactiveshell import InteractiveShell
|
||||
from IPython.core.magic import Magics, line_cell_magic, magics_class, needs_local_scope
|
||||
from IPython.display import HTML, display
|
||||
from manim import config, logger, tempconfig
|
||||
from manim.__main__ import main
|
||||
from manim.constants import RendererType
|
||||
from manim.renderer.shader import shader_program_cache
|
||||
|
||||
from ..convert import RevealJS
|
||||
from ..present import get_scenes_presentation_config
|
||||
|
||||
|
||||
@magics_class
|
||||
class ManimSlidesMagic(Magics): # type: ignore
|
||||
def __init__(self, shell: InteractiveShell) -> None:
|
||||
super().__init__(shell)
|
||||
self.rendered_files: dict[Path, Path] = {}
|
||||
|
||||
@needs_local_scope
|
||||
@line_cell_magic
|
||||
def manim_slides( # noqa: C901
|
||||
self,
|
||||
line: str,
|
||||
cell: str | None = None,
|
||||
local_ns: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
r"""
|
||||
Render Manim Slides contained in IPython cells. Works as a line or cell magic.
|
||||
|
||||
.. note::
|
||||
|
||||
This magic works pretty much like the one from Manim, except that it
|
||||
will render Manim Slides using RevealJS. For passing arguments to
|
||||
Manim Slides' convert module, use ``-manim-slides key=value``.
|
||||
|
||||
Everything that is after ``--manim-slides`` will be send to
|
||||
Manim Slides' command. E.g., use ``--manim-slides controls=true``
|
||||
to display control buttons.
|
||||
|
||||
.. hint::
|
||||
|
||||
This line and cell magic works best when used in a JupyterLab
|
||||
environment: while all of the functionality is available for
|
||||
classic Jupyter notebooks as well, it is possible that videos
|
||||
sometimes don't update on repeated execution of the same cell
|
||||
if the scene name stays the same.
|
||||
|
||||
This problem does not occur when using JupyterLab.
|
||||
|
||||
Please refer to `<https://jupyter.org/>`_ for more information about JupyterLab
|
||||
and Jupyter notebooks.
|
||||
|
||||
Usage in line mode::
|
||||
|
||||
%manim_slides [CLI options] MyAwesomeSlide
|
||||
|
||||
Usage in cell mode::
|
||||
|
||||
%%manim_slides [CLI options] MyAwesomeSlide
|
||||
|
||||
class MyAweseomeSlide(Slide):
|
||||
def construct(self):
|
||||
...
|
||||
|
||||
Run ``%manim_slides --help`` and ``%manim_slides render --help``
|
||||
for possible command line interface options.
|
||||
|
||||
.. note::
|
||||
|
||||
The maximal width of the rendered videos that are displayed in the notebook can be
|
||||
configured via the ``media_width`` configuration option. The default is set to ``25vw``,
|
||||
which is 25% of your current viewport width. To allow the output to become as large
|
||||
as possible, set ``config.media_width = "100%"``.
|
||||
|
||||
The ``media_embed`` option will embed the image/video output in the notebook. This is
|
||||
generally undesirable as it makes the notebooks very large, but is required on some
|
||||
platforms (notably Google's CoLab, where it is automatically enabled unless suppressed
|
||||
by ``config.embed = False``) and needed in cases when the notebook (or converted HTML
|
||||
file) will be moved relative to the video locations. Use-cases include building
|
||||
documentation with Sphinx and JupyterBook. See also the
|
||||
:mod:`Manim Slides directive for Sphinx
|
||||
<manim_slides.docs.manim_slides_directive>`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
First make sure to put ``from manim_slides import ManimSlidesMagic``,
|
||||
or even ``from manim_slides import *``
|
||||
in a cell and evaluate it. Then, a typical Jupyter notebook cell for Manim Slides
|
||||
could look as follows::
|
||||
|
||||
%%manim_slides -v WARNING --progress_bar None MySlide --manim-slides controls=true one_file=true
|
||||
|
||||
class MySlide(Slide):
|
||||
def construct(self):
|
||||
square = Square()
|
||||
circle = Circle()
|
||||
|
||||
self.play(Create(square))
|
||||
self.next_slide()
|
||||
self.play(Transform(square, circle))
|
||||
|
||||
Evaluating this cell will render and display the ``MySlide`` slide
|
||||
defined in the body of the cell.
|
||||
|
||||
.. note::
|
||||
|
||||
In case you want to hide the red box containing the output progress bar, the ``progress_bar`` config
|
||||
option should be set to ``None``. This can also be done by passing ``--progress_bar None`` as a
|
||||
CLI flag.
|
||||
|
||||
"""
|
||||
if local_ns is None:
|
||||
local_ns = {}
|
||||
if cell:
|
||||
exec(cell, local_ns)
|
||||
|
||||
split_args = line.split("--manim-slides", 2)
|
||||
manim_args = split_args[0].split()
|
||||
|
||||
if len(split_args) == 2:
|
||||
manim_slides_args = split_args[1].split()
|
||||
else:
|
||||
manim_slides_args = []
|
||||
|
||||
args = manim_args
|
||||
if not len(args) or "-h" in args or "--help" in args or "--version" in args:
|
||||
main(args, standalone_mode=False, prog_name="manim")
|
||||
return
|
||||
|
||||
modified_args = self.add_additional_args(args)
|
||||
args = main(modified_args, standalone_mode=False, prog_name="manim")
|
||||
with tempconfig(local_ns.get("config", {})):
|
||||
config.digest_args(args)
|
||||
logging.getLogger("manim-slides").setLevel(logging.getLogger("manim").level)
|
||||
|
||||
renderer = None
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
renderer = OpenGLRenderer()
|
||||
|
||||
try:
|
||||
scene_cls = local_ns[config["scene_names"][0]]
|
||||
scene = scene_cls(renderer=renderer)
|
||||
scene.render()
|
||||
finally:
|
||||
# Shader cache becomes invalid as the context is destroyed
|
||||
shader_program_cache.clear()
|
||||
|
||||
# Close OpenGL window here instead of waiting for the main thread to
|
||||
# finish causing the window to stay open and freeze
|
||||
if renderer is not None and renderer.window is not None:
|
||||
renderer.window.close()
|
||||
|
||||
if config["output_file"] is None:
|
||||
logger.info("No output file produced")
|
||||
return
|
||||
|
||||
local_path = Path(config["output_file"]).relative_to(Path.cwd())
|
||||
tmpfile = (
|
||||
Path(config["media_dir"]) / "jupyter" / f"{_generate_file_name()}.html"
|
||||
)
|
||||
|
||||
if local_path in self.rendered_files:
|
||||
self.rendered_files[local_path].unlink()
|
||||
pass
|
||||
self.rendered_files[local_path] = tmpfile
|
||||
tmpfile.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(local_path, tmpfile)
|
||||
|
||||
file_type = mimetypes.guess_type(config["output_file"])[0] or "video/mp4"
|
||||
embed = config["media_embed"]
|
||||
if embed is None:
|
||||
# videos need to be embedded when running in google colab.
|
||||
# do this automatically in case config.media_embed has not been
|
||||
# set explicitly.
|
||||
embed = "google.colab" in str(get_ipython())
|
||||
|
||||
if not file_type.startswith("video"):
|
||||
raise ValueError(
|
||||
f"Manim Slides only supports video files, not {file_type}"
|
||||
)
|
||||
|
||||
clsname = config["scene_names"][0]
|
||||
|
||||
kwargs = dict(arg.split("=", 1) for arg in manim_slides_args)
|
||||
|
||||
# If data_uri is set, raise a warning
|
||||
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
|
||||
# Seems like files are blocked so one_file is the only working option...
|
||||
if kwargs.get("one_file", "false").lower().strip() == "false":
|
||||
logger.warning(
|
||||
"one_file option is currently automatically enabled, "
|
||||
"because using local video files does not seem to work properly."
|
||||
)
|
||||
kwargs["one_file"] = "true"
|
||||
|
||||
presentation_configs = get_scenes_presentation_config(
|
||||
[clsname], Path("./slides")
|
||||
)
|
||||
RevealJS(presentation_configs=presentation_configs, **kwargs).convert_to(
|
||||
tmpfile
|
||||
)
|
||||
|
||||
if embed:
|
||||
result = HTML(
|
||||
"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" srcdoc="{srcdoc}"></iframe></div>""".format(
|
||||
srcdoc=tmpfile.read_text().replace('"', "'")
|
||||
)
|
||||
)
|
||||
else:
|
||||
result = HTML(
|
||||
f"""<div style="position:relative;padding-bottom:56.25%;"><iframe style="width:100%;height:100%;position:absolute;left:0px;top:0px;" frameborder="0" width="100%" height="100%" allowfullscreen allow="autoplay" src="{tmpfile.as_posix()}"></iframe></div>"""
|
||||
)
|
||||
|
||||
display(result)
|
||||
|
||||
def add_additional_args(self, args: list[str]) -> list[str]:
|
||||
additional_args = ["--jupyter"]
|
||||
# Use webm to support transparency
|
||||
if "-t" in args and "--format" not in args:
|
||||
additional_args += ["--format", "webm"]
|
||||
return additional_args + args[:-1] + [""] + [args[-1]]
|
||||
|
||||
|
||||
def _generate_file_name() -> str:
|
||||
return config["scene_names"][0] + "@" + datetime.now().strftime("%Y-%m-%d@%H-%M-%S") # type: ignore
|
52
manim_slides/logger.py
Normal file
52
manim_slides/logger.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""
|
||||
Logger utils, mostly copied from Manim Community.
|
||||
|
||||
Source code:
|
||||
https://github.com/ManimCommunity/manim/blob/d5b65b844b8ce8ff5151a2f56f9dc98cebbc1db4/manim/_config/logger_utils.py#L29-L101
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
|
||||
__all__ = ["logger"]
|
||||
|
||||
HIGHLIGHTED_KEYWORDS = [ # these keywords are highlighted specially
|
||||
"Played",
|
||||
"animations",
|
||||
"scene",
|
||||
"Reading",
|
||||
"Writing",
|
||||
"script",
|
||||
"arguments",
|
||||
"Invalid",
|
||||
"Aborting",
|
||||
"module",
|
||||
"File",
|
||||
"Rendering",
|
||||
"Rendered",
|
||||
"Pressed key",
|
||||
]
|
||||
|
||||
|
||||
def make_logger() -> logging.Logger:
|
||||
"""Make a logger similar to the one used by Manim."""
|
||||
RichHandler.KEYWORDS = HIGHLIGHTED_KEYWORDS
|
||||
rich_handler = RichHandler(
|
||||
show_time=True,
|
||||
console=Console(),
|
||||
)
|
||||
logger = logging.getLogger("manim-slides")
|
||||
logger.setLevel(logging.getLogger("manim").level)
|
||||
logger.addHandler(rich_handler)
|
||||
|
||||
if not (libav_logger := logging.getLogger("libav")).hasHandlers():
|
||||
libav_logger.addHandler(rich_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
make_logger()
|
||||
|
||||
logger = logging.getLogger("manim-slides")
|
@ -1,29 +0,0 @@
|
||||
import click
|
||||
from click_default_group import DefaultGroup
|
||||
|
||||
from . import __version__
|
||||
from .convert import convert
|
||||
from .present import list_scenes, present
|
||||
from .wizard import init, wizard
|
||||
|
||||
|
||||
@click.group(cls=DefaultGroup, default="present", default_if_no_args=True)
|
||||
@click.version_option(__version__, "-v", "--version")
|
||||
@click.help_option("-h", "--help")
|
||||
def cli() -> None:
|
||||
"""
|
||||
Manim Slides command-line utilities.
|
||||
|
||||
If no command is specified, defaults to `present`.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
cli.add_command(convert)
|
||||
cli.add_command(init)
|
||||
cli.add_command(list_scenes)
|
||||
cli.add_command(present)
|
||||
cli.add_command(wizard)
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
@ -1,81 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from importlib.util import find_spec
|
||||
from typing import Iterator
|
||||
|
||||
__all__ = [
|
||||
"MANIM",
|
||||
"MANIM_PACKAGE_NAME",
|
||||
"MANIM_AVAILABLE",
|
||||
"MANIM_IMPORTED",
|
||||
"MANIMGL",
|
||||
"MANIMGL_PACKAGE_NAME",
|
||||
"MANIMGL_AVAILABLE",
|
||||
"MANIMGL_IMPORTED",
|
||||
"logger",
|
||||
"Scene",
|
||||
"ThreeDScene",
|
||||
"config",
|
||||
"FFMPEG_BIN",
|
||||
]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def suppress_stdout() -> Iterator[None]:
|
||||
with open(os.devnull, "w") as devnull:
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = devnull
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
|
||||
|
||||
MANIM_PACKAGE_NAME = "manim"
|
||||
MANIM_AVAILABLE = find_spec(MANIM_PACKAGE_NAME) is not None
|
||||
MANIM_IMPORTED = MANIM_PACKAGE_NAME in sys.modules
|
||||
|
||||
MANIMGL_PACKAGE_NAME = "manimlib"
|
||||
MANIMGL_AVAILABLE = find_spec(MANIMGL_PACKAGE_NAME) is not None
|
||||
MANIMGL_IMPORTED = MANIMGL_PACKAGE_NAME in sys.modules
|
||||
|
||||
if MANIM_IMPORTED and MANIMGL_IMPORTED:
|
||||
from manim import logger
|
||||
|
||||
logger.warn(
|
||||
"Both manim and manimgl are installed, therefore `manim-slide` needs to need which one to use. Please only import one of the two modules so that `manim-slide` knows which one to use. Here, manim is used by default"
|
||||
)
|
||||
MANIM = True
|
||||
MANIMGL = False
|
||||
elif MANIM_IMPORTED:
|
||||
MANIM = True
|
||||
MANIMGL = False
|
||||
elif MANIMGL_IMPORTED:
|
||||
MANIM = False
|
||||
MANIMGL = True
|
||||
elif MANIM_AVAILABLE:
|
||||
MANIM = True
|
||||
MANIMGL = False
|
||||
elif MANIMGL_AVAILABLE:
|
||||
MANIM = False
|
||||
MANIMGL = True
|
||||
else:
|
||||
raise ModuleNotFoundError(
|
||||
"Either manim (community) or manimgl (3b1b) package must be installed"
|
||||
)
|
||||
|
||||
|
||||
if MANIMGL:
|
||||
from manimlib import Scene, ThreeDScene, config
|
||||
from manimlib.constants import FFMPEG_BIN
|
||||
from manimlib.logger import log as logger
|
||||
|
||||
else:
|
||||
with suppress_stdout(): # Avoids printing "Manim Community v..."
|
||||
from manim import Scene, ThreeDScene, config, logger
|
||||
|
||||
try: # For manim<v0.16.0.post0
|
||||
from manim.constants import FFMPEG_BIN
|
||||
except ImportError:
|
||||
FFMPEG_BIN = config.ffmpeg_executable
|
@ -1,861 +0,0 @@
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
from enum import IntEnum, auto, unique
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import click
|
||||
import cv2
|
||||
import numpy as np
|
||||
from pydantic import ValidationError
|
||||
from PySide6 import QtGui
|
||||
from PySide6.QtCore import Qt, QThread, Signal, Slot
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
from PySide6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
|
||||
from tqdm import tqdm
|
||||
|
||||
from .commons import config_path_option, verbosity_option
|
||||
from .config import DEFAULT_CONFIG, Config, PresentationConfig, SlideConfig, SlideType
|
||||
from .defaults import FOLDER_PATH
|
||||
from .manim import logger
|
||||
from .resources import * # noqa: F401, F403
|
||||
|
||||
os.environ.pop(
|
||||
"QT_QPA_PLATFORM_PLUGIN_PATH", None
|
||||
) # See why here: https://stackoverflow.com/a/67863156
|
||||
|
||||
WINDOW_NAME = "Manim Slides"
|
||||
WINDOW_INFO_NAME = f"{WINDOW_NAME}: Info"
|
||||
WINDOWS = platform.system() == "Windows"
|
||||
|
||||
ASPECT_RATIO_MODES = {
|
||||
"ignore": Qt.IgnoreAspectRatio,
|
||||
"keep": Qt.KeepAspectRatio,
|
||||
}
|
||||
|
||||
RESIZE_MODES = {
|
||||
"fast": Qt.FastTransformation,
|
||||
"smooth": Qt.SmoothTransformation,
|
||||
}
|
||||
|
||||
|
||||
@unique
|
||||
class State(IntEnum):
|
||||
"""Represents all possible states of a slide presentation."""
|
||||
|
||||
PLAYING = auto()
|
||||
PAUSED = auto()
|
||||
WAIT = auto()
|
||||
END = auto()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name.capitalize()
|
||||
|
||||
|
||||
def now() -> float:
|
||||
"""Returns time.time() in seconds."""
|
||||
return time.time()
|
||||
|
||||
|
||||
class Presentation:
|
||||
"""Creates presentation from a configuration object."""
|
||||
|
||||
def __init__(self, config: PresentationConfig) -> None:
|
||||
self.slides: List[SlideConfig] = config.slides
|
||||
self.files: List[str] = config.files
|
||||
|
||||
self.current_slide_index: int = 0
|
||||
self.current_animation: int = self.current_slide.start_animation
|
||||
self.current_file: Optional[str] = None
|
||||
|
||||
self.loaded_animation_cap: int = -1
|
||||
self.cap = None # cap = cv2.VideoCapture
|
||||
|
||||
self.reverse: bool = False
|
||||
self.reversed_animation: int = -1
|
||||
|
||||
self.lastframe: Optional[np.ndarray] = None
|
||||
|
||||
self.reset()
|
||||
|
||||
@property
|
||||
def current_slide(self) -> SlideConfig:
|
||||
"""Returns currently playing slide."""
|
||||
return self.slides[self.current_slide_index]
|
||||
|
||||
@property
|
||||
def first_slide(self) -> SlideConfig:
|
||||
"""Returns first slide."""
|
||||
return self.slides[0]
|
||||
|
||||
@property
|
||||
def last_slide(self) -> SlideConfig:
|
||||
"""Returns last slide."""
|
||||
return self.slides[-1]
|
||||
|
||||
def release_cap(self) -> None:
|
||||
"""Releases current Video Capture, if existing."""
|
||||
if self.cap is not None:
|
||||
self.cap.release()
|
||||
|
||||
self.loaded_animation_cap = -1
|
||||
|
||||
def load_animation_cap(self, animation: int) -> None:
|
||||
"""Loads video file of given animation."""
|
||||
# We must load a new VideoCapture file if:
|
||||
if (self.loaded_animation_cap != animation) or (
|
||||
self.reverse and self.reversed_animation != animation
|
||||
): # cap already loaded
|
||||
|
||||
logger.debug(f"Loading new cap for animation #{animation}")
|
||||
|
||||
self.release_cap()
|
||||
|
||||
file: str = self.files[animation]
|
||||
|
||||
if self.reverse:
|
||||
file = "{}_reversed{}".format(*os.path.splitext(file))
|
||||
self.reversed_animation = animation
|
||||
|
||||
self.current_file = file
|
||||
|
||||
self.cap = cv2.VideoCapture(file)
|
||||
self.loaded_animation_cap = animation
|
||||
|
||||
@property
|
||||
def current_cap(self) -> cv2.VideoCapture:
|
||||
"""Returns current VideoCapture object."""
|
||||
self.load_animation_cap(self.current_animation)
|
||||
return self.cap
|
||||
|
||||
def rewind_current_slide(self) -> None:
|
||||
"""Rewinds current slide to first frame."""
|
||||
logger.debug("Rewinding curring slide")
|
||||
if self.reverse:
|
||||
self.current_animation = self.current_slide.end_animation - 1
|
||||
else:
|
||||
self.current_animation = self.current_slide.start_animation
|
||||
|
||||
cap = self.current_cap
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
|
||||
def cancel_reverse(self) -> None:
|
||||
"""Cancels any effet produced by a reversed slide."""
|
||||
if self.reverse:
|
||||
logger.debug("Cancelling effects from previous 'reverse' action'")
|
||||
self.reverse = False
|
||||
self.reversed_animation = -1
|
||||
self.release_cap()
|
||||
|
||||
def reverse_current_slide(self) -> None:
|
||||
"""Reverses current slide."""
|
||||
self.reverse = True
|
||||
self.rewind_current_slide()
|
||||
|
||||
def load_next_slide(self) -> None:
|
||||
"""Loads next slide."""
|
||||
logger.debug("Loading next slide")
|
||||
if self.reverse:
|
||||
self.cancel_reverse()
|
||||
self.rewind_current_slide()
|
||||
elif self.current_slide.is_last():
|
||||
self.current_slide.terminated = True
|
||||
else:
|
||||
self.current_slide_index = min(
|
||||
len(self.slides) - 1, self.current_slide_index + 1
|
||||
)
|
||||
self.rewind_current_slide()
|
||||
|
||||
def load_previous_slide(self) -> None:
|
||||
"""Loads previous slide."""
|
||||
logger.debug("Loading previous slide")
|
||||
self.cancel_reverse()
|
||||
self.current_slide_index = max(0, self.current_slide_index - 1)
|
||||
self.rewind_current_slide()
|
||||
|
||||
@property
|
||||
def fps(self) -> int:
|
||||
"""Returns the number of frames per second of the current video."""
|
||||
fps = self.current_cap.get(cv2.CAP_PROP_FPS)
|
||||
if fps == 0:
|
||||
logger.warn(
|
||||
f"Something is wrong with video file {self.current_file}, as the fps returned by frame {self.current_frame_number} is 0"
|
||||
)
|
||||
return max(fps, 1) # TODO: understand why we sometimes get 0 fps
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Rests current presentation."""
|
||||
self.current_animation = 0
|
||||
self.load_animation_cap(0)
|
||||
self.current_slide_index = 0
|
||||
self.slides[-1].terminated = False
|
||||
|
||||
def load_last_slide(self) -> None:
|
||||
"""Loads last slide."""
|
||||
self.current_slide_index = len(self.slides) - 2
|
||||
assert (
|
||||
self.current_slide_index >= 0
|
||||
), "Slides should be at list of a least two elements"
|
||||
self.current_animation = self.current_slide.start_animation
|
||||
self.load_animation_cap(self.current_animation)
|
||||
self.slides[-1].terminated = False
|
||||
|
||||
@property
|
||||
def next_animation(self) -> int:
|
||||
"""Returns the next animation."""
|
||||
if self.reverse:
|
||||
return self.current_animation - 1
|
||||
else:
|
||||
return self.current_animation + 1
|
||||
|
||||
@property
|
||||
def is_last_animation(self) -> int:
|
||||
"""Returns True if current animation is the last one of current slide."""
|
||||
if self.reverse:
|
||||
return self.current_animation == self.current_slide.start_animation
|
||||
else:
|
||||
return self.next_animation == self.current_slide.end_animation
|
||||
|
||||
@property
|
||||
def current_frame_number(self) -> int:
|
||||
"""Returns current frame number."""
|
||||
return int(self.current_cap.get(cv2.CAP_PROP_POS_FRAMES))
|
||||
|
||||
def update_state(self, state) -> Tuple[np.ndarray, State]:
|
||||
"""
|
||||
Updates the current state given the previous one.
|
||||
|
||||
It does this by reading the video information and checking if the state is still correct.
|
||||
It returns the frame to show (lastframe) and the new state.
|
||||
"""
|
||||
if state == State.PAUSED:
|
||||
if self.lastframe is None:
|
||||
_, self.lastframe = self.current_cap.read()
|
||||
return self.lastframe, state
|
||||
still_playing, frame = self.current_cap.read()
|
||||
if still_playing:
|
||||
self.lastframe = frame
|
||||
elif state in [state.WAIT, state.PAUSED]:
|
||||
return self.lastframe, state
|
||||
elif self.current_slide.is_last() and self.current_slide.terminated:
|
||||
return self.lastframe, State.END
|
||||
else: # not still playing
|
||||
if self.is_last_animation:
|
||||
if self.current_slide.is_slide():
|
||||
state = State.WAIT
|
||||
elif self.current_slide.is_loop():
|
||||
if self.reverse:
|
||||
state = State.WAIT
|
||||
else:
|
||||
self.current_animation = self.current_slide.start_animation
|
||||
state = State.PLAYING
|
||||
self.rewind_current_slide()
|
||||
elif self.current_slide.is_last():
|
||||
self.current_slide.terminated = True
|
||||
elif (
|
||||
self.current_slide.is_last()
|
||||
and self.current_slide.end_animation == self.current_animation
|
||||
):
|
||||
state = State.WAIT
|
||||
else:
|
||||
# Play next video!
|
||||
self.current_animation = self.next_animation
|
||||
self.load_animation_cap(self.current_animation)
|
||||
# Reset video to position zero if it has been played before
|
||||
self.current_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
|
||||
return self.lastframe, state
|
||||
|
||||
|
||||
class Display(QThread):
|
||||
"""Displays one or more presentations one after each other."""
|
||||
|
||||
change_video_signal = Signal(np.ndarray)
|
||||
change_info_signal = Signal(dict)
|
||||
finished = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
presentations,
|
||||
config: Config = DEFAULT_CONFIG,
|
||||
start_paused=False,
|
||||
skip_all=False,
|
||||
record_to=None,
|
||||
exit_after_last_slide=False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.presentations = presentations
|
||||
self.start_paused = start_paused
|
||||
self.config = config
|
||||
self.skip_all = skip_all
|
||||
self.record_to = record_to
|
||||
self.recordings: List[Tuple[str, int, int]] = []
|
||||
|
||||
self.state = State.PLAYING
|
||||
self.lastframe: Optional[np.ndarray] = None
|
||||
self.current_presentation_index = 0
|
||||
self.run_flag = True
|
||||
|
||||
self.key = -1
|
||||
self.exit_after_last_slide = exit_after_last_slide
|
||||
|
||||
@property
|
||||
def current_presentation(self) -> Presentation:
|
||||
"""Returns the current presentation."""
|
||||
return self.presentations[self.current_presentation_index]
|
||||
|
||||
def run(self) -> None:
|
||||
"""Runs a series of presentations until end or exit."""
|
||||
while self.run_flag:
|
||||
last_time = now()
|
||||
self.lastframe, self.state = self.current_presentation.update_state(
|
||||
self.state
|
||||
)
|
||||
if self.state == State.PLAYING or self.state == State.PAUSED:
|
||||
if self.start_paused:
|
||||
self.state = State.PAUSED
|
||||
self.start_paused = False
|
||||
if self.state == State.END:
|
||||
if self.current_presentation_index == len(self.presentations) - 1:
|
||||
if self.exit_after_last_slide:
|
||||
self.run_flag = False
|
||||
continue
|
||||
else:
|
||||
self.current_presentation_index += 1
|
||||
self.state = State.PLAYING
|
||||
|
||||
self.handle_key()
|
||||
self.show_video()
|
||||
self.show_info()
|
||||
|
||||
lag = now() - last_time
|
||||
sleep_time = 1 / self.current_presentation.fps
|
||||
sleep_time = max(sleep_time - lag, 0)
|
||||
time.sleep(sleep_time)
|
||||
last_time = now()
|
||||
self.current_presentation.release_cap()
|
||||
|
||||
if self.record_to is not None:
|
||||
self.record_movie()
|
||||
|
||||
logger.debug("Closing video thread gracully and exiting")
|
||||
self.finished.emit()
|
||||
|
||||
def record_movie(self) -> None:
|
||||
logger.debug(
|
||||
f"A total of {len(self.recordings)} frames will be saved to {self.record_to}"
|
||||
)
|
||||
file, frame_number, fps = self.recordings[0]
|
||||
|
||||
cap = cv2.VideoCapture(file)
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
|
||||
_, frame = cap.read()
|
||||
|
||||
w, h = frame.shape[:2]
|
||||
fourcc = cv2.VideoWriter_fourcc(*"XVID")
|
||||
out = cv2.VideoWriter(self.record_to, fourcc, fps, (h, w))
|
||||
|
||||
out.write(frame)
|
||||
|
||||
for _file, frame_number, _ in tqdm(
|
||||
self.recordings[1:], desc="Creating recording file", leave=False
|
||||
):
|
||||
if file != _file:
|
||||
cap.release()
|
||||
file = _file
|
||||
cap = cv2.VideoCapture(_file)
|
||||
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
|
||||
_, frame = cap.read()
|
||||
out.write(frame)
|
||||
|
||||
cap.release()
|
||||
out.release()
|
||||
|
||||
def show_video(self) -> None:
|
||||
"""Shows updated video."""
|
||||
if self.record_to is not None:
|
||||
pres = self.current_presentation
|
||||
self.recordings.append(
|
||||
(pres.current_file, pres.current_frame_number, pres.fps)
|
||||
)
|
||||
|
||||
frame: np.ndarray = self.lastframe
|
||||
self.change_video_signal.emit(frame)
|
||||
|
||||
def show_info(self) -> None:
|
||||
"""Shows updated information about presentations."""
|
||||
self.change_info_signal.emit(
|
||||
{
|
||||
"animation": self.current_presentation.current_animation,
|
||||
"state": self.state,
|
||||
"slide_index": self.current_presentation.current_slide.number,
|
||||
"n_slides": len(self.current_presentation.slides),
|
||||
"type": self.current_presentation.current_slide.type,
|
||||
"scene_index": self.current_presentation_index + 1,
|
||||
"n_scenes": len(self.presentations),
|
||||
}
|
||||
)
|
||||
|
||||
@Slot(int)
|
||||
def set_key(self, key: int) -> None:
|
||||
"""Sets the next key to be handled."""
|
||||
self.key = key
|
||||
|
||||
def handle_key(self) -> None:
|
||||
"""Handles key strokes."""
|
||||
|
||||
key = self.key
|
||||
|
||||
if self.config.QUIT.match(key):
|
||||
self.run_flag = False
|
||||
elif self.state == State.PLAYING and self.config.PLAY_PAUSE.match(key):
|
||||
self.state = State.PAUSED
|
||||
elif self.state == State.PAUSED and self.config.PLAY_PAUSE.match(key):
|
||||
self.state = State.PLAYING
|
||||
elif self.state == State.WAIT and (
|
||||
self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)
|
||||
):
|
||||
self.current_presentation.load_next_slide()
|
||||
self.state = State.PLAYING
|
||||
elif (
|
||||
self.state == State.PLAYING and self.config.CONTINUE.match(key)
|
||||
) or self.skip_all:
|
||||
self.current_presentation.load_next_slide()
|
||||
elif self.config.BACK.match(key):
|
||||
if self.current_presentation.current_slide_index == 0:
|
||||
if self.current_presentation_index == 0:
|
||||
self.current_presentation.load_previous_slide()
|
||||
else:
|
||||
self.current_presentation_index -= 1
|
||||
self.current_presentation.load_last_slide()
|
||||
self.state = State.PLAYING
|
||||
else:
|
||||
self.current_presentation.load_previous_slide()
|
||||
self.state = State.PLAYING
|
||||
elif self.config.REVERSE.match(key):
|
||||
self.current_presentation.reverse_current_slide()
|
||||
self.state = State.PLAYING
|
||||
elif self.config.REWIND.match(key):
|
||||
self.current_presentation.cancel_reverse()
|
||||
self.current_presentation.rewind_current_slide()
|
||||
self.state = State.PLAYING
|
||||
|
||||
self.key = -1 # No more key to be handled
|
||||
|
||||
def stop(self):
|
||||
"""Stops current thread, without doing anything after."""
|
||||
self.run_flag = False
|
||||
self.wait()
|
||||
|
||||
|
||||
class Info(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle(WINDOW_INFO_NAME)
|
||||
|
||||
self.layout = QGridLayout()
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.animationLabel = QLabel()
|
||||
self.stateLabel = QLabel()
|
||||
self.slideLabel = QLabel()
|
||||
self.typeLabel = QLabel()
|
||||
self.sceneLabel = QLabel()
|
||||
|
||||
self.layout.addWidget(self.animationLabel, 0, 0, 1, 2)
|
||||
self.layout.addWidget(self.stateLabel, 1, 0)
|
||||
self.layout.addWidget(self.slideLabel, 1, 1)
|
||||
self.layout.addWidget(self.typeLabel, 2, 0)
|
||||
self.layout.addWidget(self.sceneLabel, 2, 1)
|
||||
|
||||
self.update_info({})
|
||||
|
||||
@Slot(dict)
|
||||
def update_info(self, info: dict):
|
||||
self.animationLabel.setText("Animation: {}".format(info.get("animation", "na")))
|
||||
self.stateLabel.setText("State: {}".format(info.get("state", "unknown")))
|
||||
self.slideLabel.setText(
|
||||
"Slide: {}/{}".format(
|
||||
info.get("slide_index", "na"), info.get("n_slides", "na")
|
||||
)
|
||||
)
|
||||
self.typeLabel.setText("Slide Type: {}".format(info.get("type", "unknown")))
|
||||
self.sceneLabel.setText(
|
||||
"Scene: {}/{}".format(
|
||||
info.get("scene_index", "na"), info.get("n_scenes", "na")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class InfoThread(QThread):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.dialog = Info()
|
||||
self.run_flag = True
|
||||
|
||||
def start(self):
|
||||
super().start()
|
||||
|
||||
self.dialog.show()
|
||||
|
||||
def stop(self):
|
||||
self.dialog.deleteLater()
|
||||
|
||||
|
||||
class App(QWidget):
|
||||
send_key_signal = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
config: Config = DEFAULT_CONFIG,
|
||||
fullscreen: bool = False,
|
||||
resolution: Tuple[int, int] = (1980, 1080),
|
||||
hide_mouse: bool = False,
|
||||
aspect_ratio: Qt.AspectRatioMode = Qt.IgnoreAspectRatio,
|
||||
resize_mode: Qt.TransformationMode = Qt.SmoothTransformation,
|
||||
background_color: str = "black",
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle(WINDOW_NAME)
|
||||
self.icon = QIcon(":/icon.png")
|
||||
self.setWindowIcon(self.icon)
|
||||
self.display_width, self.display_height = resolution
|
||||
self.aspect_ratio = aspect_ratio
|
||||
self.resize_mode = resize_mode
|
||||
self.hide_mouse = hide_mouse
|
||||
self.config = config
|
||||
if self.hide_mouse:
|
||||
self.setCursor(Qt.BlankCursor)
|
||||
|
||||
self.label = QLabel(self)
|
||||
self.label.setAlignment(Qt.AlignCenter)
|
||||
self.label.resize(self.display_width, self.display_height)
|
||||
self.label.setStyleSheet(f"background-color: {background_color}")
|
||||
|
||||
self.pixmap = QPixmap(self.width(), self.height())
|
||||
self.label.setPixmap(self.pixmap)
|
||||
self.label.setMinimumSize(1, 1)
|
||||
|
||||
# create the video capture thread
|
||||
self.thread = Display(*args, config=config, **kwargs)
|
||||
# create the info dialog
|
||||
self.info = Info()
|
||||
self.info.show()
|
||||
|
||||
# info widget will also listen to key presses
|
||||
self.info.keyPressEvent = self.keyPressEvent
|
||||
|
||||
if fullscreen:
|
||||
self.showFullScreen()
|
||||
|
||||
# connect signals
|
||||
self.thread.change_video_signal.connect(self.update_image)
|
||||
self.thread.change_info_signal.connect(self.info.update_info)
|
||||
self.thread.finished.connect(self.closeAll)
|
||||
self.send_key_signal.connect(self.thread.set_key)
|
||||
|
||||
# start the thread
|
||||
self.thread.start()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
|
||||
key = event.key()
|
||||
if self.config.HIDE_MOUSE.match(key):
|
||||
if self.hide_mouse:
|
||||
self.setCursor(Qt.ArrowCursor)
|
||||
self.hide_mouse = False
|
||||
else:
|
||||
self.setCursor(Qt.BlankCursor)
|
||||
self.hide_mouse = True
|
||||
# We send key to be handled by video display
|
||||
self.send_key_signal.emit(key)
|
||||
event.accept()
|
||||
|
||||
def closeAll(self):
|
||||
logger.debug("Closing all QT windows")
|
||||
self.thread.stop()
|
||||
self.info.deleteLater()
|
||||
self.deleteLater()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self.pixmap = self.pixmap.scaled(
|
||||
self.width(), self.height(), self.aspect_ratio, self.resize_mode
|
||||
)
|
||||
self.label.setPixmap(self.pixmap)
|
||||
self.label.resize(self.width(), self.height())
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.closeAll()
|
||||
event.accept()
|
||||
|
||||
@Slot(np.ndarray)
|
||||
def update_image(self, cv_img: dict):
|
||||
"""Updates the image_label with a new opencv image"""
|
||||
self.pixmap = self.convert_cv_qt(cv_img)
|
||||
self.label.setPixmap(self.pixmap)
|
||||
|
||||
@Slot(dict)
|
||||
def update_info(self, info: dict):
|
||||
"""Updates the image_label with a new opencv image"""
|
||||
pass
|
||||
|
||||
def convert_cv_qt(self, cv_img):
|
||||
"""Convert from an opencv image to QPixmap"""
|
||||
rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
|
||||
h, w, ch = rgb_image.shape
|
||||
bytes_per_line = ch * w
|
||||
convert_to_Qt_format = QtGui.QImage(
|
||||
rgb_image.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888
|
||||
)
|
||||
p = convert_to_Qt_format.scaled(
|
||||
self.width(),
|
||||
self.height(),
|
||||
self.aspect_ratio,
|
||||
self.resize_mode,
|
||||
)
|
||||
return QPixmap.fromImage(p)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--folder",
|
||||
metavar="DIRECTORY",
|
||||
default=FOLDER_PATH,
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
help="Set slides folder.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def list_scenes(folder) -> None:
|
||||
"""List available scenes."""
|
||||
|
||||
for i, scene in enumerate(_list_scenes(folder), start=1):
|
||||
click.secho(f"{i}: {scene}", fg="green")
|
||||
|
||||
|
||||
def _list_scenes(folder) -> List[str]:
|
||||
"""Lists available scenes in given directory."""
|
||||
scenes = []
|
||||
|
||||
for file in os.listdir(folder):
|
||||
if file.endswith(".json"):
|
||||
filepath = os.path.join(folder, file)
|
||||
try:
|
||||
_ = PresentationConfig.parse_file(filepath)
|
||||
scenes.append(os.path.basename(file)[:-5])
|
||||
except Exception as e: # Could not parse this file as a proper presentation config
|
||||
logger.warn(
|
||||
f"Something went wrong with parsing presentation config `{filepath}`: {e}"
|
||||
)
|
||||
pass
|
||||
|
||||
logger.debug(f"Found {len(scenes)} valid scene configuration files in `{folder}`.")
|
||||
|
||||
return scenes
|
||||
|
||||
|
||||
def prompt_for_scenes(folder: str) -> List[str]:
|
||||
"""Prompts the user to select scenes within a given folder."""
|
||||
|
||||
scene_choices = dict(enumerate(_list_scenes(folder), start=1))
|
||||
|
||||
for i, scene in scene_choices.items():
|
||||
click.secho(f"{i}: {scene}", fg="green")
|
||||
|
||||
click.echo()
|
||||
|
||||
click.echo("Choose number corresponding to desired scene/arguments.")
|
||||
click.echo("(Use comma separated list for multiple entries)")
|
||||
|
||||
def value_proc(value: Optional[str]) -> List[str]:
|
||||
indices = list(map(int, (value or "").strip().replace(" ", "").split(",")))
|
||||
|
||||
if not all(0 < i <= len(scene_choices) for i in indices):
|
||||
raise click.UsageError("Please only enter numbers displayed on the screen.")
|
||||
|
||||
return [scene_choices[i] for i in indices]
|
||||
|
||||
if len(scene_choices) == 0:
|
||||
raise click.UsageError(
|
||||
"No scenes were found, are you in the correct directory?"
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
scenes = click.prompt("Choice(s)", value_proc=value_proc)
|
||||
return scenes
|
||||
except ValueError as e:
|
||||
raise click.UsageError(str(e))
|
||||
|
||||
|
||||
def get_scenes_presentation_config(
|
||||
scenes: List[str], folder: str
|
||||
) -> List[PresentationConfig]:
|
||||
"""Returns a list of presentation configurations based on the user input."""
|
||||
|
||||
if len(scenes) == 0:
|
||||
scenes = prompt_for_scenes(folder)
|
||||
|
||||
presentation_configs = []
|
||||
for scene in scenes:
|
||||
config_file = os.path.join(folder, f"{scene}.json")
|
||||
if not os.path.exists(config_file):
|
||||
raise click.UsageError(
|
||||
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
|
||||
)
|
||||
try:
|
||||
presentation_configs.append(PresentationConfig.parse_file(config_file))
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e))
|
||||
|
||||
return presentation_configs
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("scenes", nargs=-1)
|
||||
@config_path_option
|
||||
@click.option(
|
||||
"--folder",
|
||||
metavar="DIRECTORY",
|
||||
default=FOLDER_PATH,
|
||||
type=click.Path(exists=True, file_okay=False),
|
||||
help="Set slides folder.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option("--start-paused", is_flag=True, help="Start paused.")
|
||||
@click.option("--fullscreen", is_flag=True, help="Fullscreen mode.")
|
||||
@click.option(
|
||||
"-s",
|
||||
"--skip-all",
|
||||
is_flag=True,
|
||||
help="Skip all slides, useful the test if slides are working. Automatically sets `--skip-after-last-slide` to True.",
|
||||
)
|
||||
@click.option(
|
||||
"-r",
|
||||
"--resolution",
|
||||
metavar="<WIDTH HEIGHT>",
|
||||
type=(int, int),
|
||||
default=(1920, 1080),
|
||||
help="Window resolution WIDTH HEIGHT used if fullscreen is not set. You may manually resize the window afterward.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--to",
|
||||
"--record-to",
|
||||
"record_to",
|
||||
metavar="FILE",
|
||||
type=click.Path(dir_okay=False),
|
||||
default=None,
|
||||
help="If set, the presentation will be recorded into a AVI video file with given name.",
|
||||
)
|
||||
@click.option(
|
||||
"--exit-after-last-slide",
|
||||
is_flag=True,
|
||||
help="At the end of last slide, the application will be exited.",
|
||||
)
|
||||
@click.option(
|
||||
"--hide-mouse",
|
||||
is_flag=True,
|
||||
help="Hide mouse cursor.",
|
||||
)
|
||||
@click.option(
|
||||
"--aspect-ratio",
|
||||
type=click.Choice(ASPECT_RATIO_MODES.keys(), case_sensitive=False),
|
||||
default="ignore",
|
||||
help="Set the aspect ratio mode to be used when rescaling video.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--resize-mode",
|
||||
type=click.Choice(RESIZE_MODES.keys(), case_sensitive=False),
|
||||
default="smooth",
|
||||
help="Set the resize (i.e., transformation) mode to be used when rescaling video.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--background-color",
|
||||
"--bgcolor",
|
||||
"background_color",
|
||||
metavar="COLOR",
|
||||
type=str,
|
||||
default="black",
|
||||
help='Set the background color for borders when using "keep" resize mode. Can be any valid CSS color, e.g., "green", "#FF6500" or "rgba(255, 255, 0, .5)".',
|
||||
show_default=True,
|
||||
)
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def present(
|
||||
scenes,
|
||||
config_path,
|
||||
folder,
|
||||
start_paused,
|
||||
fullscreen,
|
||||
skip_all,
|
||||
resolution,
|
||||
record_to,
|
||||
exit_after_last_slide,
|
||||
hide_mouse,
|
||||
aspect_ratio,
|
||||
resize_mode,
|
||||
background_color,
|
||||
) -> None:
|
||||
"""
|
||||
Present SCENE(s), one at a time, in order.
|
||||
|
||||
Each SCENE parameter must be the name of a Manim scene, with existing SCENE.json config file.
|
||||
|
||||
You can present the same SCENE multiple times by repeating the parameter.
|
||||
|
||||
Use `manim-slide list-scenes` to list all available scenes in a given folder.
|
||||
"""
|
||||
|
||||
if skip_all:
|
||||
exit_after_last_slide = True
|
||||
|
||||
presentations = [
|
||||
Presentation(presentation_config)
|
||||
for presentation_config in get_scenes_presentation_config(scenes, folder)
|
||||
]
|
||||
|
||||
if os.path.exists(config_path):
|
||||
try:
|
||||
config = Config.parse_file(config_path)
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e))
|
||||
else:
|
||||
logger.debug("No configuration file found, using default configuration.")
|
||||
config = Config()
|
||||
|
||||
if record_to is not None:
|
||||
_, ext = os.path.splitext(record_to)
|
||||
if ext.lower() != ".avi":
|
||||
raise click.UsageError(
|
||||
"Recording only support '.avi' extension. For other video formats, please convert the resulting '.avi' file afterwards."
|
||||
)
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Manim Slides")
|
||||
a = App(
|
||||
presentations,
|
||||
config=config,
|
||||
start_paused=start_paused,
|
||||
fullscreen=fullscreen,
|
||||
skip_all=skip_all,
|
||||
resolution=resolution,
|
||||
record_to=record_to,
|
||||
exit_after_last_slide=exit_after_last_slide,
|
||||
hide_mouse=hide_mouse,
|
||||
aspect_ratio=ASPECT_RATIO_MODES[aspect_ratio],
|
||||
resize_mode=RESIZE_MODES[resize_mode],
|
||||
background_color=background_color,
|
||||
)
|
||||
a.show()
|
||||
sys.exit(app.exec_())
|
||||
logger.debug("After")
|
365
manim_slides/present/__init__.py
Normal file
365
manim_slides/present/__init__.py
Normal file
@ -0,0 +1,365 @@
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional
|
||||
|
||||
import click
|
||||
from click import Context, Parameter
|
||||
from pydantic import ValidationError
|
||||
|
||||
from ..commons import config_path_option, folder_path_option, verbosity_option
|
||||
from ..config import Config, PresentationConfig
|
||||
from ..logger import logger
|
||||
|
||||
|
||||
@click.command()
|
||||
@folder_path_option
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def list_scenes(folder: Path) -> None:
|
||||
"""List available scenes."""
|
||||
for i, scene in enumerate(_list_scenes(folder), start=1):
|
||||
click.secho(f"{i}: {scene}", fg="green")
|
||||
|
||||
|
||||
def _list_scenes(folder: Path) -> list[str]:
|
||||
"""List available scenes in given directory."""
|
||||
scenes = []
|
||||
|
||||
for filepath in folder.glob("*.json"):
|
||||
try:
|
||||
_ = PresentationConfig.from_file(filepath)
|
||||
scenes.append(filepath.stem)
|
||||
except (
|
||||
Exception
|
||||
) as e: # Could not parse this file as a proper presentation config
|
||||
logger.warning(
|
||||
f"Something went wrong with parsing presentation config `{filepath}`: {e}"
|
||||
)
|
||||
|
||||
logger.debug(f"Found {len(scenes)} valid scene configuration files in `{folder}`.")
|
||||
|
||||
return scenes
|
||||
|
||||
|
||||
def prompt_for_scenes(folder: Path) -> list[str]:
|
||||
"""Prompt the user to select scenes within a given folder."""
|
||||
scene_choices = dict(enumerate(_list_scenes(folder), start=1))
|
||||
|
||||
for i, scene in scene_choices.items():
|
||||
click.secho(f"{i}: {scene}", fg="green")
|
||||
|
||||
click.echo()
|
||||
|
||||
click.echo("Choose number corresponding to desired scene/arguments.")
|
||||
click.echo("(Use comma separated list for multiple entries)")
|
||||
|
||||
def value_proc(value: Optional[str]) -> list[str]:
|
||||
indices = list(map(int, (value or "").strip().replace(" ", "").split(",")))
|
||||
|
||||
if not all(0 < i <= len(scene_choices) for i in indices):
|
||||
raise click.UsageError("Please only enter numbers displayed on the screen.")
|
||||
|
||||
return [scene_choices[i] for i in indices]
|
||||
|
||||
if len(scene_choices) == 0:
|
||||
raise click.UsageError(
|
||||
"No scenes were found, are you in the correct directory?"
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
scenes = click.prompt("Choice(s)", value_proc=value_proc)
|
||||
return scenes # type: ignore
|
||||
except ValueError as e:
|
||||
raise click.UsageError(str(e)) from None
|
||||
|
||||
|
||||
def get_scenes_presentation_config(
|
||||
scenes: list[str], folder: Path
|
||||
) -> list[PresentationConfig]:
|
||||
"""Return a list of presentation configurations based on the user input."""
|
||||
if len(scenes) == 0:
|
||||
scenes = prompt_for_scenes(folder)
|
||||
|
||||
presentation_configs = []
|
||||
for scene in scenes:
|
||||
config_file = folder / f"{scene}.json"
|
||||
if not config_file.exists():
|
||||
raise click.UsageError(
|
||||
f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class"
|
||||
)
|
||||
try:
|
||||
presentation_configs.append(PresentationConfig.from_file(config_file))
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e)) from None
|
||||
|
||||
return presentation_configs
|
||||
|
||||
|
||||
def start_at_callback(
|
||||
ctx: Context, param: Parameter, values: str
|
||||
) -> tuple[Optional[int], ...]:
|
||||
if values == "(None, None)":
|
||||
return (None, None)
|
||||
|
||||
def str_to_int_or_none(value: str) -> Optional[int]:
|
||||
if value.lower().strip() == "":
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
raise click.BadParameter(
|
||||
f"start index can only be an integer or an empty string, not `{value}`",
|
||||
ctx=ctx,
|
||||
param=param,
|
||||
) from None
|
||||
|
||||
values_tuple = values.split(",")
|
||||
n_values = len(values_tuple)
|
||||
if n_values == 2:
|
||||
return tuple(map(str_to_int_or_none, values_tuple))
|
||||
|
||||
raise click.BadParameter(
|
||||
f"exactly 2 arguments are expected but you gave {n_values}, "
|
||||
"please use commas to separate them",
|
||||
ctx=ctx,
|
||||
param=param,
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("scenes", nargs=-1)
|
||||
@config_path_option
|
||||
@folder_path_option
|
||||
@click.option("--start-paused", is_flag=True, help="Start paused.")
|
||||
@click.option(
|
||||
"-F",
|
||||
"--full-screen",
|
||||
"--fullscreen",
|
||||
"full_screen",
|
||||
is_flag=True,
|
||||
help="Toggle full screen mode.",
|
||||
)
|
||||
@click.option(
|
||||
"-s",
|
||||
"--skip-all",
|
||||
is_flag=True,
|
||||
help="Skip all slides, useful the test if slides are working. "
|
||||
"Automatically sets ``--exit-after-last-slide`` to True.",
|
||||
)
|
||||
@click.option(
|
||||
"--exit-after-last-slide",
|
||||
is_flag=True,
|
||||
help="At the end of last slide, the application will be exited.",
|
||||
)
|
||||
@click.option(
|
||||
"-H",
|
||||
"--hide-mouse",
|
||||
is_flag=True,
|
||||
help="Hide mouse cursor.",
|
||||
)
|
||||
@click.option(
|
||||
"--aspect-ratio",
|
||||
type=click.Choice(["keep", "ignore"], case_sensitive=False),
|
||||
default="keep",
|
||||
help="Set the aspect ratio mode to be used when rescaling the video.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--sa",
|
||||
"--start-at",
|
||||
"start_at",
|
||||
metavar="<SCENE,SLIDE>",
|
||||
type=str,
|
||||
callback=start_at_callback,
|
||||
default=(None, None),
|
||||
help="Start presenting at (x, y), equivalent to ``--sacn x --sasn y``, "
|
||||
"and overrides values if not None.",
|
||||
)
|
||||
@click.option(
|
||||
"--sacn",
|
||||
"--start-at-scene-number",
|
||||
"start_at_scene_number",
|
||||
metavar="INDEX",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Start presenting at a given scene number (0 is first, -1 is last).",
|
||||
)
|
||||
@click.option(
|
||||
"--sasn",
|
||||
"--start-at-slide-number",
|
||||
"start_at_slide_number",
|
||||
metavar="INDEX",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Start presenting at a given slide number (0 is first, -1 is last).",
|
||||
)
|
||||
@click.option(
|
||||
"-S",
|
||||
"--screen",
|
||||
"screen_number",
|
||||
metavar="NUMBER",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Present content on the given screen (a.k.a. display).",
|
||||
)
|
||||
@click.option(
|
||||
"--playback-rate",
|
||||
metavar="RATE",
|
||||
type=float,
|
||||
default=1.0,
|
||||
help="Playback rate of the video slides, see PySide6 docs for details. "
|
||||
" The playback rate of each slide is defined as the product of its default "
|
||||
" playback rate and the provided value.",
|
||||
)
|
||||
@click.option(
|
||||
"--next-terminates-loop",
|
||||
"next_terminates_loop",
|
||||
is_flag=True,
|
||||
help="If set, pressing next will turn any looping slide into a play slide.",
|
||||
)
|
||||
@click.option(
|
||||
"--hide-info-window",
|
||||
flag_value="always",
|
||||
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(
|
||||
"--info-window-screen",
|
||||
"info_window_screen_number",
|
||||
metavar="NUMBER",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Put info window on the given screen (a.k.a. display). "
|
||||
"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")
|
||||
@verbosity_option
|
||||
def present( # noqa: C901
|
||||
scenes: list[str],
|
||||
config_path: Path,
|
||||
folder: Path,
|
||||
start_paused: bool,
|
||||
full_screen: bool,
|
||||
skip_all: bool,
|
||||
exit_after_last_slide: bool,
|
||||
hide_mouse: bool,
|
||||
aspect_ratio: str,
|
||||
start_at: tuple[Optional[int], Optional[int], Optional[int]],
|
||||
start_at_scene_number: int,
|
||||
start_at_slide_number: int,
|
||||
screen_number: Optional[int],
|
||||
playback_rate: float,
|
||||
next_terminates_loop: bool,
|
||||
hide_info_window: Optional[Literal["always", "never"]],
|
||||
info_window_screen_number: Optional[int],
|
||||
) -> None:
|
||||
"""
|
||||
Present SCENE(s), one at a time, in order.
|
||||
|
||||
Each SCENE parameter must be the name of a Manim scene,
|
||||
with existing SCENE.json config file.
|
||||
|
||||
You can present the same SCENE multiple times by repeating the parameter.
|
||||
|
||||
Use ``manim-slide list-scenes`` to list all available
|
||||
scenes in a given folder.
|
||||
"""
|
||||
if skip_all:
|
||||
exit_after_last_slide = True
|
||||
|
||||
presentation_configs = get_scenes_presentation_config(scenes, folder)
|
||||
|
||||
if config_path.exists():
|
||||
try:
|
||||
config = Config.from_file(config_path)
|
||||
except ValidationError as e:
|
||||
raise click.UsageError(str(e)) from None
|
||||
else:
|
||||
logger.debug("No configuration file found, using default configuration.")
|
||||
config = Config()
|
||||
|
||||
if start_at[0]:
|
||||
start_at_scene_number = start_at[0]
|
||||
|
||||
if start_at[1]:
|
||||
start_at_slide_number = start_at[1]
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QScreen
|
||||
|
||||
from ..qt_utils import qapp
|
||||
from .player import Player
|
||||
|
||||
app = qapp()
|
||||
app.setApplicationName("Manim Slides")
|
||||
|
||||
screens = app.screens()
|
||||
|
||||
def get_screen(number: int) -> Optional[QScreen]:
|
||||
try:
|
||||
return screens[number]
|
||||
except IndexError:
|
||||
logger.error(
|
||||
f"Invalid screen number {number}, "
|
||||
f"allowed values are from 0 to {len(screens) - 1} (incl.)"
|
||||
)
|
||||
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:
|
||||
screen = get_screen(screen_number)
|
||||
else:
|
||||
screen = None
|
||||
|
||||
if info_window_screen_number is not None and not should_hide_info_window:
|
||||
info_window_screen = get_screen(info_window_screen_number)
|
||||
else:
|
||||
info_window_screen = None
|
||||
|
||||
aspect_ratio_modes = {
|
||||
"keep": Qt.KeepAspectRatio,
|
||||
"ignore": Qt.IgnoreAspectRatio,
|
||||
}
|
||||
|
||||
player = Player(
|
||||
config,
|
||||
presentation_configs,
|
||||
start_paused=start_paused,
|
||||
full_screen=full_screen,
|
||||
skip_all=skip_all,
|
||||
exit_after_last_slide=exit_after_last_slide,
|
||||
hide_mouse=hide_mouse,
|
||||
aspect_ratio_mode=aspect_ratio_modes[aspect_ratio],
|
||||
presentation_index=start_at_scene_number,
|
||||
slide_index=start_at_slide_number,
|
||||
screen=screen,
|
||||
playback_rate=playback_rate,
|
||||
next_terminates_loop=next_terminates_loop,
|
||||
hide_info_window=should_hide_info_window,
|
||||
info_window_screen=info_window_screen,
|
||||
)
|
||||
|
||||
player.show(screens)
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
sys.exit(app.exec())
|
599
manim_slides/present/player.py
Normal file
599
manim_slides/present/player.py
Normal file
@ -0,0 +1,599 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
|
||||
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
|
||||
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer, QVideoFrame
|
||||
from qtpy.QtMultimediaWidgets import QVideoWidget
|
||||
from qtpy.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMainWindow,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ..config import Config, PresentationConfig, SlideConfig
|
||||
from ..logger import logger
|
||||
from ..resources import * # noqa: F403
|
||||
|
||||
WINDOW_NAME = "Manim Slides"
|
||||
|
||||
|
||||
class Info(QWidget): # type: ignore[misc]
|
||||
key_press_event: Signal = Signal(QKeyEvent)
|
||||
close_event: Signal = Signal(QCloseEvent)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
aspect_ratio_mode: Qt.AspectRatioMode,
|
||||
screen: Optional[QScreen],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
if screen:
|
||||
self.setScreen(screen)
|
||||
self.move(screen.geometry().topLeft())
|
||||
|
||||
layout = QHBoxLayout()
|
||||
|
||||
# Current slide view
|
||||
|
||||
left_layout = QVBoxLayout()
|
||||
left_layout.addWidget(
|
||||
QLabel("Current slide"),
|
||||
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
|
||||
)
|
||||
main_video_widget = QVideoWidget()
|
||||
main_video_widget.setAspectRatioMode(aspect_ratio_mode)
|
||||
main_video_widget.setFixedSize(720, 480)
|
||||
self.video_sink = main_video_widget.videoSink()
|
||||
left_layout.addWidget(main_video_widget)
|
||||
|
||||
# Current slide information
|
||||
|
||||
self.scene_label = QLabel()
|
||||
self.slide_label = QLabel()
|
||||
self.start_time = datetime.now()
|
||||
self.time_label = QLabel()
|
||||
self.elapsed_label = QLabel("00h00m00s")
|
||||
self.timer = QTimer()
|
||||
self.timer.start(1000) # every second
|
||||
self.timer.timeout.connect(self.update_time)
|
||||
|
||||
bottom_left_layout = QHBoxLayout()
|
||||
bottom_left_layout.addWidget(
|
||||
QLabel("Scene:"),
|
||||
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
|
||||
)
|
||||
bottom_left_layout.addWidget(
|
||||
self.scene_label,
|
||||
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
|
||||
)
|
||||
bottom_left_layout.addWidget(
|
||||
QLabel("Slide:"),
|
||||
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
|
||||
)
|
||||
bottom_left_layout.addWidget(
|
||||
self.slide_label,
|
||||
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
|
||||
)
|
||||
bottom_left_layout.addWidget(
|
||||
QLabel("Time:"),
|
||||
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
|
||||
)
|
||||
bottom_left_layout.addWidget(
|
||||
self.time_label,
|
||||
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
|
||||
)
|
||||
bottom_left_layout.addWidget(
|
||||
QLabel("Elapsed:"),
|
||||
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight,
|
||||
)
|
||||
bottom_left_layout.addWidget(
|
||||
self.elapsed_label,
|
||||
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
|
||||
)
|
||||
left_layout.addLayout(bottom_left_layout)
|
||||
layout.addLayout(left_layout)
|
||||
|
||||
layout.addSpacing(20)
|
||||
|
||||
# Next slide preview
|
||||
|
||||
right_layout = QVBoxLayout()
|
||||
right_layout.addWidget(
|
||||
QLabel("Next slide"),
|
||||
alignment=Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter,
|
||||
)
|
||||
next_video_widget = QVideoWidget()
|
||||
next_video_widget.setAspectRatioMode(aspect_ratio_mode)
|
||||
next_video_widget.setFixedSize(360, 240)
|
||||
self.next_media_player = QMediaPlayer()
|
||||
self.next_media_player.setVideoOutput(next_video_widget)
|
||||
self.next_media_player.setLoops(-1)
|
||||
|
||||
right_layout.addWidget(next_video_widget)
|
||||
|
||||
# Notes
|
||||
|
||||
self.slide_notes = QLabel()
|
||||
self.slide_notes.setWordWrap(True)
|
||||
self.slide_notes.setTextFormat(Qt.TextFormat.MarkdownText)
|
||||
self.slide_notes.setFixedWidth(360)
|
||||
right_layout.addWidget(
|
||||
self.slide_notes,
|
||||
alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
|
||||
)
|
||||
layout.addLayout(right_layout)
|
||||
|
||||
widget = QWidget()
|
||||
|
||||
widget.setLayout(layout)
|
||||
|
||||
main_layout = QVBoxLayout()
|
||||
main_layout.addWidget(widget, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.setLayout(main_layout)
|
||||
|
||||
@Slot()
|
||||
def update_time(self) -> None:
|
||||
now = datetime.now()
|
||||
seconds = (now - self.start_time).total_seconds()
|
||||
hours, seconds = divmod(seconds, 3600)
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
self.time_label.setText(now.strftime("%Y/%m/%d %H:%M:%S"))
|
||||
self.elapsed_label.setText(
|
||||
f"{int(hours):02d}h{int(minutes):02d}m{int(seconds):02d}s"
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
|
||||
self.close_event.emit(event)
|
||||
|
||||
@Slot()
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
|
||||
self.key_press_event.emit(event)
|
||||
|
||||
|
||||
class Player(QMainWindow): # type: ignore[misc]
|
||||
presentation_changed: Signal = Signal()
|
||||
slide_changed: Signal = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
presentation_configs: list[PresentationConfig],
|
||||
*,
|
||||
start_paused: bool = False,
|
||||
full_screen: bool = False,
|
||||
skip_all: bool = False,
|
||||
exit_after_last_slide: bool = False,
|
||||
hide_mouse: bool = False,
|
||||
aspect_ratio_mode: Qt.AspectRatioMode = Qt.KeepAspectRatio,
|
||||
presentation_index: int = 0,
|
||||
slide_index: int = 0,
|
||||
screen: Optional[QScreen] = None,
|
||||
playback_rate: float = 1.0,
|
||||
next_terminates_loop: bool = False,
|
||||
hide_info_window: bool = False,
|
||||
info_window_screen: Optional[QScreen] = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
# Wizard's config
|
||||
|
||||
self.config = config
|
||||
|
||||
# Presentation configs
|
||||
|
||||
self.presentation_configs = presentation_configs
|
||||
self.__current_presentation_index = 0
|
||||
self.__current_slide_index = 0
|
||||
|
||||
self.current_presentation_index = presentation_index
|
||||
self.current_slide_index = slide_index
|
||||
|
||||
self.__current_file: Path = self.current_slide_config.file
|
||||
|
||||
self.__playing_reversed_slide = False
|
||||
|
||||
# Widgets
|
||||
|
||||
if screen:
|
||||
self.setScreen(screen)
|
||||
self.move(screen.geometry().topLeft())
|
||||
|
||||
if full_screen:
|
||||
self.setWindowState(Qt.WindowFullScreen)
|
||||
else:
|
||||
w, h = self.current_presentation_config.resolution
|
||||
geometry = self.geometry()
|
||||
geometry.setWidth(w)
|
||||
geometry.setHeight(h)
|
||||
self.setGeometry(geometry)
|
||||
|
||||
if hide_mouse:
|
||||
self.setCursor(Qt.BlankCursor)
|
||||
|
||||
self.setWindowTitle(WINDOW_NAME)
|
||||
self.icon = QIcon(":/icon.png")
|
||||
self.setWindowIcon(self.icon)
|
||||
|
||||
self.frame = QVideoFrame()
|
||||
|
||||
self.audio_output = QAudioOutput()
|
||||
self.video_widget = QVideoWidget()
|
||||
self.video_sink = self.video_widget.videoSink()
|
||||
self.video_widget.setAspectRatioMode(aspect_ratio_mode)
|
||||
self.setCentralWidget(self.video_widget)
|
||||
|
||||
self.media_player = QMediaPlayer(self)
|
||||
self.media_player.setAudioOutput(self.audio_output)
|
||||
self.media_player.setVideoOutput(self.video_widget)
|
||||
self.playback_rate = playback_rate
|
||||
|
||||
self.presentation_changed.connect(self.presentation_changed_callback)
|
||||
self.slide_changed.connect(self.slide_changed_callback)
|
||||
|
||||
self.info = Info(
|
||||
aspect_ratio_mode=aspect_ratio_mode,
|
||||
screen=info_window_screen,
|
||||
)
|
||||
self.info.close_event.connect(self.closeEvent)
|
||||
self.info.key_press_event.connect(self.keyPressEvent)
|
||||
self.video_sink.videoFrameChanged.connect(self.frame_changed)
|
||||
self.hide_info_window = hide_info_window
|
||||
|
||||
# Connecting key callbacks
|
||||
|
||||
self.config.keys.QUIT.connect(self.close)
|
||||
self.config.keys.PLAY_PAUSE.connect(self.play_pause)
|
||||
self.config.keys.NEXT.connect(self.next)
|
||||
self.config.keys.PREVIOUS.connect(self.previous)
|
||||
self.config.keys.REVERSE.connect(self.reverse)
|
||||
self.config.keys.REPLAY.connect(self.replay)
|
||||
self.config.keys.FULL_SCREEN.connect(self.full_screen)
|
||||
self.config.keys.HIDE_MOUSE.connect(self.hide_mouse)
|
||||
|
||||
self.dispatch = self.config.keys.dispatch_key_function()
|
||||
|
||||
# Misc
|
||||
|
||||
self.exit_after_last_slide = exit_after_last_slide
|
||||
self.next_terminates_loop = next_terminates_loop
|
||||
|
||||
# Setting-up everything
|
||||
|
||||
if skip_all:
|
||||
|
||||
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
|
||||
self.media_player.setLoops(1) # Otherwise looping slides never end
|
||||
if status == QMediaPlayer.MediaStatus.EndOfMedia:
|
||||
self.load_next_slide()
|
||||
|
||||
self.media_player.mediaStatusChanged.connect(media_status_changed)
|
||||
|
||||
else:
|
||||
|
||||
def media_status_changed(status: QMediaPlayer.MediaStatus) -> None:
|
||||
if (
|
||||
status == QMediaPlayer.MediaStatus.EndOfMedia
|
||||
and self.current_slide_config.auto_next
|
||||
):
|
||||
self.load_next_slide()
|
||||
|
||||
self.media_player.mediaStatusChanged.connect(media_status_changed)
|
||||
|
||||
if self.current_slide_config.loop:
|
||||
self.media_player.setLoops(-1)
|
||||
|
||||
self.load_current_media(start_paused=start_paused)
|
||||
|
||||
self.presentation_changed.emit()
|
||||
self.slide_changed.emit()
|
||||
|
||||
"""
|
||||
Properties
|
||||
"""
|
||||
|
||||
@property
|
||||
def presentations_count(self) -> int:
|
||||
return len(self.presentation_configs)
|
||||
|
||||
@property
|
||||
def current_presentation_index(self) -> int:
|
||||
return self.__current_presentation_index
|
||||
|
||||
@current_presentation_index.setter
|
||||
def current_presentation_index(self, index: int) -> None:
|
||||
if 0 <= index < self.presentations_count:
|
||||
self.__current_presentation_index = index
|
||||
elif -self.presentations_count <= index < 0:
|
||||
self.__current_presentation_index = index + self.presentations_count
|
||||
else:
|
||||
logger.warning(f"Could not set presentation index to {index}.")
|
||||
return
|
||||
|
||||
self.presentation_changed.emit()
|
||||
|
||||
@property
|
||||
def current_presentation_config(self) -> PresentationConfig:
|
||||
return self.presentation_configs[self.current_presentation_index]
|
||||
|
||||
@property
|
||||
def current_slides_count(self) -> int:
|
||||
return len(self.current_presentation_config.slides)
|
||||
|
||||
@property
|
||||
def current_slide_index(self) -> int:
|
||||
return self.__current_slide_index
|
||||
|
||||
@current_slide_index.setter
|
||||
def current_slide_index(self, index: int) -> None:
|
||||
if 0 <= index < self.current_slides_count:
|
||||
self.__current_slide_index = index
|
||||
elif -self.current_slides_count <= index < 0:
|
||||
self.__current_slide_index = index + self.current_slides_count
|
||||
else:
|
||||
logger.warning(f"Could not set slide index to {index}.")
|
||||
return
|
||||
|
||||
self.slide_changed.emit()
|
||||
|
||||
@property
|
||||
def current_slide_config(self) -> SlideConfig:
|
||||
return self.current_presentation_config.slides[self.current_slide_index]
|
||||
|
||||
@property
|
||||
def current_file(self) -> Path:
|
||||
return self.__current_file
|
||||
|
||||
@current_file.setter
|
||||
def current_file(self, file: Path) -> None:
|
||||
self.__current_file = file
|
||||
|
||||
@property
|
||||
def next_slide_config(self) -> Optional[SlideConfig]:
|
||||
if self.playing_reversed_slide:
|
||||
return self.current_slide_config
|
||||
elif self.current_slide_index < self.current_slides_count - 1:
|
||||
return self.presentation_configs[self.current_presentation_index].slides[
|
||||
self.current_slide_index + 1
|
||||
]
|
||||
elif self.current_presentation_index < self.presentations_count - 1:
|
||||
return self.presentation_configs[
|
||||
self.current_presentation_index + 1
|
||||
].slides[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def next_file(self) -> Optional[Path]:
|
||||
if slide_config := self.next_slide_config:
|
||||
return slide_config.file # type: ignore[no-any-return]
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def playing_reversed_slide(self) -> bool:
|
||||
return self.__playing_reversed_slide
|
||||
|
||||
@playing_reversed_slide.setter
|
||||
def playing_reversed_slide(self, playing_reversed_slide: bool) -> None:
|
||||
self.__playing_reversed_slide = playing_reversed_slide
|
||||
|
||||
"""
|
||||
Loading slides
|
||||
"""
|
||||
|
||||
def load_current_media(self, start_paused: bool = False) -> None:
|
||||
url = QUrl.fromLocalFile(str(self.current_file))
|
||||
self.media_player.setSource(url)
|
||||
|
||||
if self.playing_reversed_slide:
|
||||
self.media_player.setPlaybackRate(
|
||||
self.current_slide_config.reversed_playback_rate * self.playback_rate
|
||||
)
|
||||
else:
|
||||
self.media_player.setPlaybackRate(
|
||||
self.current_slide_config.playback_rate * self.playback_rate
|
||||
)
|
||||
|
||||
if start_paused:
|
||||
self.media_player.pause()
|
||||
else:
|
||||
self.media_player.play()
|
||||
|
||||
def load_current_slide(self) -> None:
|
||||
slide_config = self.current_slide_config
|
||||
self.current_file = slide_config.file
|
||||
|
||||
if slide_config.loop:
|
||||
self.media_player.setLoops(-1)
|
||||
else:
|
||||
self.media_player.setLoops(1)
|
||||
|
||||
self.load_current_media()
|
||||
|
||||
def load_previous_slide(self) -> None:
|
||||
self.playing_reversed_slide = False
|
||||
|
||||
if self.current_slide_index > 0:
|
||||
self.current_slide_index -= 1
|
||||
elif self.current_presentation_index > 0:
|
||||
self.current_presentation_index -= 1
|
||||
self.current_slide_index = self.current_slides_count - 1
|
||||
else:
|
||||
logger.info("No previous slide.")
|
||||
return
|
||||
|
||||
self.load_current_slide()
|
||||
|
||||
def load_next_slide(self) -> None:
|
||||
if self.playing_reversed_slide:
|
||||
self.playing_reversed_slide = False
|
||||
self.preview_next_slide() # Slide number did not change, but next did
|
||||
elif self.current_slide_index < self.current_slides_count - 1:
|
||||
self.current_slide_index += 1
|
||||
elif self.current_presentation_index < self.presentations_count - 1:
|
||||
self.current_presentation_index += 1
|
||||
self.current_slide_index = 0
|
||||
elif self.exit_after_last_slide:
|
||||
self.close()
|
||||
return
|
||||
else:
|
||||
logger.info("No more slide to play.")
|
||||
return
|
||||
|
||||
self.load_current_slide()
|
||||
|
||||
def load_reversed_slide(self) -> None:
|
||||
self.playing_reversed_slide = True
|
||||
self.current_file = self.current_slide_config.rev_file
|
||||
self.load_current_media()
|
||||
|
||||
"""
|
||||
Key callbacks and slots
|
||||
"""
|
||||
|
||||
@Slot()
|
||||
def presentation_changed_callback(self) -> None:
|
||||
index = self.current_presentation_index
|
||||
count = self.presentations_count
|
||||
self.info.scene_label.setText(f"{index + 1:4d}/{count:4<d}")
|
||||
|
||||
@Slot()
|
||||
def slide_changed_callback(self) -> None:
|
||||
index = self.current_slide_index
|
||||
count = self.current_slides_count
|
||||
self.info.slide_label.setText(f"{index + 1:4d}/{count:4<d}")
|
||||
self.info.slide_notes.setText(self.current_slide_config.notes)
|
||||
self.preview_next_slide()
|
||||
|
||||
def preview_next_slide(self) -> None:
|
||||
if slide_config := self.next_slide_config:
|
||||
url = QUrl.fromLocalFile(str(slide_config.file))
|
||||
self.info.next_media_player.setSource(url)
|
||||
self.info.next_media_player.play()
|
||||
|
||||
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()
|
||||
|
||||
if not self.hide_info_window:
|
||||
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()
|
||||
def close(self) -> None:
|
||||
logger.info("Closing gracefully...")
|
||||
self.info.close()
|
||||
super().close()
|
||||
|
||||
@Slot()
|
||||
def next(self) -> None:
|
||||
if self.media_player.playbackState() == QMediaPlayer.PlaybackState.PausedState:
|
||||
self.media_player.play()
|
||||
elif self.next_terminates_loop and self.media_player.loops() != 1:
|
||||
position = self.media_player.position()
|
||||
self.media_player.setLoops(1)
|
||||
self.media_player.stop()
|
||||
self.media_player.setPosition(position)
|
||||
self.media_player.play()
|
||||
else:
|
||||
self.load_next_slide()
|
||||
|
||||
@Slot()
|
||||
def previous(self) -> None:
|
||||
self.load_previous_slide()
|
||||
|
||||
@Slot()
|
||||
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.preview_next_slide()
|
||||
|
||||
@Slot()
|
||||
def replay(self) -> None:
|
||||
self.media_player.setPosition(0)
|
||||
self.media_player.play()
|
||||
|
||||
@Slot()
|
||||
def play_pause(self) -> None:
|
||||
state = self.media_player.playbackState()
|
||||
if state == QMediaPlayer.PlaybackState.PausedState:
|
||||
self.media_player.play()
|
||||
elif state == QMediaPlayer.PlaybackState.PlayingState:
|
||||
self.media_player.pause()
|
||||
|
||||
@Slot()
|
||||
def full_screen(self) -> None:
|
||||
if self.windowState() == Qt.WindowFullScreen:
|
||||
self.setWindowState(Qt.WindowNoState)
|
||||
self.info.setWindowState(Qt.WindowNoState)
|
||||
else:
|
||||
self.setWindowState(Qt.WindowFullScreen)
|
||||
self.info.setWindowState(Qt.WindowFullScreen)
|
||||
|
||||
@Slot()
|
||||
def hide_mouse(self) -> None:
|
||||
if self.cursor().shape() == Qt.BlankCursor:
|
||||
self.setCursor(Qt.ArrowCursor)
|
||||
else:
|
||||
self.setCursor(Qt.BlankCursor)
|
||||
|
||||
def frame_changed(self, frame: QVideoFrame) -> None:
|
||||
"""
|
||||
Slot to handle possibly invalid frames.
|
||||
|
||||
This slot cannot be decorated with ``@Slot`` as
|
||||
the video sinks are handled in different threads.
|
||||
|
||||
As of Qt>=6.5.3, the last frame of every video is "flushed",
|
||||
resulting in a short black screen between each slide.
|
||||
|
||||
To avoid this issue, we check every frame, and avoid playing
|
||||
invalid ones.
|
||||
|
||||
References
|
||||
----------
|
||||
1. https://github.com/jeertmans/manim-slides/issues/293
|
||||
2. https://github.com/jeertmans/manim-slides/pull/464
|
||||
|
||||
:param frame: The most recent frame.
|
||||
|
||||
"""
|
||||
if frame.isValid():
|
||||
self.frame = frame
|
||||
else:
|
||||
self.video_sink.setVideoFrame(self.frame) # Reuse previous frame
|
||||
|
||||
self.info.video_sink.setVideoFrame(self.frame)
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
|
||||
self.close()
|
||||
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
|
||||
key = event.key()
|
||||
self.dispatch(key)
|
||||
event.accept()
|
14
manim_slides/qt_utils.py
Normal file
14
manim_slides/qt_utils.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""Qt utils."""
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
|
||||
def qapp() -> QApplication:
|
||||
"""
|
||||
Return a QApplication instance, creating one
|
||||
if needed.
|
||||
"""
|
||||
if app := QApplication.instance():
|
||||
return app
|
||||
|
||||
return QApplication([])
|
54
manim_slides/render.py
Normal file
54
manim_slides/render.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""
|
||||
Alias command to either
|
||||
``manim render [OPTIONS] [ARGS]...`` or
|
||||
``manimgl -w [OPTIONS] [ARGS]...``.
|
||||
|
||||
This is especially useful for two reasons:
|
||||
|
||||
1. You can are sure to execute the rendering command with the same Python environment
|
||||
as for ``manim-slides``.
|
||||
2. You can pass options to the config.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@click.command(
|
||||
context_settings=dict(
|
||||
ignore_unknown_options=True, allow_extra_args=True, help_option_names=("-h",)
|
||||
),
|
||||
options_metavar="[-h] [--CE|--GL]",
|
||||
)
|
||||
@click.option(
|
||||
"--CE",
|
||||
is_flag=True,
|
||||
envvar="MANIM_RENDERER",
|
||||
show_envvar=True,
|
||||
help="If set, use Manim Community Edition (CE) renderer. "
|
||||
"If this or ``--GL`` is not set, defaults to CE renderer.",
|
||||
)
|
||||
@click.option(
|
||||
"--GL",
|
||||
is_flag=True,
|
||||
envvar="MANIMGL_RENDERER",
|
||||
show_envvar=True,
|
||||
help="If set, use ManimGL renderer.",
|
||||
)
|
||||
@click.argument("args", metavar="[RENDERER_ARGS]...", nargs=-1, type=click.UNPROCESSED)
|
||||
def render(ce: bool, gl: bool, args: tuple[str, ...]) -> None:
|
||||
"""
|
||||
Render SCENE(s) from the input FILE, using the specified renderer.
|
||||
|
||||
Use ``manim-slides render --help`` to see help information for
|
||||
a specific renderer.
|
||||
"""
|
||||
if ce and gl:
|
||||
raise click.UsageError("You cannot specify both --CE and --GL renderers.")
|
||||
if gl:
|
||||
completed = subprocess.run([sys.executable, "-m", "manimlib", "-w", *args])
|
||||
else:
|
||||
completed = subprocess.run([sys.executable, "-m", "manim", "render", *args])
|
||||
sys.exit(completed.returncode)
|
@ -4,7 +4,7 @@
|
||||
# Created by: The Resource Compiler for Qt version 6.4.0
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PySide6 import QtCore
|
||||
from qtpy import QtCore
|
||||
|
||||
qt_resource_data = b"\
|
||||
\x00\x00\x08\x1c\
|
||||
|
@ -1,232 +0,0 @@
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from .config import PresentationConfig, SlideConfig, SlideType
|
||||
from .defaults import FOLDER_PATH
|
||||
from .manim import FFMPEG_BIN, MANIMGL, Scene, ThreeDScene, config, logger
|
||||
|
||||
|
||||
def reverse_video_file(src: str, dst: str) -> None:
|
||||
"""Reverses a video file, writting the result to `dst`."""
|
||||
command = [FFMPEG_BIN, "-i", src, "-vf", "reverse", dst]
|
||||
logger.debug(" ".join(command))
|
||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
output, error = process.communicate()
|
||||
|
||||
if output:
|
||||
logger.debug(output.decode())
|
||||
|
||||
if error:
|
||||
logger.debug(error.decode())
|
||||
|
||||
|
||||
class Slide(Scene):
|
||||
"""
|
||||
Inherits from `manim.Scene` or `manimlib.Scene` and provide necessary tools for slides rendering.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *args: Any, output_folder: str = FOLDER_PATH, **kwargs: Any
|
||||
) -> None:
|
||||
if MANIMGL:
|
||||
if not os.path.isdir("videos"):
|
||||
os.mkdir("videos")
|
||||
kwargs["file_writer_config"] = {
|
||||
"break_into_partial_movies": True,
|
||||
"output_directory": "",
|
||||
"write_to_movie": True,
|
||||
}
|
||||
|
||||
kwargs["preview"] = False
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.output_folder = output_folder
|
||||
self.slides: List[SlideConfig] = []
|
||||
self.current_slide = 1
|
||||
self.current_animation = 0
|
||||
self.loop_start_animation: Optional[int] = None
|
||||
self.pause_start_animation = 0
|
||||
|
||||
@property
|
||||
def partial_movie_files(self) -> List[str]:
|
||||
"""Returns a list of partial movie files, a.k.a animations."""
|
||||
if MANIMGL:
|
||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.file_writer.movie_file_extension,
|
||||
}
|
||||
return get_sorted_integer_files(
|
||||
self.file_writer.partial_movie_directory, **kwargs
|
||||
)
|
||||
else:
|
||||
return self.renderer.file_writer.partial_movie_files
|
||||
|
||||
@property
|
||||
def show_progress_bar(self) -> bool:
|
||||
"""Returns True if progress bar should be displayed."""
|
||||
if MANIMGL:
|
||||
return getattr(super(Scene, self), "show_progress_bar", True)
|
||||
else:
|
||||
return config["progress_bar"] != "none"
|
||||
|
||||
@property
|
||||
def leave_progress_bar(self) -> bool:
|
||||
"""Returns True if progress bar should be left after completed."""
|
||||
if MANIMGL:
|
||||
return getattr(super(Scene, self), "leave_progress_bars", False)
|
||||
else:
|
||||
return config["progress_bar"] == "leave"
|
||||
|
||||
def play(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Overloads `self.play` and increment animation count."""
|
||||
super().play(*args, **kwargs)
|
||||
self.current_animation += 1
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Creates a new slide with previous animations."""
|
||||
self.slides.append(
|
||||
SlideConfig(
|
||||
type=SlideType.slide,
|
||||
start_animation=self.pause_start_animation,
|
||||
end_animation=self.current_animation,
|
||||
number=self.current_slide,
|
||||
)
|
||||
)
|
||||
self.current_slide += 1
|
||||
self.pause_start_animation = self.current_animation
|
||||
|
||||
def add_last_slide(self) -> None:
|
||||
"""Adds a 'last' slide to the end of slides."""
|
||||
self.slides.append(
|
||||
SlideConfig(
|
||||
type=SlideType.last,
|
||||
start_animation=self.pause_start_animation,
|
||||
end_animation=self.current_animation,
|
||||
number=self.current_slide,
|
||||
)
|
||||
)
|
||||
|
||||
def start_loop(self) -> None:
|
||||
"""Starts a loop."""
|
||||
assert self.loop_start_animation is None, "You cannot nest loops"
|
||||
self.loop_start_animation = self.current_animation
|
||||
|
||||
def end_loop(self) -> None:
|
||||
"""Ends an existing loop."""
|
||||
assert (
|
||||
self.loop_start_animation is not None
|
||||
), "You have to start a loop before ending it"
|
||||
self.slides.append(
|
||||
SlideConfig(
|
||||
type=SlideType.loop,
|
||||
start_animation=self.loop_start_animation,
|
||||
end_animation=self.current_animation,
|
||||
number=self.current_slide,
|
||||
)
|
||||
)
|
||||
self.current_slide += 1
|
||||
self.loop_start_animation = None
|
||||
self.pause_start_animation = self.current_animation
|
||||
|
||||
def save_slides(self, use_cache: bool = True) -> None:
|
||||
"""
|
||||
Saves slides, optionally using cached files.
|
||||
|
||||
Note that cached files only work with Manim.
|
||||
"""
|
||||
self.add_last_slide()
|
||||
|
||||
if not os.path.exists(self.output_folder):
|
||||
os.mkdir(self.output_folder)
|
||||
|
||||
files_folder = os.path.join(self.output_folder, "files")
|
||||
if not os.path.exists(files_folder):
|
||||
os.mkdir(files_folder)
|
||||
|
||||
scene_name = type(self).__name__
|
||||
scene_files_folder = os.path.join(files_folder, scene_name)
|
||||
|
||||
old_animation_files = set()
|
||||
|
||||
if not os.path.exists(scene_files_folder):
|
||||
os.mkdir(scene_files_folder)
|
||||
elif not use_cache:
|
||||
shutil.rmtree(scene_files_folder)
|
||||
os.mkdir(scene_files_folder)
|
||||
else:
|
||||
old_animation_files.update(os.listdir(scene_files_folder))
|
||||
|
||||
files = []
|
||||
for src_file in tqdm(
|
||||
self.partial_movie_files,
|
||||
desc=f"Copying animation files to '{scene_files_folder}' and generating reversed animations",
|
||||
leave=self.leave_progress_bar,
|
||||
ascii=True if platform.system() == "Windows" else None,
|
||||
disable=not self.show_progress_bar,
|
||||
):
|
||||
filename = os.path.basename(src_file)
|
||||
rev_filename = "{}_reversed{}".format(*os.path.splitext(filename))
|
||||
|
||||
dst_file = os.path.join(scene_files_folder, filename)
|
||||
# We only copy animation if it was not present
|
||||
if filename in old_animation_files:
|
||||
old_animation_files.remove(filename)
|
||||
else:
|
||||
shutil.copyfile(src_file, dst_file)
|
||||
|
||||
# We only reverse video if it was not present
|
||||
if rev_filename in old_animation_files:
|
||||
old_animation_files.remove(rev_filename)
|
||||
else:
|
||||
rev_file = os.path.join(scene_files_folder, rev_filename)
|
||||
reverse_video_file(src_file, rev_file)
|
||||
|
||||
files.append(dst_file)
|
||||
|
||||
logger.info(
|
||||
f"Copied {len(files)} animations to '{os.path.abspath(scene_files_folder)}' and generated reversed animations"
|
||||
)
|
||||
|
||||
slide_path = os.path.join(self.output_folder, "%s.json" % (scene_name,))
|
||||
|
||||
with open(slide_path, "w") as f:
|
||||
f.write(PresentationConfig(slides=self.slides, files=files).json(indent=2))
|
||||
|
||||
logger.info(
|
||||
f"Slide '{scene_name}' configuration written in '{os.path.abspath(slide_path)}'"
|
||||
)
|
||||
|
||||
def run(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""MANIMGL renderer"""
|
||||
super().run(*args, **kwargs)
|
||||
self.save_slides(use_cache=False)
|
||||
|
||||
def render(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""MANIM render"""
|
||||
# We need to disable the caching limit since we rely on intermidiate files
|
||||
max_files_cached = config["max_files_cached"]
|
||||
config["max_files_cached"] = float("inf")
|
||||
|
||||
super().render(*args, **kwargs)
|
||||
|
||||
config["max_files_cached"] = max_files_cached
|
||||
|
||||
self.save_slides()
|
||||
|
||||
|
||||
class ThreeDSlide(Slide, ThreeDScene):
|
||||
"""
|
||||
Inherits from `manim.ThreeDScene` or `manimlib.ThreeDScene` and provide necessary tools for slides rendering.
|
||||
|
||||
Note that ManimGL does not need ThreeDScene for 3D rendering in recent versions, see `example.py`.
|
||||
"""
|
||||
|
||||
pass
|
63
manim_slides/slide/__init__.py
Normal file
63
manim_slides/slide/__init__.py
Normal file
@ -0,0 +1,63 @@
|
||||
__all__ = [
|
||||
"API_NAME",
|
||||
"MANIM",
|
||||
"MANIMGL",
|
||||
"Slide",
|
||||
"ThreeDSlide",
|
||||
]
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
class ManimApiNotFoundError(ImportError):
|
||||
"""Error raised if specified manim API could be imported."""
|
||||
|
||||
_msg = "Could not import the specified manim API"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(self._msg)
|
||||
|
||||
|
||||
API_NAMES = {
|
||||
"manim": "manim",
|
||||
"manimce": "manim",
|
||||
"manimlib": "manimlib",
|
||||
"manimgl": "manimlib",
|
||||
}
|
||||
|
||||
MANIM_API: str = "MANIM_API"
|
||||
FORCE_MANIM_API: str = "FORCE_" + MANIM_API
|
||||
|
||||
API: str = os.environ.get(MANIM_API, "manim").lower()
|
||||
|
||||
|
||||
if API not in API_NAMES:
|
||||
raise ImportError(
|
||||
f"Specified MANIM_API={API!r} is not in valid options: {API_NAMES}",
|
||||
)
|
||||
|
||||
API_NAME = API_NAMES[API]
|
||||
|
||||
if not os.environ.get(FORCE_MANIM_API):
|
||||
if "manim" in sys.modules:
|
||||
API_NAME = "manim"
|
||||
elif "manimlib" in sys.modules:
|
||||
API_NAME = "manimlib"
|
||||
|
||||
MANIM: bool = API_NAME == "manim"
|
||||
MANIMGL: bool = API_NAME == "manimlib"
|
||||
|
||||
if MANIM:
|
||||
try:
|
||||
from .manim import Slide, ThreeDSlide
|
||||
except ImportError as e:
|
||||
raise ManimApiNotFoundError from e
|
||||
elif MANIMGL:
|
||||
try:
|
||||
from .manimlib import Slide, ThreeDSlide
|
||||
except ImportError as e:
|
||||
raise ManimApiNotFoundError from e
|
||||
else:
|
||||
raise ManimApiNotFoundError
|
162
manim_slides/slide/animation.py
Normal file
162
manim_slides/slide/animation.py
Normal file
@ -0,0 +1,162 @@
|
||||
"""
|
||||
Additional animations for Manim objects.
|
||||
|
||||
Like with Manim, animations are classes that must be put inside a
|
||||
:meth:`Scene.play<manim.scene.scene.Scene.play>` call.
|
||||
|
||||
For each of the provided classes, there exists a method variant
|
||||
that directly calls ``self.play(Animation(...))``, see
|
||||
:class:`Slide<manim_slides.slide.Slide>`.
|
||||
"""
|
||||
|
||||
__all__ = ["Wipe", "Zoom"]
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import MANIM
|
||||
|
||||
if MANIM:
|
||||
from manim import LEFT, AnimationGroup, FadeIn, FadeOut
|
||||
from manim.mobject.mobject import Mobject
|
||||
else:
|
||||
from manimlib import LEFT, AnimationGroup, FadeIn, FadeOut
|
||||
|
||||
Mobject = Any
|
||||
|
||||
|
||||
class Wipe(AnimationGroup): # type: ignore[misc]
|
||||
"""
|
||||
Creates a wipe animation that will shift all the current objects and future objects
|
||||
by a given value.
|
||||
|
||||
:param current: A sequence of mobjects to remove from the scene.
|
||||
:param future: A sequence of mobjects to add to the scene.
|
||||
:param shift: The shift vector, used for both fading in and out.
|
||||
:param fade_in_kwargs: Keyword arguments passed to
|
||||
:class:`FadeIn<manim.animation.fading.FadeIn>`.
|
||||
:param fade_out_kwargs: Keyword arguments passed to
|
||||
:class:`FadeOut<manim.animation.fading.FadeOut>`.
|
||||
:param kwargs: Keyword arguments passed to
|
||||
:class:`AnimationGroup<manim.animation.composition.AnimationGroup>`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim-slides:: WipeClassExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
from manim_slides.slide.animation import Wipe
|
||||
|
||||
class WipeClassExample(Slide):
|
||||
def construct(self):
|
||||
circle = Circle(radius=3, color=BLUE)
|
||||
square = Square()
|
||||
|
||||
self.play(FadeIn(circle))
|
||||
self.next_slide()
|
||||
|
||||
self.play(Wipe(circle, square, shift=3 * LEFT))
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
current: Optional[Sequence[Mobject]] = None,
|
||||
future: Optional[Sequence[Mobject]] = None,
|
||||
shift: np.ndarray = LEFT,
|
||||
fade_in_kwargs: Optional[Mapping[str, Any]] = None,
|
||||
fade_out_kwargs: Optional[Mapping[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
animations = []
|
||||
|
||||
if future:
|
||||
if fade_in_kwargs is None:
|
||||
fade_in_kwargs = {}
|
||||
|
||||
for mobject in future:
|
||||
animations.append(FadeIn(mobject, shift=shift, **fade_in_kwargs))
|
||||
|
||||
if current:
|
||||
if fade_out_kwargs is None:
|
||||
fade_out_kwargs = {}
|
||||
|
||||
for mobject in current:
|
||||
animations.append(FadeOut(mobject, shift=shift, **fade_out_kwargs))
|
||||
|
||||
super().__init__(*animations, **kwargs)
|
||||
|
||||
|
||||
class Zoom(AnimationGroup): # type: ignore[misc]
|
||||
"""
|
||||
Creates a zoom animation that will fade out all the current objects, and fade in all
|
||||
the future objects. Objects are faded in a direction that goes towards the camera.
|
||||
|
||||
:param current: A sequence of mobjects to remove from the scene.
|
||||
:param future: A sequence of mobjects to add to the scene.
|
||||
:param scale: How much the objects are scaled (up or down).
|
||||
:param out: If set, the objects fade in the opposite direction.
|
||||
:param fade_in_kwargs: Keyword arguments passed to
|
||||
:class:`FadeIn<manim.animation.fading.FadeIn>`.
|
||||
:param fade_out_kwargs: Keyword arguments passed to
|
||||
:class:`FadeOut<manim.animation.fading.FadeOut>`.
|
||||
:param kwargs: Keyword arguments passed to
|
||||
:class:`AnimationGroup<manim.animation.composition.AnimationGroup>`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim-slides:: ZoomClassExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
from manim_slides.slide.animation import Zoom
|
||||
|
||||
class ZoomClassExample(Slide):
|
||||
def construct(self):
|
||||
circles = [Circle(radius=i) for i in range(1, 4)]
|
||||
|
||||
self.play(FadeIn(circles[0]))
|
||||
self.next_slide()
|
||||
|
||||
for i in range(2):
|
||||
self.play(Zoom(circles[i], circles[i+1]))
|
||||
self.next_slide()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
current: Optional[Sequence[Mobject]] = None,
|
||||
future: Optional[Sequence[Mobject]] = None,
|
||||
scale: float = 4.0,
|
||||
out: bool = False,
|
||||
fade_in_kwargs: Optional[Mapping[str, Any]] = None,
|
||||
fade_out_kwargs: Optional[Mapping[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
scale_in = 1.0 / scale
|
||||
scale_out = scale
|
||||
|
||||
if out:
|
||||
scale_in, scale_out = scale_out, scale_in
|
||||
|
||||
animations = []
|
||||
|
||||
if future:
|
||||
if fade_in_kwargs is None:
|
||||
fade_in_kwargs = {}
|
||||
|
||||
for mobject in future:
|
||||
animations.append(FadeIn(mobject, scale=scale_in, **fade_in_kwargs))
|
||||
|
||||
if current:
|
||||
if fade_out_kwargs is None:
|
||||
fade_out_kwargs = {}
|
||||
|
||||
for mobject in current:
|
||||
animations.append(FadeOut(mobject, scale=scale_out, **fade_out_kwargs))
|
||||
|
||||
super().__init__(*animations, **kwargs)
|
755
manim_slides/slide/base.py
Normal file
755
manim_slides/slide/base.py
Normal file
@ -0,0 +1,755 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["BaseSlide"]
|
||||
|
||||
import platform
|
||||
import shutil
|
||||
from abc import abstractmethod
|
||||
from collections.abc import MutableMapping, Sequence, ValuesView
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from ..config import BaseSlideConfig, PresentationConfig, PreSlideConfig, SlideConfig
|
||||
from ..defaults import FOLDER_PATH
|
||||
from ..logger import logger
|
||||
from ..utils import concatenate_video_files, merge_basenames, reverse_video_file
|
||||
from . import MANIM
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .animation import Wipe, Zoom
|
||||
|
||||
if MANIM:
|
||||
from manim.mobject.mobject import Mobject
|
||||
else:
|
||||
Mobject = Any
|
||||
|
||||
LEFT: np.ndarray = np.array([-1.0, 0.0, 0.0])
|
||||
|
||||
|
||||
class BaseSlide:
|
||||
disable_caching: bool = False
|
||||
flush_cache: bool = False
|
||||
skip_reversing: bool = False
|
||||
max_duration_before_split_reverse: float | None = 4.0
|
||||
num_processes: int | None = None
|
||||
|
||||
def __init__(
|
||||
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._output_folder: Path = output_folder
|
||||
self._slides: list[PreSlideConfig] = []
|
||||
self._base_slide_config: BaseSlideConfig = BaseSlideConfig()
|
||||
self._current_slide = 1
|
||||
self._current_animation = 0
|
||||
self._start_animation = 0
|
||||
self._canvas: MutableMapping[str, Mobject] = {}
|
||||
self._wait_time_between_slides = 0.0
|
||||
self._skip_animations = False
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _frame_height(self) -> float:
|
||||
"""Return the scene's frame height."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _frame_width(self) -> float:
|
||||
"""Return the scene's frame width."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _background_color(self) -> str:
|
||||
"""Return the scene's background color."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _resolution(self) -> tuple[int, int]:
|
||||
"""Return the scene's resolution used during rendering."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _partial_movie_files(self) -> list[Path]:
|
||||
"""Return a list of partial movie files, a.k.a animations."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _show_progress_bar(self) -> bool:
|
||||
"""Return True if progress bar should be displayed."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _leave_progress_bar(self) -> bool:
|
||||
"""Return True if progress bar should be left after completed."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _start_at_animation_number(self) -> int | None:
|
||||
"""If set, return the animation number at which rendering start."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def canvas(self) -> MutableMapping[str, Mobject]:
|
||||
"""
|
||||
Return the canvas associated to the current slide.
|
||||
|
||||
The canvas is a mapping between names and Mobjects,
|
||||
for objects that are assumed to stay in multiple slides.
|
||||
|
||||
For example, a section title or a slide number.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim-slides:: CanvasExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class CanvasExample(Slide):
|
||||
def update_canvas(self):
|
||||
self.counter += 1
|
||||
old_slide_number = self.canvas["slide_number"]
|
||||
new_slide_number = Text(f"{self.counter}").move_to(old_slide_number)
|
||||
self.play(Transform(old_slide_number, new_slide_number))
|
||||
|
||||
def construct(self):
|
||||
title = Text("My Title").to_corner(UL)
|
||||
|
||||
self.counter = 1
|
||||
slide_number = Text("1").to_corner(DL)
|
||||
|
||||
self.add_to_canvas(title=title, slide_number=slide_number)
|
||||
|
||||
self.play(FadeIn(title), FadeIn(slide_number))
|
||||
self.next_slide()
|
||||
|
||||
circle = Circle(radius=2)
|
||||
dot = Dot()
|
||||
|
||||
self.update_canvas()
|
||||
self.play(Create(circle))
|
||||
self.play(MoveAlongPath(dot, circle))
|
||||
|
||||
self.next_slide()
|
||||
self.update_canvas()
|
||||
|
||||
square = Square()
|
||||
|
||||
self.wipe(self.mobjects_without_canvas, square)
|
||||
self.next_slide()
|
||||
|
||||
self.update_canvas()
|
||||
self.play(
|
||||
Transform(
|
||||
self.canvas["title"],
|
||||
Text("New Title").to_corner(UL)
|
||||
)
|
||||
)
|
||||
self.next_slide()
|
||||
|
||||
self.remove_from_canvas("title", "slide_number")
|
||||
self.wipe(self.mobjects_without_canvas, [])
|
||||
|
||||
"""
|
||||
return self._canvas
|
||||
|
||||
def add_to_canvas(self, **objects: Mobject) -> None:
|
||||
"""
|
||||
Add objects to the canvas, using key values as names.
|
||||
|
||||
:param objects: A mapping between names and Mobjects.
|
||||
|
||||
.. note::
|
||||
|
||||
This method does not actually do anything in terms of
|
||||
animations. You must still call :code:`self.add` or
|
||||
play some animation that introduces each Mobject for
|
||||
it to appear. The same applies when removing objects.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`canvas` for usage examples.
|
||||
"""
|
||||
self._canvas.update(objects)
|
||||
|
||||
def remove_from_canvas(self, *names: str) -> None:
|
||||
"""
|
||||
Remove objects from the canvas.
|
||||
|
||||
:param names: The names of objects to remove.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`canvas` for usage examples.
|
||||
"""
|
||||
for name in names:
|
||||
self._canvas.pop(name)
|
||||
|
||||
@property
|
||||
def canvas_mobjects(self) -> ValuesView[Mobject]:
|
||||
"""Return Mobjects contained in the canvas."""
|
||||
return self.canvas.values()
|
||||
|
||||
@property
|
||||
def mobjects_without_canvas(self) -> Sequence[Mobject]:
|
||||
"""
|
||||
Return the list of Mobjects contained in the scene, minus those present in
|
||||
the canvas.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`canvas` for usage examples.
|
||||
"""
|
||||
return [
|
||||
mobject
|
||||
for mobject in self.mobjects # type: ignore[attr-defined]
|
||||
if mobject not in self.canvas_mobjects
|
||||
]
|
||||
|
||||
@property
|
||||
def wait_time_between_slides(self) -> float:
|
||||
r"""
|
||||
Return the wait duration (in seconds) added between two slides.
|
||||
|
||||
By default, this value is set to 0.
|
||||
|
||||
Setting this value to something bigger than 0 will result in a
|
||||
:code:`self.wait` animation called at the end of every slide.
|
||||
|
||||
.. note::
|
||||
This is useful because animations are usually only terminated
|
||||
when a new animation is played. You can observe the small difference
|
||||
in the examples below: the circle is not fully complete in the first
|
||||
slide of the first example, but well in the second example.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim-slides:: WithoutWaitExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class WithoutWaitExample(Slide):
|
||||
def construct(self):
|
||||
circle = Circle(radius=2)
|
||||
arrow = Arrow().next_to(circle, RIGHT).scale(-1)
|
||||
text = Text("Small\ngap").next_to(arrow, RIGHT)
|
||||
|
||||
self.play(Create(arrow), FadeIn(text))
|
||||
self.play(Create(circle))
|
||||
self.next_slide()
|
||||
|
||||
self.play(FadeOut(circle))
|
||||
|
||||
.. manim-slides:: WithWaitExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class WithWaitExample(Slide):
|
||||
def construct(self):
|
||||
self.wait_time_between_slides = 0.1 # A small value > 1 / FPS
|
||||
circle = Circle(radius=2)
|
||||
arrow = Arrow().next_to(circle, RIGHT).scale(-1)
|
||||
text = Text("No more\ngap").next_to(arrow, RIGHT)
|
||||
|
||||
self.play(Create(arrow), FadeIn(text))
|
||||
self.play(Create(circle))
|
||||
self.next_slide()
|
||||
|
||||
self.play(FadeOut(circle))
|
||||
|
||||
"""
|
||||
return self._wait_time_between_slides
|
||||
|
||||
@wait_time_between_slides.setter
|
||||
def wait_time_between_slides(self, wait_time: float) -> None:
|
||||
self._wait_time_between_slides = max(wait_time, 0.0)
|
||||
|
||||
def play(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Overload 'self.play' and increment animation count."""
|
||||
super().play(*args, **kwargs) # type: ignore[misc]
|
||||
self._current_animation += 1
|
||||
|
||||
@BaseSlideConfig.wrapper("base_slide_config")
|
||||
def next_slide(
|
||||
self,
|
||||
*,
|
||||
base_slide_config: BaseSlideConfig,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Create a new slide with previous animations, and setup options
|
||||
for the next slide.
|
||||
|
||||
This usually means that the user will need to press some key before the
|
||||
next slide is played. By default, this is the right arrow key.
|
||||
|
||||
:param args:
|
||||
Positional arguments passed to
|
||||
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
||||
or ignored if `manimlib` API is used.
|
||||
:param skip_animations:
|
||||
Exclude the next slide from the output.
|
||||
|
||||
If `manim` is used, this is also passed to :meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
||||
which will avoid rendering the corresponding animations.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`start_skip_animations`
|
||||
:meth:`stop_skip_animations`
|
||||
:param loop:
|
||||
If set, next slide will be looping.
|
||||
:param auto_next:
|
||||
If set, next slide will play immediately play the next slide
|
||||
upon terminating.
|
||||
|
||||
.. warning::
|
||||
|
||||
Only supported by ``manim-slides present``
|
||||
and ``manim-slides convert --to=html``.
|
||||
:param playback_rate:
|
||||
Playback rate at which the video is played.
|
||||
|
||||
.. warning::
|
||||
|
||||
Only supported by ``manim-slides present``.
|
||||
:param reversed_playback_rate:
|
||||
Playback rate at which the reversed video is played.
|
||||
|
||||
.. warning::
|
||||
|
||||
Only supported by ``manim-slides present``.
|
||||
:param notes:
|
||||
Presenter notes, in Markdown format.
|
||||
|
||||
.. note::
|
||||
PowerPoint does not support Markdown formatting,
|
||||
so the text will be displayed as is.
|
||||
|
||||
.. warning::
|
||||
|
||||
Only supported by ``manim-slides present``,
|
||||
``manim-slides convert --to=html`` and
|
||||
``manim-slides convert --to=pptx``.
|
||||
:param dedent_notes:
|
||||
If set, apply :func:`textwrap.dedent` to notes.
|
||||
:param pathlib.Path src:
|
||||
An optional path to a video file to include as next slide.
|
||||
|
||||
The video will be copied into the output folder, but no rescaling
|
||||
is applied.
|
||||
:param kwargs:
|
||||
Keyword arguments passed to
|
||||
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
|
||||
or ignored if `manimlib` API is used.
|
||||
|
||||
.. note::
|
||||
|
||||
Calls to :func:`next_slide` at the very beginning or at the end are
|
||||
not needed, since they are automatically added.
|
||||
|
||||
.. warning::
|
||||
|
||||
When rendered with RevealJS, loops cannot be in the first nor
|
||||
the last slide.
|
||||
|
||||
.. seealso::
|
||||
|
||||
When using ``manim`` API, this method will also call
|
||||
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following contains 3 slides:
|
||||
|
||||
#. the first with nothing on it;
|
||||
#. the second with "Hello World!" fading in;
|
||||
#. and the last with the text fading out;
|
||||
|
||||
.. manim-slides:: NextSlideExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class NextSlideExample(Slide):
|
||||
def construct(self):
|
||||
text = Text("Hello World!")
|
||||
|
||||
self.play(FadeIn(text))
|
||||
|
||||
self.next_slide()
|
||||
self.play(FadeOut(text))
|
||||
|
||||
The following contains one slide that will loop endlessly.
|
||||
|
||||
.. manim-slides:: LoopExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class LoopExample(Slide):
|
||||
def construct(self):
|
||||
dot = Dot(color=BLUE, radius=1)
|
||||
|
||||
self.play(FadeIn(dot))
|
||||
|
||||
self.next_slide(loop=True)
|
||||
|
||||
self.play(Indicate(dot, scale_factor=2))
|
||||
|
||||
self.next_slide()
|
||||
|
||||
self.play(FadeOut(dot))
|
||||
|
||||
The following contains one slide that triggers the next slide
|
||||
upon terminating.
|
||||
|
||||
.. manim-slides:: AutoNextExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class AutoNextExample(Slide):
|
||||
def construct(self):
|
||||
square = Square(color=RED, side_length=2)
|
||||
|
||||
self.play(GrowFromCenter(square))
|
||||
|
||||
self.next_slide(auto_next=True)
|
||||
|
||||
self.play(Wiggle(square))
|
||||
|
||||
self.next_slide()
|
||||
|
||||
self.wipe(square)
|
||||
|
||||
The following contains speaker notes. On the webbrowser,
|
||||
the speaker view can be triggered by pressing :kbd:`S`.
|
||||
|
||||
.. manim-slides:: SpeakerNotesExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class SpeakerNotesExample(Slide):
|
||||
def construct(self):
|
||||
self.next_slide(notes="Some introduction")
|
||||
square = Square(color=GREEN, side_length=2)
|
||||
|
||||
self.play(GrowFromCenter(square))
|
||||
|
||||
self.next_slide(notes="We now rotate the slide")
|
||||
|
||||
self.play(Rotate(square, PI / 2))
|
||||
|
||||
self.next_slide(notes="Bye bye")
|
||||
|
||||
self.zoom(square)
|
||||
|
||||
"""
|
||||
if self._current_animation > self._start_animation:
|
||||
if self.wait_time_between_slides > 0.0:
|
||||
self.wait(self.wait_time_between_slides) # type: ignore[attr-defined]
|
||||
|
||||
self._slides.append(
|
||||
PreSlideConfig.from_base_slide_config_and_animation_indices(
|
||||
self._base_slide_config,
|
||||
self._start_animation,
|
||||
self._current_animation,
|
||||
)
|
||||
)
|
||||
|
||||
self._current_slide += 1
|
||||
|
||||
if base_slide_config.src is not None:
|
||||
self._slides.append(
|
||||
PreSlideConfig.from_base_slide_config_and_animation_indices(
|
||||
base_slide_config,
|
||||
self._current_animation,
|
||||
self._current_animation,
|
||||
)
|
||||
)
|
||||
|
||||
base_slide_config = BaseSlideConfig() # default
|
||||
self._current_slide += 1
|
||||
|
||||
if self._skip_animations:
|
||||
base_slide_config.skip_animations = True
|
||||
|
||||
self._base_slide_config = base_slide_config
|
||||
self._start_animation = self._current_animation
|
||||
|
||||
def _add_last_slide(self) -> None:
|
||||
"""Add a 'last' slide to the end of slides."""
|
||||
if (
|
||||
len(self._slides) > 0
|
||||
and self._current_animation == self._slides[-1].end_animation
|
||||
):
|
||||
return
|
||||
|
||||
self._slides.append(
|
||||
PreSlideConfig.from_base_slide_config_and_animation_indices(
|
||||
self._base_slide_config,
|
||||
self._start_animation,
|
||||
self._current_animation,
|
||||
)
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
.. warning:
|
||||
Caching files only work with Manim.
|
||||
"""
|
||||
self._add_last_slide()
|
||||
|
||||
files_folder = self._output_folder / "files"
|
||||
|
||||
scene_name = str(self)
|
||||
scene_files_folder = files_folder / scene_name
|
||||
|
||||
if flush_cache and scene_files_folder.exists():
|
||||
shutil.rmtree(scene_files_folder)
|
||||
|
||||
scene_files_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
files: list[Path] = self._partial_movie_files
|
||||
|
||||
# We must filter slides that end before the animation offset
|
||||
if offset := self._start_at_animation_number:
|
||||
self._slides = [
|
||||
slide for slide in self._slides if slide.end_animation > offset
|
||||
]
|
||||
for slide in self._slides:
|
||||
slide.start_animation = max(0, slide.start_animation - offset)
|
||||
slide.end_animation -= offset
|
||||
|
||||
slides: list[SlideConfig] = []
|
||||
|
||||
for pre_slide_config in tqdm(
|
||||
self._slides,
|
||||
desc=f"Concatenating animations to '{scene_files_folder}' and generating reversed animations",
|
||||
leave=self._leave_progress_bar,
|
||||
ascii=True if platform.system() == "Windows" else None,
|
||||
disable=not self._show_progress_bar,
|
||||
unit=" slides",
|
||||
):
|
||||
if pre_slide_config.skip_animations:
|
||||
continue
|
||||
if pre_slide_config.src:
|
||||
slide_files = [pre_slide_config.src]
|
||||
else:
|
||||
slide_files = files[pre_slide_config.slides_slice]
|
||||
|
||||
try:
|
||||
file = merge_basenames(slide_files)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
f"Failed to merge basenames of files for slide: {pre_slide_config!r}"
|
||||
) from e
|
||||
dst_file = scene_files_folder / file.name
|
||||
rev_file = scene_files_folder / f"{file.stem}_reversed{file.suffix}"
|
||||
|
||||
# We only concat animations if it was not present
|
||||
if not use_cache or not dst_file.exists():
|
||||
concatenate_video_files(slide_files, dst_file)
|
||||
|
||||
# We only reverse video if it was not present
|
||||
if not use_cache or not rev_file.exists():
|
||||
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(
|
||||
SlideConfig.from_pre_slide_config_and_files(
|
||||
pre_slide_config, dst_file, rev_file
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Generated {len(slides)} slides to '{scene_files_folder.absolute()}'"
|
||||
)
|
||||
|
||||
slide_path = self._output_folder / f"{scene_name}.json"
|
||||
|
||||
PresentationConfig(
|
||||
slides=slides,
|
||||
resolution=self._resolution,
|
||||
background_color=self._background_color,
|
||||
).to_file(slide_path)
|
||||
|
||||
logger.info(
|
||||
f"Slide '{scene_name}' configuration written in '{slide_path.absolute()}'"
|
||||
)
|
||||
|
||||
def start_skip_animations(self) -> None:
|
||||
"""
|
||||
Start skipping animations.
|
||||
|
||||
This automatically applies ``skip_animations=True``
|
||||
to all subsequent calls to :meth:`next_slide`.
|
||||
|
||||
This is useful when you want to skip animations from multiple slides in a row,
|
||||
without having to manually set ``skip_animations=True``.
|
||||
"""
|
||||
self._skip_animations = True
|
||||
|
||||
def stop_skip_animations(self) -> None:
|
||||
"""Stop skipping animations."""
|
||||
self._skip_animations = False
|
||||
|
||||
def wipe(
|
||||
self,
|
||||
*args: Any,
|
||||
direction: np.ndarray = LEFT,
|
||||
return_animation: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Wipe | None:
|
||||
"""
|
||||
Play a wipe animation that will shift all the current objects outside of the
|
||||
current scene's scope, and all the future objects inside.
|
||||
|
||||
:param args: Positional arguments passed to
|
||||
:class:`Wipe<manim_slides.slide.animation.Wipe>`.
|
||||
:param direction: The wipe direction, that will be scaled by the scene size.
|
||||
:param return_animation: If set, return the animation instead of
|
||||
playing it. This is useful to combine multiple animations with this one.
|
||||
:param kwargs: Keyword arguments passed to
|
||||
:class:`Wipe<manim_slides.slide.animation.Wipe>`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim-slides:: WipeExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class WipeExample(Slide):
|
||||
def construct(self):
|
||||
circle = Circle(radius=3, color=BLUE)
|
||||
square = Square()
|
||||
text = Text("This is a wipe example").next_to(square, DOWN)
|
||||
beautiful = Text("Beautiful, no?")
|
||||
|
||||
self.play(FadeIn(circle))
|
||||
self.next_slide()
|
||||
|
||||
self.wipe(circle, Group(square, text))
|
||||
self.next_slide()
|
||||
|
||||
self.wipe(Group(square, text), beautiful, direction=UP)
|
||||
self.next_slide()
|
||||
|
||||
anim = self.wipe(
|
||||
beautiful,
|
||||
circle,
|
||||
direction=DOWN + RIGHT,
|
||||
return_animation=True
|
||||
)
|
||||
self.play(anim)
|
||||
|
||||
"""
|
||||
from .animation import Wipe
|
||||
|
||||
shift_amount = np.asarray(direction) * np.array(
|
||||
[self._frame_width, self._frame_height, 0.0]
|
||||
)
|
||||
|
||||
kwargs.setdefault("shift", shift_amount)
|
||||
|
||||
animation = Wipe(
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if return_animation:
|
||||
return animation
|
||||
|
||||
self.play(animation)
|
||||
return None
|
||||
|
||||
def zoom(
|
||||
self,
|
||||
*args: Any,
|
||||
return_animation: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Zoom | None:
|
||||
"""
|
||||
Play a zoom animation that will fade out all the current objects, and fade in
|
||||
all the future objects. Objects are faded in a direction that goes towards the
|
||||
camera.
|
||||
|
||||
:param args: Positional arguments passed to
|
||||
:class:`Zoom<manim_slides.slide.animation.Zoom>`.
|
||||
:param return_animation: If set, return the animation instead of
|
||||
playing it. This is useful to combine multiple animations with this one.
|
||||
:param kwargs: Keyword arguments passed to
|
||||
:class:`Zoom<manim_slides.slide.animation.Zoom>`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim-slides:: ZoomExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import Slide
|
||||
|
||||
class ZoomExample(Slide):
|
||||
def construct(self):
|
||||
circle = Circle(radius=3, color=BLUE)
|
||||
square = Square()
|
||||
|
||||
self.play(FadeIn(circle))
|
||||
self.next_slide()
|
||||
|
||||
self.zoom(circle, square)
|
||||
self.next_slide()
|
||||
|
||||
anim = self.zoom(
|
||||
square,
|
||||
circle,
|
||||
out=True,
|
||||
scale=10.0,
|
||||
return_animation=True
|
||||
)
|
||||
self.play(anim)
|
||||
|
||||
"""
|
||||
from .animation import Zoom
|
||||
|
||||
animation = Zoom(*args, **kwargs)
|
||||
|
||||
if return_animation:
|
||||
return animation
|
||||
|
||||
self.play(animation)
|
||||
return None
|
227
manim_slides/slide/manim.py
Normal file
227
manim_slides/slide/manim.py
Normal file
@ -0,0 +1,227 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from manim import Scene, ThreeDScene, config
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
from manim.utils.color import rgba_to_color
|
||||
|
||||
from ..config import BaseSlideConfig
|
||||
from .base import BaseSlide
|
||||
|
||||
|
||||
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
"""
|
||||
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provides necessary tools
|
||||
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
|
||||
def _frame_height(self) -> float:
|
||||
return self._frame_shape[0]
|
||||
|
||||
@property
|
||||
def _frame_width(self) -> float:
|
||||
return self._frame_shape[1]
|
||||
|
||||
@property
|
||||
def _background_color(self) -> str:
|
||||
if isinstance(self.renderer, OpenGLRenderer):
|
||||
return rgba_to_color(self.renderer.background_color).to_hex() # type: ignore
|
||||
else:
|
||||
return self.renderer.camera.background_color.to_hex() # type: ignore
|
||||
|
||||
@property
|
||||
def _resolution(self) -> tuple[int, int]:
|
||||
if isinstance(self.renderer, OpenGLRenderer):
|
||||
return self.renderer.get_pixel_shape() # type: ignore
|
||||
else:
|
||||
return (
|
||||
self.renderer.camera.pixel_width,
|
||||
self.renderer.camera.pixel_height,
|
||||
)
|
||||
|
||||
@property
|
||||
def _partial_movie_files(self) -> list[Path]:
|
||||
# When rendering with -na,b (manim only)
|
||||
# the animations not in [a,b] will be skipped,
|
||||
# but animation before a will have a None source file.
|
||||
return [
|
||||
Path(file)
|
||||
for file in self.renderer.file_writer.partial_movie_files
|
||||
if file is not None
|
||||
]
|
||||
|
||||
@property
|
||||
def _show_progress_bar(self) -> bool:
|
||||
return config["progress_bar"] != "none" # type: ignore
|
||||
|
||||
@property
|
||||
def _leave_progress_bar(self) -> bool:
|
||||
return config["progress_bar"] == "leave" # type: ignore
|
||||
|
||||
@property
|
||||
def _start_at_animation_number(self) -> Optional[int]:
|
||||
return config["from_animation_number"] # type: ignore
|
||||
|
||||
def play(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Overload 'self.play' and increment animation count."""
|
||||
super().play(*args, **kwargs)
|
||||
|
||||
if self._base_slide_config.skip_animations:
|
||||
# Manim will not render the animations, so we reset the animation
|
||||
# counter to the previous value
|
||||
self._current_animation -= 1
|
||||
|
||||
def next_section(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
Alias to :meth:`next_slide`.
|
||||
|
||||
:param args:
|
||||
Positional arguments to be passed to :meth:`next_slide`.
|
||||
:param kwargs:
|
||||
Keyword arguments to be passed to :meth:`next_slide`.
|
||||
|
||||
.. attention::
|
||||
|
||||
This method is only available when using ``manim`` API.
|
||||
"""
|
||||
self.next_slide(*args, **kwargs)
|
||||
|
||||
@BaseSlideConfig.wrapper("base_slide_config")
|
||||
def next_slide(
|
||||
self,
|
||||
*args: Any,
|
||||
base_slide_config: BaseSlideConfig,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
Scene.next_section(
|
||||
self,
|
||||
*args,
|
||||
skip_animations=base_slide_config.skip_animations | self._skip_animations,
|
||||
**kwargs,
|
||||
)
|
||||
BaseSlide.next_slide.__wrapped__(
|
||||
self,
|
||||
base_slide_config=base_slide_config,
|
||||
)
|
||||
|
||||
def render(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""MANIM renderer."""
|
||||
# We need to disable the caching limit since we rely on intermediate files
|
||||
max_files_cached = config["max_files_cached"]
|
||||
config["max_files_cached"] = float("inf")
|
||||
|
||||
flush_manim_cache = config["flush_cache"]
|
||||
|
||||
if flush_manim_cache:
|
||||
# We need to postpone flushing *after* we saved slides
|
||||
config["flush_cache"] = False
|
||||
|
||||
super().render(*args, **kwargs)
|
||||
|
||||
config["max_files_cached"] = max_files_cached
|
||||
|
||||
self._save_slides(
|
||||
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]
|
||||
"""
|
||||
Inherits from :class:`Slide` and
|
||||
:class:`ThreeDScene<manim.scene.three_d_scene.ThreeDScene>` and provide necessary
|
||||
tools for slides rendering.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim-slides:: ThreeDExample
|
||||
|
||||
from manim import *
|
||||
from manim_slides import ThreeDSlide
|
||||
|
||||
class ThreeDExample(ThreeDSlide):
|
||||
def construct(self):
|
||||
title = Text("A 2D Text")
|
||||
|
||||
self.play(FadeIn(title))
|
||||
self.next_slide()
|
||||
|
||||
sphere = Sphere([0, 0, -3])
|
||||
|
||||
self.move_camera(phi=PI/3, theta=-PI/4, distance=7)
|
||||
self.play(
|
||||
GrowFromCenter(sphere),
|
||||
Transform(title, Text("A 3D Text"))
|
||||
)
|
||||
self.next_slide()
|
||||
|
||||
bye = Text("Bye!")
|
||||
|
||||
self.next_slide(loop=True)
|
||||
self.wipe(
|
||||
self.mobjects_without_canvas,
|
||||
[bye],
|
||||
direction=UP
|
||||
)
|
||||
self.wait(.5)
|
||||
self.wipe(
|
||||
self.mobjects_without_canvas,
|
||||
[title, sphere],
|
||||
direction=DOWN
|
||||
)
|
||||
self.wait(.5)
|
||||
self.next_slide()
|
||||
|
||||
self.play(*[FadeOut(mobject) for mobject in self.mobjects])
|
||||
|
||||
"""
|
||||
|
||||
pass
|
73
manim_slides/slide/manimlib.py
Normal file
73
manim_slides/slide/manimlib.py
Normal file
@ -0,0 +1,73 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, Optional
|
||||
|
||||
from manimlib import Scene, ThreeDCamera
|
||||
|
||||
from .base import BaseSlide
|
||||
|
||||
|
||||
class Slide(BaseSlide, Scene): # type: ignore[misc]
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
kwargs.setdefault("file_writer_config", {}).update(
|
||||
subdivide_output=True,
|
||||
)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def _frame_height(self) -> float:
|
||||
return float(self.camera.get_frame_height())
|
||||
|
||||
@property
|
||||
def _frame_width(self) -> float:
|
||||
return float(self.camera.get_frame_width())
|
||||
|
||||
@property
|
||||
def _background_color(self) -> str:
|
||||
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
|
||||
def _resolution(self) -> tuple[int, int]:
|
||||
return self.camera.get_pixel_width(), self.camera.get_pixel_height()
|
||||
|
||||
@property
|
||||
def _partial_movie_files(self) -> list[Path]:
|
||||
partial_movie_directory = self.file_writer.partial_movie_directory
|
||||
extension = self.file_writer.movie_file_extension
|
||||
return sorted(partial_movie_directory.glob(f"*{extension}"))
|
||||
|
||||
@property
|
||||
def _show_progress_bar(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def _leave_progress_bar(self) -> bool:
|
||||
return self.leave_progress_bars # type: ignore
|
||||
|
||||
@property
|
||||
def _start_at_animation_number(self) -> Optional[int]:
|
||||
return self.start_at_animation_number # type: ignore
|
||||
|
||||
def run(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""MANIMGL renderer."""
|
||||
super().run(*args, **kwargs)
|
||||
self._save_slides(
|
||||
use_cache=False,
|
||||
flush_cache=self.flush_cache,
|
||||
skip_reversing=self.skip_reversing,
|
||||
)
|
||||
|
||||
|
||||
class ThreeDSlide(Slide):
|
||||
CONFIG: ClassVar[dict[str, Any]] = {
|
||||
"camera_class": ThreeDCamera,
|
||||
}
|
||||
pass
|
0
manim_slides/templates/__init__.py
Normal file
0
manim_slides/templates/__init__.py
Normal file
334
manim_slides/templates/revealjs.html
Normal file
334
manim_slides/templates/revealjs.html
Normal file
@ -0,0 +1,334 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
|
||||
<title>{{ title }}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/theme/{{ reveal_theme }}.min.css">
|
||||
|
||||
<!-- Theme used for syntax highlighting of code -->
|
||||
<!-- <link rel="stylesheet" href="lib/css/zenburn.css"> -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/zenburn.min.css">
|
||||
<!-- <link rel="stylesheet" href="index.css"> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="reveal">
|
||||
<div class="slides">
|
||||
{% for presentation_config in presentation_configs -%}
|
||||
{%- set outer_loop = loop %}
|
||||
{% for slide_config in presentation_config.slides %}
|
||||
{% if one_file %}
|
||||
{% set file = file_to_data_uri(slide_config.file) %}
|
||||
{% else %}
|
||||
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
|
||||
{% endif %}
|
||||
<section
|
||||
data-background-size={{ background_size }}
|
||||
data-background-color="{{ presentation_config.background_color }}"
|
||||
data-background-video="{{ file }}"
|
||||
{% if loop.index == 1 and outer_loop.index == 1 %}
|
||||
data-background-video-muted
|
||||
{% endif %}
|
||||
{% if slide_config.loop %}
|
||||
data-background-video-loop
|
||||
{% endif %}
|
||||
{% if slide_config.auto_next %}
|
||||
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
|
||||
{% endif %}
|
||||
>
|
||||
{% if slide_config.notes != "" %}
|
||||
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/reveal.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/notes/notes.min.js"></script>
|
||||
|
||||
<!-- To include plugins, see: https://revealjs.com/plugins/ -->
|
||||
{% if has_notes %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/{{ reveal_version }}/plugin/markdown/markdown.min.js"></script>
|
||||
{% endif %}
|
||||
|
||||
<!-- <script src="index.js"></script> -->
|
||||
<script>
|
||||
Reveal.initialize({
|
||||
{% if has_notes %}
|
||||
/// The list of RevealJS plugins.
|
||||
plugins: [ RevealMarkdown, RevealNotes ],
|
||||
{% endif %}
|
||||
// The "normal" size of the presentation, aspect ratio will
|
||||
// be preserved when the presentation is scaled to fit different
|
||||
// resolutions. Can be specified using percentage units.
|
||||
width: {{ width }},
|
||||
height: {{ height }},
|
||||
// Factor of the display size that should remain empty around
|
||||
// the content
|
||||
margin: {{ margin }},
|
||||
// Bounds for smallest/largest possible scale to apply to content
|
||||
minScale: {{ min_scale }},
|
||||
maxScale: {{ max_scale }},
|
||||
// Display presentation control arrows
|
||||
controls: {{ controls }},
|
||||
// Help the user learn the controls by providing hints, for example by
|
||||
// bouncing the down arrow when they first encounter a vertical slide
|
||||
controlsTutorial: {{ controls_tutorial }},
|
||||
// Determines where controls appear, "edges" or "bottom-right"
|
||||
controlsLayout: {{ controls_layout }},
|
||||
// Visibility rule for backwards navigation arrows; "faded", "hidden"
|
||||
// or "visible"
|
||||
controlsBackArrows: {{ controls_back_arrows }},
|
||||
// Display a presentation progress bar
|
||||
progress: {{ progress }},
|
||||
// Display the page number of the current slide
|
||||
// - true: Show slide number
|
||||
// - false: Hide slide number
|
||||
//
|
||||
// Can optionally be set as a string that specifies the number formatting:
|
||||
// - "h.v": Horizontal . vertical slide number (default)
|
||||
// - "h/v": Horizontal / vertical slide number
|
||||
// - "c": Flattened slide number
|
||||
// - "c/t": Flattened slide number / total slides
|
||||
//
|
||||
// Alternatively, you can provide a function that returns the slide
|
||||
// number for the current slide. The function should take in a slide
|
||||
// object and return an array with one string [slideNumber] or
|
||||
// three strings [n1,delimiter,n2]. See #formatSlideNumber().
|
||||
slideNumber: {{ slide_number }},
|
||||
// Can be used to limit the contexts in which the slide number appears
|
||||
// - "all": Always show the slide number
|
||||
// - "print": Only when printing to PDF
|
||||
// - "speaker": Only in the speaker view
|
||||
showSlideNumber: {{ show_slide_number }},
|
||||
// Use 1 based indexing for # links to match slide number (default is zero
|
||||
// based)
|
||||
hashOneBasedIndex: {{ hash_one_based_index }},
|
||||
// Add the current slide number to the URL hash so that reloading the
|
||||
// page/copying the URL will return you to the same slide
|
||||
hash: {{ hash }},
|
||||
// Flags if we should monitor the hash and change slides accordingly
|
||||
respondToHashChanges: {{ respond_to_hash_changes }},
|
||||
// Enable support for jump-to-slide navigation shortcuts
|
||||
jumpToSlide: {{ jump_to_slide }},
|
||||
// Push each slide change to the browser history. Implies `hash: true`
|
||||
history: {{ history }},
|
||||
// Enable keyboard shortcuts for navigation
|
||||
keyboard: {{ keyboard }},
|
||||
// Optional function that blocks keyboard events when retuning false
|
||||
//
|
||||
// If you set this to 'focused', we will only capture keyboard events
|
||||
// for embedded decks when they are in focus
|
||||
keyboardCondition: {{ keyboard_condition }},
|
||||
// Disables the default reveal.js slide layout (scaling and centering)
|
||||
// so that you can use custom CSS layout
|
||||
disableLayout: {{ disable_layout }},
|
||||
// Enable the slide overview mode
|
||||
overview: {{ overview }},
|
||||
// Vertical centering of slides
|
||||
center: {{ center }},
|
||||
// Enables touch navigation on devices with touch input
|
||||
touch: {{ touch }},
|
||||
// Loop the presentation
|
||||
loop: {{ loop }},
|
||||
// Change the presentation direction to be RTL
|
||||
rtl: {{ rtl }},
|
||||
// Changes the behavior of our navigation directions.
|
||||
//
|
||||
// "default"
|
||||
// Left/right arrow keys step between horizontal slides, up/down
|
||||
// arrow keys step between vertical slides. Space key steps through
|
||||
// all slides (both horizontal and vertical).
|
||||
//
|
||||
// "linear"
|
||||
// Removes the up/down arrows. Left/right arrows step through all
|
||||
// slides (both horizontal and vertical).
|
||||
//
|
||||
// "grid"
|
||||
// When this is enabled, stepping left/right from a vertical stack
|
||||
// to an adjacent vertical stack will land you at the same vertical
|
||||
// index.
|
||||
//
|
||||
// Consider a deck with six slides ordered in two vertical stacks:
|
||||
// 1.1 2.1
|
||||
// 1.2 2.2
|
||||
// 1.3 2.3
|
||||
//
|
||||
// If you're on slide 1.3 and navigate right, you will normally move
|
||||
// from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
|
||||
// from 1.3 -> 2.3.
|
||||
navigationMode: {{ navigation_mode }},
|
||||
// Randomizes the order of slides each time the presentation loads
|
||||
shuffle: {{ shuffle }},
|
||||
// Turns fragments on and off globally
|
||||
fragments: {{ fragments }},
|
||||
// Flags whether to include the current fragment in the URL,
|
||||
// so that reloading brings you to the same fragment position
|
||||
fragmentInURL: {{ fragment_in_url }},
|
||||
// Flags if the presentation is running in an embedded mode,
|
||||
// i.e. contained within a limited portion of the screen
|
||||
embedded: {{ embedded }},
|
||||
// Flags if we should show a help overlay when the question-mark
|
||||
// key is pressed
|
||||
help: {{ help }},
|
||||
// Flags if it should be possible to pause the presentation (blackout)
|
||||
pause: {{ pause }},
|
||||
// Flags if speaker notes should be visible to all viewers
|
||||
showNotes: {{ show_notes }},
|
||||
// Global override for autolaying embedded media (video/audio/iframe)
|
||||
// - null: Media will only autoplay if data-autoplay is present
|
||||
// - true: All media will autoplay, regardless of individual setting
|
||||
// - false: No media will autoplay, regardless of individual setting
|
||||
autoPlayMedia: {{ auto_play_media }},
|
||||
// Global override for preloading lazy-loaded iframes
|
||||
// - null: Iframes with data-src AND data-preload will be loaded when within
|
||||
// the viewDistance, iframes with only data-src will be loaded when visible
|
||||
// - true: All iframes with data-src will be loaded when within the viewDistance
|
||||
// - false: All iframes with data-src will be loaded only when visible
|
||||
preloadIframes: {{ preload_iframes }},
|
||||
// Can be used to globally disable auto-animation
|
||||
autoAnimate: {{ auto_animate }},
|
||||
// Optionally provide a custom element matcher that will be
|
||||
// used to dictate which elements we can animate between.
|
||||
autoAnimateMatcher: {{ auto_animate_matcher }},
|
||||
// Default settings for our auto-animate transitions, can be
|
||||
// overridden per-slide or per-element via data arguments
|
||||
autoAnimateEasing: {{ auto_animate_easing }},
|
||||
autoAnimateDuration: {{ auto_animate_duration }},
|
||||
autoAnimateUnmatched: {{ auto_animate_unmatched }},
|
||||
// CSS properties that can be auto-animated. Position & scale
|
||||
// is matched separately so there's no need to include styles
|
||||
// like top/right/bottom/left, width/height or margin.
|
||||
autoAnimateStyles: {{ auto_animate_styles }},
|
||||
// Controls automatic progression to the next slide
|
||||
// - 0: Auto-sliding only happens if the data-autoslide HTML attribute
|
||||
// is present on the current slide or fragment
|
||||
// - 1+: All slides will progress automatically at the given interval
|
||||
// - false: No auto-sliding, even if data-autoslide is present
|
||||
autoSlide: {{ auto_slide }},
|
||||
// Stop auto-sliding after user input
|
||||
autoSlideStoppable: {{ auto_slide_stoppable }},
|
||||
// Use this method for navigation when auto-sliding (defaults to navigateNext)
|
||||
autoSlideMethod: {{ auto_slide_method }},
|
||||
// Specify the average time in seconds that you think you will spend
|
||||
// presenting each slide. This is used to show a pacing timer in the
|
||||
// speaker view
|
||||
defaultTiming: {{ default_timing }},
|
||||
// Enable slide navigation via mouse wheel
|
||||
mouseWheel: {{ mouse_wheel }},
|
||||
// Opens links in an iframe preview overlay
|
||||
// Add `data-preview-link` and `data-preview-link="false"` to customize each link
|
||||
// individually
|
||||
previewLinks: {{ preview_links }},
|
||||
// Exposes the reveal.js API through window.postMessage
|
||||
postMessage: {{ post_message }},
|
||||
// Dispatches all reveal.js events to the parent window through postMessage
|
||||
postMessageEvents: {{ post_message_events }},
|
||||
// Focuses body when page changes visibility to ensure keyboard shortcuts work
|
||||
focusBodyOnPageVisibilityChange: {{ focus_body_on_page_visibility_change }},
|
||||
// Transition style
|
||||
transition: {{ transition }}, // none/fade/slide/convex/concave/zoom
|
||||
// Transition speed
|
||||
transitionSpeed: {{ transition_speed }}, // default/fast/slow
|
||||
// Transition style for full page slide backgrounds
|
||||
backgroundTransition: {{ background_transition }}, // none/fade/slide/convex/concave/zoom
|
||||
// The maximum number of pages a single slide can expand onto when printing
|
||||
// to PDF, unlimited by default
|
||||
pdfMaxPagesPerSlide: {{ pdf_max_pages_per_slide }},
|
||||
// Prints each fragment on a separate slide
|
||||
pdfSeparateFragments: {{ pdf_separate_fragments }},
|
||||
// Offset used to reduce the height of content within exported PDF pages.
|
||||
// This exists to account for environment differences based on how you
|
||||
// print to PDF. CLI printing options, like phantomjs and wkpdf, can end
|
||||
// on precisely the total height of the document whereas in-browser
|
||||
// printing has to end one pixel before.
|
||||
pdfPageHeightOffset: {{ pdf_page_height_offset }},
|
||||
// Number of slides away from the current that are visible
|
||||
viewDistance: {{ view_distance }},
|
||||
// Number of slides away from the current that are visible on mobile
|
||||
// devices. It is advisable to set this to a lower number than
|
||||
// viewDistance in order to save resources.
|
||||
mobileViewDistance: {{ mobile_view_distance }},
|
||||
// The display mode that will be used to show slides
|
||||
display: {{ display }},
|
||||
// Hide cursor if inactive
|
||||
hideInactiveCursor: {{ hide_inactive_cursor }},
|
||||
// Time before the cursor is hidden (in ms)
|
||||
hideCursorTime: {{ hide_cursor_time }}
|
||||
});
|
||||
// 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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fixBase64VideoBackground(event) {
|
||||
// Analyze all slides backgrounds
|
||||
for (const slide of Reveal.getBackgroundsElement().querySelectorAll('.slide-background')) {
|
||||
// Get the slide video and its sources for each background
|
||||
const video = slide.querySelector('video');
|
||||
if (video) {
|
||||
setVideoBase64(video);
|
||||
} else {
|
||||
// Listen to the creation of the video element
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
for (const addedNode of mutation.addedNodes) {
|
||||
if (addedNode.tagName === 'VIDEO') {
|
||||
setVideoBase64(addedNode);
|
||||
observer.disconnect(); // Stop observing once the video is handled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(slide, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Setup base64 videos
|
||||
Reveal.on( 'ready', fixBase64VideoBackground );
|
||||
{% endif %}
|
||||
</script>
|
||||
{% if env['READTHEDOCS'] %}
|
||||
<style>
|
||||
readthedocs-flyout, readthedocs-notification {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
197
manim_slides/utils.py
Normal file
197
manim_slides/utils.py
Normal file
@ -0,0 +1,197 @@
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from collections.abc import Iterator
|
||||
from multiprocessing import Pool
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
import av
|
||||
from tqdm import tqdm
|
||||
|
||||
from .logger import logger
|
||||
|
||||
|
||||
def concatenate_video_files(files: list[Path], dest: Path) -> None:
|
||||
"""Concatenate multiple video files into one."""
|
||||
if len(files) == 1:
|
||||
shutil.copy(files[0], dest)
|
||||
return
|
||||
|
||||
def _filter(files: list[Path]) -> Iterator[Path]:
|
||||
"""Patch possibly empty video files."""
|
||||
for file in files:
|
||||
with av.open(str(file)) as container:
|
||||
if len(container.streams.video) > 0:
|
||||
yield file
|
||||
else:
|
||||
logger.warning(
|
||||
f"Skipping video file {file} because it does "
|
||||
"not contain any video stream. "
|
||||
"This is probably caused by Manim, see: "
|
||||
"https://github.com/jeertmans/manim-slides/issues/390."
|
||||
)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
||||
f.writelines(f"file '{file}'\n" for file in _filter(files))
|
||||
tmp_file = f.name
|
||||
|
||||
with (
|
||||
av.open(tmp_file, format="concat", options={"safe": "0"}) as input_container,
|
||||
av.open(str(dest), mode="w") as output_container,
|
||||
):
|
||||
input_video_stream = input_container.streams.video[0]
|
||||
output_video_stream = output_container.add_stream(
|
||||
template=input_video_stream,
|
||||
)
|
||||
|
||||
if len(input_container.streams.audio) > 0:
|
||||
input_audio_stream = input_container.streams.audio[0]
|
||||
output_audio_stream = output_container.add_stream(
|
||||
template=input_audio_stream,
|
||||
)
|
||||
|
||||
for packet in input_container.demux():
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
ptype = packet.stream.type
|
||||
|
||||
if ptype == "video":
|
||||
packet.stream = output_video_stream
|
||||
elif ptype == "audio":
|
||||
packet.stream = output_audio_stream
|
||||
else:
|
||||
continue # We don't support subtitles
|
||||
output_container.mux(packet)
|
||||
|
||||
os.unlink(tmp_file) # https://stackoverflow.com/a/54768241
|
||||
|
||||
|
||||
def merge_basenames(files: list[Path]) -> Path:
|
||||
"""Merge multiple filenames by concatenating basenames."""
|
||||
if len(files) == 0:
|
||||
raise ValueError("Cannot merge an empty list of files!")
|
||||
|
||||
dirname: Path = files[0].parent
|
||||
ext = files[0].suffix
|
||||
|
||||
basenames = list(file.stem for file in files)
|
||||
|
||||
basenames_str = ",".join(f"{len(b)}:{b}" for b in basenames)
|
||||
|
||||
# We use hashes to prevent too-long filenames, see issue #123:
|
||||
# https://github.com/jeertmans/manim-slides/issues/123
|
||||
basename = hashlib.sha256(basenames_str.encode()).hexdigest()
|
||||
|
||||
logger.debug(f"Generated a new basename for basenames: {basenames} -> '{basename}'")
|
||||
|
||||
return dirname.joinpath(basename + ext)
|
||||
|
||||
|
||||
def link_nodes(*nodes: av.filter.context.FilterContext) -> None:
|
||||
"""Code from https://github.com/PyAV-Org/PyAV/issues/239."""
|
||||
for c, n in zip(nodes, nodes[1:]):
|
||||
c.link_to(n)
|
||||
|
||||
|
||||
def reverse_video_file_in_one_chunk(src_and_dest: tuple[Path, Path]) -> None:
|
||||
"""Reverses a video file, writing the result to `dest`."""
|
||||
src, dest = src_and_dest
|
||||
with (
|
||||
av.open(str(src)) as input_container,
|
||||
av.open(str(dest), mode="w") as output_container,
|
||||
):
|
||||
input_stream = input_container.streams.video[0]
|
||||
output_stream = output_container.add_stream(
|
||||
codec_name="libx264", rate=input_stream.base_rate
|
||||
)
|
||||
output_stream.width = input_stream.width
|
||||
output_stream.height = input_stream.height
|
||||
output_stream.pix_fmt = input_stream.pix_fmt
|
||||
|
||||
graph = av.filter.Graph()
|
||||
link_nodes(
|
||||
graph.add_buffer(template=input_stream),
|
||||
graph.add("reverse"),
|
||||
graph.add("buffersink"),
|
||||
)
|
||||
graph.configure()
|
||||
|
||||
frames_count = 0
|
||||
for frame in input_container.decode(video=0):
|
||||
graph.push(frame)
|
||||
frames_count += 1
|
||||
|
||||
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
|
||||
|
||||
for _ in range(frames_count):
|
||||
frame = graph.pull()
|
||||
frame.pict_type = "NONE" # Otherwise we get a warning saying it is changed
|
||||
output_container.mux(output_stream.encode(frame))
|
||||
|
||||
for packet in output_stream.encode():
|
||||
output_container.mux(packet)
|
||||
|
||||
|
||||
def reverse_video_file(
|
||||
src: Path,
|
||||
dest: Path,
|
||||
max_segment_duration: Optional[float] = 4.0,
|
||||
num_processes: Optional[int] = None,
|
||||
**tqdm_kwargs: Any,
|
||||
) -> None:
|
||||
"""Reverses a video file, writing the result to `dest`."""
|
||||
with av.open(str(src)) as input_container: # Fast path if file is short enough
|
||||
input_stream = input_container.streams.video[0]
|
||||
if max_segment_duration is None:
|
||||
return reverse_video_file_in_one_chunk((src, dest))
|
||||
elif input_stream.duration:
|
||||
if (
|
||||
float(input_stream.duration * input_stream.time_base)
|
||||
<= max_segment_duration
|
||||
):
|
||||
return reverse_video_file_in_one_chunk((src, dest))
|
||||
else: # pragma: no cover
|
||||
logger.debug(
|
||||
f"Could not determine duration of {src}, falling back to segmentation."
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdirname:
|
||||
tmpdir = Path(tmpdirname)
|
||||
with av.open(
|
||||
str(tmpdir / f"%04d.{src.suffix}"),
|
||||
"w",
|
||||
format="segment",
|
||||
options={"segment_time": str(max_segment_duration)},
|
||||
) as output_container:
|
||||
output_stream = output_container.add_stream(
|
||||
template=input_stream,
|
||||
)
|
||||
|
||||
for packet in input_container.demux(input_stream):
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
packet.stream = output_stream
|
||||
output_container.mux(packet)
|
||||
|
||||
src_files = list(tmpdir.iterdir())
|
||||
rev_files = [
|
||||
src_file.with_stem("rev_" + src_file.stem) for src_file in src_files
|
||||
]
|
||||
|
||||
with Pool(num_processes, maxtasksperchild=1) as pool:
|
||||
for _ in tqdm(
|
||||
pool.imap_unordered(
|
||||
reverse_video_file_in_one_chunk, zip(src_files, rev_files)
|
||||
),
|
||||
desc="Reversing large file by cutting it in segments",
|
||||
total=len(src_files),
|
||||
unit=" files",
|
||||
**tqdm_kwargs,
|
||||
):
|
||||
pass # We just consume the iterator
|
||||
|
||||
concatenate_video_files(rev_files[::-1], dest)
|
@ -1,192 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .commons import config_options, verbosity_option
|
||||
from .config import Config, Key
|
||||
from .defaults import CONFIG_PATH
|
||||
from .manim import logger
|
||||
from .resources import * # noqa: F401, F403
|
||||
|
||||
WINDOW_NAME: str = "Configuration Wizard"
|
||||
|
||||
keymap = {}
|
||||
for key in Qt.Key:
|
||||
keymap[key.value] = key.name.partition("_")[2]
|
||||
|
||||
|
||||
class KeyInput(QDialog):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.key = None
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
|
||||
self.setWindowTitle("Keyboard Input")
|
||||
self.label = QLabel("Press any key to register it")
|
||||
self.layout.addWidget(self.label)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def keyPressEvent(self, event: Any) -> None:
|
||||
self.key = event.key()
|
||||
self.deleteLater()
|
||||
event.accept()
|
||||
|
||||
|
||||
class Wizard(QWidget):
|
||||
def __init__(self, config: Config):
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle(WINDOW_NAME)
|
||||
self.config = config
|
||||
self.icon = QIcon(":/icon.png")
|
||||
self.setWindowIcon(self.icon)
|
||||
|
||||
QBtn = QDialogButtonBox.Save | QDialogButtonBox.Cancel
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.saveConfig)
|
||||
self.buttonBox.rejected.connect(self.closeWithoutSaving)
|
||||
|
||||
self.buttons = []
|
||||
|
||||
self.layout = QGridLayout()
|
||||
|
||||
for i, (key, value) in enumerate(self.config.dict().items()):
|
||||
# Create label for key name information
|
||||
label = QLabel()
|
||||
key_info = value["name"] or key
|
||||
label.setText(key_info)
|
||||
self.layout.addWidget(label, i, 0)
|
||||
|
||||
# Create button that will pop-up a dialog and ask to input a new key
|
||||
value = value["ids"].pop()
|
||||
button = QPushButton(keymap[value])
|
||||
button.setToolTip(
|
||||
f"Click to modify the key associated to action {key_info}"
|
||||
)
|
||||
self.buttons.append(button)
|
||||
button.clicked.connect(
|
||||
partial(self.openDialog, i, getattr(self.config, key))
|
||||
)
|
||||
self.layout.addWidget(button, i, 1)
|
||||
|
||||
self.layout.addWidget(self.buttonBox, len(self.buttons), 1)
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def closeWithoutSaving(self) -> None:
|
||||
logger.debug("Closing configuration wizard without saving")
|
||||
self.deleteLater()
|
||||
sys.exit(0)
|
||||
|
||||
def closeEvent(self, event: Any) -> None:
|
||||
self.closeWithoutSaving()
|
||||
event.accept()
|
||||
|
||||
def saveConfig(self) -> None:
|
||||
try:
|
||||
Config.parse_obj(self.config.dict())
|
||||
except ValueError:
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Critical)
|
||||
msg.setText("Error")
|
||||
msg.setInformativeText(
|
||||
"Two or more actions share a common key: make sure actions have distinct key codes."
|
||||
)
|
||||
msg.setWindowTitle("Error: duplicated keys")
|
||||
msg.exec_()
|
||||
return
|
||||
|
||||
self.deleteLater()
|
||||
|
||||
def openDialog(self, button_number: int, key: Key) -> None:
|
||||
button = self.buttons[button_number]
|
||||
dialog = KeyInput()
|
||||
dialog.exec_()
|
||||
if dialog.key is not None:
|
||||
key_name = keymap[dialog.key]
|
||||
key.set_ids(dialog.key)
|
||||
button.setText(key_name)
|
||||
|
||||
|
||||
@click.command()
|
||||
@config_options
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def wizard(config_path, force, merge):
|
||||
"""Launch configuration wizard."""
|
||||
return _init(config_path, force, merge, skip_interactive=False)
|
||||
|
||||
|
||||
@click.command()
|
||||
@config_options
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def init(config_path, force, merge, skip_interactive=False):
|
||||
"""Initialize a new default configuration file."""
|
||||
return _init(config_path, force, merge, skip_interactive=True)
|
||||
|
||||
|
||||
def _init(config_path, force, merge, skip_interactive=False):
|
||||
"""Actual initialization code for configuration file, with optional interactive mode."""
|
||||
|
||||
if os.path.exists(config_path):
|
||||
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
|
||||
|
||||
if not force and not merge:
|
||||
choice = click.prompt(
|
||||
"Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?",
|
||||
type=click.Choice(["o", "m", "q"], case_sensitive=False),
|
||||
)
|
||||
|
||||
force = choice == "o"
|
||||
merge = choice == "m"
|
||||
|
||||
if not force and not merge:
|
||||
logger.debug("Exiting without doing anything")
|
||||
sys.exit(0)
|
||||
|
||||
config = Config()
|
||||
|
||||
if force:
|
||||
logger.debug(f"Overwriting `{config_path}` if exists")
|
||||
elif merge:
|
||||
logger.debug("Merging new config into `{config_path}`")
|
||||
|
||||
if not skip_interactive:
|
||||
if os.path.exists(config_path):
|
||||
config = Config.parse_file(config_path)
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Manim Slides Wizard")
|
||||
window = Wizard(config)
|
||||
window.show()
|
||||
app.exec()
|
||||
|
||||
config = window.config
|
||||
|
||||
if merge:
|
||||
config = Config.parse_file(config_path).merge_with(config)
|
||||
|
||||
with open(config_path, "w") as config_file:
|
||||
config_file.write(config.json(indent=2))
|
||||
|
||||
click.secho(f"Configuration file successfully saved to `{config_path}`")
|
85
manim_slides/wizard/__init__.py
Normal file
85
manim_slides/wizard/__init__.py
Normal file
@ -0,0 +1,85 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from ..commons import config_options, verbosity_option
|
||||
from ..config import Config
|
||||
from ..defaults import CONFIG_PATH
|
||||
from ..logger import logger
|
||||
|
||||
|
||||
@click.command()
|
||||
@config_options
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def wizard(config_path: Path, force: bool, merge: bool) -> None:
|
||||
"""Launch configuration wizard."""
|
||||
return _init(config_path, force, merge, skip_interactive=False)
|
||||
|
||||
|
||||
@click.command()
|
||||
@config_options
|
||||
@click.help_option("-h", "--help")
|
||||
@verbosity_option
|
||||
def init(
|
||||
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
|
||||
) -> None:
|
||||
"""Initialize a new default configuration file."""
|
||||
return _init(config_path, force, merge, skip_interactive=True)
|
||||
|
||||
|
||||
def _init(
|
||||
config_path: Path, force: bool, merge: bool, skip_interactive: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Actual initialization code for configuration file, with optional interactive
|
||||
mode.
|
||||
"""
|
||||
if config_path.exists():
|
||||
click.secho(f"The `{CONFIG_PATH}` configuration file exists")
|
||||
|
||||
if not force and not merge:
|
||||
choice = click.prompt(
|
||||
"Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?",
|
||||
type=click.Choice(["o", "m", "q"], case_sensitive=False),
|
||||
)
|
||||
|
||||
force = choice == "o"
|
||||
merge = choice == "m"
|
||||
|
||||
if not force and not merge:
|
||||
logger.debug("Exiting without doing anything")
|
||||
sys.exit(0)
|
||||
|
||||
config = Config()
|
||||
|
||||
if force:
|
||||
logger.debug(f"Overwriting `{config_path}` if exists")
|
||||
elif merge:
|
||||
logger.debug("Merging new config into `{config_path}`")
|
||||
|
||||
if not skip_interactive:
|
||||
if config_path.exists():
|
||||
config = Config.from_file(config_path)
|
||||
|
||||
from ..qt_utils import qapp
|
||||
from .wizard import Wizard
|
||||
|
||||
app = qapp()
|
||||
app.setApplicationName("Manim Slides Wizard")
|
||||
window = Wizard(config)
|
||||
window.show()
|
||||
app.exec()
|
||||
|
||||
if window.closed_without_saving:
|
||||
sys.exit(0)
|
||||
|
||||
config = window.config
|
||||
|
||||
if merge:
|
||||
config = Config.from_file(config_path).merge_with(config)
|
||||
|
||||
config.to_file(config_path)
|
||||
|
||||
click.secho(f"Configuration file successfully saved to `{config_path}`")
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user