mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Compare commits
190 commits
main
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
733a1fa3d3 |
||
|
|
bc05b06e1a |
||
|
|
f565d48e5d |
||
|
|
64511b98e4 | ||
|
|
6b3c191dd1 | ||
|
|
d3fa15fba1 | ||
|
|
7df4abd334 | ||
|
|
8b24a433ae |
||
|
|
6fb7fa4b2f |
||
|
|
40c166153d | ||
|
|
3481d59baa | ||
|
|
33884470e4 | ||
|
|
0ec0c5cb76 | ||
|
|
d3d7a814c1 |
||
|
|
f6611bac46 |
||
|
|
8b5d48d424 |
||
|
|
84870ed0a9 |
||
|
|
97d3f3e774 | ||
|
|
4504dd11df | ||
|
|
dec8b93ca8 | ||
|
|
98967c9fe5 | ||
|
|
c76c868213 | ||
|
|
186ad964bb |
||
|
|
071a724cef | ||
|
|
06c989fbe1 | ||
|
|
1f60d48740 | ||
|
|
22a284ed5b | ||
|
|
74b9925b11 |
||
|
|
05c8e37ac7 |
||
|
|
669f1447bb | ||
|
|
41f40657c9 | ||
|
|
2995ad4040 | ||
|
|
f60c54581d | ||
|
|
92c489fedb | ||
|
|
ef7bcd1d2a | ||
|
|
8603510d13 |
||
|
|
89ff65200c | ||
|
|
fbe41c1902 | ||
|
|
7dbb19b926 | ||
|
|
60a36ee028 | ||
|
|
88b84eeada | ||
|
|
9e07cbad72 |
||
|
|
45aef8e556 |
||
|
|
bd816db5f7 |
||
|
|
c5954f88a8 | ||
|
|
61b8cfd52f |
||
|
|
b579779ce7 | ||
|
|
3531ab2bc5 | ||
|
|
f9586c88eb |
||
|
|
0223143739 |
||
|
|
715b61168e | ||
|
|
d1b7d20c65 |
||
|
|
a2c04954b8 |
||
|
|
53882628c0 |
||
|
|
d83bd04661 |
||
|
|
dcc6f4b901 | ||
|
|
8dbc427a92 | ||
|
|
31fc6e7649 |
||
|
|
771f0152f6 |
||
|
|
6156d861f2 |
||
|
|
172ea97d70 |
||
|
|
6d61818c7e |
||
|
|
df21b2dc31 |
||
|
|
fe3e505556 | ||
|
|
04140dae18 |
||
|
|
36bb003745 |
||
|
|
3618b4677f |
||
|
|
9aef19f482 | ||
|
|
5be4c13148 |
||
|
|
7de544da1c |
||
|
|
620766e6dd |
||
|
|
19980e3c70 |
||
|
|
3911643720 |
||
|
|
f9e6090af2 |
||
|
|
d98497690c |
||
|
|
9f72377c9d |
||
|
|
9360129365 |
||
|
|
9fcf94f2cb | ||
|
|
89cb646049 |
||
|
|
497debad8e |
||
|
|
23a9d40bd7 |
||
|
|
2726a4d60d |
||
|
|
4ea40d2f58 |
||
|
|
85db7ac248 |
||
|
|
b94eb15229 |
||
|
|
91a6bf0658 |
||
|
|
0dc1547bee |
||
|
|
0426552ec9 |
||
|
|
b51d3fe200 |
||
|
|
e18fdd2e74 |
||
|
|
b830b7d63e |
||
|
|
97fedbca13 |
||
|
|
4e6af02ceb |
||
|
|
7631878ab4 |
||
|
|
088d6be3de |
||
|
|
d7fa8f051c |
||
|
|
2304541dad |
||
|
|
bfaa9003ea |
||
|
|
f41b9251eb |
||
|
|
11f179224b |
||
|
|
867ce8da98 |
||
|
|
db26fe6e5e |
||
|
|
560ca7fe85 |
||
|
|
9f0eafb6b8 |
||
|
|
38fdaec214 | ||
|
|
793e853ae8 |
||
|
|
157ed6e4ea |
||
|
|
3f431a12f7 |
||
|
|
7e8c5c8144 |
||
|
|
7844c848f0 |
||
|
|
d09b03bb45 |
||
|
|
424cb27c6a |
||
|
|
53f637b865 |
||
|
|
ce752bf57a |
||
|
|
7a634dda95 |
||
|
|
51cf463cbe | ||
|
|
d6ea8412d6 |
||
|
|
5dcab4c4a5 |
||
|
|
08264dcf76 |
||
|
|
e550c5586c | ||
|
|
557784ad2f | ||
|
|
69ddb0e8d5 |
||
|
|
22844a23ce |
||
|
|
c6dfc1583b |
||
|
|
9b98fe5af2 |
||
|
|
eb1ff92ccd |
||
|
|
393c2a15d4 | ||
|
|
985141f5ea | ||
|
|
feff6ba8bc | ||
|
|
d98ba1cf51 | ||
|
|
4fb492c821 | ||
|
|
e50b82fea7 | ||
|
|
bffbc22794 | ||
|
|
7f4b287ac5 | ||
|
|
7e97df490d | ||
|
|
8537fd6eaa | ||
|
|
17f0204934 | ||
|
|
df97462ca4 | ||
|
|
95f97b7dc6 | ||
|
|
d8f261d414 | ||
|
|
115bcf968f | ||
|
|
7a7ed422fc | ||
|
|
46ba43cc97 | ||
|
|
ab44633082 | ||
|
|
389c1a1587 | ||
|
|
603e7128ef | ||
|
|
388604b3f7 | ||
|
|
d9dd887cb1 | ||
|
|
f82ec48e95 | ||
|
|
f73b35d86c | ||
|
|
b7fe84d6c8 | ||
|
|
5a7da79631 | ||
|
|
9a94781cf1 | ||
|
|
1a821d7013 | ||
|
|
d418acc865 | ||
|
|
7dea0446d0 | ||
|
|
1e57feccdb | ||
|
|
5d8ea6a4a3 | ||
|
|
76694e528b | ||
|
|
9904627b19 | ||
|
|
aa18dc44d1 | ||
|
|
c2d1a406f1 | ||
|
|
520577dde3 | ||
|
|
1d4f0d9665 | ||
|
|
56e9914916 | ||
|
|
4231751e27 | ||
|
|
30dd07eee7 | ||
|
|
db7274e325 | ||
|
|
5ae747e879 | ||
|
|
b411dbd298 | ||
|
|
9c40838fcb | ||
|
|
f90c9a2e95 | ||
|
|
068919ef12 | ||
|
|
d169cd556f | ||
|
|
eda8bb0e06 | ||
|
|
6151860418 | ||
|
|
67e1ca98c8 | ||
|
|
3888d20c6c | ||
|
|
5569c9f3d2 | ||
|
|
178778dce8 | ||
|
|
ed9e797b65 | ||
|
|
e77e6c8766 | ||
|
|
be45dcafa4 | ||
|
|
5ec1f255dd | ||
|
|
9f55388fea | ||
|
|
120db6fcea | ||
|
|
334f5d7877 | ||
|
|
59ab3925b7 | ||
|
|
31cebcced1 | ||
|
|
8b345d9a07 |
259 changed files with 8731 additions and 15174 deletions
1
.clang-format
Normal file
1
.clang-format
Normal file
|
|
@ -0,0 +1 @@
|
|||
BasedOnStyle: Microsoft
|
||||
|
|
@ -34,7 +34,7 @@ Manim is an animation engine for explanatory math videos. It's used to create pr
|
|||
|
||||
## Installation
|
||||
|
||||
> [!CAUTION]
|
||||
> [!WARNING]
|
||||
> These instructions are for the community version _only_. Trying to use these instructions to install [3b1b/manim](https://github.com/3b1b/manim) or instructions there to install this version will cause problems. Read [this](https://docs.manim.community/en/stable/faq/installation.html#why-are-there-different-versions-of-manim) and decide which version you wish to install, then only follow the instructions for your desired version.
|
||||
|
||||
Manim requires a few dependencies that must be installed prior to using it. If you
|
||||
|
|
@ -71,7 +71,7 @@ In order to view the output of this scene, save the code in a file called `examp
|
|||
manim -p -ql example.py SquareToCircle
|
||||
```
|
||||
|
||||
You should see your native video player program pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this
|
||||
You should see a window pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this
|
||||
[GitHub repository](example_scenes). You can also visit the [official gallery](https://docs.manim.community/en/stable/examples.html) for more advanced examples.
|
||||
|
||||
Manim also ships with a `%%manim` IPython magic which allows to use it conveniently in JupyterLab (as well as classic Jupyter) notebooks. See the
|
||||
|
|
@ -84,7 +84,8 @@ The general usage of Manim is as follows:
|
|||
|
||||

|
||||
|
||||
The `-p` flag in the command above is for previewing, meaning the video file will automatically open when it is done rendering. The `-ql` flag is for a faster rendering at a lower quality.
|
||||
The `-p` flag in the command above is for previewing, meaning a window will show up to render it in real time.
|
||||
The `-ql` flag is for a faster rendering at a lower quality.
|
||||
|
||||
Some other useful flags include:
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ msgid "Bases: :py:class:`manim.mobject.three_d.three_dimensions.Cylinder`"
|
|||
msgstr ""
|
||||
|
||||
#: ../../../manim/mobject/three_d/three_dimensions.py:docstring of manim.mobject.three_d.three_dimensions.Line3D:1
|
||||
msgid "A cylindrical line, for use in ThreeDScene."
|
||||
msgid "A cylindrical line."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/mobject/three_d/three_dimensions.py:docstring of manim.mobject.three_d.three_dimensions.Line3D:4
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ msgid "A spherical dot."
|
|||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.mobject.three_d.three_dimensions.rst:40:<autosummary>:1
|
||||
msgid "A cylindrical line, for use in ThreeDScene."
|
||||
msgid "A cylindrical line."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.mobject.three_d.three_dimensions.rst:40:<autosummary>:1
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Manim \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:2
|
||||
msgid "SpecialThreeDScene"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:4
|
||||
msgid "Qualified name: ``manim.scene.three\\_d\\_scene.SpecialThreeDScene``"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:1
|
||||
msgid "Bases: :py:class:`manim.scene.three_d_scene.ThreeDScene`"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:1
|
||||
msgid "An extension of :class:`ThreeDScene` with more settings."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:3
|
||||
msgid "It has some extra configuration for axes, spheres, and an override for low quality rendering. Further key differences are:"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:7
|
||||
msgid "The camera shades applicable 3DMobjects by default, except if rendering in low quality."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:9
|
||||
msgid "Some default params for Spheres and Axes have been added."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:14
|
||||
msgid "Methods"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:1
|
||||
msgid "Return a set of 3D axes."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:1
|
||||
msgid "Returns the default_angled_camera position."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:1
|
||||
msgid "Returns a sphere with the passed keyword arguments as properties."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.set_camera_to_default_position:1
|
||||
msgid "Sets the camera to its default position."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:25
|
||||
msgid "Attributes"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0
|
||||
msgid "Returns"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:3
|
||||
msgid "A set of 3D axes."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0
|
||||
msgid "Return type"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:3
|
||||
msgid "Dictionary of phi, theta, focal_distance, and gamma."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0
|
||||
msgid "Parameters"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:3
|
||||
msgid "Any valid parameter of :class:`~.Sphere` or :class:`~.Surface`."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:5
|
||||
msgid "The sphere object."
|
||||
msgstr ""
|
||||
|
||||
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Manim \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:2
|
||||
msgid "ThreeDScene"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:4
|
||||
msgid "Qualified name: ``manim.scene.three\\_d\\_scene.ThreeDScene``"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene:1
|
||||
msgid "Bases: :py:class:`manim.scene.scene.Scene`"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene:1
|
||||
msgid "This is a Scene, with special configurations and properties that make it suitable for Three Dimensional Scenes."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:14
|
||||
msgid "Methods"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
msgid "This method is used to prevent the rotation and movement of mobjects as the camera moves around."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
msgid "This method is used to prevent the rotation and tilting of mobjects as the camera moves around."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:1
|
||||
msgid "This method creates a 3D camera rotation illusion around the current camera orientation."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:1
|
||||
msgid "This method begins an ambient rotation of the camera about the Z_AXIS, in the anticlockwise direction"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:1
|
||||
msgid "This method returns a list of all of the Mobjects in the Scene that are moving, that are also in the animations passed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:1
|
||||
msgid "This method animates the movement of the camera to the given spherical coordinates."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
msgid "This method undoes what add_fixed_in_frame_mobjects does."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
msgid "This method \"unfixes\" the orientation of the mobjects passed, meaning they will no longer be at the same angle relative to the camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:1
|
||||
msgid "This method sets the orientation of the camera in the scene."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_to_default_angled_camera_orientation:1
|
||||
msgid "This method sets the default_angled_camera_orientation to the keyword arguments passed, and sets the camera to that orientation."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.stop_3dillusion_camera_rotation:1
|
||||
msgid "This method stops all illusion camera rotations."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.stop_ambient_camera_rotation:1
|
||||
msgid "This method stops all ambient camera rotation."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:33
|
||||
msgid "Attributes"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:1
|
||||
msgid "This method is used to prevent the rotation and movement of mobjects as the camera moves around. The mobject is essentially overlaid, and is not impacted by the camera's movement in any way."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_to_default_angled_camera_orientation:0
|
||||
msgid "Parameters"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:6
|
||||
msgid "The Mobjects whose orientation must be fixed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:1
|
||||
msgid "This method is used to prevent the rotation and tilting of mobjects as the camera moves around. The mobject can still move in the x,y,z directions, but will always be at the angle (relative to the camera) that it was at when it was passed through this method.)"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:7
|
||||
msgid "The Mobject(s) whose orientation must be fixed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:9
|
||||
msgid "Some valid kwargs are use_static_center_func : bool center_func : function"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:11
|
||||
msgid "Some valid kwargs are"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:11
|
||||
msgid "use_static_center_func : bool center_func : function"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:4
|
||||
msgid "The rate at which the camera rotation illusion should operate."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:5
|
||||
msgid "The polar angle the camera should move around. Defaults to the current phi angle."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:7
|
||||
msgid "The azimutal angle the camera should move around. Defaults to the current theta angle."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:4
|
||||
msgid "The rate at which the camera should rotate about the Z_AXIS. Negative rate means clockwise rotation."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:7
|
||||
msgid "one of 3 options: [\"theta\", \"phi\", \"gamma\"]. defaults to theta."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:4
|
||||
msgid "The animations whose mobjects will be checked."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:4
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:3
|
||||
msgid "The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:6
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:5
|
||||
msgid "The azimuthal angle i.e the angle that spins the camera around the Z_AXIS."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:8
|
||||
msgid "The radial focal_distance between ORIGIN and Camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:10
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:9
|
||||
msgid "The rotation of the camera about the vector from the ORIGIN to the Camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:12
|
||||
msgid "The zoom factor of the camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:14
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:13
|
||||
msgid "The new center of the camera frame in cartesian coordinates."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:16
|
||||
msgid "Any other animations to be played at the same time."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:1
|
||||
msgid "This method undoes what add_fixed_in_frame_mobjects does. It allows the mobject to be affected by the movement of the camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:5
|
||||
msgid "The Mobjects whose position and orientation must be unfixed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:1
|
||||
msgid "This method \"unfixes\" the orientation of the mobjects passed, meaning they will no longer be at the same angle relative to the camera. This only makes sense if the mobject was passed through add_fixed_orientation_mobjects first."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:6
|
||||
msgid "The Mobjects whose orientation must be unfixed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:7
|
||||
msgid "The focal_distance of the Camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:11
|
||||
msgid "The zoom factor of the scene."
|
||||
msgstr ""
|
||||
|
||||
|
||||
|
|
@ -7,6 +7,7 @@ This page contains a list of changes made between releases.
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
changelog/experimental
|
||||
changelog/0.20.1-changelog
|
||||
changelog/0.20.0-changelog
|
||||
changelog/0.19.2-changelog
|
||||
|
|
|
|||
97
docs/source/changelog/experimental.md
Normal file
97
docs/source/changelog/experimental.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Migrating from v0.19.0 to v0.20.0
|
||||
|
||||
This constitutes a list of all the changes needed to migrate your code
|
||||
to work with the latest version of Manim
|
||||
|
||||
## Manager
|
||||
If you ever used `Scene.render`, you must replace it with {class}`.Manager`.
|
||||
|
||||
Original code:
|
||||
```py
|
||||
scene = SceneClass()
|
||||
scene.render()
|
||||
```
|
||||
should be changed to:
|
||||
```py
|
||||
with Manager(SceneClass) as manager:
|
||||
manager.render()
|
||||
```
|
||||
|
||||
If you are a plugin author that subclasses `Scene` and changed `Scene.render`, you should migrate
|
||||
your code to use the specific public methods on {class}`.Manager` instead.
|
||||
|
||||
## ThreeDScene and Camera
|
||||
`ThreeDScene` has been completely removed, and all of its functionality has been replaced
|
||||
with methods on {class}`.Camera`, which can be accessed via {attr}`.Scene.camera`.
|
||||
|
||||
For example, the following code
|
||||
```py
|
||||
class MyScene(ThreeDScene):
|
||||
def construct(self):
|
||||
t = Text("Hello")
|
||||
self.add_fixed_in_frame_mobjects(t)
|
||||
self.begin_ambient_camera_rotation()
|
||||
self.wait(3)
|
||||
```
|
||||
should be changed to
|
||||
```py
|
||||
# change ThreeDScene -> Scene
|
||||
class MyScene(Scene):
|
||||
def construct(self):
|
||||
t = Text("Hello")
|
||||
# add_fixed_in_frame_mobjects() no longer exists.
|
||||
# Now you must use Mobject.fix_in_frame() manually for each Mobject.
|
||||
t.fix_in_frame()
|
||||
self.add(t)
|
||||
|
||||
# access the method on the camera
|
||||
self.camera.begin_ambient_rotation()
|
||||
self.add(self.camera)
|
||||
self.wait(3)
|
||||
```
|
||||
|
||||
## Animation
|
||||
`Animation.interpolate_mobject` has been combined into `Animation.interpolate`.
|
||||
|
||||
Methods `Animation._setup_scene` and `Animation.clean_up_from_scene` have been removed
|
||||
in favor of `Animation.begin` and `Animation.finish`. If you need to access the scene,
|
||||
you can use a simple buffer to communicate. Note that this buffer cannot access
|
||||
methods on the {class}`.Scene`, but can only do basic actions like {meth}`.Scene.add`,
|
||||
{meth}`.Scene.remove`, and {meth}`.Scene.replace`.
|
||||
|
||||
For example, the following code:
|
||||
```py
|
||||
class MyAnimation(Animation):
|
||||
def begin(self) -> None:
|
||||
self._sqrs = VGroup(Square())
|
||||
|
||||
def _setup_scene(self, scene: Scene) -> None:
|
||||
scene.add(self._sqrs)
|
||||
self.scene = scene
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
sqr = Square().move_to((alpha, 0, 0))
|
||||
self._sqrs.add(sqr)
|
||||
self.scene.add(sqr)
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
scene.remove(self._sqrs)
|
||||
```
|
||||
|
||||
should be changed to
|
||||
```py
|
||||
class MyAnimation(Animation):
|
||||
def begin(self) -> None:
|
||||
self._sqrs = VGroup(Square())
|
||||
self.buffer.add(self._sqrs)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
sqr = Square().move_to((alpha, 0, 0))
|
||||
self._sqrs.add(sqr)
|
||||
self.buffer.add(sqr)
|
||||
# tell the scene to empty the buffer
|
||||
self.apply_buffer = True
|
||||
|
||||
def finish(self) -> None:
|
||||
self.buffer.remove(self._sqrs)
|
||||
```
|
||||
|
|
@ -24,8 +24,8 @@ to the bottom of the file:
|
|||
.. code-block:: python
|
||||
|
||||
with tempconfig({"quality": "medium_quality", "disable_caching": True}):
|
||||
scene = SceneName()
|
||||
scene.render()
|
||||
manager = Manager(SceneName)
|
||||
manager.render()
|
||||
|
||||
Where ``SceneName`` is the name of the scene you want to run. You can then run the
|
||||
file directly, and can thus follow the instructions for most profilers.
|
||||
|
|
@ -58,8 +58,8 @@ to ``square_to_circle.py``:
|
|||
|
||||
|
||||
with tempconfig({"quality": "medium_quality", "disable_caching": True}):
|
||||
scene = SquareToCircle()
|
||||
scene.render()
|
||||
manager = Manager(SquareToCircle)
|
||||
manager.render()
|
||||
|
||||
Now run the following in the terminal:
|
||||
|
||||
|
|
|
|||
|
|
@ -213,11 +213,11 @@ The decorator can be used with or without parentheses. **By default, the test on
|
|||
circle = Circle()
|
||||
scene.play(Animation(circle))
|
||||
|
||||
You can also specify, when needed, which base scene you need (ThreeDScene, for example) :
|
||||
You can also specify, when needed, which base scene you need (VectorScene, for example) :
|
||||
|
||||
.. code:: python
|
||||
|
||||
@frames_comparison(last_frame=False, base_scene=ThreeDScene)
|
||||
@frames_comparison(last_frame=False, base_scene=VectorScene)
|
||||
def test_circle(scene):
|
||||
circle = Circle()
|
||||
scene.play(Animation(circle))
|
||||
|
|
|
|||
|
|
@ -597,25 +597,25 @@ Special Camera Settings
|
|||
|
||||
.. manim:: FixedInFrameMObjectTest
|
||||
:save_last_frame:
|
||||
:ref_classes: ThreeDScene
|
||||
:ref_methods: ThreeDScene.set_camera_orientation ThreeDScene.add_fixed_in_frame_mobjects
|
||||
:ref_classes: Scene
|
||||
:ref_methods: Camera.set_orientation OpenGLMobject.fix_in_frame
|
||||
|
||||
class FixedInFrameMObjectTest(ThreeDScene):
|
||||
class FixedInFrameMObjectTest(Scene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=-45 * DEGREES)
|
||||
self.camera.set_orientation(theta=-45 * DEGREES, phi=75 * DEGREES)
|
||||
text3d = Text("This is a 3D text")
|
||||
self.add_fixed_in_frame_mobjects(text3d)
|
||||
text3d.fix_in_frame()
|
||||
text3d.to_corner(UL)
|
||||
self.add(axes)
|
||||
self.wait()
|
||||
|
||||
.. manim:: ThreeDLightSourcePosition
|
||||
:save_last_frame:
|
||||
:ref_classes: ThreeDScene ThreeDAxes Surface
|
||||
:ref_methods: ThreeDScene.set_camera_orientation
|
||||
:ref_classes: Scene ThreeDAxes Surface
|
||||
:ref_methods: Camera.set_orientation
|
||||
|
||||
class ThreeDLightSourcePosition(ThreeDScene):
|
||||
class ThreeDLightSourcePosition(Scene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
sphere = Surface(
|
||||
|
|
@ -626,49 +626,57 @@ Special Camera Settings
|
|||
]), v_range=[0, TAU], u_range=[-PI / 2, PI / 2],
|
||||
checkerboard_colors=[RED_D, RED_E], resolution=(15, 32)
|
||||
)
|
||||
self.renderer.camera.light_source.move_to(3*IN) # changes the source of the light
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
# TODO: implement light source
|
||||
self.camera.light_source.move_to(3*IN) # changes the source of the light
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.add(axes, sphere)
|
||||
|
||||
|
||||
.. manim:: ThreeDCameraRotation
|
||||
:ref_classes: ThreeDScene ThreeDAxes
|
||||
:ref_methods: ThreeDScene.begin_ambient_camera_rotation ThreeDScene.stop_ambient_camera_rotation
|
||||
:ref_classes: Circle Scene ThreeDAxes
|
||||
:ref_methods: Camera.begin_ambient_rotation Camera.stop_ambient_rotation
|
||||
|
||||
class ThreeDCameraRotation(ThreeDScene):
|
||||
class ThreeDCameraRotation(Scene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
circle=Circle()
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
circle = Circle()
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.add(circle,axes)
|
||||
self.begin_ambient_camera_rotation(rate=0.1)
|
||||
self.camera.begin_ambient_rotation(rate=0.1)
|
||||
self.add(self.camera)
|
||||
self.wait()
|
||||
self.stop_ambient_camera_rotation()
|
||||
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.stop_ambient_rotation()
|
||||
self.play(
|
||||
self.camera.animate.set_orientation(
|
||||
theta=30 * DEGREES, phi=75 * DEGREES
|
||||
),
|
||||
)
|
||||
self.wait()
|
||||
|
||||
.. manim:: ThreeDCameraIllusionRotation
|
||||
:ref_classes: ThreeDScene ThreeDAxes
|
||||
:ref_methods: ThreeDScene.begin_3dillusion_camera_rotation ThreeDScene.stop_3dillusion_camera_rotation
|
||||
.. manim:: ThreeDCameraPrecession
|
||||
:ref_classes: Circle Scene ThreeDAxes
|
||||
:ref_methods: Camera.begin_precession Camera.stop_precession
|
||||
|
||||
class ThreeDCameraIllusionRotation(ThreeDScene):
|
||||
class ThreeDCameraPrecession(Scene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
circle=Circle()
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
circle = Circle()
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.add(circle,axes)
|
||||
self.begin_3dillusion_camera_rotation(rate=2)
|
||||
self.camera.begin_precession(rate=2)
|
||||
self.add(self.camera)
|
||||
self.wait(PI/2)
|
||||
self.stop_3dillusion_camera_rotation()
|
||||
self.camera.stop_precession()
|
||||
|
||||
.. manim:: ThreeDSurfacePlot
|
||||
:save_last_frame:
|
||||
:ref_classes: ThreeDScene Surface
|
||||
:ref_classes: Scene Surface
|
||||
:ref_methods: Camera.set_orientation
|
||||
|
||||
class ThreeDSurfacePlot(ThreeDScene):
|
||||
class ThreeDSurfacePlot(Scene):
|
||||
def construct(self):
|
||||
resolution_fa = 24
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=-30 * DEGREES)
|
||||
self.camera.set_orientation(theta=-30 * DEGREES, phi=75 * DEGREES)
|
||||
|
||||
def param_gauss(u, v):
|
||||
x = u
|
||||
|
|
|
|||
|
|
@ -357,10 +357,10 @@ A list of all config options
|
|||
'log_dir', 'log_to_file', 'max_files_cached', 'media_dir', 'media_width',
|
||||
'movie_file_extension', 'notify_outdated_version', 'output_file', 'partial_movie_dir',
|
||||
'pixel_height', 'pixel_width', 'plugins', 'preview',
|
||||
'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame',
|
||||
'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
|
||||
'progress_bar', 'quality', 'right_side', 'save_last_frame',
|
||||
'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
|
||||
'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent',
|
||||
'upto_animation_number', 'use_opengl_renderer', 'verbosity', 'video_dir',
|
||||
'upto_animation_number', 'verbosity', 'video_dir',
|
||||
'window_position', 'window_monitor', 'window_size', 'write_all', 'write_to_movie',
|
||||
'enable_wireframe', 'force_window']
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
A deep dive into Manim's internals
|
||||
==================================
|
||||
|
||||
**Author:** `Benjamin Hackl <https://benjamin-hackl.at>`__
|
||||
**Authors:** `Benjamin Hackl <https://benjamin-hackl.at>`__ and `Aarush Deshpande <https://github.com/JasonGrace2282>`__
|
||||
|
||||
.. admonition:: Disclaimer
|
||||
|
||||
This guide reflects the state of the library as of version ``v0.16.0``
|
||||
This guide reflects the state of the library as of version ``v0.20.0``
|
||||
and primarily treats the Cairo renderer. The situation in the latest
|
||||
version of Manim might be different; in case of substantial deviations
|
||||
we will add a note below.
|
||||
|
|
@ -84,7 +84,7 @@ discussing the contents of the following chapters on a very high level.
|
|||
to prepare a scene for rendering; right until the point where the user-overridden
|
||||
``construct`` method is ran. This includes a brief discussion on using Manim's CLI
|
||||
versus other means of rendering (e.g., via Jupyter notebooks, or in your Python
|
||||
script by calling the :meth:`.Scene.render` method yourself).
|
||||
script by calling the :meth:`.Manager.render` method yourself).
|
||||
- `Mobject Initialization`_: For the second chapter we dive into creating and handling
|
||||
Mobjects, the basic elements that should be displayed in our scene.
|
||||
We discuss the :class:`.Mobject` base class, how there are essentially
|
||||
|
|
@ -107,6 +107,25 @@ discussing the contents of the following chapters on a very high level.
|
|||
:meth:`.Scene.construct` has been run, the library combines the partial movie
|
||||
files to one video.
|
||||
|
||||
.. hint::
|
||||
|
||||
As we move forward, try to keep in mind the responsibilities of every
|
||||
class we introduce. We'll talk more about them in detail, but here's a brief
|
||||
overview
|
||||
|
||||
* :class:`.Scene` is responsible for managing the classes :class:`Mobject`, :class:`.Animation`,
|
||||
and :class:`.Camera`.
|
||||
|
||||
* :class:`.Manager` is responsible for coordinating the :class:`.Scene`, :class:`.Renderer`,
|
||||
and :class:`.FileWriter`.
|
||||
|
||||
* :class:`.FileWriter` is responsible for writing frames and partial movie files, as well
|
||||
as combining them all into a final movie file.
|
||||
|
||||
* :class:`.Renderer` is an abstract class which has to be subclassed.
|
||||
It's job is to take information related to the :class:`.Camera`, and the mobjects
|
||||
on the :class:`.Scene` at a certain frame, and to return the pixels in a frame.
|
||||
|
||||
And with that, let us get *in medias res*.
|
||||
|
||||
Preliminaries
|
||||
|
|
@ -123,8 +142,8 @@ like
|
|||
::
|
||||
|
||||
with tempconfig({"quality": "medium_quality", "preview": True}):
|
||||
scene = ToyExample()
|
||||
scene.render()
|
||||
manager = Manager(ToyExample)
|
||||
manager.render()
|
||||
|
||||
or whether you are rendering the code in a Jupyter notebook, you are still telling your
|
||||
python interpreter to import the library. The usual pattern used to do this is
|
||||
|
|
@ -202,8 +221,8 @@ have created a file ``toy_example.py`` which looks like this::
|
|||
self.play(FadeOut(blue_circle, small_dot))
|
||||
|
||||
with tempconfig({"quality": "medium_quality", "preview": True}):
|
||||
scene = ToyExample()
|
||||
scene.render()
|
||||
manager = Manager(ToyExample)
|
||||
manager.render()
|
||||
|
||||
With such a file, the desired scene is rendered by simply running this Python
|
||||
script via ``python toy_example.py``. Then, as described above, the library
|
||||
|
|
@ -218,10 +237,10 @@ dictionary, and upon leaving the context the original version of the
|
|||
configuration is restored. TL;DR: it provides a fancy way of temporarily setting
|
||||
configuration options.
|
||||
|
||||
Inside the context manager, two things happen: an actual ``ToyExample``-scene
|
||||
object is instantiated, and the ``render`` method is called. Every way of using
|
||||
Inside the context manager, two things happen: a :class:`.Manager` is created for
|
||||
the ``ToyExample``-scene, and the ``render`` method is called. Every way of using
|
||||
Manim ultimately does something along of these lines, the library always instantiates
|
||||
the scene object and then calls its ``render`` method. To illustrate that this
|
||||
the manager of the scene object and then calls its ``render`` method. To illustrate that this
|
||||
really is the case, let us briefly look at the two most common ways of rendering
|
||||
scenes:
|
||||
|
||||
|
|
@ -243,54 +262,75 @@ and the code creating the scene class and calling its render method is located
|
|||
`here <https://github.com/ManimCommunity/manim/blob/ac1ee9a683ce8b92233407351c681f7d71a4f2db/manim/utils/ipython_magic.py#L137-L138>`__.
|
||||
|
||||
|
||||
Now that we know that either way, a :class:`.Scene` object is created, let us investigate
|
||||
what Manim does when that happens. When instantiating our scene object
|
||||
Now that we know that either way, a :class:`.Manager` for a :class:`.Scene` object is created, let us investigate
|
||||
what Manim does when that happens. When instantiating our manager
|
||||
|
||||
::
|
||||
|
||||
scene = ToyExample()
|
||||
manager = Manager(ToyExample)
|
||||
|
||||
the ``Scene.__init__`` method is called, given that we did not implement our own initialization
|
||||
method. Inspecting the corresponding code (see
|
||||
`here <https://github.com/ManimCommunity/manim/blob/main/manim/scene/scene.py>`__)
|
||||
reveals that ``Scene.__init__`` first sets several attributes of the scene objects that do not
|
||||
depend on any configuration options set in ``config``. Then the scene inspects the value of
|
||||
``config.renderer``, and based on its value, either instantiates a ``CairoRenderer`` or an
|
||||
``OpenGLRenderer`` object and assigns it to its ``renderer`` attribute.
|
||||
The :meth:`.Manager.__init__` method is called. Looking at the source code (`here <https://github.com/ManimCommunity/manim/blob/experimental/manim/manager.py>`__),
|
||||
we see that the :meth:`.Scene.__init__` method is called,
|
||||
given that we did not implement our own initialization
|
||||
method. Inspecting the corresponding code (see `here <https://github.com/ManimCommunity/manim/blob/main/manim/scene/scene.py>`__)
|
||||
reveals that :class:`Scene.__init__` first sets several attributes of the scene objects that do not
|
||||
depend on any configuration options set in ``config``. It then initializes it's :class:`.Camera`.
|
||||
The purpose of a :class:`.Camera` is to keep track of what you can see in the scene. Think of it
|
||||
as a pair of eyes, that limit how far you can look sideways and vertically.
|
||||
|
||||
The scene then asks its renderer to initialize the scene by calling
|
||||
The :class:`.Scene` also sets up :attr:`.Scene.mobjects`. This attribute keeps track of all the :class:`.Mobject`
|
||||
that have been added to the scene.
|
||||
|
||||
::
|
||||
The :class:`.Manager` then continues on to create a :class:`.Window`, which is the popopen interactive window,
|
||||
and creates the renderer::
|
||||
|
||||
self.renderer.init_scene(self)
|
||||
self.renderer = self.create_renderer()
|
||||
self.renderer.use_window()
|
||||
|
||||
Inspecting both the default Cairo renderer and the OpenGL renderer shows that the ``init_scene``
|
||||
method effectively makes the renderer instantiate a :class:`.SceneFileWriter` object, which
|
||||
basically is Manim's interface to ``libav`` (FFMPEG) and actually writes the movie file. The Cairo
|
||||
renderer (see the implementation `here <https://github.com/ManimCommunity/manim/blob/main/manim/renderer/cairo_renderer.py>`__) does not require any further initialization. The OpenGL renderer
|
||||
does some additional setup to enable the realtime rendering preview window, which we do not go
|
||||
into detail further here.
|
||||
If you hover over :attr:`.Manager.renderer`, you might see that the type is a :class:`.RendererProtocol`.
|
||||
A :class:`~typing.Protocol` is a contract for a class. It says that whatever the class is, it will implement
|
||||
the methods defined inside the protocol. In this case, it means that the renderer will have all the methods
|
||||
defined in :class:`.RendererProtocol`.
|
||||
|
||||
.. warning::
|
||||
.. note::
|
||||
|
||||
Currently, there is a lot of interplay between a scene and its renderer. This is a flaw
|
||||
in Manim's current architecture, and we are working on reducing this interdependency to
|
||||
achieve a less convoluted code flow.
|
||||
The point of using :class:`~typing.Protocol` is so that in the future, plugins
|
||||
can swap out the renderer with their own version - either for speed, or for a different
|
||||
behavior.
|
||||
|
||||
After the renderer has been instantiated and initialized its file writer, the scene populates
|
||||
further initial attributes (notable mention: the ``mobjects`` attribute which keeps track
|
||||
of the mobjects that have been added to the scene). It is then done with its instantiation
|
||||
and ready to be rendered.
|
||||
|
||||
For the rest of this article to take a concrete example, we'll use :class:`.OpenGLRenderer`.
|
||||
|
||||
Finally, the :class:`.Manager` creates a :class:`.FileWriter`. This is the object that actually
|
||||
writes the partial movie files.
|
||||
|
||||
The rest of this article is concerned with the last line in our toy example script::
|
||||
|
||||
scene.render()
|
||||
manager.render()
|
||||
|
||||
This is where the actual magic happens.
|
||||
|
||||
.. note::
|
||||
|
||||
TODO TO REVIEWERS - Replace this link with the proper permanent link
|
||||
|
||||
Inspecting the `implementation of the render method <https://github.com/ManimCommunity/manim/blob/df1a60421ea1119cbbbd143ef288d294851baaac/manim/scene/scene.py#L211>`__
|
||||
reveals that there are several hooks that can be used for pre- or postprocessing
|
||||
a scene. Unsurprisingly, :meth:`.Scene.render` describes the full *render cycle*
|
||||
we see that there are two passes of rendering.
|
||||
|
||||
.. note::
|
||||
|
||||
As of the experimental branch at June 30th, 2024, two pass rendering
|
||||
does not exist. This will proceed to explain the single pass rendering system.
|
||||
|
||||
Looking around, we find that there are several hooks that can be used for pre- or postprocessing
|
||||
a scene (check out :meth:`.Manager._setup`, and :meth:`.Manager._tear_down`).
|
||||
|
||||
.. note::
|
||||
|
||||
You might notice :attr:`.Manager.virtual_animation_start_time` and :attr:`.Manager.real_animation_start_time`
|
||||
when looking through :meth:`.Manager._setup`. These will be explained later.
|
||||
|
||||
Unsurprisingly, :meth:`.Manager.render` describes the full *render cycle*
|
||||
of a scene. During this life cycle, there are three custom methods whose base
|
||||
implementation is empty and that can be overwritten to suit your purposes. In
|
||||
the order they are called, these customizable methods are:
|
||||
|
|
@ -308,14 +348,14 @@ the order they are called, these customizable methods are:
|
|||
Python scripts).
|
||||
|
||||
After these three methods are run, the animations have been fully rendered,
|
||||
and Manim calls :meth:`.CairoRenderer.scene_finished` to gracefully
|
||||
and Manim calls :meth:`.Manager.tear_down` to gracefully
|
||||
complete the rendering process. This checks whether any animations have been
|
||||
played -- and if so, it tells the :class:`.SceneFileWriter` to close the output
|
||||
file. If not, Manim assumes that a static image should be output
|
||||
which it then renders using the same strategy by calling the render loop
|
||||
(see below) once.
|
||||
|
||||
**Back in our toy example,** the call to :meth:`.Scene.render` first
|
||||
**Back in our toy example,** the call to :meth:`.Manager.render` first
|
||||
triggers :meth:`.Scene.setup` (which only consists of ``pass``), followed by
|
||||
a call of :meth:`.Scene.construct`. At this point, our *animation script*
|
||||
is run, starting with the initialization of ``orange_square``.
|
||||
|
|
@ -348,16 +388,12 @@ of :class:`.Mobject`, you will find that not too much happens in there:
|
|||
- and finally, ``init_colors`` is called.
|
||||
|
||||
Digging deeper, you will find that :meth:`.Mobject.reset_points` simply
|
||||
sets the ``points`` attribute of the mobject to an empty NumPy vector,
|
||||
sets the ``points`` attribute of the mobject to an empty NumPy array,
|
||||
while the other two methods, :meth:`.Mobject.generate_points` and
|
||||
:meth:`.Mobject.init_colors` are just implemented as ``pass``.
|
||||
|
||||
This makes sense: :class:`.Mobject` is not supposed to be used as
|
||||
an *actual* object that is displayed on screen; in fact the camera
|
||||
(which we will discuss later in more detail; it is the class that is,
|
||||
for the Cairo renderer, responsible for "taking a picture" of the
|
||||
current scene) does not process "pure" :class:`Mobjects <.Mobject>`
|
||||
in any way, they *cannot* even appear in the rendered output.
|
||||
an *actual* object that is displayed on screen.
|
||||
|
||||
This is where different types of mobjects come into play. Roughly
|
||||
speaking, the Cairo renderer setup knows three different types of
|
||||
|
|
@ -376,24 +412,24 @@ mobjects that can be rendered:
|
|||
|
||||
As just mentioned, :class:`VMobjects <.VMobject>` represent vectorized
|
||||
mobjects. To render a :class:`.VMobject`, the camera looks at the
|
||||
``points`` attribute of a :class:`.VMobject` and divides it into sets
|
||||
of four points each. Each of these sets is then used to construct a
|
||||
cubic Bézier curve with the first and last entry describing the
|
||||
end points of the curve ("anchors"), and the second and third entry
|
||||
describing the control points in between ("handles").
|
||||
:attr:`~.VMobject.points` attribute of a :class:`.VMobject` and divides it into sets
|
||||
of three points each. Each of these sets is then used to construct a
|
||||
quadratic Bézier curve with the first and last entry describing the
|
||||
end points of the curve ("anchors"), and the second entry
|
||||
describing the control points in between ("handle").
|
||||
|
||||
.. hint::
|
||||
To learn more about Bézier curves, take a look at the excellent
|
||||
online textbook `A Primer on Bézier curves <https://pomax.github.io/bezierinfo/>`__
|
||||
by `Pomax <https://twitter.com/TheRealPomax>`__ -- there is a playground representing
|
||||
cubic Bézier curves `in §1 <https://pomax.github.io/bezierinfo/#introduction>`__,
|
||||
quadratic Bézier curves `in §1 <https://pomax.github.io/bezierinfo/#introduction>`__,
|
||||
the red and yellow points are "anchors", and the green and blue
|
||||
points are "handles".
|
||||
|
||||
In contrast to :class:`.Mobject`, :class:`.VMobject` can be displayed
|
||||
on screen (even though, technically, it is still considered a base class).
|
||||
To illustrate how points are processed, consider the following short example
|
||||
of a :class:`.VMobject` with 8 points (and thus made out of 8/4 = 2 cubic
|
||||
of a :class:`.VMobject` with 6 points (and thus made out of 6/3 = 2 cubic
|
||||
Bézier curves). The resulting :class:`.VMobject` is drawn in green.
|
||||
The handles are drawn as red dots with a line to their closest anchor.
|
||||
|
||||
|
|
@ -430,6 +466,7 @@ The handles are drawn as red dots with a line to their closest anchor.
|
|||
|
||||
|
||||
.. warning::
|
||||
|
||||
Manually setting the points of your :class:`.VMobject` is usually
|
||||
discouraged; there are specialized methods that can take care of
|
||||
that for you -- but it might be relevant when implementing your own,
|
||||
|
|
@ -561,59 +598,12 @@ is not a "flat" list of mobjects, but a list of mobjects which
|
|||
might contain mobjects themselves, and so on.
|
||||
|
||||
Stepping through the code in :meth:`.Scene.add`, we see that first
|
||||
it is checked whether we are currently using the OpenGL renderer
|
||||
(which we are not) -- adding mobjects to the scene works slightly
|
||||
different (and actually easier!) for the OpenGL renderer. Then, the
|
||||
code branch for the Cairo renderer is entered and the list of so-called
|
||||
foreground mobjects (which are rendered on top of all other mobjects)
|
||||
is added to the list of passed mobjects. This is to ensure that the
|
||||
foreground mobjects will stay above of the other mobjects, even after
|
||||
adding the new ones. In our case, the list of foreground mobjects
|
||||
is actually empty, and nothing changes.
|
||||
we remove all the mobjects that are being added -- this is to make
|
||||
sure we don't add a :class:`.Mobject` twice! After that, we can safely
|
||||
add it to :attr:`.Scene.mobjects`.
|
||||
|
||||
Next, :meth:`.Scene.restructure_mobjects` is called with the list
|
||||
of mobjects to be added as the ``to_remove`` argument, which might
|
||||
sound odd at first. Practically, this ensures that mobjects are not
|
||||
added twice, as mentioned above: if they were present in the scene
|
||||
``Scene.mobjects`` list before (even if they were contained as a
|
||||
child of some other mobject), they are first removed from the list.
|
||||
The way :meth:`.Scene.restructure_mobjects` works is rather aggressive:
|
||||
It always operates on a given list of mobjects; in the ``add`` method
|
||||
two different lists occur: the default one, ``Scene.mobjects`` (no extra
|
||||
keyword argument is passed), and ``Scene.moving_mobjects`` (which we will
|
||||
discuss later in more detail). It iterates through all of the members of
|
||||
the list, and checks whether any of the mobjects passed in ``to_remove``
|
||||
are contained as children (in any nesting level). If so, **their parent
|
||||
mobject is deconstructed** and their siblings are inserted directly
|
||||
one level higher. Consider the following example::
|
||||
|
||||
>>> from manim import Scene, Square, Circle, Group
|
||||
>>> test_scene = Scene()
|
||||
>>> mob1 = Square()
|
||||
>>> mob2 = Circle()
|
||||
>>> mob_group = Group(mob1, mob2)
|
||||
>>> test_scene.add(mob_group)
|
||||
<manim.scene.scene.Scene object at ...>
|
||||
>>> test_scene.mobjects
|
||||
[Group]
|
||||
>>> test_scene.restructure_mobjects(to_remove=[mob1])
|
||||
<manim.scene.scene.Scene object at ...>
|
||||
>>> test_scene.mobjects
|
||||
[Circle]
|
||||
|
||||
Note that the group is disbanded and the circle moves into the
|
||||
root layer of mobjects in ``test_scene.mobjects``.
|
||||
|
||||
After the mobject list is "restructured", the mobject to be added
|
||||
are simply appended to ``Scene.mobjects``. In our toy example,
|
||||
the ``Scene.mobjects`` list is actually empty, so the
|
||||
``restructure_mobjects`` method does not actually do anything. The
|
||||
``orange_square`` is simply added to ``Scene.mobjects``, and as
|
||||
the aforementioned ``Scene.moving_mobjects`` list is, at this point,
|
||||
also still empty, nothing happens and :meth:`.Scene.add` returns.
|
||||
|
||||
We will hear more about the ``moving_mobject`` list when we discuss
|
||||
the render loop. Before we do that, let us look at the next line
|
||||
We will hear more from :class:`.Scene` soon.
|
||||
Before we do that, let us look at the next line
|
||||
of code in our toy example, which includes the initialization of
|
||||
an animation class,
|
||||
::
|
||||
|
|
@ -642,11 +632,11 @@ the run time of animations is also fixed and known beforehand.
|
|||
The initialization of animations actually is not very exciting,
|
||||
:meth:`.Animation.__init__` merely sets some attributes derived
|
||||
from the passed keyword arguments and additionally ensures that
|
||||
the ``Animation.starting_mobject`` and ``Animation.mobject``
|
||||
the :attr:`~Animation.starting_mobject` and :attr:`~.Animation.mobject`
|
||||
attributes are populated. Once the animation is played, the
|
||||
``starting_mobject`` attribute holds an unmodified copy of the
|
||||
:attr:`~.Animation.starting_mobject` attribute holds an unmodified copy of the
|
||||
mobject the animation is attached to; during the initialization
|
||||
it is set to a placeholder mobject. The ``mobject`` attribute
|
||||
it is set to a placeholder mobject. The :attr:`~.Animation.mobject` attribute
|
||||
is set to the mobject the animation is attached to.
|
||||
|
||||
Animations have a few special methods which are called during the
|
||||
|
|
@ -681,77 +671,80 @@ animation (like its ``run_time``, the ``rate_func``, etc.) are
|
|||
processed there -- and then the animation object is fully
|
||||
initialized and ready to be played.
|
||||
|
||||
The Animation Buffer
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
There's an attribute of animations that we have glossed
|
||||
over, and that is :attr:`.Animation.buffer`, of type :class:`.SceneBuffer`.
|
||||
The :attr:`~.Animation.buffer` is the animations way of communicating
|
||||
with what happens on the scene. If you want to modify
|
||||
the scene during the interpolation stage (outside of :meth:`~.Animation.begin` or :meth:`~.Animation.finish`),
|
||||
the attribute :attr:`.Animation.apply_buffer` is what tells the scene that the buffer
|
||||
should be processed.
|
||||
|
||||
For example, an animation that adds a circle to the scene every frame might look like this
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CircleAnimation(Animation):
|
||||
def begin(self) -> None:
|
||||
self.circles = VGroup()
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
# create and arrange the circles
|
||||
self.circles.add(Circle())
|
||||
self.circles().arrange()
|
||||
# add the new circle to the scene
|
||||
self.buffer.add(self.circles[-1])
|
||||
# make sure the scene actually realizes something changed
|
||||
self.apply_buffer = True
|
||||
|
||||
Every time the :class:`.Scene` applies the buffer, it gets emptied out
|
||||
for use the next time.
|
||||
|
||||
The ``play`` call: preparing to enter Manim's render loop
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
We are finally there, the render loop is in our reach. Let us
|
||||
walk through the code that is run when :meth:`.Scene.play` is called.
|
||||
|
||||
.. hint::
|
||||
.. note::
|
||||
|
||||
Recall that this article is specifically about the Cairo renderer.
|
||||
Up to here, things were more or less the same for the OpenGL renderer
|
||||
as well; while some base mobjects might be different, the control flow
|
||||
and lifecycle of mobjects is still more or less the same. There are more
|
||||
substantial differences when it comes to the rendering loop.
|
||||
In the future, control will not be passed to the Manager.
|
||||
Instead, the Scene will keep track of every animation and
|
||||
at the very end, the Manager will render everything.
|
||||
|
||||
As you will see when inspecting the method, :meth:`.Scene.play` almost
|
||||
immediately passes over to the ``play`` method of the renderer,
|
||||
in our case :class:`.CairoRenderer.play`. The one thing :meth:`.Scene.play`
|
||||
takes care of is the management of subcaptions that you might have
|
||||
passed to it (see the the documentation of :meth:`.Scene.play` and
|
||||
:meth:`.Scene.add_subcaption` for more information).
|
||||
immediately passes over to the :class:`~.Manager._play` method of the :class:`.Manager`.
|
||||
The one thing :meth:`.Scene.play` does before that is preparing the animations.
|
||||
Whenever :attr:`.Mobject.animate` is called, it creates a new object called a
|
||||
:class:`._AnimationBuilder`. We have to make sure to convert that into an actual
|
||||
animation by calling it's :meth:`._AnimationBuilder.build` method.
|
||||
We also have to update the animations with the correct rate functions, lag ratios,
|
||||
and run time.
|
||||
|
||||
.. note::
|
||||
|
||||
Methods in :class:`.Manager` starting with an underscore ``_`` are intended to be
|
||||
private, and are not guaranteed to be stable across versions of Manim. The :class:`.Manager`
|
||||
class provides some "public" methods (methods not prefixed with ``_``) that can be overridden to
|
||||
change the behavior of the program.
|
||||
|
||||
.. warning::
|
||||
|
||||
As has been said before, the communication between scene and renderer
|
||||
is not in a very clean state at this point, so the following paragraphs
|
||||
might be confusing if you don't run a debugger and step through the
|
||||
code yourself a bit.
|
||||
|
||||
Inside :meth:`.CairoRenderer.play`, the renderer first checks whether
|
||||
it may skip rendering of the current play call. This might happen, for example,
|
||||
when ``-s`` is passed to the CLI (i.e., only the last frame should be rendered),
|
||||
or when the ``-n`` flag is passed and the current play call is outside of the
|
||||
specified render bounds. The "skipping status" is updated in form of the
|
||||
call to :meth:`.CairoRenderer.update_skipping_status`.
|
||||
|
||||
Next, the renderer asks the scene to process the animations in the play
|
||||
call so that renderer obtains all of the information it needs. To
|
||||
be more concrete, :meth:`.Scene.compile_animation_data` is called,
|
||||
which then takes care of several things:
|
||||
|
||||
- The method processes all animations and the keyword arguments passed
|
||||
to the initial :meth:`.Scene.play` call. In particular, this means
|
||||
that it makes sure all arguments passed to the play call are actually
|
||||
animations (or ``.animate`` syntax calls, which are also assembled to
|
||||
be actual :class:`.Animation`-objects at that point). It also propagates
|
||||
any animation-related keyword arguments (like ``run_time``,
|
||||
or ``rate_func``) passed to :class:`.Scene.play` to each individual
|
||||
animation. The processed animations are then stored in the ``animations``
|
||||
attribute of the scene (which the renderer later reads...).
|
||||
- It adds all mobjects to which the animations that are played are
|
||||
bound to to the scene (provided the animation is not an mobject-introducing
|
||||
animation -- for these, the addition to the scene happens later).
|
||||
- In case the played animation is a :class:`.Wait` animation (this is the
|
||||
case in a :meth:`.Scene.wait` call), the method checks whether a static
|
||||
image should be rendered, or whether the render loop should be processed
|
||||
as usual (see :meth:`.Scene.should_update_mobjects` for the exact conditions,
|
||||
basically it checks whether there are any time-dependent updater functions
|
||||
and so on).
|
||||
- Finally, the method determines the total run time of the play call (which
|
||||
at this point is computed as the maximum of the run times of the passed
|
||||
animations). This is stored in the ``duration`` attribute of the scene.
|
||||
Subcaptions and audio is still in progress
|
||||
|
||||
|
||||
After the animation data has been compiled by the scene, the renderer
|
||||
continues to prepare for entering the render loop. It now checks the
|
||||
skipping status which has been determined before. If the renderer can
|
||||
skip this play call, it does so: it sets the current play call hash (which
|
||||
we will get back to in a moment) to ``None`` and increases the time of the
|
||||
renderer by the determined animation run time.
|
||||
After the :class:`.Scene` has done all the processing of animations,
|
||||
it hands out control to the :class:`.Manager`. The :class:`.Manager`
|
||||
then updates the skipping status of the :class:`.Scene`. This makes sure
|
||||
that if ``-s`` or ``-n`` is used for sections, the scene does the correct
|
||||
thing.
|
||||
|
||||
Otherwise, the renderer checks whether or not Manim's caching system should
|
||||
The next important line is::
|
||||
|
||||
self._write_hashed_movie_file()
|
||||
|
||||
Here, the :class:`.Manager` checks whether or not Manim's caching system should
|
||||
be used. The idea of the caching system is simple: for every play call, a
|
||||
hash value is computed, which is then stored and upon re-rendering the scene,
|
||||
the hash is generated again and checked against the stored value. If it is the
|
||||
|
|
@ -761,8 +754,8 @@ to learn more, the :func:`.get_hash_from_play_call` function in the
|
|||
:mod:`.utils.hashing` module is essentially the entry point to the caching
|
||||
mechanism.
|
||||
|
||||
In the event that the animation has to be rendered, the renderer asks
|
||||
its :class:`.SceneFileWriter` to open an output container. The process
|
||||
In the event that the animation has to be rendered, the manager asks
|
||||
its :class:`.FileWriter` to open an output container. The process
|
||||
is started by a call to ``libav`` and opens a container to which rendered
|
||||
raw frames can be written. As long as the output is open, the container
|
||||
can be accessed via the ``output_container`` attribute of the file writer.
|
||||
|
|
@ -770,31 +763,18 @@ With the writing process in place, the renderer then asks the scene
|
|||
to "begin" the animations.
|
||||
|
||||
First, it literally *begins* all of the animations by calling their
|
||||
setup methods (:meth:`.Animation._setup_scene`, :meth:`.Animation.begin`).
|
||||
setup methods (:meth:`.Animation.begin`).
|
||||
In doing so, the mobjects that are newly introduced by an animation
|
||||
(like via :class:`.Create` etc.) are added to the scene. Furthermore, the
|
||||
animation suspends updater functions being called on its mobject, and
|
||||
it sets its mobject to the state that corresponds to the first frame
|
||||
of the animation.
|
||||
|
||||
After this has happened for all animations in the current ``play`` call,
|
||||
the Cairo renderer determines which of the scene's mobjects can be
|
||||
painted statically to the background, and which ones have to be
|
||||
redrawn every frame. It does so by calling
|
||||
:meth:`.Scene.get_moving_and_static_mobjects`, and the resulting
|
||||
partition of mobjects is stored in the corresponding ``moving_mobjects``
|
||||
and ``static_mobjects`` attributes.
|
||||
.. note::
|
||||
|
||||
.. NOTE::
|
||||
Implementation of figuring out which mobjects have to be redrawn
|
||||
is still in progress.
|
||||
|
||||
The mechanism that determines static and moving mobjects is
|
||||
specific for the Cairo renderer, the OpenGL renderer works differently.
|
||||
Basically, moving mobjects are determined by checking whether they,
|
||||
any of their children, or any of the mobjects "below" them (in the
|
||||
sense of the order in which mobjects are processed in the scene)
|
||||
either have an update function attached, or whether they appear
|
||||
in one of the current animations. See the implementation of
|
||||
:meth:`.Scene.get_moving_mobjects` for more details.
|
||||
|
||||
Up to this very point, we did not actually render any (partial)
|
||||
image or movie files from the scene yet. This is, however, about to change.
|
||||
|
|
@ -835,68 +815,28 @@ Time to render some frames.
|
|||
|
||||
The render loop (for real this time)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Now we get to the meat of rendering, which happens in :meth:`.Manager._progress_through_animations`.
|
||||
|
||||
As mentioned above, due to the mechanism that determines static and moving
|
||||
mobjects in the scene, the renderer knows which mobjects it can paint
|
||||
statically to the background of the scene. Practically, this means that
|
||||
it partially renders a scene (to produce a background image), and then
|
||||
when iterating through the time progression of the animation only the
|
||||
"moving mobjects" are re-painted on top of the static background.
|
||||
|
||||
The renderer calls :meth:`.CairoRenderer.save_static_frame_data`, which
|
||||
first checks whether there are currently any static mobjects, and if there
|
||||
are, it updates the frame (only with the static mobjects; more about how
|
||||
exactly this works in a moment) and then saves a NumPy array representing
|
||||
the rendered frame in the ``static_image`` attribute. In our toy example,
|
||||
there are no static mobjects, and so the ``static_image`` attribute is
|
||||
simply set to ``None``.
|
||||
|
||||
Next, the renderer asks the scene whether the current animation is
|
||||
a "frozen frame" animation, which would mean that the renderer actually
|
||||
does not have to repaint the moving mobjects in every frame of the time
|
||||
progression. It can then just take the latest static frame, and display it
|
||||
throughout the animation.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
An animation is considered a "frozen frame" animation if only a
|
||||
static :class:`.Wait` animation is played. See the description
|
||||
of :meth:`.Scene.compile_animation_data` above, or the
|
||||
implementation of :meth:`.Scene.should_update_mobjects` for
|
||||
more details.
|
||||
|
||||
If this is not the case (just as in our toy example), the renderer
|
||||
then calls the :meth:`.Scene.play_internal` method, which is the
|
||||
integral part of the render loop (in which the library steps through
|
||||
the time progression of the animation and renders the corresponding
|
||||
frames).
|
||||
|
||||
Within :meth:`.Scene.play_internal`, the following steps are performed:
|
||||
|
||||
- The scene determines the run time of the animations by calling
|
||||
:meth:`.Scene.get_run_time`. This method basically takes the maximum
|
||||
- The manager determines the run time of the animations by calling
|
||||
:meth:`.Manager._calc_run_time`. This method basically takes the maximum
|
||||
``run_time`` attribute of all of the animations passed to the
|
||||
:meth:`.Scene.play` call.
|
||||
- Then the *time progression* is constructed via the (internal)
|
||||
:meth:`.Scene._get_animation_time_progression` method, which wraps
|
||||
the actual :meth:`.Scene.get_time_progression` method. The time
|
||||
progression is a ``tqdm`` `progress bar object <https://tqdm.github.io>`__
|
||||
for an iterator over ``np.arange(0, run_time, 1 / config.frame_rate)``. In
|
||||
- Then, the progressbar is created by :meth:`.Manager._create_progressbar`,
|
||||
which returns a ``tqdm`` `progress bar object <https://tqdm.github.io>`__
|
||||
object (from the ``tqdm`` library), or a fake progressbar if
|
||||
:attr:`.ManimConfig.write_to_movie` is ``False``.
|
||||
- Then the *time progression* is constructed via
|
||||
:meth:`.Manager._calc_time_progression` method, which returns
|
||||
``np.arange(0, run_time, 1 / config.frame_rate)``. In
|
||||
other words, the time progression holds the time stamps (relative to the
|
||||
current animations, so starting at 0 and ending at the total animation run time,
|
||||
with the step size determined by the render frame rate) of the timeline where
|
||||
a new animation frame should be rendered.
|
||||
- Then the scene iterates over the time progression: for each time stamp ``t``,
|
||||
:meth:`.Scene.update_to_time` is called, which ...
|
||||
|
||||
- ... first computes the time passed since the last update (which might be 0,
|
||||
especially for the initial call) and references it as ``dt``,
|
||||
- then (in the order in which the animations are passed to :meth:`.Scene.play`)
|
||||
calls :meth:`.Animation.update_mobjects` to trigger all updater functions that
|
||||
are attached to the respective animation except for the "main mobject" of
|
||||
the animation (that is, for example, for :class:`.Transform` the unmodified
|
||||
copies of start and target mobject -- see :meth:`.Animation.get_all_mobjects_to_update`
|
||||
for more details),
|
||||
we find the time difference between the current and previous frame (AKA ``dt``).
|
||||
We then update the animations in the scene using ``dt`` by
|
||||
- iterating over each animation
|
||||
- next, we update the animations mobjects
|
||||
- then the relative time progression with respect to the current animation
|
||||
is computed (``alpha = t / animation.run_time``), which is then used to
|
||||
update the state of the animation with a call to :meth:`.Animation.interpolate`.
|
||||
|
|
@ -904,62 +844,29 @@ Within :meth:`.Scene.play_internal`, the following steps are performed:
|
|||
of all mobjects in the scene, all meshes, and finally those attached to
|
||||
the scene itself are run.
|
||||
|
||||
After updating the animations, we pass ``dt`` to :meth:`.Manager._update_frame` which...
|
||||
|
||||
- ... updates the total time passed
|
||||
- Updates all the mobjects by calling :meth:`.Scene._update_mobjects`. This in turn
|
||||
iterates over all the mobjects on the screen and updates them.
|
||||
- After that, the current state of the scene is computed by :meth:`.Scene.get_state`,
|
||||
which returns a :class:`.SceneState`.
|
||||
- The state is then passed into :meth:`.Manager._render_frame`, which gets
|
||||
the renderer to create the pixels. With :class:`.OpenGLRenderer`, this
|
||||
also updates the window. :meth:`~.Manager._render_frame` also checks if it should write a frame,
|
||||
and if so, writes a frame via the :class:`.FileWriter`.
|
||||
- Finally, it uses a concept of virtual time vs real time to see
|
||||
if the right amount of time has passed in the window. The virtual
|
||||
time is the amount of time that is supposed to have passed (that is, ``t``).
|
||||
The real time is how much time has actually passed in the window
|
||||
(current time - start time of play). If the animations are progressing
|
||||
faster than they would in real life, it will slow down the window by calling
|
||||
:meth:`~.Manager._update_frame` with ``dt=0`` until that's no longer the case.
|
||||
This is to make sure that animations never go too fast: it doesn't do anything if
|
||||
animations are too slow!
|
||||
|
||||
At this point, the internal (Python) state of all mobjects has been updated
|
||||
to match the currently processed timestamp. If rendering should not be skipped,
|
||||
then it is now time to *take a picture*!
|
||||
|
||||
.. NOTE::
|
||||
|
||||
The update of the internal state (iteration over the time progression) happens
|
||||
*always* once :meth:`.Scene.play_internal` is entered. This ensures that even
|
||||
if frames do not need to be rendered (because, e.g., the ``-n`` CLI flag has
|
||||
been passed, something has been cached, or because we might be in a *Section*
|
||||
with skipped rendering), updater functions still run correctly, and the state
|
||||
of the first frame that *is* rendered is kept consistent.
|
||||
|
||||
To render an image, the scene calls the corresponding method of its renderer,
|
||||
:meth:`.CairoRenderer.render` and passes just the list of *moving mobjects* (remember,
|
||||
the *static mobjects* are assumed to have already been painted statically to
|
||||
the background of the scene). All of the hard work then happens when the renderer
|
||||
updates its current frame via a call to :meth:`.CairoRenderer.update_frame`:
|
||||
|
||||
First, the renderer prepares its :class:`.Camera` by checking whether the renderer
|
||||
has a ``static_image`` different from ``None`` stored already. If so, it sets the
|
||||
image as the *background image* of the camera via :meth:`.Camera.set_frame_to_background`,
|
||||
and otherwise it just resets the camera via :meth:`.Camera.reset`. The camera is then
|
||||
asked to capture the scene with a call to :meth:`.Camera.capture_mobjects`.
|
||||
|
||||
Things get a bit technical here, and at some point it is more efficient to
|
||||
delve into the implementation -- but here is a summary of what happens once the
|
||||
camera is asked to capture the scene:
|
||||
|
||||
- First, a flat list of mobjects is created (so submobjects get extracted from
|
||||
their parents). This list is then processed in groups of the same type of
|
||||
mobjects (e.g., a batch of vectorized mobjects, followed by a batch of image mobjects,
|
||||
followed by more vectorized mobjects, etc. -- in many cases there will just be
|
||||
one batch of vectorized mobjects).
|
||||
- Depending on the type of the currently processed batch, the camera uses dedicated
|
||||
*display functions* to convert the :class:`.Mobject` Python object to
|
||||
a NumPy array stored in the camera's ``pixel_array`` attribute.
|
||||
The most important example in that context is the display function for
|
||||
vectorized mobjects, :meth:`.Camera.display_multiple_vectorized_mobjects`,
|
||||
or the more particular (in case you did not add a background image to your
|
||||
:class:`.VMobject`), :meth:`.Camera.display_multiple_non_background_colored_vmobjects`.
|
||||
This method first gets the current Cairo context, and then, for every (vectorized)
|
||||
mobject in the batch, calls :meth:`.Camera.display_vectorized`. There,
|
||||
the actual background stroke, fill, and then stroke of the mobject is
|
||||
drawn onto the context. See :meth:`.Camera.apply_stroke` and
|
||||
:meth:`.Camera.set_cairo_context_color` for more details -- but it does not get
|
||||
much deeper than that, in the latter method the actual Bézier curves
|
||||
determined by the points of the mobject are drawn; this is where the low-level
|
||||
interaction with Cairo happens.
|
||||
|
||||
After all batches have been processed, the camera has an image representation
|
||||
of the Scene at the current time stamp in form of a NumPy array stored in its
|
||||
``pixel_array`` attribute. The renderer then takes this array and passes it to
|
||||
its :class:`.SceneFileWriter`. This concludes one iteration of the render loop,
|
||||
and once the time progression has been processed completely, a final bit
|
||||
of cleanup is performed before the :meth:`.Scene.play_internal` call is completed.
|
||||
to match the currently processed timestamp.
|
||||
|
||||
A TL;DR for the render loop, in the context of our toy example, reads as follows:
|
||||
|
||||
|
|
@ -968,23 +875,20 @@ A TL;DR for the render loop, in the context of our toy example, reads as follows
|
|||
medium render quality, the frame rate is 30 frames per second, and so the time
|
||||
progression with steps ``[0, 1/30, 2/30, ..., 89/30]`` is created.
|
||||
- In the internal render loop, each of these time stamps is processed:
|
||||
there are no updater functions, so effectively the scene updates the
|
||||
there are no updater functions, so effectively the manager updates the
|
||||
state of the transformation animation to the desired time stamp (for example,
|
||||
at time stamp ``t = 45/30``, the animation is completed to a rate of
|
||||
``alpha = 0.5``).
|
||||
- Then the scene asks the renderer to do its job. The renderer asks its camera
|
||||
to capture the scene, the only mobject that needs to be processed at this point
|
||||
is the main mobject attached to the transformation; the camera converts the
|
||||
current state of the mobject to entries in a NumPy array. The renderer passes
|
||||
this array to the file writer.
|
||||
- Then the manager asks the renderer to do its job. The renderer then produces
|
||||
the pixels, which are then fed into the :class:`.FileWriter`.
|
||||
- At the end of the loop, 90 frames have been passed to the file writer.
|
||||
|
||||
Completing the render loop
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The last few steps in the :meth:`.Scene.play_internal` call are not too
|
||||
The last few steps in the :meth:`.Manager._play` call are not too
|
||||
exciting: for every animation, the corresponding :meth:`.Animation.finish`
|
||||
and :meth:`.Animation.clean_up_from_scene` methods are called.
|
||||
method is called.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
|
|
@ -999,10 +903,6 @@ and :meth:`.Animation.clean_up_from_scene` methods are called.
|
|||
would be slightly longer than 1 second. We decided against this at some point.
|
||||
|
||||
In the end, the time progression is closed (which completes the displayed progress bar)
|
||||
in the terminal. With the closing of the time progression, the
|
||||
:meth:`.Scene.play_internal` call is completed, and we return to the renderer,
|
||||
which now orders the :class:`.SceneFileWriter` to close the output container that has
|
||||
been opened for this animation: a partial movie file is written.
|
||||
|
||||
This pretty much concludes the walkthrough of a :class:`.Scene.play` call,
|
||||
and actually there is not too much more to say for our toy example either: at
|
||||
|
|
@ -1025,5 +925,4 @@ which triggers the combination of the partial movie files into the final product
|
|||
And there you go! This is a more or less detailed description of how Manim works
|
||||
under the hood. While we did not discuss every single line of code in detail
|
||||
in this walkthrough, it should still give you a fairly good idea of how the general
|
||||
structural design of the library and at least the Cairo rendering flow in particular
|
||||
looks like.
|
||||
structural design of the library looks like.
|
||||
|
|
|
|||
|
|
@ -39,12 +39,8 @@ Cameras
|
|||
|
||||
.. inheritance-diagram::
|
||||
manim.camera.camera
|
||||
manim.camera.mapping_camera
|
||||
manim.camera.moving_camera
|
||||
manim.camera.multi_camera
|
||||
manim.camera.three_d_camera
|
||||
:parts: 1
|
||||
:top-classes: manim.camera.camera.Camera, manim.mobject.mobject.Mobject
|
||||
:top-classes: manim.camera.camera.Camera, manim.mobject.opengl.opengl_mobject.OpenGLMobject
|
||||
|
||||
Mobjects
|
||||
********
|
||||
|
|
|
|||
|
|
@ -297,9 +297,9 @@ Creating a custom animation
|
|||
|
||||
Even though Manim has many built-in animations, you will find times when you need to smoothly animate from one state of a :class:`~.Mobject` to another.
|
||||
If you find yourself in that situation, then you can define your own custom animation.
|
||||
You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate_mobject`.
|
||||
The :meth:`~.Animation.interpolate_mobject` method receives alpha as a parameter that starts at 0 and changes throughout the animation.
|
||||
So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate_mobject method.
|
||||
You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate`.
|
||||
The :meth:`~.Animation.interpolate` method receives alpha as a parameter that starts at 0 and changes throughout the animation.
|
||||
So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate method.
|
||||
Then you get all the benefits of :class:`~.Animation` such as playing it for different run times or using different rate functions.
|
||||
|
||||
Let's say you start with a number and want to create a :class:`~.Transform` animation that transforms it to a target number.
|
||||
|
|
@ -312,11 +312,11 @@ The class can have a constructor with three arguments, a :class:`~.DecimalNumber
|
|||
The constructor will pass the :class:`~.DecimalNumber` Mobject to the super constructor (in this case, the :class:`~.Animation` constructor) and will set start and end.
|
||||
|
||||
The only thing that you need to do is to define how you want it to look at every step of the animation.
|
||||
Manim provides you with the alpha value in the :meth:`~.Animation.interpolate_mobject` method based on frame rate of video, rate function, and run time of animation played.
|
||||
Manim provides you with the alpha value in the :meth:`~.Animation.interpolate` method based on frame rate of video, rate function, and run time of animation played.
|
||||
The alpha parameter holds a value between 0 and 1 representing the step of the currently playing animation.
|
||||
For example, 0 means the beginning of the animation, 0.5 means halfway through the animation, and 1 means the end of the animation.
|
||||
|
||||
In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate_mobject` method of the ``Count`` animation.
|
||||
In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate` method of the ``Count`` animation.
|
||||
Suppose you are starting at 50 and incrementing until the :class:`~.DecimalNumber` reaches 100 at the end of the animation.
|
||||
|
||||
* If alpha is 0, you want the value to be 50.
|
||||
|
|
@ -338,7 +338,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:`
|
|||
|
||||
.. manim:: CountingScene
|
||||
:ref_classes: Animation DecimalNumber
|
||||
:ref_methods: Animation.interpolate_mobject Scene.play
|
||||
:ref_methods: Animation.interpolate Scene.play
|
||||
|
||||
class Count(Animation):
|
||||
def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
|
||||
|
|
@ -348,7 +348,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:`
|
|||
self.start = start
|
||||
self.end = end
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
# Set value of DecimalNumber according to alpha
|
||||
value = self.start + (self.rate_func(alpha) * (self.end - self.start))
|
||||
self.mobject.set_value(value)
|
||||
|
|
|
|||
|
|
@ -266,6 +266,44 @@ You can also skip rendering all animations belonging to a section like this:
|
|||
|
||||
|
||||
|
||||
Groups
|
||||
******
|
||||
Sections are a powerful tool to organize your animations into different parts. However, sometimes it's
|
||||
more useful to look at bigger parts of your animations. *Groups* are effectively sections of sections.
|
||||
|
||||
The syntax is fairly simple::
|
||||
|
||||
class MyScene(Scene):
|
||||
# enable groups
|
||||
groups_api = True
|
||||
|
||||
@group
|
||||
def introduction(self) -> None:
|
||||
self.play(Write(Text("Hello World!")))
|
||||
self.next_section(...)
|
||||
self.play(Write(Text("This is a group!")))
|
||||
self.next_section(...)
|
||||
|
||||
@group
|
||||
def main_part(self) -> None:
|
||||
self.play(Write(Text("This is the main part!")))
|
||||
self.next_section(...)
|
||||
self.play(Write(Text("This is a group as well!")))
|
||||
self.next_section(...)
|
||||
|
||||
@group
|
||||
def conclusion(self) -> None:
|
||||
self.play(FadeOut(*self.mobjects))
|
||||
|
||||
You can then play specific groups by using the ``--groups`` flag::
|
||||
|
||||
manim --groups introduction,conclusion scene.py
|
||||
|
||||
Note that they must be separated by commas and without spaces.
|
||||
Alternatively, you can set it on Manim's global ``config`` variable::
|
||||
|
||||
config.groups = ["introduction", "conclusion"]
|
||||
|
||||
|
||||
Some command line flags
|
||||
***********************
|
||||
|
|
|
|||
9
example_scenes/bench.py
Normal file
9
example_scenes/bench.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from manim import *
|
||||
|
||||
|
||||
class Test(Scene):
|
||||
def construct(scene):
|
||||
scene.camera.set_euler_angles(phi=75 * DEGREES, theta=-45 * DEGREES)
|
||||
text = Tex("This is a 3D tex").fix_in_frame()
|
||||
scene.add(text)
|
||||
scene.wait()
|
||||
148
example_scenes/new_test_new.py
Normal file
148
example_scenes/new_test_new.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
# import pyglet
|
||||
from pyglet.gl import Config
|
||||
from pyglet.window import Window
|
||||
|
||||
import manim.utils.color.manim_colors as col
|
||||
from manim._config import tempconfig
|
||||
from manim.animation.creation import DrawBorderThenFill
|
||||
from manim.camera.camera import Camera
|
||||
from manim.constants import LEFT, OUT, RIGHT, UP
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square
|
||||
from manim.mobject.logo import ManimBanner
|
||||
from manim.mobject.text.numbers import DecimalNumber
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
if __name__ == "__main__":
|
||||
with tempconfig({"renderer": "opengl"}):
|
||||
win = Window(
|
||||
width=1920,
|
||||
height=1080,
|
||||
fullscreen=True,
|
||||
vsync=True,
|
||||
config=Config(double_buffer=True, samples=0),
|
||||
)
|
||||
renderer = OpenGLRenderer(1920, 1080, background_color=col.GRAY)
|
||||
# vm = OpenGLVMobject([col.RED, col.GREEN])
|
||||
vm = (
|
||||
Circle(
|
||||
radius=1,
|
||||
stroke_color=col.YELLOW,
|
||||
)
|
||||
.shift(3 * RIGHT + OUT)
|
||||
.set_opacity(0.6)
|
||||
)
|
||||
vm2 = Square(stroke_color=col.GREEN, fill_opacity=0, stroke_opacity=1).move_to(
|
||||
(0, 0, -0.5)
|
||||
)
|
||||
vm3 = ManimBanner().set_opacity(0.6)
|
||||
vm4 = (
|
||||
Circle(0.5, col.GREEN)
|
||||
.set_opacity(0.6)
|
||||
.shift(OUT)
|
||||
.set_fill(col.BLUE, opacity=0.2)
|
||||
)
|
||||
# vm.set_points_as_corners([[-1920/2, 0, 0], [1920/2, 0, 0], [0, 1080/2, 0]])
|
||||
# print(vm.color)
|
||||
# print(vm.fill_color)
|
||||
# print(vm.stroke_color)
|
||||
|
||||
clock_mobject = DecimalNumber(0.0).shift(4 * LEFT + 2.5 * UP)
|
||||
clock_mobject.fix_in_frame()
|
||||
|
||||
camera = Camera()
|
||||
camera.save_state()
|
||||
# renderer.init_camera(camera)
|
||||
|
||||
# renderer.render(camera, [vm, vm2])
|
||||
# image = renderer.get_pixels()
|
||||
# print(image.shape)
|
||||
# Image.fromarray(image, "RGBA").show()
|
||||
# exit(0)
|
||||
renderer.use_window()
|
||||
|
||||
# clock = pyglet.clock.get_default()
|
||||
|
||||
def update_circle(dt):
|
||||
vm.move_to((np.sin(dt) * 4, np.cos(dt) * 4, -1))
|
||||
|
||||
def p2m(x, y, z):
|
||||
from manim._config import config
|
||||
|
||||
return (
|
||||
config.frame_width * (x / config.pixel_width - 0.5),
|
||||
config.frame_height * (y / config.pixel_height - 0.5),
|
||||
z,
|
||||
)
|
||||
|
||||
@win.event
|
||||
def on_close():
|
||||
win.close()
|
||||
|
||||
@win.event
|
||||
def on_mouse_motion(x, y, dx, dy):
|
||||
# vm.move_to((14.2222 * (x / 1920 - 0.5), 8 * (y / 1080 - 0.5), 0))
|
||||
# camera.move_to(p2m(x,y,camera.get_center()[2]))
|
||||
from scipy.spatial.transform import Rotation
|
||||
|
||||
camera.set_orientation(
|
||||
Rotation.from_rotvec(
|
||||
(-UP * (x / 1920 - 0.5) + RIGHT * (y / 1080 - 0.5)) * 2 * 3.1415
|
||||
)
|
||||
)
|
||||
# vm.set_color(col.RED.interpolate(col.GREEN,x/1920))
|
||||
# print(x,y)
|
||||
|
||||
@win.event
|
||||
def on_draw():
|
||||
# dt = clock.update_time()
|
||||
renderer.render(camera, [vm2, vm3, vm4, clock_mobject, vm])
|
||||
# update_circle(counter)
|
||||
|
||||
@win.event
|
||||
def on_resize(width, height):
|
||||
super(Window, win).on_resize(width, height)
|
||||
|
||||
# pyglet.app.run()
|
||||
has_started = False
|
||||
is_finished = False
|
||||
|
||||
run_time = 5
|
||||
new_vm = Square(fill_color=col.GREEN, stroke_color=col.BLUE).shift(
|
||||
2.5 * RIGHT - UP + 2 * OUT
|
||||
)
|
||||
animation = DrawBorderThenFill(vm3, run_time=run_time)
|
||||
|
||||
real_time = 0
|
||||
virtual_time = 0
|
||||
start_timestamp = time.time()
|
||||
dt = 1 / 30
|
||||
|
||||
while True:
|
||||
# pyglet.app.platform_event_loop.step()
|
||||
win.switch_to()
|
||||
if not has_started:
|
||||
animation.begin()
|
||||
has_started = True
|
||||
|
||||
real_time = time.time() - start_timestamp
|
||||
while virtual_time < real_time:
|
||||
virtual_time += dt
|
||||
if not is_finished:
|
||||
if virtual_time >= run_time:
|
||||
animation.finish()
|
||||
buffer = str(animation.buffer)
|
||||
print(f"buffer = {buffer}")
|
||||
has_finished = True
|
||||
else:
|
||||
animation.update_mobjects(dt)
|
||||
animation.interpolate(virtual_time / run_time)
|
||||
# update_circle(virtual_time)
|
||||
clock_mobject.set_value(virtual_time)
|
||||
win.dispatch_event("on_draw")
|
||||
win.dispatch_events()
|
||||
win.flip()
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
from pathlib import Path
|
||||
|
||||
from manim.opengl import *
|
||||
|
||||
import manim.utils.opengl as opengl
|
||||
from manim import *
|
||||
from manim.opengl import *
|
||||
from manim.mobject.opengl.opengl_surface import OpenGLTexturedSurface
|
||||
from manim.mobject.opengl.opengl_three_dimensions import OpenGLSurfaceMesh
|
||||
from manim.mobject.opengl.shader import FullScreenQuad, Mesh, Shader
|
||||
|
||||
# Copied from https://3b1b.github.io/manim/getting_started/example_scenes.html#surfaceexample.
|
||||
# Lines that do not yet work with the Community Version are commented.
|
||||
|
|
|
|||
114
example_scenes/test_new.py
Normal file
114
example_scenes/test_new.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import numpy as np
|
||||
import pyglet
|
||||
from pyglet.gl import Config
|
||||
from pyglet.window import Window
|
||||
|
||||
import manim.utils.color.manim_colors as col
|
||||
from manim._config import tempconfig
|
||||
from manim.camera.camera import Camera
|
||||
from manim.constants import OUT, RIGHT, UP
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square
|
||||
from manim.mobject.logo import ManimBanner
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.text.numbers import DecimalNumber
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
if __name__ == "__main__":
|
||||
with tempconfig({"renderer": "opengl"}):
|
||||
win = Window(
|
||||
width=1920,
|
||||
height=1080,
|
||||
vsync=True,
|
||||
config=Config(double_buffer=True, samples=0),
|
||||
)
|
||||
renderer = OpenGLRenderer(1920, 1080, background_color=col.GRAY)
|
||||
# vm = OpenGLVMobject([col.RED, col.GREEN])
|
||||
vm = (
|
||||
Circle(
|
||||
radius=1,
|
||||
stroke_color=col.YELLOW,
|
||||
)
|
||||
.shift(RIGHT)
|
||||
.set_opacity(0.5)
|
||||
)
|
||||
vm2 = Square(stroke_color=col.GREEN, fill_opacity=0, stroke_opacity=1).move_to(
|
||||
(0, 0, -0.5)
|
||||
)
|
||||
vm3 = ManimBanner().set_opacity(1.0)
|
||||
vm4 = (
|
||||
Circle(0.5, col.GREEN)
|
||||
.set_opacity(0.6)
|
||||
.shift(OUT)
|
||||
.set_fill(col.BLUE, opacity=0.2)
|
||||
)
|
||||
# vm.set_points_as_corners([[-1920/2, 0, 0], [1920/2, 0, 0], [0, 1080/2, 0]])
|
||||
# print(vm.color)
|
||||
# print(vm.fill_color)
|
||||
# print(vm.stroke_color)
|
||||
|
||||
camera = Camera()
|
||||
camera.save_state()
|
||||
renderer.init_camera(camera)
|
||||
|
||||
# renderer.render(camera, [vm, vm2])
|
||||
# image = renderer.get_pixels()
|
||||
# print(image.shape)
|
||||
# Image.fromarray(image, "RGBA").show()
|
||||
# exit(0)
|
||||
renderer.use_window()
|
||||
|
||||
clock = pyglet.clock.get_default()
|
||||
|
||||
def update_circle(dt):
|
||||
vm.move_to((np.sin(dt) * 4, np.cos(dt) * 4, -1))
|
||||
|
||||
def p2m(x, y, z):
|
||||
from manim._config import config
|
||||
|
||||
return (
|
||||
config.frame_width * (x / config.pixel_width - 0.5),
|
||||
config.frame_height * (y / config.pixel_height - 0.5),
|
||||
z,
|
||||
)
|
||||
|
||||
@win.event
|
||||
def on_close():
|
||||
win.close()
|
||||
|
||||
@win.event
|
||||
def on_mouse_motion(x, y, dx, dy):
|
||||
# vm.move_to((14.2222 * (x / 1920 - 0.5), 8 * (y / 1080 - 0.5), 0))
|
||||
# camera.move_to(p2m(x,y,camera.get_center()[2]))
|
||||
from scipy.spatial.transform import Rotation
|
||||
|
||||
camera.set_orientation(
|
||||
Rotation.from_rotvec(
|
||||
(-UP * (x / 1920 - 0.5) + RIGHT * (y / 1080 - 0.5)) * 2 * 3.1415
|
||||
)
|
||||
)
|
||||
# vm.set_color(col.RED.interpolate(col.GREEN,x/1920))
|
||||
# print(x,y)
|
||||
|
||||
@win.event
|
||||
def on_draw():
|
||||
dt = clock.update_time()
|
||||
fps: OpenGLVMobject = DecimalNumber(dt)
|
||||
fps.fix_in_frame()
|
||||
renderer.render(camera, [vm, vm2, vm3, vm4, fps])
|
||||
# update_circle(counter)
|
||||
|
||||
@win.event
|
||||
def on_resize(width, height):
|
||||
super(Window, win).on_resize(width, height)
|
||||
|
||||
pyglet.app.run()
|
||||
# while True:
|
||||
# pyglet.clock.tick()
|
||||
# pyglet.app.platform_event_loop.step()
|
||||
# win.switch_to()
|
||||
# counter += 0.01
|
||||
# update_circle(counter)
|
||||
# win.dispatch_event("on_draw")
|
||||
# win.dispatch_events()
|
||||
# win.flip()
|
||||
96
example_scenes/test_new_rendering.py
Normal file
96
example_scenes/test_new_rendering.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
from manim import *
|
||||
|
||||
|
||||
class Test(Scene):
|
||||
groups_api = True
|
||||
|
||||
@group
|
||||
def first_section(self) -> None:
|
||||
line = Line()
|
||||
line.add_updater(lambda m, dt: m.rotate(PI * dt))
|
||||
line.to_edge(LEFT)
|
||||
self.add(line)
|
||||
square = Square()
|
||||
tex = Tex(
|
||||
"Hello, ",
|
||||
"world",
|
||||
r" $e^{i\theta}$",
|
||||
stroke_color=RED,
|
||||
fill_color=BLUE,
|
||||
stroke_width=2,
|
||||
).to_edge(RIGHT)
|
||||
tex.set_color_by_tex("world", GREEN)
|
||||
self.add(tex)
|
||||
self.play(Create(tex), Rotate(square, PI / 2))
|
||||
self.wait(1)
|
||||
self.play(FadeOut(square))
|
||||
|
||||
@group
|
||||
def three_mobjects(self) -> None:
|
||||
hexagon = RegularPolygon(6)
|
||||
circle = Circle()
|
||||
star = Star()
|
||||
VGroup(hexagon, circle, star).arrange()
|
||||
self.play(
|
||||
Succession(
|
||||
Create(hexagon),
|
||||
DrawBorderThenFill(circle),
|
||||
SpinInFromNothing(star),
|
||||
)
|
||||
)
|
||||
self.play(FadeOut(VGroup(hexagon, circle, star)))
|
||||
|
||||
@group
|
||||
def manim_banner(self) -> None:
|
||||
banner = ManimBanner().scale(0.5)
|
||||
self.play(banner.create())
|
||||
self.play(banner.expand())
|
||||
self.wait(1)
|
||||
self.play(Unwrite(banner))
|
||||
|
||||
@group
|
||||
def graph(self):
|
||||
vertices = [1, 2, 3]
|
||||
edges = [(1, 2), (2, 3), (3, 1)]
|
||||
graph = Graph(vertices, edges, layout="circular")
|
||||
self.play(Create(graph))
|
||||
self.play(
|
||||
graph.animate.add_vertices(
|
||||
4,
|
||||
5,
|
||||
vertex_config={4: {"fill_color": RED}, 5: {"fill_color": RED}},
|
||||
positions={4: [2, 1, 0], 5: [2, -1, 0]},
|
||||
)
|
||||
)
|
||||
self.play( # TODO: this animation is currently broken
|
||||
graph.animate.add_edges(
|
||||
(2, 4),
|
||||
(3, 5),
|
||||
(4, 5),
|
||||
edge_config={
|
||||
(2, 4): {"stroke_color": GREEN},
|
||||
(3, 5): {"stroke_color": GREEN},
|
||||
(4, 5): {"stroke_color": YELLOW},
|
||||
},
|
||||
)
|
||||
)
|
||||
self.wait(1)
|
||||
self.play(graph.animate.remove_vertices(1))
|
||||
self.play(graph.animate.remove_edges((4, 5)))
|
||||
self.play(Uncreate(graph))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with (
|
||||
tempconfig(
|
||||
{
|
||||
"preview": True,
|
||||
"write_to_movie": False,
|
||||
"disable_caching": True,
|
||||
"frame_rate": 60,
|
||||
"disable_caching_warning": True,
|
||||
}
|
||||
),
|
||||
Manager(Test) as manager,
|
||||
):
|
||||
manager.render()
|
||||
|
|
@ -17,98 +17,91 @@ except PackageNotFoundError:
|
|||
|
||||
# Importing the config module should be the first thing we do, since other
|
||||
# modules depend on the global config dict for initialization.
|
||||
from ._config import *
|
||||
from manim._config import *
|
||||
|
||||
# many scripts depend on this -> has to be loaded first
|
||||
from .utils.commands import *
|
||||
from manim.utils.commands import *
|
||||
|
||||
# isort: on
|
||||
import numpy as np
|
||||
|
||||
from .animation.animation import *
|
||||
from .animation.changing import *
|
||||
from .animation.composition import *
|
||||
from .animation.creation import *
|
||||
from .animation.fading import *
|
||||
from .animation.growing import *
|
||||
from .animation.indication import *
|
||||
from .animation.movement import *
|
||||
from .animation.numbers import *
|
||||
from .animation.rotation import *
|
||||
from .animation.specialized import *
|
||||
from .animation.speedmodifier import *
|
||||
from .animation.transform import *
|
||||
from .animation.transform_matching_parts import *
|
||||
from .animation.updaters.mobject_update_utils import *
|
||||
from .animation.updaters.update import *
|
||||
from .camera.camera import *
|
||||
from .camera.mapping_camera import *
|
||||
from .camera.moving_camera import *
|
||||
from .camera.multi_camera import *
|
||||
from .camera.three_d_camera import *
|
||||
from .constants import *
|
||||
from .mobject.frame import *
|
||||
from .mobject.geometry.arc import *
|
||||
from .mobject.geometry.boolean_ops import *
|
||||
from .mobject.geometry.labeled import *
|
||||
from .mobject.geometry.line import *
|
||||
from .mobject.geometry.polygram import *
|
||||
from .mobject.geometry.shape_matchers import *
|
||||
from .mobject.geometry.tips import *
|
||||
from .mobject.graph import *
|
||||
from .mobject.graphing.coordinate_systems import *
|
||||
from .mobject.graphing.functions import *
|
||||
from .mobject.graphing.number_line import *
|
||||
from .mobject.graphing.probability import *
|
||||
from .mobject.graphing.scale import *
|
||||
from .mobject.logo import *
|
||||
from .mobject.matrix import *
|
||||
from .mobject.mobject import *
|
||||
from .mobject.opengl.dot_cloud import *
|
||||
from .mobject.opengl.opengl_point_cloud_mobject import *
|
||||
from .mobject.svg.brace import *
|
||||
from .mobject.svg.svg_mobject import *
|
||||
from .mobject.table import *
|
||||
from .mobject.text.code_mobject import *
|
||||
from .mobject.text.numbers import *
|
||||
from .mobject.text.tex_mobject import *
|
||||
from .mobject.text.text_mobject import *
|
||||
from .mobject.three_d.polyhedra import *
|
||||
from .mobject.three_d.three_d_utils import *
|
||||
from .mobject.three_d.three_dimensions import *
|
||||
from .mobject.types.image_mobject import *
|
||||
from .mobject.types.point_cloud_mobject import *
|
||||
from .mobject.types.vectorized_mobject import *
|
||||
from .mobject.value_tracker import *
|
||||
from .mobject.vector_field import *
|
||||
from .renderer.cairo_renderer import *
|
||||
from .scene.moving_camera_scene import *
|
||||
from .scene.scene import *
|
||||
from .scene.scene_file_writer import *
|
||||
from .scene.section import *
|
||||
from .scene.three_d_scene import *
|
||||
from .scene.vector_space_scene import *
|
||||
from .scene.zoomed_scene import *
|
||||
from .utils import color, rate_functions, unit
|
||||
from .utils.bezier import *
|
||||
from .utils.color import *
|
||||
from .utils.config_ops import *
|
||||
from .utils.debug import *
|
||||
from .utils.file_ops import *
|
||||
from .utils.images import *
|
||||
from .utils.iterables import *
|
||||
from .utils.paths import *
|
||||
from .utils.rate_functions import *
|
||||
from .utils.simple_functions import *
|
||||
from .utils.sounds import *
|
||||
from .utils.space_ops import *
|
||||
from .utils.tex import *
|
||||
from .utils.tex_templates import *
|
||||
from manim.animation.animation import *
|
||||
from manim.animation.changing import *
|
||||
from manim.animation.composition import *
|
||||
from manim.animation.creation import *
|
||||
from manim.animation.fading import *
|
||||
from manim.animation.growing import *
|
||||
from manim.animation.indication import *
|
||||
from manim.animation.movement import *
|
||||
from manim.animation.numbers import *
|
||||
from manim.animation.rotation import *
|
||||
from manim.animation.specialized import *
|
||||
from manim.animation.speedmodifier import *
|
||||
from manim.animation.transform import *
|
||||
from manim.animation.transform_matching_parts import *
|
||||
from manim.animation.updaters.mobject_update_utils import *
|
||||
from manim.animation.updaters.update import *
|
||||
from manim.constants import *
|
||||
from manim.file_writer import *
|
||||
from manim.manager import *
|
||||
from manim.mobject.frame import *
|
||||
from manim.mobject.geometry.arc import *
|
||||
from manim.mobject.geometry.boolean_ops import *
|
||||
from manim.mobject.geometry.labeled import *
|
||||
from manim.mobject.geometry.line import *
|
||||
from manim.mobject.geometry.polygram import *
|
||||
from manim.mobject.geometry.shape_matchers import *
|
||||
from manim.mobject.geometry.tips import *
|
||||
from manim.mobject.graph import *
|
||||
from manim.mobject.graphing.coordinate_systems import *
|
||||
from manim.mobject.graphing.functions import *
|
||||
from manim.mobject.graphing.number_line import *
|
||||
from manim.mobject.graphing.probability import *
|
||||
from manim.mobject.graphing.scale import *
|
||||
from manim.mobject.logo import *
|
||||
from manim.mobject.matrix import *
|
||||
from manim.mobject.mobject import *
|
||||
from manim.mobject.opengl.dot_cloud import *
|
||||
from manim.mobject.opengl.opengl_point_cloud_mobject import *
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import *
|
||||
from manim.mobject.svg.brace import *
|
||||
from manim.mobject.svg.svg_mobject import *
|
||||
from manim.mobject.table import *
|
||||
from manim.mobject.text.code_mobject import *
|
||||
from manim.mobject.text.numbers import *
|
||||
from manim.mobject.text.tex_mobject import *
|
||||
from manim.mobject.text.text_mobject import *
|
||||
from manim.mobject.three_d.polyhedra import *
|
||||
from manim.mobject.three_d.three_d_utils import *
|
||||
from manim.mobject.three_d.three_dimensions import *
|
||||
from manim.mobject.types.image_mobject import *
|
||||
from manim.mobject.types.point_cloud_mobject import *
|
||||
from manim.mobject.types.vectorized_mobject import *
|
||||
from manim.mobject.value_tracker import *
|
||||
from manim.mobject.vector_field import *
|
||||
from manim.scene.scene import *
|
||||
from manim.scene.sections import *
|
||||
from manim.scene.vector_space_scene import *
|
||||
from manim.utils import color, rate_functions, unit
|
||||
from manim.utils.bezier import *
|
||||
from manim.utils.color import *
|
||||
from manim.utils.config_ops import *
|
||||
from manim.utils.debug import *
|
||||
from manim.utils.file_ops import *
|
||||
from manim.utils.images import *
|
||||
from manim.utils.iterables import *
|
||||
from manim.utils.paths import *
|
||||
from manim.utils.rate_functions import *
|
||||
from manim.utils.simple_functions import *
|
||||
from manim.utils.sounds import *
|
||||
from manim.utils.space_ops import *
|
||||
from manim.utils.tex import *
|
||||
from manim.utils.tex_templates import *
|
||||
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
|
||||
from .utils.ipython_magic import ManimMagic
|
||||
from manim.utils.ipython_magic import ManimMagic
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
|
|
@ -116,4 +109,4 @@ else:
|
|||
if ipy is not None:
|
||||
ipy.register_magics(ManimMagic)
|
||||
|
||||
from .plugins import *
|
||||
from manim.plugins import *
|
||||
|
|
|
|||
|
|
@ -7,18 +7,18 @@
|
|||
|
||||
# Each of the following will be set to True if the corresponding CLI flag
|
||||
# is present when executing manim. If the flag is not present, they will
|
||||
# be set to the value found here. For example, running manim with the -w
|
||||
# flag will set WRITE_TO_MOVIE to True. However, since the default value
|
||||
# of WRITE_TO_MOVIE defined in this file is also True, running manim
|
||||
# without the -w value will also output a movie file. To change that, set
|
||||
# WRITE_TO_MOVIE = False so that running manim without the -w flag will not
|
||||
# generate a movie file. Note all of the following accept boolean values.
|
||||
# be set to the value found here. For example, running manim with the --format=mp4
|
||||
# flag will set FORMAT to mp4. However, since the default value
|
||||
# of FORMAT defined in this file is also mp4, running manim
|
||||
# without the --format=mp4 value will also output an mp4 movie file. To change that, set
|
||||
# FORMAT = webm so that running manim without the --format=mp4 flag will not
|
||||
# generate an mp4 movie file.
|
||||
|
||||
# --notify_outdated_version
|
||||
notify_outdated_version = True
|
||||
|
||||
# -w, --write_to_movie
|
||||
write_to_movie = True
|
||||
write_to_movie = False
|
||||
|
||||
format = mp4
|
||||
|
||||
|
|
@ -29,15 +29,9 @@ save_last_frame = False
|
|||
# -a, --write_all
|
||||
write_all = False
|
||||
|
||||
# -g, --save_pngs
|
||||
save_pngs = False
|
||||
|
||||
# -0, --zero_pad
|
||||
zero_pad = 4
|
||||
|
||||
# -i, --save_as_gif
|
||||
save_as_gif = False
|
||||
|
||||
# --save_sections
|
||||
save_sections = False
|
||||
|
||||
|
|
@ -94,7 +88,7 @@ text_dir = {media_dir}/texts
|
|||
partial_movie_dir = {video_dir}/partial_movie_files/{scene_name}
|
||||
|
||||
# --renderer [cairo|opengl]
|
||||
renderer = cairo
|
||||
renderer = opengl
|
||||
|
||||
# --enable_gui
|
||||
enable_gui = False
|
||||
|
|
@ -121,12 +115,6 @@ window_monitor = 0
|
|||
# --force_window
|
||||
force_window = False
|
||||
|
||||
# --use_projection_fill_shaders
|
||||
use_projection_fill_shaders = False
|
||||
|
||||
# --use_projection_stroke_shaders
|
||||
use_projection_stroke_shaders = False
|
||||
|
||||
movie_file_extension = .mp4
|
||||
|
||||
# These now override the --quality option.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import logging
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Iterator, Mapping, MutableMapping
|
||||
from collections.abc import Iterator, Mapping, MutableMapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, NoReturn
|
||||
|
||||
|
|
@ -276,6 +276,7 @@ class ManimConfig(MutableMapping):
|
|||
"frame_x_radius",
|
||||
"frame_y_radius",
|
||||
"from_animation_number",
|
||||
"groups",
|
||||
"images_dir",
|
||||
"input_file",
|
||||
"media_embed",
|
||||
|
|
@ -294,10 +295,8 @@ class ManimConfig(MutableMapping):
|
|||
"preview",
|
||||
"progress_bar",
|
||||
"quality",
|
||||
"save_as_gif",
|
||||
"save_sections",
|
||||
"save_last_frame",
|
||||
"save_pngs",
|
||||
"scene_names",
|
||||
"seed",
|
||||
"show_in_file_browser",
|
||||
|
|
@ -309,8 +308,6 @@ class ManimConfig(MutableMapping):
|
|||
"renderer",
|
||||
"enable_gui",
|
||||
"gui_location",
|
||||
"use_projection_fill_shaders",
|
||||
"use_projection_stroke_shaders",
|
||||
"verbosity",
|
||||
"video_dir",
|
||||
"sections_dir",
|
||||
|
|
@ -329,6 +326,22 @@ class ManimConfig(MutableMapping):
|
|||
def __init__(self) -> None:
|
||||
self._d: dict[str, Any | None] = dict.fromkeys(self._OPTS)
|
||||
|
||||
def _warn_about_config_options(self) -> None:
|
||||
"""Warns about incorrect config options, or permutations of config options."""
|
||||
logger = logging.getLogger("manim")
|
||||
if self.format == "webm":
|
||||
logger.warning(
|
||||
"Output format set as webm, this can be slower than other formats",
|
||||
)
|
||||
if not self.preview and not self.write_to_movie:
|
||||
logger.warning(
|
||||
"preview and write_to_movie disabled, this is a dry run. Try passing -p or -w."
|
||||
)
|
||||
elif self.preview and self.write_to_movie:
|
||||
logger.warning(
|
||||
"Both preview and write_to_movie enabled, this can be slower than just previewing."
|
||||
)
|
||||
|
||||
# behave like a dict
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self._d)
|
||||
|
|
@ -579,8 +592,6 @@ class ManimConfig(MutableMapping):
|
|||
"write_to_movie",
|
||||
"save_last_frame",
|
||||
"write_all",
|
||||
"save_pngs",
|
||||
"save_as_gif",
|
||||
"save_sections",
|
||||
"preview",
|
||||
"show_in_file_browser",
|
||||
|
|
@ -591,8 +602,6 @@ class ManimConfig(MutableMapping):
|
|||
"custom_folders",
|
||||
"enable_gui",
|
||||
"fullscreen",
|
||||
"use_projection_fill_shaders",
|
||||
"use_projection_stroke_shaders",
|
||||
"enable_wireframe",
|
||||
"force_window",
|
||||
"no_latex_cleanup",
|
||||
|
|
@ -700,6 +709,8 @@ class ManimConfig(MutableMapping):
|
|||
if quality:
|
||||
self.quality = _determine_quality(quality)
|
||||
|
||||
self.groups = parser["CLI"].get("groups", fallback="", raw=True) or []
|
||||
|
||||
return self
|
||||
|
||||
def digest_args(self, args: argparse.Namespace) -> Self:
|
||||
|
|
@ -754,8 +765,6 @@ class ManimConfig(MutableMapping):
|
|||
"show_in_file_browser",
|
||||
"write_to_movie",
|
||||
"save_last_frame",
|
||||
"save_pngs",
|
||||
"save_as_gif",
|
||||
"save_sections",
|
||||
"write_all",
|
||||
"disable_caching",
|
||||
|
|
@ -769,8 +778,6 @@ class ManimConfig(MutableMapping):
|
|||
"background_color",
|
||||
"enable_gui",
|
||||
"fullscreen",
|
||||
"use_projection_fill_shaders",
|
||||
"use_projection_stroke_shaders",
|
||||
"zero_pad",
|
||||
"enable_wireframe",
|
||||
"force_window",
|
||||
|
|
@ -943,6 +950,7 @@ class ManimConfig(MutableMapping):
|
|||
def notify_outdated_version(self, value: bool) -> None:
|
||||
self._set_boolean("notify_outdated_version", value)
|
||||
|
||||
# TODO: Rename to write_to_file
|
||||
@property
|
||||
def write_to_movie(self) -> bool:
|
||||
"""Whether to render the scene to a movie file (-w)."""
|
||||
|
|
@ -970,24 +978,6 @@ class ManimConfig(MutableMapping):
|
|||
def write_all(self, value: bool) -> None:
|
||||
self._set_boolean("write_all", value)
|
||||
|
||||
@property
|
||||
def save_pngs(self) -> bool:
|
||||
"""Whether to save all frames in the scene as images files (-g)."""
|
||||
return self._d["save_pngs"]
|
||||
|
||||
@save_pngs.setter
|
||||
def save_pngs(self, value: bool) -> None:
|
||||
self._set_boolean("save_pngs", value)
|
||||
|
||||
@property
|
||||
def save_as_gif(self) -> bool:
|
||||
"""Whether to save the rendered scene in .gif format (-i)."""
|
||||
return self._d["save_as_gif"]
|
||||
|
||||
@save_as_gif.setter
|
||||
def save_as_gif(self, value: bool) -> None:
|
||||
self._set_boolean("save_as_gif", value)
|
||||
|
||||
@property
|
||||
def save_sections(self) -> bool:
|
||||
"""Whether to save single videos for each section in addition to the movie file."""
|
||||
|
|
@ -1059,10 +1049,6 @@ class ManimConfig(MutableMapping):
|
|||
[None, "png", "gif", "mp4", "mov", "webm"],
|
||||
)
|
||||
self.resolve_movie_file_extension(self.transparent)
|
||||
if self.format == "webm":
|
||||
logger.warning(
|
||||
"Output format set as webm, this can be slower than other formats",
|
||||
)
|
||||
|
||||
@property
|
||||
def ffmpeg_loglevel(self) -> str:
|
||||
|
|
@ -1218,6 +1204,24 @@ class ManimConfig(MutableMapping):
|
|||
def upto_animation_number(self, value: int) -> None:
|
||||
self._set_pos_number("upto_animation_number", value, True)
|
||||
|
||||
@property
|
||||
def groups(self) -> tuple[str, ...]:
|
||||
"""The name of the groups to play.
|
||||
|
||||
If not passed, it will play all groups. Otherwise,
|
||||
it will play only the groups passed in.
|
||||
"""
|
||||
return self._d["groups"] # type: ignore[misc]
|
||||
|
||||
@groups.setter
|
||||
def groups(self, value: str | Sequence[str]) -> None:
|
||||
if isinstance(value, str):
|
||||
self._set_str("groups", value.replace(" ", "").split(","))
|
||||
else:
|
||||
if not all(isinstance(v, str) for v in value):
|
||||
raise ValueError("groups must be a string or a sequence of strings")
|
||||
self._d["groups"] = tuple(value)
|
||||
|
||||
@property
|
||||
def max_files_cached(self) -> int:
|
||||
"""Maximum number of files cached. Use -1 for infinity (no flag)."""
|
||||
|
|
@ -1478,24 +1482,6 @@ class ManimConfig(MutableMapping):
|
|||
def fullscreen(self, value: bool) -> None:
|
||||
self._set_boolean("fullscreen", value)
|
||||
|
||||
@property
|
||||
def use_projection_fill_shaders(self) -> bool:
|
||||
"""Use shaders for OpenGLVMobject fill which are compatible with transformation matrices."""
|
||||
return self._d["use_projection_fill_shaders"]
|
||||
|
||||
@use_projection_fill_shaders.setter
|
||||
def use_projection_fill_shaders(self, value: bool) -> None:
|
||||
self._set_boolean("use_projection_fill_shaders", value)
|
||||
|
||||
@property
|
||||
def use_projection_stroke_shaders(self) -> bool:
|
||||
"""Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices."""
|
||||
return self._d["use_projection_stroke_shaders"]
|
||||
|
||||
@use_projection_stroke_shaders.setter
|
||||
def use_projection_stroke_shaders(self, value: bool) -> None:
|
||||
self._set_boolean("use_projection_stroke_shaders", value)
|
||||
|
||||
@property
|
||||
def zero_pad(self) -> int:
|
||||
"""PNG zero padding. A number between 0 (no zero padding) and 9 (9 columns minimum)."""
|
||||
|
|
|
|||
|
|
@ -2,32 +2,45 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
||||
from .. import config, logger
|
||||
from ..constants import RendererType
|
||||
from ..mobject import mobject
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.opengl import opengl_mobject
|
||||
from ..utils.rate_functions import linear, smooth
|
||||
|
||||
__all__ = ["Animation", "Wait", "Add", "override_animation"]
|
||||
|
||||
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from copy import deepcopy
|
||||
from functools import partialmethod
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
from typing import TYPE_CHECKING, Any, Self, assert_never, cast, overload
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
_AnimationBuilder,
|
||||
)
|
||||
|
||||
from .. import logger
|
||||
from ..utils.rate_functions import linear, smooth
|
||||
from .protocol import AnimationProtocol, MobjectAnimation
|
||||
from .scene_buffer import SceneBuffer, SceneOperation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
M = TypeVar("M", bound=Mobject)
|
||||
|
||||
|
||||
__all__ = ["Animation", "Wait", "override_animation"]
|
||||
|
||||
|
||||
DEFAULT_ANIMATION_RUN_TIME: float = 1.0
|
||||
DEFAULT_ANIMATION_LAG_RATIO: float = 0.0
|
||||
|
||||
|
||||
class Animation:
|
||||
class Animation(AnimationProtocol):
|
||||
"""An animation.
|
||||
|
||||
Animations have a fixed time span.
|
||||
|
|
@ -69,9 +82,9 @@ class Animation:
|
|||
.. NOTE::
|
||||
|
||||
In the current implementation of this class, the specified rate function is applied
|
||||
within :meth:`.Animation.interpolate_mobject` call as part of the call to
|
||||
within :meth:`.Animation.interpolate` call as part of the call to
|
||||
:meth:`.Animation.interpolate_submobject`. For subclasses of :class:`.Animation`
|
||||
that are implemented by overriding :meth:`interpolate_mobject`, the rate function
|
||||
that are implemented by overriding :meth:`interpolate`, the rate function
|
||||
has to be applied manually (e.g., by passing ``self.rate_func(alpha)`` instead
|
||||
of just ``alpha``).
|
||||
|
||||
|
|
@ -127,37 +140,33 @@ class Animation:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject | OpenGLMobject | None,
|
||||
mobject: Mobject | None,
|
||||
lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO,
|
||||
run_time: float = DEFAULT_ANIMATION_RUN_TIME,
|
||||
rate_func: Callable[[float], float] = smooth,
|
||||
reverse_rate_function: bool = False,
|
||||
name: str = None,
|
||||
remover: bool = False, # remove a mobject from the screen?
|
||||
name: str = "",
|
||||
remover: bool = False, # remove a mobject from the screen at end of animation
|
||||
suspend_mobject_updating: bool = True,
|
||||
introducer: bool = False,
|
||||
*,
|
||||
_on_finish: Callable[[], None] = lambda _: None,
|
||||
use_override: bool = True, # included here to avoid TypeError if passed from a subclass' constructor
|
||||
) -> None:
|
||||
self._typecheck_input(mobject)
|
||||
self.run_time: float = run_time
|
||||
self.rate_func: Callable[[float], float] = rate_func
|
||||
self.reverse_rate_function: bool = reverse_rate_function
|
||||
self.name: str | None = name
|
||||
self.name: str = name
|
||||
self.remover: bool = remover
|
||||
self.introducer: bool = introducer
|
||||
self.suspend_mobject_updating: bool = suspend_mobject_updating
|
||||
self.lag_ratio: float = lag_ratio
|
||||
self._on_finish: Callable[[Scene], None] = _on_finish
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
self.starting_mobject: OpenGLMobject = OpenGLMobject()
|
||||
self.mobject: OpenGLMobject = (
|
||||
mobject if mobject is not None else OpenGLMobject()
|
||||
)
|
||||
else:
|
||||
self.starting_mobject: Mobject = Mobject()
|
||||
self.mobject: Mobject = mobject if mobject is not None else Mobject()
|
||||
|
||||
self.buffer = SceneBuffer()
|
||||
self.apply_buffer = False # ask scene to apply buffer
|
||||
|
||||
self.starting_mobject: Mobject = Mobject()
|
||||
self.mobject: Mobject = mobject if mobject is not None else Mobject()
|
||||
|
||||
if hasattr(self, "CONFIG"):
|
||||
logger.error(
|
||||
|
|
@ -183,7 +192,7 @@ class Animation:
|
|||
def _typecheck_input(self, mobject: Mobject | None) -> None:
|
||||
if mobject is None:
|
||||
logger.debug("Animation with empty mobject")
|
||||
elif not isinstance(mobject, (Mobject, OpenGLMobject)):
|
||||
elif not isinstance(mobject, Mobject):
|
||||
raise TypeError("Animation only works on Mobjects")
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
@ -194,6 +203,17 @@ class Animation:
|
|||
def __repr__(self) -> str:
|
||||
return str(self)
|
||||
|
||||
def update_rate_info(
|
||||
self,
|
||||
run_time: float | None = None,
|
||||
rate_func: Callable[[float], float] | None = None,
|
||||
lag_ratio: float | None = None,
|
||||
):
|
||||
self.run_time = run_time or self.run_time
|
||||
self.rate_func = rate_func or self.rate_func
|
||||
self.lag_ratio = lag_ratio or self.lag_ratio
|
||||
return self
|
||||
|
||||
def begin(self) -> None:
|
||||
"""Begin the animation.
|
||||
|
||||
|
|
@ -213,10 +233,12 @@ class Animation:
|
|||
self.mobject.suspend_updating()
|
||||
self.interpolate(0)
|
||||
|
||||
# TODO: Figure out a way to check
|
||||
# if self.mobject in scene.get_mobject_family
|
||||
if self.introducer:
|
||||
self.buffer.add(self.mobject)
|
||||
|
||||
def finish(self) -> None:
|
||||
# TODO: begin and finish should require a scene as parameter.
|
||||
# That way Animation.clean_up_from_screen and Scene.add_mobjects_from_animations
|
||||
# could be removed as they fulfill basically the same purpose.
|
||||
"""Finish the animation.
|
||||
|
||||
This method gets called when the animation is over.
|
||||
|
|
@ -226,45 +248,14 @@ class Animation:
|
|||
if self.suspend_mobject_updating and self.mobject is not None:
|
||||
self.mobject.resume_updating()
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
"""Clean up the :class:`~.Scene` after finishing the animation.
|
||||
if self.remover:
|
||||
self.buffer.remove(self.mobject)
|
||||
|
||||
This includes to :meth:`~.Scene.remove` the Animation's
|
||||
:class:`~.Mobject` if the animation is a remover.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
scene
|
||||
The scene the animation should be cleaned up from.
|
||||
"""
|
||||
self._on_finish(scene)
|
||||
if self.is_remover():
|
||||
scene.remove(self.mobject)
|
||||
|
||||
def _setup_scene(self, scene: Scene) -> None:
|
||||
"""Setup up the :class:`~.Scene` before starting the animation.
|
||||
|
||||
This includes to :meth:`~.Scene.add` the Animation's
|
||||
:class:`~.Mobject` if the animation is an introducer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
scene
|
||||
The scene the animation should be cleaned up from.
|
||||
"""
|
||||
if scene is None:
|
||||
return
|
||||
if (
|
||||
self.is_introducer()
|
||||
and self.mobject not in scene.get_mobject_family_members()
|
||||
):
|
||||
scene.add(self.mobject)
|
||||
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
# Keep track of where the mobject starts
|
||||
return self.mobject.copy()
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject | OpenGLMobject]:
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
"""Get all mobjects involved in the animation.
|
||||
|
||||
Ordering must match the ordering of arguments to interpolate_submobject
|
||||
|
|
@ -277,14 +268,7 @@ class Animation:
|
|||
return self.mobject, self.starting_mobject
|
||||
|
||||
def get_all_families_zipped(self) -> Iterable[tuple]:
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
return zip(
|
||||
*(mob.get_family() for mob in self.get_all_mobjects()), strict=False
|
||||
)
|
||||
return zip(
|
||||
*(mob.family_members_with_points() for mob in self.get_all_mobjects()),
|
||||
strict=False,
|
||||
)
|
||||
return zip(*(mob.get_family() for mob in self.get_all_mobjects()), strict=False)
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
"""
|
||||
|
|
@ -297,7 +281,24 @@ class Animation:
|
|||
for mob in self.get_all_mobjects_to_update():
|
||||
mob.update(dt)
|
||||
|
||||
def get_all_mobjects_to_update(self) -> list[Mobject]:
|
||||
def process_subanimation_buffer(self, buffer: SceneBuffer):
|
||||
"""
|
||||
This is used in animations that are proxies around
|
||||
other animations, like :class:`.AnimationGroup`
|
||||
"""
|
||||
for op, args, kwargs in buffer:
|
||||
match op:
|
||||
case SceneOperation.ADD:
|
||||
self.buffer.add(*args, **kwargs)
|
||||
case SceneOperation.REMOVE:
|
||||
self.buffer.remove(*args, **kwargs)
|
||||
case SceneOperation.REPLACE:
|
||||
self.buffer.replace(*args, **kwargs)
|
||||
case _:
|
||||
assert_never(op)
|
||||
buffer.clear()
|
||||
|
||||
def get_all_mobjects_to_update(self) -> Sequence[Mobject]:
|
||||
"""Get all mobjects to be updated during the animation.
|
||||
|
||||
Returns
|
||||
|
|
@ -308,9 +309,9 @@ class Animation:
|
|||
# The surrounding scene typically handles
|
||||
# updating of self.mobject. Besides, in
|
||||
# most cases its updating is suspended anyway
|
||||
return list(filter(lambda m: m is not self.mobject, self.get_all_mobjects()))
|
||||
return [m for m in self.get_all_mobjects() if m is not self.mobject]
|
||||
|
||||
def copy(self) -> Animation:
|
||||
def copy(self) -> Self:
|
||||
"""Create a copy of the animation.
|
||||
|
||||
Returns
|
||||
|
|
@ -324,19 +325,6 @@ class Animation:
|
|||
|
||||
# TODO: stop using alpha as parameter name in different meanings.
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
"""Set the animation progress.
|
||||
|
||||
This method gets called for every frame during an animation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alpha
|
||||
The relative time to set the animation to, 0 meaning the start, 1 meaning
|
||||
the end.
|
||||
"""
|
||||
self.interpolate_mobject(alpha)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
"""Interpolates the mobject of the :class:`Animation` based on alpha value.
|
||||
|
||||
Parameters
|
||||
|
|
@ -346,10 +334,10 @@ class Animation:
|
|||
is completed. For example, alpha-values of 0, 0.5, and 1 correspond
|
||||
to the animation being completed 0%, 50%, and 100%, respectively.
|
||||
"""
|
||||
families = list(self.get_all_families_zipped())
|
||||
families = tuple(self.get_all_families_zipped())
|
||||
for i, mobs in enumerate(families):
|
||||
sub_alpha = self.get_sub_alpha(alpha, i, len(families))
|
||||
self.interpolate_submobject(*mobs, sub_alpha)
|
||||
self.interpolate_submobject(*mobs, sub_alpha) # type: ignore[call-arg]
|
||||
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
|
|
@ -358,8 +346,7 @@ class Animation:
|
|||
# target_copy: Mobject, #Todo: fix - signature of interpolate_submobject differs in Transform().
|
||||
alpha: float,
|
||||
) -> Animation:
|
||||
# Typically implemented by subclass
|
||||
pass
|
||||
raise NotImplementedError("Implement in subclass")
|
||||
|
||||
def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int) -> float:
|
||||
"""Get the animation progress of any submobjects subanimation.
|
||||
|
|
@ -385,13 +372,14 @@ class Animation:
|
|||
full_length = (num_submobjects - 1) * lag_ratio + 1
|
||||
value = alpha * full_length
|
||||
lower = index * lag_ratio
|
||||
raw_sub_alpha = np.clip((value - lower), 0, 1)
|
||||
if self.reverse_rate_function:
|
||||
return self.rate_func(1 - (value - lower))
|
||||
return self.rate_func(1 - raw_sub_alpha)
|
||||
else:
|
||||
return self.rate_func(value - lower)
|
||||
return self.rate_func(raw_sub_alpha)
|
||||
|
||||
# Getters and setters
|
||||
def set_run_time(self, run_time: float) -> Animation:
|
||||
def set_run_time(self, run_time: float) -> Self:
|
||||
"""Set the run time of the animation.
|
||||
|
||||
Parameters
|
||||
|
|
@ -426,7 +414,7 @@ class Animation:
|
|||
def set_rate_func(
|
||||
self,
|
||||
rate_func: Callable[[float], float],
|
||||
) -> Animation:
|
||||
) -> Self:
|
||||
"""Set the rate function of the animation.
|
||||
|
||||
Parameters
|
||||
|
|
@ -455,7 +443,7 @@ class Animation:
|
|||
"""
|
||||
return self.rate_func
|
||||
|
||||
def set_name(self, name: str) -> Animation:
|
||||
def set_name(self, name: str) -> Self:
|
||||
"""Set the name of the animation.
|
||||
|
||||
Parameters
|
||||
|
|
@ -471,26 +459,6 @@ class Animation:
|
|||
self.name = name
|
||||
return self
|
||||
|
||||
def is_remover(self) -> bool:
|
||||
"""Test if the animation is a remover.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
``True`` if the animation is a remover, ``False`` otherwise.
|
||||
"""
|
||||
return self.remover
|
||||
|
||||
def is_introducer(self) -> bool:
|
||||
"""Test if the animation is an introducer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
``True`` if the animation is an introducer, ``False`` otherwise.
|
||||
"""
|
||||
return self.introducer
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs) -> None:
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
|
@ -540,9 +508,19 @@ class Animation:
|
|||
cls.__init__ = cls._original__init__
|
||||
|
||||
|
||||
@overload
|
||||
def prepare_animation(anim: MobjectAnimation[M]) -> MobjectAnimation[M]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def prepare_animation(
|
||||
anim: Animation | mobject._AnimationBuilder | opengl_mobject._AnimationBuilder,
|
||||
) -> Animation:
|
||||
anim: AnimationProtocol | _AnimationBuilder | Mobject,
|
||||
) -> AnimationProtocol: ...
|
||||
|
||||
|
||||
def prepare_animation(
|
||||
anim: AnimationProtocol | _AnimationBuilder | Mobject,
|
||||
) -> AnimationProtocol:
|
||||
r"""Returns either an unchanged animation, or the animation built
|
||||
from a passed animation factory.
|
||||
|
||||
|
|
@ -569,16 +547,17 @@ def prepare_animation(
|
|||
TypeError: Object 42 cannot be converted to an animation
|
||||
|
||||
"""
|
||||
if isinstance(anim, mobject._AnimationBuilder):
|
||||
if isinstance(anim, _AnimationBuilder):
|
||||
return anim.build()
|
||||
|
||||
if isinstance(anim, opengl_mobject._AnimationBuilder):
|
||||
return anim.build()
|
||||
|
||||
if isinstance(anim, Animation):
|
||||
return anim
|
||||
|
||||
raise TypeError(f"Object {anim} cannot be converted to an animation")
|
||||
# if it has these three methods it probably is an AnimationProtocol
|
||||
# but we don't use isinstance because it's slow
|
||||
try:
|
||||
for method in ("begin", "finish", "update_mobjects"):
|
||||
getattr(anim, method)
|
||||
return cast(AnimationProtocol, anim)
|
||||
except AttributeError:
|
||||
raise TypeError(f"Object {anim} cannot be converted to an animation") from None
|
||||
|
||||
|
||||
class Wait(Animation):
|
||||
|
|
@ -615,12 +594,9 @@ class Wait(Animation):
|
|||
if stop_condition and frozen_frame:
|
||||
raise ValueError("A static Wait animation cannot have a stop condition.")
|
||||
|
||||
self.duration: float = run_time
|
||||
self.stop_condition = stop_condition
|
||||
self.is_static_wait: bool = frozen_frame
|
||||
self.is_static_wait: bool = bool(frozen_frame)
|
||||
super().__init__(None, run_time=run_time, rate_func=rate_func, **kwargs)
|
||||
# quick fix to work in opengl setting:
|
||||
self.mobject.shader_wrapper_list = []
|
||||
|
||||
def begin(self) -> None:
|
||||
pass
|
||||
|
|
@ -628,9 +604,6 @@ class Wait(Animation):
|
|||
def finish(self) -> None:
|
||||
pass
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
pass
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
pass
|
||||
|
||||
|
|
@ -760,9 +733,10 @@ def override_animation(
|
|||
self.play(FadeIn(MySquare()))
|
||||
|
||||
"""
|
||||
_F = TypeVar("_F", bound=Callable)
|
||||
|
||||
def decorator(func):
|
||||
func._override_animation = animation_class
|
||||
def decorator(func: _F) -> _F:
|
||||
func._override_animation = animation_class # type: ignore[attr-defined]
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["AnimatedBoundary", "TracedPath"]
|
||||
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any, Self
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.utils.color import (
|
||||
BLUE_B,
|
||||
BLUE_D,
|
||||
|
|
@ -20,6 +21,11 @@ from manim.utils.color import (
|
|||
)
|
||||
from manim.utils.rate_functions import RateFunction, smooth
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
class AnimatedBoundary(VGroup):
|
||||
"""Boundary of a :class:`.VMobject` with animated color change.
|
||||
|
|
@ -62,7 +68,7 @@ class AnimatedBoundary(VGroup):
|
|||
]
|
||||
self.add(*self.boundary_copies)
|
||||
self.total_time = 0.0
|
||||
self.add_updater(lambda m, dt: self.update_boundary_copies(dt))
|
||||
self.add_updater(lambda _, dt: self.update_boundary_copies(dt))
|
||||
|
||||
def update_boundary_copies(self, dt: float) -> None:
|
||||
# Not actual time, but something which passes at
|
||||
|
|
@ -102,7 +108,7 @@ class AnimatedBoundary(VGroup):
|
|||
return self
|
||||
|
||||
|
||||
class TracedPath(VMobject, metaclass=ConvertToOpenGL):
|
||||
class TracedPath(VMobject):
|
||||
"""Traces the path of a point returned by a function call.
|
||||
|
||||
Parameters
|
||||
|
|
@ -146,23 +152,32 @@ class TracedPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
traced_point_func: Callable,
|
||||
traced_point_func: Callable[
|
||||
[], npt.NDArray[npt.float]
|
||||
], # TODO: Replace with Callable[[], Point3D]
|
||||
stroke_width: float = 2,
|
||||
stroke_color: ParsableManimColor | None = WHITE,
|
||||
dissipating_time: float | None = None,
|
||||
fill_opacity: float = 0.0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(stroke_color=stroke_color, stroke_width=stroke_width, **kwargs)
|
||||
):
|
||||
super().__init__(
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
fill_opacity=fill_opacity,
|
||||
**kwargs,
|
||||
)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.dissipating_time = dissipating_time
|
||||
self.time = 1.0 if self.dissipating_time else None
|
||||
self.add_updater(self.update_path)
|
||||
|
||||
def update_path(self, mob: Mobject, dt: float) -> None:
|
||||
def update_path(self, _mob: Mobject, dt: float) -> None:
|
||||
new_point = self.traced_point_func()
|
||||
if not self.has_points():
|
||||
self.start_new_path(new_point)
|
||||
self.add_line_to(new_point)
|
||||
if not np.allclose(self.get_end(), new_point):
|
||||
self.add_line_to(new_point)
|
||||
if self.dissipating_time:
|
||||
assert self.time is not None
|
||||
self.time += dt
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim._config import config
|
||||
from manim.animation.animation import Animation, prepare_animation
|
||||
from manim.constants import RendererType
|
||||
from manim.mobject.mobject import Group, Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
from manim.scene.scene import Scene
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from manim.utils.iterables import remove_list_redundancies
|
||||
from manim.utils.parameter_parsing import flatten_iterable_parameters
|
||||
from manim.utils.rate_functions import linear
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
|
||||
__all__ = ["AnimationGroup", "Succession", "LaggedStart", "LaggedStartMap"]
|
||||
|
||||
|
|
@ -54,25 +54,21 @@ class AnimationGroup(Animation):
|
|||
def __init__(
|
||||
self,
|
||||
*animations: Animation | Iterable[Animation],
|
||||
group: Group | VGroup | OpenGLGroup | OpenGLVGroup | None = None,
|
||||
group: Group | VGroup | None = None,
|
||||
run_time: float | None = None,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
lag_ratio: float = 0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
arg_anim = flatten_iterable_parameters(animations)
|
||||
|
||||
self.animations = [prepare_animation(anim) for anim in arg_anim]
|
||||
self.rate_func = rate_func
|
||||
if group is None:
|
||||
mobjects = remove_list_redundancies(
|
||||
[anim.mobject for anim in self.animations if not anim.is_introducer()],
|
||||
[anim.mobject for anim in self.animations if not anim.introducer],
|
||||
)
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
self.group: Group | VGroup | OpenGLGroup | OpenGLVGroup = OpenGLGroup(
|
||||
*mobjects
|
||||
)
|
||||
else:
|
||||
self.group = Group(*mobjects)
|
||||
self.group = Group(*mobjects)
|
||||
else:
|
||||
self.group = group
|
||||
super().__init__(
|
||||
|
|
@ -80,7 +76,7 @@ class AnimationGroup(Animation):
|
|||
)
|
||||
self.run_time: float = self.init_run_time(run_time)
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject | OpenGLMobject]:
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
return list(self.group)
|
||||
|
||||
def begin(self) -> None:
|
||||
|
|
@ -89,30 +85,30 @@ class AnimationGroup(Animation):
|
|||
f"Trying to play {self} without animations, this is not supported. "
|
||||
"Please add at least one subanimation."
|
||||
)
|
||||
|
||||
for anim in self.animations:
|
||||
if self.introducer:
|
||||
anim.introducer = True
|
||||
anim.begin()
|
||||
self.process_subanimation_buffer(anim.buffer)
|
||||
|
||||
self.anim_group_time = 0.0
|
||||
if self.suspend_mobject_updating:
|
||||
self.group.suspend_updating()
|
||||
for anim in self.animations:
|
||||
anim.begin()
|
||||
|
||||
def _setup_scene(self, scene: Scene) -> None:
|
||||
for anim in self.animations:
|
||||
anim._setup_scene(scene)
|
||||
|
||||
def finish(self) -> None:
|
||||
for anim in self.animations:
|
||||
anim.finish()
|
||||
self.anims_begun[:] = True
|
||||
self.anims_finished[:] = True
|
||||
if self.suspend_mobject_updating:
|
||||
self.group.resume_updating()
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
self._on_finish(scene)
|
||||
for anim in self.animations:
|
||||
if self.remover:
|
||||
anim.remover = self.remover
|
||||
anim.clean_up_from_scene(scene)
|
||||
anim.remover = True
|
||||
anim.finish()
|
||||
self.process_subanimation_buffer(anim.buffer)
|
||||
|
||||
if self.suspend_mobject_updating:
|
||||
self.group.resume_updating()
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
for anim in self.anims_with_timings["anim"][
|
||||
|
|
@ -251,16 +247,6 @@ class Succession(AnimationGroup):
|
|||
if self.active_animation:
|
||||
self.active_animation.update_mobjects(dt)
|
||||
|
||||
def _setup_scene(self, scene: Scene | None) -> None:
|
||||
if scene is None:
|
||||
return
|
||||
if self.is_introducer():
|
||||
for anim in self.animations:
|
||||
if not anim.is_introducer() and anim.mobject is not None:
|
||||
scene.add(anim.mobject)
|
||||
|
||||
self.scene = scene
|
||||
|
||||
def update_active_animation(self, index: int) -> None:
|
||||
self.active_index = index
|
||||
if index >= len(self.animations):
|
||||
|
|
@ -269,8 +255,9 @@ class Succession(AnimationGroup):
|
|||
self.active_end_time: float | None = None
|
||||
else:
|
||||
self.active_animation = self.animations[index]
|
||||
self.active_animation._setup_scene(self.scene)
|
||||
self.active_animation.begin()
|
||||
self.process_subanimation_buffer(self.active_animation.buffer)
|
||||
self.apply_buffer = True
|
||||
self.active_start_time = self.anims_with_timings[index]["start"]
|
||||
self.active_end_time = self.anims_with_timings[index]["end"]
|
||||
|
||||
|
|
@ -281,6 +268,7 @@ class Succession(AnimationGroup):
|
|||
"""
|
||||
if self.active_animation is not None:
|
||||
self.active_animation.finish()
|
||||
self.process_subanimation_buffer(self.active_animation.buffer)
|
||||
self.update_active_animation(self.active_index + 1)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
|
|
|
|||
|
|
@ -82,19 +82,26 @@ from typing import TYPE_CHECKING
|
|||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Self
|
||||
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
from manim.constants import RIGHT, TAU
|
||||
from manim.mobject.opengl.opengl_surface import OpenGLSurface
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.utils.color import ManimColor
|
||||
from manim.utils.space_ops import rotate_vector
|
||||
|
||||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
from ..animation.composition import Succession
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.types.vectorized_mobject import VMobject
|
||||
from ..mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from ..mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from ..utils.bezier import integer_interpolate
|
||||
from ..utils.rate_functions import double_smooth, linear
|
||||
|
||||
|
|
@ -113,11 +120,7 @@ class ShowPartial(Animation):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: VMobject | OpenGLVMobject | OpenGLSurface | None,
|
||||
**kwargs,
|
||||
):
|
||||
def __init__(self, mobject: VMobject | OpenGLSurface | None, **kwargs: Any):
|
||||
pointwise = getattr(mobject, "pointwise_become_partial", None)
|
||||
if not callable(pointwise):
|
||||
raise TypeError(f"{self.__class__.__name__} only works for VMobjects.")
|
||||
|
|
@ -128,10 +131,11 @@ class ShowPartial(Animation):
|
|||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
alpha: float,
|
||||
) -> None:
|
||||
) -> Self:
|
||||
submobject.pointwise_become_partial(
|
||||
starting_submobject, *self._get_bounds(alpha)
|
||||
)
|
||||
return self
|
||||
|
||||
def _get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
raise NotImplementedError("Please use Create or ShowPassingFlash")
|
||||
|
|
@ -166,7 +170,7 @@ class Create(ShowPartial):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: VMobject | OpenGLVMobject | OpenGLSurface,
|
||||
mobject: VMobject | OpenGLSurface,
|
||||
lag_ratio: float = 1.0,
|
||||
introducer: bool = True,
|
||||
**kwargs,
|
||||
|
|
@ -196,7 +200,7 @@ class Uncreate(Create):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: VMobject | OpenGLVMobject,
|
||||
mobject: VMobject,
|
||||
reverse_rate_function: bool = True,
|
||||
remover: bool = True,
|
||||
**kwargs,
|
||||
|
|
@ -224,11 +228,13 @@ class DrawBorderThenFill(Animation):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject | OpenGLVMobject,
|
||||
vmobject: VMobject,
|
||||
run_time: float = 2,
|
||||
rate_func: Callable[[float], float] = double_smooth,
|
||||
stroke_width: float = 2,
|
||||
stroke_color: str = None,
|
||||
stroke_color: ManimColor | None = None,
|
||||
draw_border_animation_config: dict = {}, # what does this dict accept?
|
||||
fill_animation_config: dict = {},
|
||||
introducer: bool = True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
|
@ -244,13 +250,15 @@ class DrawBorderThenFill(Animation):
|
|||
self.stroke_color = stroke_color
|
||||
self.outline = self.get_outline()
|
||||
|
||||
def _typecheck_input(self, vmobject: VMobject | OpenGLVMobject) -> None:
|
||||
if not isinstance(vmobject, (VMobject, OpenGLVMobject)):
|
||||
def _typecheck_input(self, vmobject: VMobject) -> None:
|
||||
if not isinstance(vmobject, VMobject):
|
||||
raise TypeError(
|
||||
f"{self.__class__.__name__} only works for vectorized Mobjects"
|
||||
)
|
||||
|
||||
def begin(self) -> None:
|
||||
# this self.get_outline() has to be called
|
||||
# before super().begin(), for whatever reason
|
||||
self.outline = self.get_outline()
|
||||
super().begin()
|
||||
|
||||
|
|
@ -261,7 +269,7 @@ class DrawBorderThenFill(Animation):
|
|||
sm.set_stroke(color=self.get_stroke_color(sm), width=self.stroke_width)
|
||||
return outline
|
||||
|
||||
def get_stroke_color(self, vmobject: VMobject | OpenGLVMobject) -> ManimColor:
|
||||
def get_stroke_color(self, vmobject: VMobject) -> ManimColor:
|
||||
if self.stroke_color:
|
||||
return self.stroke_color
|
||||
elif vmobject.get_stroke_width() > 0:
|
||||
|
|
@ -275,9 +283,9 @@ class DrawBorderThenFill(Animation):
|
|||
self,
|
||||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
outline,
|
||||
outline: Mobject,
|
||||
alpha: float,
|
||||
) -> None: # Fixme: not matching the parent class? What is outline doing here?
|
||||
) -> None:
|
||||
index: int
|
||||
subalpha: float
|
||||
index, subalpha = integer_interpolate(0, 2, alpha)
|
||||
|
|
@ -317,13 +325,13 @@ class Write(DrawBorderThenFill):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject | OpenGLVMobject,
|
||||
vmobject: VMobject,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
reverse: bool = False,
|
||||
run_time: float | None = None,
|
||||
lag_ratio: float | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
run_time: float | None = kwargs.pop("run_time", None)
|
||||
lag_ratio: float | None = kwargs.pop("lag_ratio", None)
|
||||
run_time, lag_ratio = self._set_default_config_from_length(
|
||||
vmobject,
|
||||
run_time,
|
||||
|
|
@ -343,7 +351,7 @@ class Write(DrawBorderThenFill):
|
|||
|
||||
def _set_default_config_from_length(
|
||||
self,
|
||||
vmobject: VMobject | OpenGLVMobject,
|
||||
vmobject: VMobject,
|
||||
run_time: float | None,
|
||||
lag_ratio: float | None,
|
||||
) -> tuple[float, float]:
|
||||
|
|
@ -354,18 +362,15 @@ class Write(DrawBorderThenFill):
|
|||
lag_ratio = min(4.0 / max(1.0, length), 0.2)
|
||||
return run_time, lag_ratio
|
||||
|
||||
def reverse_submobjects(self) -> None:
|
||||
self.mobject.invert(recursive=True)
|
||||
|
||||
def begin(self) -> None:
|
||||
if self.reverse:
|
||||
self.reverse_submobjects()
|
||||
self.mobject.reverse_submobjects(recursive=True)
|
||||
super().begin()
|
||||
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
if self.reverse:
|
||||
self.reverse_submobjects()
|
||||
self.mobject.reverse_submobjects(recursive=True)
|
||||
|
||||
|
||||
class Unwrite(Write):
|
||||
|
|
@ -452,28 +457,33 @@ class SpiralIn(Animation):
|
|||
self,
|
||||
shapes: Mobject,
|
||||
scale_factor: float = 8,
|
||||
fade_in_fraction=0.3,
|
||||
**kwargs,
|
||||
fade_in_fraction: float = 0.3,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.shapes = shapes.copy()
|
||||
self.scale_factor = scale_factor
|
||||
self.shape_center = shapes.get_center()
|
||||
self.fade_in_fraction = fade_in_fraction
|
||||
for shape in shapes:
|
||||
shape.final_position = shape.get_center()
|
||||
shape.initial_position = (
|
||||
shape.final_position
|
||||
+ (shape.final_position - self.shape_center) * self.scale_factor
|
||||
)
|
||||
shape.move_to(shape.initial_position)
|
||||
shape.save_state()
|
||||
self.final_positions = [shape.get_center() for shape in shapes]
|
||||
self.initial_positions = [
|
||||
final_pos + (final_pos - self.shape_center) * self.scale_factor
|
||||
for final_pos in self.final_positions
|
||||
]
|
||||
|
||||
super().__init__(shapes, introducer=True, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
alpha = self.rate_func(alpha)
|
||||
for original_shape, shape in zip(self.shapes, self.mobject, strict=True):
|
||||
shape.restore()
|
||||
for i, shape in enumerate(self.mobject):
|
||||
initial_pos = self.initial_positions[i]
|
||||
final_pos = self.final_positions[i]
|
||||
# Avoid shape.rotate() in order to preserve the bounding box of the shape.
|
||||
# Instead, rotate the vector itself to calculate the current shape position.
|
||||
vector = initial_pos - self.shape_center + (final_pos - initial_pos) * alpha
|
||||
vector = rotate_vector(vector, TAU * alpha)
|
||||
shape.move_to(self.shape_center + vector)
|
||||
|
||||
original_shape = self.shapes[i]
|
||||
fill_opacity = original_shape.get_fill_opacity()
|
||||
stroke_opacity = original_shape.get_stroke_opacity()
|
||||
new_fill_opacity = min(
|
||||
|
|
@ -482,9 +492,6 @@ class SpiralIn(Animation):
|
|||
new_stroke_opacity = min(
|
||||
stroke_opacity, alpha * stroke_opacity / self.fade_in_fraction
|
||||
)
|
||||
shape.shift((shape.final_position - shape.initial_position) * alpha)
|
||||
shape.rotate(TAU * alpha, about_point=self.shape_center)
|
||||
shape.rotate(-TAU * alpha, about_point=shape.get_center_of_mass())
|
||||
shape.set_fill(opacity=new_fill_opacity)
|
||||
shape.set_stroke(opacity=new_stroke_opacity)
|
||||
|
||||
|
|
@ -524,7 +531,7 @@ class ShowIncreasingSubsets(Animation):
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
n_submobs = len(self.all_submobs)
|
||||
value = (
|
||||
1 - self.rate_func(alpha)
|
||||
|
|
|
|||
|
|
@ -23,12 +23,15 @@ from typing import Any
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
|
||||
from ..animation.transform import Transform
|
||||
from ..constants import ORIGIN
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..scene.scene import Scene
|
||||
|
||||
|
||||
class _Fade(Transform):
|
||||
|
|
@ -64,7 +67,7 @@ class _Fade(Transform):
|
|||
self.point_target = False
|
||||
if shift is None:
|
||||
if target_position is not None:
|
||||
if isinstance(target_position, (Mobject, OpenGLMobject)):
|
||||
if isinstance(target_position, Mobject):
|
||||
target_position = target_position.get_center()
|
||||
shift = target_position - mobject.get_center()
|
||||
self.point_target = True
|
||||
|
|
@ -74,12 +77,12 @@ class _Fade(Transform):
|
|||
self.scale_factor = scale
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def _create_faded_mobject(self, fadeIn: bool) -> Mobject:
|
||||
def _create_faded_mobject(self, fade_in: bool) -> Mobject:
|
||||
"""Create a faded, shifted and scaled copy of the mobject.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fadeIn
|
||||
fade_in
|
||||
Whether the faded mobject is used to fade in.
|
||||
|
||||
Returns
|
||||
|
|
@ -89,7 +92,7 @@ class _Fade(Transform):
|
|||
"""
|
||||
faded_mobject: Mobject = self.mobject.copy() # type: ignore[assignment]
|
||||
faded_mobject.fade(1)
|
||||
direction_modifier = -1 if fadeIn and not self.point_target else 1
|
||||
direction_modifier = -1 if fade_in and not self.point_target else 1
|
||||
faded_mobject.shift(self.shift_vector * direction_modifier)
|
||||
faded_mobject.scale(self.scale_factor)
|
||||
return faded_mobject
|
||||
|
|
@ -137,10 +140,10 @@ class FadeIn(_Fade):
|
|||
super().__init__(*mobjects, introducer=True, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject # type: ignore[return-value]
|
||||
return self.mobject
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
return self._create_faded_mobject(fadeIn=True)
|
||||
return self._create_faded_mobject(fade_in=True)
|
||||
|
||||
|
||||
class FadeOut(_Fade):
|
||||
|
|
@ -185,8 +188,8 @@ class FadeOut(_Fade):
|
|||
super().__init__(*mobjects, remover=True, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
return self._create_faded_mobject(fadeIn=False)
|
||||
return self._create_faded_mobject(fade_in=False)
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
def begin(self) -> None:
|
||||
super().begin()
|
||||
self.interpolate(0)
|
||||
|
|
|
|||
|
|
@ -39,12 +39,10 @@ from ..utils.paths import spiral_path
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.geometry.line import Arrow
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.typing import Point3DLike, Vector3DLike
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
from ..mobject.mobject import Mobject
|
||||
|
||||
|
||||
class GrowFromPoint(Transform):
|
||||
"""Introduce an :class:`~.Mobject` by growing it from a point.
|
||||
|
|
@ -87,10 +85,10 @@ class GrowFromPoint(Transform):
|
|||
self.point_color = point_color
|
||||
super().__init__(mobject, introducer=True, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject
|
||||
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
start = super().create_starting_mobject()
|
||||
start.scale(0)
|
||||
start.move_to(self.point)
|
||||
|
|
@ -169,7 +167,7 @@ class GrowFromEdge(GrowFromPoint):
|
|||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
point = mobject.get_critical_point(edge)
|
||||
point = mobject.get_bounding_box_point(edge)
|
||||
super().__init__(mobject, point, point_color=point_color, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -203,7 +201,7 @@ class GrowArrow(GrowFromPoint):
|
|||
point = arrow.get_start()
|
||||
super().__init__(arrow, point, point_color=point_color, **kwargs)
|
||||
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
start_arrow = self.mobject.copy()
|
||||
start_arrow.scale(0, scale_tips=True, about_point=self.point)
|
||||
if self.point_color:
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@ from manim.mobject.geometry.arc import Circle, Dot
|
|||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.scene.scene import Scene
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
|
||||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
|
|
@ -60,8 +59,12 @@ from ..animation.movement import Homotopy
|
|||
from ..animation.transform import Transform
|
||||
from ..animation.updaters.update import UpdateFromFunc
|
||||
from ..constants import *
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from ..mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from ..mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from ..typing import Point3D, Point3DLike, Vector3DLike
|
||||
from ..utils.bezier import interpolate, inverse_interpolate
|
||||
from ..utils.color import GREY, PURE_YELLOW, ParsableManimColor
|
||||
|
|
@ -161,7 +164,7 @@ class Indicate(Transform):
|
|||
self.scale_factor = scale_factor
|
||||
super().__init__(mobject, rate_func=rate_func, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
def create_target(self) -> Mobject:
|
||||
target = self.mobject.copy()
|
||||
target.scale(self.scale_factor)
|
||||
target.set_color(self.color)
|
||||
|
|
@ -319,8 +322,8 @@ class ShowPassingFlash(ShowPartial):
|
|||
lower = max(lower, 0)
|
||||
return (lower, upper)
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
for submob, start in self.get_all_families_zipped():
|
||||
submob.pointwise_become_partial(start, 0, 1)
|
||||
|
||||
|
|
@ -407,6 +410,7 @@ class ApplyWave(Homotopy):
|
|||
time_width: float = 1,
|
||||
ripples: int = 1,
|
||||
run_time: float = 2,
|
||||
introducer: bool = True,
|
||||
**kwargs: Any,
|
||||
):
|
||||
x_min = mobject.get_left()[0]
|
||||
|
|
@ -482,7 +486,9 @@ class ApplyWave(Homotopy):
|
|||
return_value: tuple[float, float, float] = np.array([x, y, z]) + nudge
|
||||
return return_value
|
||||
|
||||
super().__init__(homotopy, mobject, run_time=run_time, **kwargs)
|
||||
super().__init__(
|
||||
homotopy, mobject, run_time=run_time, introducer=introducer, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class Wiggle(Animation):
|
||||
|
|
@ -568,6 +574,7 @@ class Wiggle(Animation):
|
|||
return self
|
||||
|
||||
|
||||
# TODO: get rid of this if condition madness
|
||||
class Circumscribe(Succession):
|
||||
r"""Draw a temporary line surrounding the mobject.
|
||||
|
||||
|
|
|
|||
|
|
@ -82,9 +82,7 @@ class Homotopy(Animation):
|
|||
**kwargs: Any,
|
||||
):
|
||||
self.homotopy = homotopy
|
||||
self.apply_function_kwargs = (
|
||||
apply_function_kwargs if apply_function_kwargs is not None else {}
|
||||
)
|
||||
self.apply_function_kwargs = apply_function_kwargs or {}
|
||||
super().__init__(mobject, run_time=run_time, **kwargs)
|
||||
|
||||
def function_at_time_t(self, t: float) -> MappingFunction:
|
||||
|
|
@ -100,7 +98,7 @@ class Homotopy(Animation):
|
|||
starting_submobject: Mobject,
|
||||
alpha: float,
|
||||
) -> Self:
|
||||
submobject.points = starting_submobject.points
|
||||
submobject.match_points(starting_submobject)
|
||||
submobject.apply_function(
|
||||
self.function_at_time_t(alpha),
|
||||
**self.apply_function_kwargs,
|
||||
|
|
@ -161,7 +159,7 @@ class PhaseFlow(Animation):
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
if hasattr(self, "last_alpha"):
|
||||
dt = self.virtual_time * (
|
||||
self.rate_func(alpha) - self.rate_func(self.last_alpha)
|
||||
|
|
@ -197,6 +195,6 @@ class MoveAlongPath(Animation):
|
|||
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs
|
||||
)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
point = self.path.point_from_proportion(self.rate_func(alpha))
|
||||
self.mobject.move_to(point)
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class ChangingDecimal(Animation):
|
|||
if not isinstance(decimal_mob, DecimalNumber):
|
||||
raise TypeError("ChangingDecimal can only take in a DecimalNumber")
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
self.mobject.set_value(self.number_update_func(self.rate_func(alpha))) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
|
|
|
|||
82
manim/animation/protocol.py
Normal file
82
manim/animation/protocol.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.utils.rate_functions import RateFunction
|
||||
|
||||
from .scene_buffer import SceneBuffer
|
||||
|
||||
M = TypeVar("M", bound="Mobject", default="Mobject")
|
||||
|
||||
|
||||
__all__ = ("AnimationProtocol",)
|
||||
|
||||
|
||||
class AnimationProtocol(Protocol):
|
||||
"""A protocol that all animations must implement."""
|
||||
|
||||
buffer: SceneBuffer
|
||||
"""The interface to the scene. This can be used to add, remove, or replace mobjects on the scene."""
|
||||
|
||||
apply_buffer: bool
|
||||
"""Normally, the buffer is only applied at the beginning and end of an animation.
|
||||
|
||||
To apply it mid animation, set :attr:`apply_buffer` to ``True``."""
|
||||
|
||||
def begin(self) -> object:
|
||||
"""Called before the animation starts.
|
||||
|
||||
This is where all setup for the animation should be done, such
|
||||
as creating copies/targets of the mobject to animate, etc.
|
||||
"""
|
||||
|
||||
def finish(self) -> object:
|
||||
"""Called after the animation finishes.
|
||||
|
||||
This is where all cleanup should happen, such as removing
|
||||
mobjects from the scene, etc.
|
||||
"""
|
||||
|
||||
def interpolate(self, alpha: float) -> object:
|
||||
"""This is called every frame of the animation.
|
||||
|
||||
This method should update the animation to the given ``alpha`` value.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alpha : a value in the interval :math:`[0, 1]` representing the proportion of the animation that has passed.
|
||||
"""
|
||||
|
||||
def get_run_time(self) -> float:
|
||||
"""Compute and return the run time of the animation."""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_rate_info(
|
||||
self,
|
||||
run_time: float | None,
|
||||
rate_func: RateFunction | None,
|
||||
lag_ratio: float | None,
|
||||
) -> object:
|
||||
"""Update the rate information for the animation.
|
||||
|
||||
If any value is ``None``, it should not update
|
||||
the animation's corresponding attribute.
|
||||
"""
|
||||
|
||||
def update_mobjects(self, dt: float) -> object:
|
||||
"""Update the mobjects during the animation.
|
||||
|
||||
This method is called every frame of the animation
|
||||
"""
|
||||
|
||||
|
||||
class MobjectAnimation(AnimationProtocol, Protocol[M]):
|
||||
mobject: M
|
||||
"""The mobject that is being animated."""
|
||||
|
||||
suspend_mobject_updating: bool
|
||||
"""Whether to suspend updating the mobject during the animation."""
|
||||
|
|
@ -4,18 +4,16 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["Rotating", "Rotate"]
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ..animation.animation import Animation
|
||||
from ..animation.transform import Transform
|
||||
from ..constants import OUT, PI, TAU
|
||||
from ..utils.rate_functions import linear
|
||||
from manim.animation.animation import Animation
|
||||
from manim.constants import ORIGIN, OUT, PI, TAU
|
||||
from manim.utils.rate_functions import RateFunction, linear
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from ..typing import Point3DLike, Vector3DLike
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.typing import Point3DLike, Vector3DLike
|
||||
from manim.utils.rate_functions import RateFunction
|
||||
|
||||
|
||||
class Rotating(Animation):
|
||||
|
|
@ -92,18 +90,30 @@ class Rotating(Animation):
|
|||
axis: Vector3DLike = OUT,
|
||||
about_point: Point3DLike | None = None,
|
||||
about_edge: Vector3DLike | None = None,
|
||||
run_time: float = 5,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
rate_func: RateFunction = linear,
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
):
|
||||
super().__init__(
|
||||
mobject,
|
||||
rate_func=rate_func,
|
||||
suspend_mobject_updating=suspend_mobject_updating,
|
||||
**kwargs,
|
||||
)
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
self.about_point = about_point
|
||||
self.about_edge = about_edge
|
||||
super().__init__(mobject, run_time=run_time, rate_func=rate_func, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.become(self.starting_mobject)
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
pairs = zip(
|
||||
self.mobject.family_members_with_points(),
|
||||
self.starting_mobject.family_members_with_points(),
|
||||
strict=True,
|
||||
)
|
||||
for sm1, sm2 in pairs:
|
||||
sm1.points[:] = sm2.points
|
||||
|
||||
self.mobject.rotate(
|
||||
self.rate_func(alpha) * self.angle,
|
||||
axis=self.axis,
|
||||
|
|
@ -112,7 +122,7 @@ class Rotating(Animation):
|
|||
)
|
||||
|
||||
|
||||
class Rotate(Transform):
|
||||
class Rotate(Rotating):
|
||||
"""Animation that rotates a Mobject.
|
||||
|
||||
Parameters
|
||||
|
|
@ -144,7 +154,7 @@ class Rotate(Transform):
|
|||
rate_func=linear,
|
||||
),
|
||||
Rotate(Square(side_length=0.5), angle=2*PI, rate_func=linear),
|
||||
)
|
||||
)
|
||||
|
||||
See also
|
||||
--------
|
||||
|
|
@ -157,28 +167,16 @@ class Rotate(Transform):
|
|||
mobject: Mobject,
|
||||
angle: float = PI,
|
||||
axis: Vector3DLike = OUT,
|
||||
about_point: Point3DLike | None = None,
|
||||
about_edge: Vector3DLike | None = None,
|
||||
run_time: float = 1,
|
||||
about_edge: Vector3DLike = ORIGIN,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if "path_arc" not in kwargs:
|
||||
kwargs["path_arc"] = angle
|
||||
if "path_arc_axis" not in kwargs:
|
||||
kwargs["path_arc_axis"] = axis
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
self.about_edge = about_edge
|
||||
self.about_point = about_point
|
||||
if self.about_point is None:
|
||||
self.about_point = mobject.get_center()
|
||||
super().__init__(mobject, path_arc_centers=self.about_point, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
target = self.mobject.copy()
|
||||
target.rotate(
|
||||
self.angle,
|
||||
axis=self.axis,
|
||||
about_point=self.about_point,
|
||||
about_edge=self.about_edge,
|
||||
):
|
||||
super().__init__(
|
||||
mobject,
|
||||
angle,
|
||||
axis,
|
||||
run_time=run_time,
|
||||
about_edge=about_edge,
|
||||
introducer=True,
|
||||
**kwargs,
|
||||
)
|
||||
return target
|
||||
|
|
|
|||
79
manim/animation/scene_buffer.py
Normal file
79
manim/animation/scene_buffer.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator, Sequence
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
|
||||
__all__ = ["SceneBuffer", "SceneOperation"]
|
||||
|
||||
|
||||
class SceneOperation(Enum):
|
||||
ADD = "add"
|
||||
REMOVE = "remove"
|
||||
REPLACE = "replace"
|
||||
|
||||
|
||||
class SceneBuffer:
|
||||
"""
|
||||
A "buffer" between :class:`.Scene` and :class:`.Animation`
|
||||
|
||||
Operations an animation wants to do on :class:`.Scene` should be
|
||||
done here (eg. :meth:`.Scene.add`, :meth:`.Scene.remove`). The
|
||||
scene will then apply these changes at specific points (namely
|
||||
at the beginning and end of animations)
|
||||
|
||||
It is the scenes job to clear the buffer in between the beginning
|
||||
and end of animations.
|
||||
|
||||
To iterate over the operations, simply iterate over the buffer.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> buffer = SceneBuffer()
|
||||
>>> buffer.add(Square())
|
||||
>>> buffer.remove(Circle())
|
||||
>>> buffer.replace(Square(), Circle(), flag=True)
|
||||
>>> for operation in buffer:
|
||||
... print(operation)
|
||||
(SceneOperation.ADD, (Square(),), {})
|
||||
(SceneOperation.REMOVE, (Circle(),), {})
|
||||
(SceneOperation.REPLACE, (Square(), Circle()), {"flag": True})
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.operations: list[
|
||||
tuple[SceneOperation, Sequence[Mobject], dict[str, Any]]
|
||||
] = []
|
||||
|
||||
def add(self, *mobs: Mobject, **kwargs: Any) -> None:
|
||||
"""Add mobjects to the scene."""
|
||||
self.operations.append((SceneOperation.ADD, mobs, kwargs))
|
||||
|
||||
def remove(self, *mobs: Mobject, **kwargs: Any) -> None:
|
||||
"""Remove mobjects from the scene."""
|
||||
self.operations.append((SceneOperation.REMOVE, mobs, kwargs))
|
||||
|
||||
def replace(self, mob: Mobject, *replacements: Mobject, **kwargs: Any) -> None:
|
||||
"""Replace a ``mob`` with ``replacements`` on the scene."""
|
||||
self.operations.append((SceneOperation.REPLACE, (mob, *replacements), kwargs))
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear the buffer."""
|
||||
self.operations.clear()
|
||||
|
||||
def __str__(self) -> str:
|
||||
operations = self.operations
|
||||
return f"{type(self).__name__}({operations=})"
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __iter__(
|
||||
self,
|
||||
) -> Iterator[tuple[SceneOperation, Sequence[Mobject], dict[str, Any]]]:
|
||||
return iter(self.operations)
|
||||
|
|
@ -12,10 +12,10 @@ from numpy import piecewise
|
|||
from ..animation.animation import Animation, Wait, prepare_animation
|
||||
from ..animation.composition import AnimationGroup
|
||||
from ..mobject.mobject import Mobject, _AnimationBuilder
|
||||
from ..scene.scene import Scene
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mobject.mobject import Updater
|
||||
from .protocol import MobjectAnimation
|
||||
|
||||
__all__ = ["ChangeSpeed"]
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ class ChangeSpeed(Animation):
|
|||
affects_speed_updaters: bool = True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if issubclass(type(anim), AnimationGroup):
|
||||
if isinstance(anim, AnimationGroup):
|
||||
self.anim = type(anim)(
|
||||
*map(self.setup, anim.animations),
|
||||
group=anim.group,
|
||||
|
|
@ -209,11 +209,11 @@ class ChangeSpeed(Animation):
|
|||
super().__init__(
|
||||
self.anim.mobject,
|
||||
rate_func=self.rate_func,
|
||||
run_time=scaled_total_time * self.anim.run_time,
|
||||
run_time=scaled_total_time * self.anim.get_run_time(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def setup(self, anim):
|
||||
def setup(self, anim: MobjectAnimation):
|
||||
if type(anim) is Wait:
|
||||
anim.interpolate = types.MethodType(
|
||||
lambda self, alpha: self.rate_func(alpha), anim
|
||||
|
|
@ -282,15 +282,11 @@ class ChangeSpeed(Animation):
|
|||
def update_mobjects(self, dt: float) -> None:
|
||||
self.anim.update_mobjects(dt)
|
||||
|
||||
def begin(self) -> None:
|
||||
self.anim.begin()
|
||||
self.process_subanimation_buffer(self.anim.buffer)
|
||||
|
||||
def finish(self) -> None:
|
||||
ChangeSpeed.is_changing_dt = False
|
||||
self.anim.finish()
|
||||
|
||||
def begin(self) -> None:
|
||||
self.anim.begin()
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
self.anim.clean_up_from_scene(scene)
|
||||
|
||||
def _setup_scene(self, scene) -> None:
|
||||
self.anim._setup_scene(scene)
|
||||
self.process_subanimation_buffer(self.anim.buffer)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.typing import PathFuncType
|
||||
|
||||
__all__ = [
|
||||
"Transform",
|
||||
"ReplacementTransform",
|
||||
|
|
@ -28,30 +30,31 @@ __all__ = [
|
|||
|
||||
import inspect
|
||||
import types
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.data_structures import MethodWithArgs
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
|
||||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
from ..constants import (
|
||||
DEFAULT_POINTWISE_FUNCTION_RUN_TIME,
|
||||
DEGREES,
|
||||
ORIGIN,
|
||||
OUT,
|
||||
RendererType,
|
||||
)
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..utils.paths import path_along_arc, path_along_circles
|
||||
from ..utils.rate_functions import smooth, squish_rate_func
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..scene.scene import Scene
|
||||
from ..typing import Point3DLike, Point3DLike_Array
|
||||
from manim.typing import Point3DLike, Point3DLike_Array
|
||||
|
||||
|
||||
class Transform(Animation):
|
||||
|
|
@ -179,46 +182,33 @@ class Transform(Animation):
|
|||
@property
|
||||
def path_func(
|
||||
self,
|
||||
) -> Callable[
|
||||
[Iterable[np.ndarray], Iterable[np.ndarray], float],
|
||||
Iterable[np.ndarray],
|
||||
]:
|
||||
) -> PathFuncType:
|
||||
return self._path_func
|
||||
|
||||
@path_func.setter
|
||||
def path_func(
|
||||
self,
|
||||
path_func: Callable[
|
||||
[Iterable[np.ndarray], Iterable[np.ndarray], float],
|
||||
Iterable[np.ndarray],
|
||||
],
|
||||
path_func: PathFuncType,
|
||||
) -> None:
|
||||
if path_func is not None:
|
||||
self._path_func = path_func
|
||||
|
||||
def begin(self) -> None:
|
||||
# Use a copy of target_mobject for the align_data
|
||||
# call so that the actual target_mobject stays
|
||||
# preserved.
|
||||
self.target_mobject = self.create_target()
|
||||
self.target_copy = self.target_mobject.copy()
|
||||
# Note, this potentially changes the structure
|
||||
# of both mobject and target_mobject
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
self.mobject.align_data_and_family(self.target_copy)
|
||||
else:
|
||||
self.mobject.align_data(self.target_copy)
|
||||
self.mobject.align_data_and_family(self.target_copy)
|
||||
|
||||
super().begin()
|
||||
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
def create_target(self) -> Mobject:
|
||||
# Has no meaningful effect here, but may be useful
|
||||
# in subclasses
|
||||
return self.target_mobject
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
if self.replace_mobject_with_target_in_scene:
|
||||
scene.replace(self.mobject, self.target_mobject)
|
||||
self.buffer.replace(self.mobject, self.target_mobject)
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
return [
|
||||
|
|
@ -228,15 +218,13 @@ class Transform(Animation):
|
|||
self.target_copy,
|
||||
]
|
||||
|
||||
def get_all_families_zipped(self) -> Iterable[tuple]: # more precise typing?
|
||||
def get_all_families_zipped(self) -> zip[tuple[Mobject, Mobject, Mobject]]:
|
||||
mobs = [
|
||||
self.mobject,
|
||||
self.starting_mobject,
|
||||
self.target_copy,
|
||||
]
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
return zip(*(mob.get_family() for mob in mobs), strict=True)
|
||||
return zip(*(mob.family_members_with_points() for mob in mobs), strict=True)
|
||||
return zip(*(mob.get_family() for mob in mobs), strict=True)
|
||||
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
|
|
@ -483,7 +471,7 @@ class ApplyMethod(Transform):
|
|||
"Whoops, looks like you accidentally invoked "
|
||||
"the method you want to animate",
|
||||
)
|
||||
assert isinstance(method.__self__, (Mobject, OpenGLMobject))
|
||||
assert isinstance(method.__self__, Mobject)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
method = self.method
|
||||
|
|
@ -627,7 +615,7 @@ class ApplyFunction(Transform):
|
|||
|
||||
def create_target(self) -> Any:
|
||||
target = self.function(self.mobject.copy())
|
||||
if not isinstance(target, (Mobject, OpenGLMobject)):
|
||||
if not isinstance(target, Mobject):
|
||||
raise TypeError(
|
||||
"Functions passed to ApplyFunction must return object of type Mobject",
|
||||
)
|
||||
|
|
@ -692,6 +680,7 @@ class ApplyComplexFunction(ApplyMethod):
|
|||
super().__init__(method, function, **kwargs)
|
||||
|
||||
def _init_path_func(self) -> None:
|
||||
# TODO: this seems broken?
|
||||
func1 = self.function(complex(1))
|
||||
self.path_arc = np.log(func1).imag
|
||||
super()._init_path_func()
|
||||
|
|
@ -840,10 +829,7 @@ class FadeTransform(Transform):
|
|||
self.stretch = stretch
|
||||
self.dim_to_match = dim_to_match
|
||||
mobject.save_state()
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
group = OpenGLGroup(mobject, target_mobject.copy())
|
||||
else:
|
||||
group = Group(mobject, target_mobject.copy())
|
||||
group = Group(mobject, target_mobject.copy())
|
||||
super().__init__(group, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
|
|
@ -884,11 +870,11 @@ class FadeTransform(Transform):
|
|||
def get_all_families_zipped(self):
|
||||
return Animation.get_all_families_zipped(self)
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
Animation.clean_up_from_scene(self, scene)
|
||||
scene.remove(self.mobject)
|
||||
def finish(self):
|
||||
Animation.finish(self) # TODO: is this really needed over super()?
|
||||
self.buffer.remove(self.mobject)
|
||||
self.mobject[0].restore()
|
||||
scene.add(self.to_add_on_completion)
|
||||
self.buffer.add(self.to_add_on_completion)
|
||||
|
||||
|
||||
class FadeTransformPieces(FadeTransform):
|
||||
|
|
|
|||
|
|
@ -4,24 +4,28 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["TransformMatchingShapes", "TransformMatchingTex"]
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup, OpenGLVMobject
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
|
||||
from .._config import config
|
||||
from ..constants import RendererType
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from .composition import AnimationGroup
|
||||
from .fading import FadeIn, FadeOut
|
||||
from .transform import FadeTransformPieces, Transform
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..scene.scene import Scene
|
||||
|
||||
|
||||
class TransformMatchingAbstractBase(AnimationGroup):
|
||||
"""Abstract base class for transformations that keep track of matching parts.
|
||||
|
|
@ -76,14 +80,7 @@ class TransformMatchingAbstractBase(AnimationGroup):
|
|||
key_map: dict | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
if isinstance(mobject, OpenGLVMobject):
|
||||
group_type = OpenGLVGroup
|
||||
elif isinstance(mobject, OpenGLMobject):
|
||||
group_type = OpenGLGroup
|
||||
elif isinstance(mobject, VMobject):
|
||||
group_type = VGroup
|
||||
else:
|
||||
group_type = Group
|
||||
group_type = VGroup if isinstance(mobject, VMobject) else Group
|
||||
|
||||
source_map = self.get_shape_map(mobject)
|
||||
target_map = self.get_shape_map(target_mobject)
|
||||
|
|
@ -146,19 +143,20 @@ class TransformMatchingAbstractBase(AnimationGroup):
|
|||
key = self.get_mobject_key(sm)
|
||||
if key not in shape_map:
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
shape_map[key] = OpenGLVGroup()
|
||||
shape_map[key] = VGroup()
|
||||
else:
|
||||
shape_map[key] = VGroup()
|
||||
shape_map[key].add(sm)
|
||||
return shape_map
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
# Interpolate all animations back to 0 to ensure source mobjects remain unchanged.
|
||||
for anim in self.animations:
|
||||
anim.interpolate(0)
|
||||
scene.remove(self.mobject)
|
||||
scene.remove(*self.to_remove)
|
||||
scene.add(self.to_add)
|
||||
self.buffer.remove(self.mobject)
|
||||
self.buffer.remove(*self.to_remove)
|
||||
self.buffer.add(self.to_add)
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject: Mobject):
|
||||
|
|
@ -282,7 +280,7 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
|
|||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject: Mobject) -> list[Mobject]:
|
||||
if isinstance(mobject, (Group, VGroup, OpenGLGroup, OpenGLVGroup)):
|
||||
if isinstance(mobject, (Group, VGroup)):
|
||||
return [
|
||||
p
|
||||
for s in mobject.submobjects
|
||||
|
|
|
|||
|
|
@ -3,12 +3,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"assert_is_mobject_method",
|
||||
"always",
|
||||
"f_always",
|
||||
"always_redraw",
|
||||
"always_shift",
|
||||
"always_rotate",
|
||||
"turn_animation_into_updater",
|
||||
"cycle_animation",
|
||||
]
|
||||
|
|
@ -16,41 +13,56 @@ __all__ = [
|
|||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.constants import DEGREES, RIGHT
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.opengl import OpenGLMobject
|
||||
from manim.utils.space_ops import normalize
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.animation.animation import Animation
|
||||
import types
|
||||
from typing import Concatenate
|
||||
|
||||
from typing_extensions import ParamSpec, TypeIs
|
||||
|
||||
from manim.animation.protocol import MobjectAnimation
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
def assert_is_mobject_method(method: Callable) -> None:
|
||||
assert inspect.ismethod(method)
|
||||
mobject = method.__self__
|
||||
assert isinstance(mobject, (Mobject, OpenGLMobject))
|
||||
M = TypeVar("M", bound=Mobject)
|
||||
|
||||
|
||||
def always(method: Callable, *args, **kwargs) -> Mobject:
|
||||
assert_is_mobject_method(method)
|
||||
mobject = method.__self__
|
||||
# TODO: figure out how to typehint as MethodType[Mobject] to avoid the cast
|
||||
# madness in always/f_always
|
||||
def is_mobject_method(method: Callable[..., Any]) -> TypeIs[types.MethodType]:
|
||||
return inspect.ismethod(method) and isinstance(method.__self__, Mobject)
|
||||
|
||||
|
||||
def always(
|
||||
method: Callable[Concatenate[M, P], object], *args: P.args, **kwargs: P.kwargs
|
||||
) -> M:
|
||||
if not is_mobject_method(method):
|
||||
raise ValueError("always must take a method of a Mobject")
|
||||
mobject = cast(M, method.__self__)
|
||||
func = method.__func__
|
||||
mobject.add_updater(lambda m: func(m, *args, **kwargs))
|
||||
return mobject
|
||||
|
||||
|
||||
def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mobject:
|
||||
def f_always(
|
||||
method: Callable[Concatenate[M, ...], None],
|
||||
*arg_generators: Callable[[], object],
|
||||
**kwargs,
|
||||
) -> M:
|
||||
"""
|
||||
More functional version of always, where instead
|
||||
of taking in args, it takes in functions which output
|
||||
the relevant arguments.
|
||||
"""
|
||||
assert_is_mobject_method(method)
|
||||
mobject = method.__self__
|
||||
if not is_mobject_method(method):
|
||||
raise ValueError("f_always must take a method of a Mobject")
|
||||
mobject = cast(M, method.__self__)
|
||||
func = method.__func__
|
||||
|
||||
def updater(mob):
|
||||
|
|
@ -61,7 +73,7 @@ def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mo
|
|||
return mobject
|
||||
|
||||
|
||||
def always_redraw(func: Callable[[], Mobject]) -> Mobject:
|
||||
def always_redraw(func: Callable[[], M]) -> M:
|
||||
"""Redraw the mobject constructed by a function every frame.
|
||||
|
||||
This function returns a mobject with an attached updater that
|
||||
|
|
@ -76,7 +88,6 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
|
|||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: TangentAnimation
|
||||
|
||||
class TangentAnimation(Scene):
|
||||
|
|
@ -106,81 +117,11 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
|
|||
return mob
|
||||
|
||||
|
||||
def always_shift(
|
||||
mobject: Mobject, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1
|
||||
) -> Mobject:
|
||||
"""A mobject which is continuously shifted along some direction
|
||||
at a certain rate.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobject
|
||||
The mobject to shift.
|
||||
direction
|
||||
The direction to shift. The vector is normalized, the specified magnitude
|
||||
is not relevant.
|
||||
rate
|
||||
Length in Manim units which the mobject travels in one
|
||||
second along the specified direction.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: ShiftingSquare
|
||||
|
||||
class ShiftingSquare(Scene):
|
||||
def construct(self):
|
||||
sq = Square().set_fill(opacity=1)
|
||||
tri = Triangle()
|
||||
VGroup(sq, tri).arrange(LEFT)
|
||||
|
||||
# construct a square which is continuously
|
||||
# shifted to the right
|
||||
always_shift(sq, RIGHT, rate=5)
|
||||
|
||||
self.add(sq)
|
||||
self.play(tri.animate.set_fill(opacity=1))
|
||||
"""
|
||||
mobject.add_updater(lambda m, dt: m.shift(dt * rate * normalize(direction)))
|
||||
return mobject
|
||||
|
||||
|
||||
def always_rotate(mobject: Mobject, rate: float = 20 * DEGREES, **kwargs) -> Mobject:
|
||||
"""A mobject which is continuously rotated at a certain rate.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobject
|
||||
The mobject to be rotated.
|
||||
rate
|
||||
The angle which the mobject is rotated by
|
||||
over one second.
|
||||
kwags
|
||||
Further arguments to be passed to :meth:`.Mobject.rotate`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: SpinningTriangle
|
||||
|
||||
class SpinningTriangle(Scene):
|
||||
def construct(self):
|
||||
tri = Triangle().set_fill(opacity=1).set_z_index(2)
|
||||
sq = Square().to_edge(LEFT)
|
||||
|
||||
# will keep spinning while there is an animation going on
|
||||
always_rotate(tri, rate=2*PI, about_point=ORIGIN)
|
||||
|
||||
self.add(tri, sq)
|
||||
self.play(sq.animate.to_edge(RIGHT), rate_func=linear, run_time=1)
|
||||
"""
|
||||
mobject.add_updater(lambda m, dt: m.rotate(dt * rate, **kwargs))
|
||||
return mobject
|
||||
|
||||
|
||||
def turn_animation_into_updater(
|
||||
animation: Animation, cycle: bool = False, delay: float = 0, **kwargs
|
||||
) -> Mobject:
|
||||
animation: MobjectAnimation[M],
|
||||
cycle: bool = False,
|
||||
delay: float = 0,
|
||||
) -> M:
|
||||
"""
|
||||
Add an updater to the animation's mobject which applies
|
||||
the interpolation and update functions of the animation
|
||||
|
|
@ -209,10 +150,12 @@ def turn_animation_into_updater(
|
|||
mobject = animation.mobject
|
||||
animation.suspend_mobject_updating = False
|
||||
animation.begin()
|
||||
animation.total_time = -delay
|
||||
|
||||
def update(m: Mobject, dt: float):
|
||||
if animation.total_time >= 0:
|
||||
total_time = -delay
|
||||
|
||||
def update(m: M, dt: float):
|
||||
nonlocal total_time
|
||||
if total_time >= 0:
|
||||
run_time = animation.get_run_time()
|
||||
|
||||
# handle zero/negative runtime safely
|
||||
|
|
@ -224,7 +167,7 @@ def turn_animation_into_updater(
|
|||
m.remove_updater(update)
|
||||
return
|
||||
|
||||
time_ratio = animation.total_time / run_time
|
||||
time_ratio = total_time / run_time
|
||||
if cycle:
|
||||
alpha = time_ratio % 1
|
||||
else:
|
||||
|
|
@ -235,11 +178,11 @@ def turn_animation_into_updater(
|
|||
return
|
||||
animation.interpolate(alpha)
|
||||
animation.update_mobjects(dt)
|
||||
animation.total_time += dt
|
||||
total_time += dt
|
||||
|
||||
mobject.add_updater(update)
|
||||
return mobject
|
||||
|
||||
|
||||
def cycle_animation(animation: Animation, **kwargs) -> Mobject:
|
||||
def cycle_animation(animation: MobjectAnimation[M], **kwargs) -> M:
|
||||
return turn_animation_into_updater(animation, cycle=True, **kwargs)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Any
|
|||
from manim.animation.animation import Animation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
|
||||
|
||||
class UpdateFromFunc(Animation):
|
||||
|
|
@ -25,22 +25,22 @@ class UpdateFromFunc(Animation):
|
|||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
update_function: Callable[[Mobject], Any],
|
||||
update_function: Callable[[Mobject], object],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.update_function = update_function
|
||||
super().__init__(
|
||||
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs
|
||||
)
|
||||
self.update_function = update_function
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject) # type: ignore[arg-type]
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject)
|
||||
|
||||
|
||||
class UpdateFromAlphaFunc(UpdateFromFunc):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject, self.rate_func(alpha)) # type: ignore[call-arg, arg-type]
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject, self.rate_func(alpha)) # type: ignore[call-arg]
|
||||
|
||||
|
||||
class MaintainPositionRelativeTo(Animation):
|
||||
|
|
@ -54,7 +54,7 @@ class MaintainPositionRelativeTo(Animation):
|
|||
)
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
target = self.tracked_mobject.get_center()
|
||||
location = self.mobject.get_center()
|
||||
self.mobject.shift(target - location + self.diff)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,170 +0,0 @@
|
|||
"""A camera module that supports spatial mapping between objects for distortion effects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MappingCamera", "OldMultiCamera", "SplitScreenCamera"]
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..camera.camera import Camera
|
||||
from ..mobject.types.vectorized_mobject import VMobject
|
||||
from ..utils.config_ops import DictAsObject
|
||||
|
||||
# TODO: Add an attribute to mobjects under which they can specify that they should just
|
||||
# map their centers but remain otherwise undistorted (useful for labels, etc.)
|
||||
|
||||
|
||||
class MappingCamera(Camera):
|
||||
"""Parameters
|
||||
----------
|
||||
mapping_func : callable
|
||||
Function to map 3D points to new 3D points (identity by default).
|
||||
min_num_curves : int
|
||||
Minimum number of curves for VMobjects to avoid visual glitches.
|
||||
allow_object_intrusion : bool
|
||||
If True, modifies original mobjects; else works on copies.
|
||||
kwargs : dict
|
||||
Additional arguments passed to Camera base class.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mapping_func=lambda p: p,
|
||||
min_num_curves=50,
|
||||
allow_object_intrusion=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.mapping_func = mapping_func
|
||||
self.min_num_curves = min_num_curves
|
||||
self.allow_object_intrusion = allow_object_intrusion
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def points_to_pixel_coords(self, mobject, points):
|
||||
# Map points with custom function before converting to pixels
|
||||
return super().points_to_pixel_coords(
|
||||
mobject,
|
||||
np.apply_along_axis(self.mapping_func, 1, points),
|
||||
)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
"""Capture mobjects for rendering after applying the spatial mapping.
|
||||
|
||||
Copies mobjects unless intrusion is allowed, and ensures
|
||||
vector objects have enough curves for smooth distortion.
|
||||
"""
|
||||
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
|
||||
if self.allow_object_intrusion:
|
||||
mobject_copies = mobjects
|
||||
else:
|
||||
mobject_copies = [mobject.copy() for mobject in mobjects]
|
||||
for mobject in mobject_copies:
|
||||
if (
|
||||
isinstance(mobject, VMobject)
|
||||
and 0 < mobject.get_num_curves() < self.min_num_curves
|
||||
):
|
||||
mobject.insert_n_curves(self.min_num_curves)
|
||||
super().capture_mobjects(
|
||||
mobject_copies,
|
||||
include_submobjects=False,
|
||||
excluded_mobjects=None,
|
||||
)
|
||||
|
||||
|
||||
# Note: This allows layering of multiple cameras onto the same portion of the pixel array,
|
||||
# the later cameras overwriting the former
|
||||
#
|
||||
# TODO: Add optional separator borders between cameras (or perhaps peel this off into a
|
||||
# CameraPlusOverlay class)
|
||||
|
||||
|
||||
# TODO, the classes below should likely be deleted
|
||||
class OldMultiCamera(Camera):
|
||||
"""Parameters
|
||||
----------
|
||||
cameras_with_start_positions : tuple
|
||||
Tuples of (Camera, (start_y, start_x)) indicating camera and
|
||||
its pixel offset on the final frame.
|
||||
"""
|
||||
|
||||
def __init__(self, *cameras_with_start_positions, **kwargs):
|
||||
self.shifted_cameras = [
|
||||
DictAsObject(
|
||||
{
|
||||
"camera": camera_with_start_positions[0],
|
||||
"start_x": camera_with_start_positions[1][1],
|
||||
"start_y": camera_with_start_positions[1][0],
|
||||
"end_x": camera_with_start_positions[1][1]
|
||||
+ camera_with_start_positions[0].pixel_width,
|
||||
"end_y": camera_with_start_positions[1][0]
|
||||
+ camera_with_start_positions[0].pixel_height,
|
||||
},
|
||||
)
|
||||
for camera_with_start_positions in cameras_with_start_positions
|
||||
]
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
self.pixel_array[
|
||||
shifted_camera.start_y : shifted_camera.end_y,
|
||||
shifted_camera.start_x : shifted_camera.end_x,
|
||||
] = shifted_camera.camera.pixel_array
|
||||
|
||||
def set_background(self, pixel_array, **kwargs):
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.set_background(
|
||||
pixel_array[
|
||||
shifted_camera.start_y : shifted_camera.end_y,
|
||||
shifted_camera.start_x : shifted_camera.end_x,
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def set_pixel_array(self, pixel_array, **kwargs):
|
||||
super().set_pixel_array(pixel_array, **kwargs)
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.set_pixel_array(
|
||||
pixel_array[
|
||||
shifted_camera.start_y : shifted_camera.end_y,
|
||||
shifted_camera.start_x : shifted_camera.end_x,
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def init_background(self):
|
||||
super().init_background()
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.init_background()
|
||||
|
||||
|
||||
# A OldMultiCamera which, when called with two full-size cameras, initializes itself
|
||||
# as a split screen, also taking care to resize each individual camera within it
|
||||
|
||||
|
||||
class SplitScreenCamera(OldMultiCamera):
|
||||
"""Initializes a split screen camera setup with two side-by-side cameras.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
left_camera : Camera
|
||||
right_camera : Camera
|
||||
kwargs : dict
|
||||
"""
|
||||
|
||||
def __init__(self, left_camera, right_camera, **kwargs):
|
||||
Camera.__init__(self, **kwargs) # to set attributes such as pixel_width
|
||||
self.left_camera = left_camera
|
||||
self.right_camera = right_camera
|
||||
|
||||
half_width = math.ceil(self.pixel_width / 2)
|
||||
for camera in [self.left_camera, self.right_camera]:
|
||||
camera.reset_pixel_shape(camera.pixel_height, half_width)
|
||||
|
||||
super().__init__(
|
||||
(left_camera, (0, 0)),
|
||||
(right_camera, (0, half_width)),
|
||||
)
|
||||
|
|
@ -1,274 +0,0 @@
|
|||
"""Defines the MovingCamera class, a camera that can pan and zoom through a scene.
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:mod:`.moving_camera_scene`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MovingCamera"]
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from cairo import Context
|
||||
|
||||
from manim.typing import PixelArray, Point3D, Point3DLike
|
||||
|
||||
from .. import config
|
||||
from ..camera.camera import Camera
|
||||
from ..constants import DOWN, LEFT, RIGHT, UP
|
||||
from ..mobject.frame import ScreenRectangle
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..utils.color import WHITE, ManimColor
|
||||
|
||||
|
||||
class MovingCamera(Camera):
|
||||
"""A camera that follows and matches the size and position of its 'frame', a Rectangle (or similar Mobject).
|
||||
|
||||
The frame defines the region of space the camera displays and can move or resize dynamically.
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:class:`.MovingCameraScene`
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frame: Mobject | None = None,
|
||||
fixed_dimension: int = 0, # width
|
||||
default_frame_stroke_color: ManimColor = WHITE,
|
||||
default_frame_stroke_width: int = 0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Frame is a Mobject, (should almost certainly be a rectangle)
|
||||
determining which region of space the camera displays
|
||||
"""
|
||||
self.fixed_dimension = fixed_dimension
|
||||
self.default_frame_stroke_color = default_frame_stroke_color
|
||||
self.default_frame_stroke_width = default_frame_stroke_width
|
||||
if frame is None:
|
||||
frame = ScreenRectangle(height=config["frame_height"])
|
||||
frame.set_stroke(
|
||||
self.default_frame_stroke_color,
|
||||
self.default_frame_stroke_width,
|
||||
)
|
||||
self.frame = frame
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# TODO, make these work for a rotated frame
|
||||
@property
|
||||
def frame_height(self) -> float:
|
||||
"""Returns the height of the frame.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The height of the frame.
|
||||
"""
|
||||
return self.frame.height
|
||||
|
||||
@frame_height.setter
|
||||
def frame_height(self, frame_height: float) -> None:
|
||||
"""Sets the height of the frame in MUnits.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_height
|
||||
The new frame_height.
|
||||
"""
|
||||
self.frame.stretch_to_fit_height(frame_height)
|
||||
|
||||
@property
|
||||
def frame_width(self) -> float:
|
||||
"""Returns the width of the frame
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The width of the frame.
|
||||
"""
|
||||
return self.frame.width
|
||||
|
||||
@frame_width.setter
|
||||
def frame_width(self, frame_width: float) -> None:
|
||||
"""Sets the width of the frame in MUnits.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_width
|
||||
The new frame_width.
|
||||
"""
|
||||
self.frame.stretch_to_fit_width(frame_width)
|
||||
|
||||
@property
|
||||
def frame_center(self) -> Point3D:
|
||||
"""Returns the centerpoint of the frame in cartesian coordinates.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The cartesian coordinates of the center of the frame.
|
||||
"""
|
||||
return self.frame.get_center()
|
||||
|
||||
@frame_center.setter
|
||||
def frame_center(self, frame_center: Point3DLike | Mobject) -> None:
|
||||
"""Sets the centerpoint of the frame.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_center
|
||||
The point to which the frame must be moved.
|
||||
If is of type mobject, the frame will be moved to
|
||||
the center of that mobject.
|
||||
"""
|
||||
self.frame.move_to(frame_center)
|
||||
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
# self.reset_frame_center()
|
||||
# self.realign_frame_shape()
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_cached_cairo_context(self, pixel_array: PixelArray) -> None:
|
||||
"""Since the frame can be moving around, the cairo
|
||||
context used for updating should be regenerated
|
||||
at each frame. So no caching.
|
||||
"""
|
||||
return None
|
||||
|
||||
def cache_cairo_context(self, pixel_array: PixelArray, ctx: Context) -> None:
|
||||
"""Since the frame can be moving around, the cairo
|
||||
context used for updating should be regenerated
|
||||
at each frame. So no caching.
|
||||
"""
|
||||
pass
|
||||
|
||||
# def reset_frame_center(self):
|
||||
# self.frame_center = self.frame.get_center()
|
||||
|
||||
# def realign_frame_shape(self):
|
||||
# height, width = self.frame_shape
|
||||
# if self.fixed_dimension == 0:
|
||||
# self.frame_shape = (height, self.frame.width
|
||||
# else:
|
||||
# self.frame_shape = (self.frame.height, width)
|
||||
# self.resize_frame_shape(fixed_dimension=self.fixed_dimension)
|
||||
|
||||
def get_mobjects_indicating_movement(self) -> list[Mobject]:
|
||||
"""Returns all mobjects whose movement implies that the camera
|
||||
should think of all other mobjects on the screen as moving
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[Mobject]
|
||||
"""
|
||||
return [self.frame]
|
||||
|
||||
def auto_zoom(
|
||||
self,
|
||||
mobjects: Iterable[Mobject],
|
||||
margin: float = 0,
|
||||
only_mobjects_in_frame: bool = False,
|
||||
animate: bool = True,
|
||||
) -> Mobject:
|
||||
"""Zooms on to a given array of mobjects (or a singular mobject)
|
||||
and automatically resizes to frame all the mobjects.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
This method only works when 2D-objects in the XY-plane are considered, it
|
||||
will not work correctly when the camera has been rotated.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
The mobject or array of mobjects that the camera will focus on.
|
||||
|
||||
margin
|
||||
The width of the margin that is added to the frame (optional, 0 by default).
|
||||
|
||||
only_mobjects_in_frame
|
||||
If set to ``True``, only allows focusing on mobjects that are already in frame.
|
||||
|
||||
animate
|
||||
If set to ``False``, applies the changes instead of returning the corresponding animation
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[_AnimationBuilder, ScreenRectangle]
|
||||
_AnimationBuilder that zooms the camera view to a given list of mobjects
|
||||
or ScreenRectangle with position and size updated to zoomed position.
|
||||
|
||||
"""
|
||||
(
|
||||
scene_critical_x_left,
|
||||
scene_critical_x_right,
|
||||
scene_critical_y_up,
|
||||
scene_critical_y_down,
|
||||
) = self._get_bounding_box(mobjects, only_mobjects_in_frame)
|
||||
|
||||
# calculate center x and y
|
||||
x = (scene_critical_x_left + scene_critical_x_right) / 2
|
||||
y = (scene_critical_y_up + scene_critical_y_down) / 2
|
||||
|
||||
# calculate proposed width and height of zoomed scene
|
||||
new_width = abs(scene_critical_x_left - scene_critical_x_right)
|
||||
new_height = abs(scene_critical_y_up - scene_critical_y_down)
|
||||
|
||||
m_target = self.frame.animate if animate else self.frame
|
||||
# zoom to fit all mobjects along the side that has the largest size
|
||||
if new_width / self.frame.width > new_height / self.frame.height:
|
||||
return m_target.set_x(x).set_y(y).set(width=new_width + margin)
|
||||
else:
|
||||
return m_target.set_x(x).set_y(y).set(height=new_height + margin)
|
||||
|
||||
def _get_bounding_box(
|
||||
self, mobjects: Iterable[Mobject], only_mobjects_in_frame: bool
|
||||
) -> tuple[float, float, float, float]:
|
||||
bounding_box_located = False
|
||||
scene_critical_x_left: float = 0
|
||||
scene_critical_x_right: float = 1
|
||||
scene_critical_y_up: float = 1
|
||||
scene_critical_y_down: float = 0
|
||||
|
||||
for m in mobjects:
|
||||
if (m == self.frame) or (
|
||||
only_mobjects_in_frame and not self.is_in_frame(m)
|
||||
):
|
||||
# detected camera frame, should not be used to calculate final position of camera
|
||||
continue
|
||||
|
||||
# initialize scene critical points with first mobjects critical points
|
||||
if not bounding_box_located:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
bounding_box_located = True
|
||||
|
||||
else:
|
||||
if m.get_critical_point(LEFT)[0] < scene_critical_x_left:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
|
||||
if m.get_critical_point(RIGHT)[0] > scene_critical_x_right:
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
|
||||
if m.get_critical_point(UP)[1] > scene_critical_y_up:
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
|
||||
if m.get_critical_point(DOWN)[1] < scene_critical_y_down:
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
|
||||
if not bounding_box_located:
|
||||
raise Exception(
|
||||
"Could not determine bounding box of the mobjects given to 'auto_zoom'."
|
||||
)
|
||||
|
||||
return (
|
||||
scene_critical_x_left,
|
||||
scene_critical_x_right,
|
||||
scene_critical_y_up,
|
||||
scene_critical_y_down,
|
||||
)
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
"""A camera supporting multiple perspectives."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MultiCamera"]
|
||||
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Self
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.types.image_mobject import ImageMobjectFromCamera
|
||||
|
||||
from ..camera.moving_camera import MovingCamera
|
||||
from ..utils.iterables import list_difference_update
|
||||
|
||||
|
||||
class MultiCamera(MovingCamera):
|
||||
"""Camera Object that allows for multiple perspectives."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
image_mobjects_from_cameras: Iterable[ImageMobjectFromCamera] | None = None,
|
||||
allow_cameras_to_capture_their_own_display: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialises the MultiCamera
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image_mobjects_from_cameras
|
||||
|
||||
kwargs
|
||||
Any valid keyword arguments of MovingCamera.
|
||||
"""
|
||||
self.image_mobjects_from_cameras: list[ImageMobjectFromCamera] = []
|
||||
if image_mobjects_from_cameras is not None:
|
||||
for imfc in image_mobjects_from_cameras:
|
||||
self.add_image_mobject_from_camera(imfc)
|
||||
self.allow_cameras_to_capture_their_own_display = (
|
||||
allow_cameras_to_capture_their_own_display
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def add_image_mobject_from_camera(
|
||||
self, image_mobject_from_camera: ImageMobjectFromCamera
|
||||
) -> None:
|
||||
"""Adds an ImageMobject that's been obtained from the camera
|
||||
into the list ``self.image_mobject_from_cameras``
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image_mobject_from_camera
|
||||
The ImageMobject to add to self.image_mobject_from_cameras
|
||||
"""
|
||||
# A silly method to have right now, but maybe there are things
|
||||
# we want to guarantee about any imfc's added later.
|
||||
imfc = image_mobject_from_camera
|
||||
assert isinstance(imfc.camera, MovingCamera)
|
||||
self.image_mobjects_from_cameras.append(imfc)
|
||||
|
||||
def update_sub_cameras(self) -> None:
|
||||
"""Reshape sub_camera pixel_arrays"""
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
pixel_height, pixel_width = self.pixel_array.shape[:2]
|
||||
# imfc.camera.frame_shape = (
|
||||
# imfc.camera.frame.height,
|
||||
# imfc.camera.frame.width,
|
||||
# )
|
||||
imfc.camera.reset_pixel_shape(
|
||||
int(pixel_height * imfc.height / self.frame_height),
|
||||
int(pixel_width * imfc.width / self.frame_width),
|
||||
)
|
||||
|
||||
def reset(self) -> Self:
|
||||
"""Resets the MultiCamera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
MultiCamera
|
||||
The reset MultiCamera
|
||||
"""
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
imfc.camera.reset()
|
||||
super().reset()
|
||||
return self
|
||||
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
self.update_sub_cameras()
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
to_add = list(mobjects)
|
||||
if not self.allow_cameras_to_capture_their_own_display:
|
||||
to_add = list_difference_update(to_add, imfc.get_family())
|
||||
imfc.camera.capture_mobjects(to_add, **kwargs)
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_mobjects_indicating_movement(self) -> list[Mobject]:
|
||||
"""Returns all mobjects whose movement implies that the camera
|
||||
should think of all other mobjects on the screen as moving
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
"""
|
||||
return [self.frame] + [
|
||||
imfc.camera.frame for imfc in self.image_mobjects_from_cameras
|
||||
]
|
||||
|
|
@ -1,459 +0,0 @@
|
|||
"""A camera that can be positioned and oriented in three-dimensional space."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["ThreeDCamera"]
|
||||
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.three_d.three_d_utils import (
|
||||
get_3d_vmob_end_corner,
|
||||
get_3d_vmob_end_corner_unit_normal,
|
||||
get_3d_vmob_start_corner,
|
||||
get_3d_vmob_start_corner_unit_normal,
|
||||
)
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.value_tracker import ValueTracker
|
||||
from manim.typing import (
|
||||
FloatRGBA_Array,
|
||||
MatrixMN,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
Point3DLike,
|
||||
)
|
||||
|
||||
from .. import config
|
||||
from ..camera.camera import Camera
|
||||
from ..constants import *
|
||||
from ..mobject.types.point_cloud_mobject import Point
|
||||
from ..utils.color import get_shaded_rgb
|
||||
from ..utils.family import extract_mobject_family_members
|
||||
from ..utils.space_ops import rotation_about_z, rotation_matrix
|
||||
|
||||
|
||||
class ThreeDCamera(Camera):
|
||||
def __init__(
|
||||
self,
|
||||
focal_distance: float = 20.0,
|
||||
shading_factor: float = 0.2,
|
||||
default_distance: float = 5.0,
|
||||
light_source_start_point: Point3DLike = 9 * DOWN + 7 * LEFT + 10 * OUT,
|
||||
should_apply_shading: bool = True,
|
||||
exponential_projection: bool = False,
|
||||
phi: float = 0,
|
||||
theta: float = -90 * DEGREES,
|
||||
gamma: float = 0,
|
||||
zoom: float = 1,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initializes the ThreeDCamera
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*kwargs
|
||||
Any keyword argument of Camera.
|
||||
"""
|
||||
self._frame_center = Point(kwargs.get("frame_center", ORIGIN), stroke_width=0)
|
||||
super().__init__(**kwargs)
|
||||
self.focal_distance = focal_distance
|
||||
self.phi = phi
|
||||
self.theta = theta
|
||||
self.gamma = gamma
|
||||
self.zoom = zoom
|
||||
self.shading_factor = shading_factor
|
||||
self.default_distance = default_distance
|
||||
self.light_source_start_point = light_source_start_point
|
||||
self.light_source = Point(self.light_source_start_point)
|
||||
self.should_apply_shading = should_apply_shading
|
||||
self.exponential_projection = exponential_projection
|
||||
self.max_allowable_norm = 3 * config["frame_width"]
|
||||
self.phi_tracker = ValueTracker(self.phi)
|
||||
self.theta_tracker = ValueTracker(self.theta)
|
||||
self.focal_distance_tracker = ValueTracker(self.focal_distance)
|
||||
self.gamma_tracker = ValueTracker(self.gamma)
|
||||
self.zoom_tracker = ValueTracker(self.zoom)
|
||||
self.fixed_orientation_mobjects: dict[Mobject, Callable[[], Point3D]] = {}
|
||||
self.fixed_in_frame_mobjects: set[Mobject] = set()
|
||||
self.reset_rotation_matrix()
|
||||
|
||||
@property
|
||||
def frame_center(self) -> Point3D:
|
||||
return self._frame_center.points[0]
|
||||
|
||||
@frame_center.setter
|
||||
def frame_center(self, point: Point3DLike) -> None:
|
||||
self._frame_center.move_to(point)
|
||||
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
self.reset_rotation_matrix()
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_value_trackers(self) -> list[ValueTracker]:
|
||||
"""A list of :class:`ValueTrackers <.ValueTracker>` of phi, theta, focal_distance,
|
||||
gamma and zoom.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
list of ValueTracker objects
|
||||
"""
|
||||
return [
|
||||
self.phi_tracker,
|
||||
self.theta_tracker,
|
||||
self.focal_distance_tracker,
|
||||
self.gamma_tracker,
|
||||
self.zoom_tracker,
|
||||
]
|
||||
|
||||
def modified_rgbas(
|
||||
self, vmobject: VMobject, rgbas: FloatRGBA_Array
|
||||
) -> FloatRGBA_Array:
|
||||
if not self.should_apply_shading:
|
||||
return rgbas
|
||||
if vmobject.shade_in_3d and (vmobject.get_num_points() > 0):
|
||||
light_source_point = self.light_source.points[0]
|
||||
if len(rgbas) < 2:
|
||||
shaded_rgbas = rgbas.repeat(2, axis=0)
|
||||
else:
|
||||
shaded_rgbas = np.array(rgbas[:2])
|
||||
shaded_rgbas[0, :3] = get_shaded_rgb(
|
||||
shaded_rgbas[0, :3],
|
||||
get_3d_vmob_start_corner(vmobject),
|
||||
get_3d_vmob_start_corner_unit_normal(vmobject),
|
||||
light_source_point,
|
||||
)
|
||||
shaded_rgbas[1, :3] = get_shaded_rgb(
|
||||
shaded_rgbas[1, :3],
|
||||
get_3d_vmob_end_corner(vmobject),
|
||||
get_3d_vmob_end_corner_unit_normal(vmobject),
|
||||
light_source_point,
|
||||
)
|
||||
return shaded_rgbas
|
||||
return rgbas
|
||||
|
||||
def get_stroke_rgbas(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
background: bool = False,
|
||||
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_stroke_rgbas(background))
|
||||
|
||||
def get_fill_rgbas(
|
||||
self, vmobject: VMobject
|
||||
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_fill_rgbas())
|
||||
|
||||
def get_mobjects_to_display(
|
||||
self, *args: Any, **kwargs: Any
|
||||
) -> list[Mobject]: # NOTE : DocStrings From parent
|
||||
mobjects = super().get_mobjects_to_display(*args, **kwargs)
|
||||
rot_matrix = self.get_rotation_matrix()
|
||||
|
||||
def z_key(mob: Mobject) -> float:
|
||||
if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d):
|
||||
return np.inf # type: ignore[no-any-return]
|
||||
# Assign a number to a three dimensional mobjects
|
||||
# based on how close it is to the camera
|
||||
distance: float = np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2]
|
||||
return distance
|
||||
|
||||
return sorted(mobjects, key=z_key)
|
||||
|
||||
def get_phi(self) -> float:
|
||||
"""Returns the Polar angle (the angle off Z_AXIS) phi.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The Polar angle in radians.
|
||||
"""
|
||||
return self.phi_tracker.get_value()
|
||||
|
||||
def get_theta(self) -> float:
|
||||
"""Returns the Azimuthal i.e the angle that spins the camera around the Z_AXIS.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The Azimuthal angle in radians.
|
||||
"""
|
||||
return self.theta_tracker.get_value()
|
||||
|
||||
def get_focal_distance(self) -> float:
|
||||
"""Returns focal_distance of the Camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The focal_distance of the Camera in MUnits.
|
||||
"""
|
||||
return self.focal_distance_tracker.get_value()
|
||||
|
||||
def get_gamma(self) -> float:
|
||||
"""Returns the rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The angle of rotation of the camera about the vector
|
||||
from the ORIGIN to the Camera in radians
|
||||
"""
|
||||
return self.gamma_tracker.get_value()
|
||||
|
||||
def get_zoom(self) -> float:
|
||||
"""Returns the zoom amount of the camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The zoom amount of the camera.
|
||||
"""
|
||||
return self.zoom_tracker.get_value()
|
||||
|
||||
def set_phi(self, value: float) -> None:
|
||||
"""Sets the polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new value of the polar angle in radians.
|
||||
"""
|
||||
self.phi_tracker.set_value(value)
|
||||
|
||||
def set_theta(self, value: float) -> None:
|
||||
"""Sets the azimuthal angle i.e the angle that spins the camera around Z_AXIS in radians.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new value of the azimuthal angle in radians.
|
||||
"""
|
||||
self.theta_tracker.set_value(value)
|
||||
|
||||
def set_focal_distance(self, value: float) -> None:
|
||||
"""Sets the focal_distance of the Camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The focal_distance of the Camera.
|
||||
"""
|
||||
self.focal_distance_tracker.set_value(value)
|
||||
|
||||
def set_gamma(self, value: float) -> None:
|
||||
"""Sets the angle of rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new angle of rotation of the camera.
|
||||
"""
|
||||
self.gamma_tracker.set_value(value)
|
||||
|
||||
def set_zoom(self, value: float) -> None:
|
||||
"""Sets the zoom amount of the camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The zoom amount of the camera.
|
||||
"""
|
||||
self.zoom_tracker.set_value(value)
|
||||
|
||||
def reset_rotation_matrix(self) -> None:
|
||||
"""Sets the value of self.rotation_matrix to
|
||||
the matrix corresponding to the current position of the camera
|
||||
"""
|
||||
self.rotation_matrix = self.generate_rotation_matrix()
|
||||
|
||||
def get_rotation_matrix(self) -> MatrixMN:
|
||||
"""Returns the matrix corresponding to the current position of the camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The matrix corresponding to the current position of the camera.
|
||||
"""
|
||||
return self.rotation_matrix
|
||||
|
||||
def generate_rotation_matrix(self) -> MatrixMN:
|
||||
"""Generates a rotation matrix based off the current position of the camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The matrix corresponding to the current position of the camera.
|
||||
"""
|
||||
phi = self.get_phi()
|
||||
theta = self.get_theta()
|
||||
gamma = self.get_gamma()
|
||||
matrices = [
|
||||
rotation_about_z(-theta - 90 * DEGREES),
|
||||
rotation_matrix(-phi, RIGHT),
|
||||
rotation_about_z(gamma),
|
||||
]
|
||||
result = np.identity(3)
|
||||
for matrix in matrices:
|
||||
result = np.dot(matrix, result)
|
||||
return result
|
||||
|
||||
def project_points(self, points: Point3D_Array) -> Point3D_Array:
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed array of points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points
|
||||
The list of points to project.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The points after projecting.
|
||||
"""
|
||||
frame_center = self.frame_center
|
||||
focal_distance = self.get_focal_distance()
|
||||
zoom = self.get_zoom()
|
||||
rot_matrix = self.get_rotation_matrix()
|
||||
|
||||
points = points - frame_center
|
||||
points = np.dot(points, rot_matrix.T)
|
||||
zs = points[:, 2]
|
||||
for i in 0, 1:
|
||||
if self.exponential_projection:
|
||||
# Proper projection would involve multiplying
|
||||
# x and y by d / (d-z). But for points with high
|
||||
# z value that causes weird artifacts, and applying
|
||||
# the exponential helps smooth it out.
|
||||
factor = np.exp(zs / focal_distance)
|
||||
lt0 = zs < 0
|
||||
factor[lt0] = focal_distance / (focal_distance - zs[lt0])
|
||||
else:
|
||||
factor = focal_distance / (focal_distance - zs)
|
||||
factor[(focal_distance - zs) < 0] = 10**6
|
||||
points[:, i] *= factor * zoom
|
||||
return points
|
||||
|
||||
def project_point(self, point: Point3D) -> Point3D:
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed point.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
point
|
||||
The point to project.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The point after projection.
|
||||
"""
|
||||
return self.project_points(point.reshape((1, 3)))[0, :]
|
||||
|
||||
def transform_points_pre_display(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
points: Point3D_Array,
|
||||
) -> Point3D_Array: # TODO: Write Docstrings for this Method.
|
||||
points = super().transform_points_pre_display(mobject, points)
|
||||
fixed_orientation = mobject in self.fixed_orientation_mobjects
|
||||
fixed_in_frame = mobject in self.fixed_in_frame_mobjects
|
||||
|
||||
if fixed_in_frame:
|
||||
return points
|
||||
if fixed_orientation:
|
||||
center_func = self.fixed_orientation_mobjects[mobject]
|
||||
center = center_func()
|
||||
new_center = self.project_point(center)
|
||||
return points + (new_center - center)
|
||||
else:
|
||||
return self.project_points(points)
|
||||
|
||||
def add_fixed_orientation_mobjects(
|
||||
self,
|
||||
*mobjects: Mobject,
|
||||
use_static_center_func: bool = False,
|
||||
center_func: Callable[[], Point3D] | None = None,
|
||||
) -> None:
|
||||
"""This method allows the mobject to have a fixed orientation,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, facing the camera, it
|
||||
will continue to face the camera even as the camera moves.
|
||||
Highly useful when adding labels to graphs and the like.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*mobjects
|
||||
The mobject whose orientation must be fixed.
|
||||
use_static_center_func
|
||||
Whether or not to use the function that takes the mobject's
|
||||
center as centerpoint, by default False
|
||||
center_func
|
||||
The function which returns the centerpoint
|
||||
with respect to which the mobject will be oriented, by default None
|
||||
"""
|
||||
|
||||
# This prevents the computation of mobject.get_center
|
||||
# every single time a projection happens
|
||||
def get_static_center_func(mobject: Mobject) -> Callable[[], Point3D]:
|
||||
point = mobject.get_center()
|
||||
return lambda: point
|
||||
|
||||
for mobject in mobjects:
|
||||
if center_func:
|
||||
func = center_func
|
||||
elif use_static_center_func:
|
||||
func = get_static_center_func(mobject)
|
||||
else:
|
||||
func = mobject.get_center
|
||||
for submob in mobject.get_family():
|
||||
self.fixed_orientation_mobjects[submob] = func
|
||||
|
||||
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""This method allows the mobject to have a fixed position,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, at the top of the frame, it
|
||||
will continue to be displayed at the top of the frame.
|
||||
|
||||
Highly useful when displaying Titles or formulae or the like.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**mobjects
|
||||
The mobject to fix in frame.
|
||||
"""
|
||||
for mobject in extract_mobject_family_members(mobjects):
|
||||
self.fixed_in_frame_mobjects.add(mobject)
|
||||
|
||||
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""If a mobject was fixed in its orientation by passing it through
|
||||
:meth:`.add_fixed_orientation_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer have a fixed orientation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
The mobjects whose orientation need not be fixed any longer.
|
||||
"""
|
||||
for mobject in extract_mobject_family_members(mobjects):
|
||||
if mobject in self.fixed_orientation_mobjects:
|
||||
del self.fixed_orientation_mobjects[mobject]
|
||||
|
||||
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""If a mobject was fixed in frame by passing it through
|
||||
:meth:`.add_fixed_in_frame_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer be fixed in frame.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
The mobjects which need not be fixed in frame any longer.
|
||||
"""
|
||||
for mobject in extract_mobject_family_members(mobjects):
|
||||
if mobject in self.fixed_in_frame_mobjects:
|
||||
self.fixed_in_frame_mobjects.remove(mobject)
|
||||
|
|
@ -84,7 +84,9 @@ def checkhealth() -> None:
|
|||
self.execution_time = timeit.timeit(self._inner_construct, number=1)
|
||||
|
||||
with mn.tempconfig({"preview": True, "disable_caching": True}):
|
||||
scene = CheckHealthDemo()
|
||||
scene.render()
|
||||
with mn.Manager(CheckHealthDemo) as manager:
|
||||
manager.render()
|
||||
|
||||
click.echo(f"Scene rendered in {scene.execution_time:.2f} seconds.")
|
||||
click.echo(
|
||||
f"Scene rendered in {manager.scene.execution_time:.2f} seconds."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ from manim.cli.render.ease_of_access_options import ease_of_access_options
|
|||
from manim.cli.render.global_options import global_options
|
||||
from manim.cli.render.output_options import output_options
|
||||
from manim.cli.render.render_options import render_options
|
||||
from manim.constants import EPILOG, RendererType
|
||||
from manim.constants import EPILOG
|
||||
from manim.manager import Manager
|
||||
from manim.utils.module_ops import scene_classes_from_file
|
||||
|
||||
__all__ = ["render"]
|
||||
|
|
@ -75,14 +76,6 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
|
|||
|
||||
SCENES is an optional list of scenes in the file.
|
||||
"""
|
||||
if kwargs["save_as_gif"]:
|
||||
logger.warning("--save_as_gif is deprecated, please use --format=gif instead!")
|
||||
kwargs["format"] = "gif"
|
||||
|
||||
if kwargs["save_pngs"]:
|
||||
logger.warning("--save_pngs is deprecated, please use --format=png instead!")
|
||||
kwargs["format"] = "png"
|
||||
|
||||
if kwargs["show_in_file_browser"]:
|
||||
logger.warning(
|
||||
"The short form of show_in_file_browser is deprecated and will be moved to support --format.",
|
||||
|
|
@ -94,38 +87,13 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
|
|||
|
||||
config.digest_args(click_args)
|
||||
file = Path(config.input_file)
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
try:
|
||||
renderer = OpenGLRenderer()
|
||||
keep_running = True
|
||||
while keep_running:
|
||||
for SceneClass in scene_classes_from_file(file):
|
||||
with tempconfig({}):
|
||||
scene = SceneClass(renderer)
|
||||
rerun = scene.render()
|
||||
if rerun or config["write_all"]:
|
||||
renderer.num_plays = 0
|
||||
continue
|
||||
else:
|
||||
keep_running = False
|
||||
break
|
||||
if config["write_all"]:
|
||||
keep_running = False
|
||||
|
||||
except Exception:
|
||||
error_console.print_exception()
|
||||
sys.exit(1)
|
||||
else:
|
||||
try:
|
||||
for SceneClass in scene_classes_from_file(file):
|
||||
try:
|
||||
with tempconfig({}):
|
||||
scene = SceneClass()
|
||||
scene.render()
|
||||
except Exception:
|
||||
error_console.print_exception()
|
||||
sys.exit(1)
|
||||
with tempconfig({}), Manager(SceneClass) as manager:
|
||||
manager.render()
|
||||
except Exception:
|
||||
error_console.print_exception()
|
||||
sys.exit(1)
|
||||
|
||||
if config.notify_outdated_version:
|
||||
manim_info_url = "https://pypi.org/pypi/manim/json"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ if TYPE_CHECKING:
|
|||
|
||||
__all__ = ["global_options"]
|
||||
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,11 @@ output_options = option_group(
|
|||
help="Zero padding for PNG file names.",
|
||||
),
|
||||
option(
|
||||
"-w",
|
||||
"--write_to_movie",
|
||||
is_flag=True,
|
||||
default=None,
|
||||
help="Write the video rendered with opengl to a file.",
|
||||
help="Write the video to a file.",
|
||||
),
|
||||
option(
|
||||
"--media_dir",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from cloup import Choice, option, option_group
|
||||
|
||||
from manim.constants import QUALITIES, RendererType
|
||||
from manim.constants import QUALITIES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from click import Context, Option
|
||||
|
|
@ -16,6 +16,8 @@ __all__ = ["render_options"]
|
|||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
__all__ = ["render_options"]
|
||||
|
||||
|
||||
def validate_scene_range(
|
||||
ctx: Context, param: Option, value: str | None
|
||||
|
|
@ -112,6 +114,13 @@ render_options = option_group(
|
|||
"renders all scenes after n_0.",
|
||||
default=None,
|
||||
),
|
||||
option(
|
||||
"-g",
|
||||
"--groups",
|
||||
callback=lambda ctx, param, value: value.split(","),
|
||||
help="Render only the specified groups.",
|
||||
default=[],
|
||||
),
|
||||
option(
|
||||
"-a",
|
||||
"--write_all",
|
||||
|
|
@ -165,29 +174,6 @@ render_options = option_group(
|
|||
default=None,
|
||||
help="Render at this frame rate.",
|
||||
),
|
||||
option(
|
||||
"--renderer",
|
||||
type=Choice(
|
||||
[renderer_type.value for renderer_type in RendererType],
|
||||
case_sensitive=False,
|
||||
),
|
||||
help="Select a renderer for your Scene.",
|
||||
default="cairo",
|
||||
),
|
||||
option(
|
||||
"-g",
|
||||
"--save_pngs",
|
||||
is_flag=True,
|
||||
default=None,
|
||||
help="Save each frame as png (Deprecated).",
|
||||
),
|
||||
option(
|
||||
"-i",
|
||||
"--save_as_gif",
|
||||
default=None,
|
||||
is_flag=True,
|
||||
help="Save as a gif (Deprecated).",
|
||||
),
|
||||
option(
|
||||
"--save_sections",
|
||||
default=None,
|
||||
|
|
@ -200,16 +186,4 @@ render_options = option_group(
|
|||
is_flag=True,
|
||||
help="Render scenes with alpha channel.",
|
||||
),
|
||||
option(
|
||||
"--use_projection_fill_shaders",
|
||||
is_flag=True,
|
||||
help="Use shaders for OpenGLVMobject fill which are compatible with transformation matrices.",
|
||||
default=None,
|
||||
),
|
||||
option(
|
||||
"--use_projection_stroke_shaders",
|
||||
is_flag=True,
|
||||
help="Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices.",
|
||||
default=None,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -67,12 +67,11 @@ __all__ = [
|
|||
"PI",
|
||||
"TAU",
|
||||
"DEGREES",
|
||||
"RADIANS",
|
||||
"QUALITIES",
|
||||
"DEFAULT_QUALITY",
|
||||
"EPILOG",
|
||||
"CONTEXT_SETTINGS",
|
||||
"SHIFT_VALUE",
|
||||
"CTRL_VALUE",
|
||||
"RendererType",
|
||||
"LineJointType",
|
||||
"CapStyleType",
|
||||
|
|
@ -194,6 +193,9 @@ TAU = 2 * PI
|
|||
DEGREES = TAU / 360
|
||||
"""The exchange rate between radians and degrees."""
|
||||
|
||||
RADIANS: float = 1.0
|
||||
"""Just a default to select for camera."""
|
||||
|
||||
|
||||
class QualityDict(TypedDict):
|
||||
flag: str | None
|
||||
|
|
@ -245,8 +247,6 @@ QUALITIES: dict[str, QualityDict] = {
|
|||
DEFAULT_QUALITY = "high_quality"
|
||||
|
||||
EPILOG = "Made with <3 by Manim Community developers."
|
||||
SHIFT_VALUE = 65505
|
||||
CTRL_VALUE = 65507
|
||||
|
||||
CONTEXT_SETTINGS = Context.settings(
|
||||
align_option_groups=True,
|
||||
|
|
|
|||
7
manim/event_handler/__init__.py
Normal file
7
manim/event_handler/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from manim.event_handler.event_dispatcher import EventDispatcher
|
||||
|
||||
# This is supposed to be a Singleton
|
||||
# i.e., during runtime there should be only one object of Event Dispatcher
|
||||
EVENT_DISPATCHER = EventDispatcher()
|
||||
93
manim/event_handler/event_dispatcher.py
Normal file
93
manim/event_handler/event_dispatcher.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Self
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.event_handler.event_listener import EventListener
|
||||
from manim.event_handler.event_type import EventType
|
||||
|
||||
|
||||
class EventDispatcher:
|
||||
def __init__(self) -> None:
|
||||
self.event_listeners: dict[EventType, list[EventListener]] = {
|
||||
event_type: [] for event_type in EventType
|
||||
}
|
||||
self.mouse_point = np.array((0.0, 0.0, 0.0))
|
||||
self.mouse_drag_point = np.array((0.0, 0.0, 0.0))
|
||||
self.pressed_keys: set[int] = set()
|
||||
self.draggable_object_listeners: list[EventListener] = []
|
||||
|
||||
def add_listener(self, event_listener: EventListener) -> Self:
|
||||
assert isinstance(event_listener, EventListener)
|
||||
self.event_listeners[event_listener.event_type].append(event_listener)
|
||||
return self
|
||||
|
||||
def remove_listener(self, event_listener: EventListener) -> Self:
|
||||
assert isinstance(event_listener, EventListener)
|
||||
try:
|
||||
while event_listener in self.event_listeners[event_listener.event_type]:
|
||||
self.event_listeners[event_listener.event_type].remove(event_listener)
|
||||
except Exception:
|
||||
# raise ValueError("Handler is not handling this event, so cannot remove it.")
|
||||
pass
|
||||
return self
|
||||
|
||||
def dispatch(self, event_type: EventType, **event_data: Any) -> bool | None:
|
||||
if event_type == EventType.MouseMotionEvent:
|
||||
self.mouse_point = event_data["point"]
|
||||
elif event_type == EventType.MouseDragEvent:
|
||||
self.mouse_drag_point = event_data["point"]
|
||||
elif event_type == EventType.KeyPressEvent:
|
||||
self.pressed_keys.add(event_data["symbol"]) # Modifiers?
|
||||
elif event_type == EventType.KeyReleaseEvent:
|
||||
self.pressed_keys.difference_update({event_data["symbol"]}) # Modifiers?
|
||||
elif event_type == EventType.MousePressEvent:
|
||||
self.draggable_object_listeners = [
|
||||
listener
|
||||
for listener in self.event_listeners[EventType.MouseDragEvent]
|
||||
if listener.mobject.is_point_touching(self.mouse_point)
|
||||
]
|
||||
elif event_type == EventType.MouseReleaseEvent:
|
||||
self.draggable_object_listeners = []
|
||||
|
||||
propagate_event = None
|
||||
|
||||
if event_type == EventType.MouseDragEvent:
|
||||
for listener in self.draggable_object_listeners:
|
||||
assert isinstance(listener, EventListener)
|
||||
propagate_event = listener.callback(listener.mobject, event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return propagate_event
|
||||
|
||||
elif event_type.value.startswith("mouse"):
|
||||
for listener in self.event_listeners[event_type]:
|
||||
if listener.mobject.is_point_touching(self.mouse_point):
|
||||
propagate_event = listener.callback(listener.mobject, event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return propagate_event
|
||||
|
||||
elif event_type.value.startswith("key"):
|
||||
for listener in self.event_listeners[event_type]:
|
||||
propagate_event = listener.callback(listener.mobject, event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return propagate_event
|
||||
|
||||
return propagate_event
|
||||
|
||||
def get_listeners_count(self) -> int:
|
||||
return sum([len(value) for key, value in self.event_listeners.items()])
|
||||
|
||||
def get_mouse_point(self) -> np.ndarray:
|
||||
return self.mouse_point
|
||||
|
||||
def get_mouse_drag_point(self) -> np.ndarray:
|
||||
return self.mouse_drag_point
|
||||
|
||||
def is_key_pressed(self, symbol: int) -> bool:
|
||||
return symbol in self.pressed_keys
|
||||
|
||||
__iadd__ = add_listener
|
||||
__isub__ = remove_listener
|
||||
__call__ = dispatch
|
||||
__len__ = get_listeners_count
|
||||
34
manim/event_handler/event_listener.py
Normal file
34
manim/event_handler/event_listener.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from manim.event_handler.event_type import EventType
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
|
||||
|
||||
class EventListener:
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
event_type: EventType,
|
||||
event_callback: Callable[[Mobject, dict[str, str]], None],
|
||||
) -> None:
|
||||
self.mobject = mobject
|
||||
self.event_type = event_type
|
||||
self.callback = event_callback
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return_val = False
|
||||
if isinstance(other, EventListener):
|
||||
with contextlib.suppress(Exception):
|
||||
return_val = (
|
||||
self.callback == other.callback
|
||||
and self.mobject == other.mobject
|
||||
and self.event_type == other.event_type
|
||||
)
|
||||
return return_val
|
||||
13
manim/event_handler/event_type.py
Normal file
13
manim/event_handler/event_type.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
MouseMotionEvent = "mouse_motion_event"
|
||||
MousePressEvent = "mouse_press_event"
|
||||
MouseReleaseEvent = "mouse_release_event"
|
||||
MouseDragEvent = "mouse_drag_event"
|
||||
MouseScrollEvent = "mouse_scroll_event"
|
||||
KeyPressEvent = "key_press_event"
|
||||
KeyReleaseEvent = "key_release_event"
|
||||
14
manim/event_handler/window.py
Normal file
14
manim/event_handler/window.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class WindowProtocol(Protocol):
|
||||
@property
|
||||
def is_closing(self) -> bool: ...
|
||||
|
||||
def swap_buffers(self) -> object: ...
|
||||
|
||||
def close(self) -> object: ...
|
||||
|
||||
def clear(self) -> object: ...
|
||||
4
manim/file_writer/__init__.py
Normal file
4
manim/file_writer/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from .file_writer import FileWriter
|
||||
from .sections import *
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["SceneFileWriter"]
|
||||
__all__ = ["FileWriter"]
|
||||
|
||||
import json
|
||||
import shutil
|
||||
|
|
@ -10,7 +10,7 @@ import warnings
|
|||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from tempfile import NamedTemporaryFile, _TemporaryFileWrapper
|
||||
from tempfile import NamedTemporaryFile
|
||||
from threading import Thread
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
|
|
@ -31,11 +31,11 @@ with warnings.catch_warnings():
|
|||
from pydub import AudioSegment
|
||||
|
||||
from manim import __version__
|
||||
|
||||
from .. import config, logger
|
||||
from .._config.logger_utils import set_file_logger
|
||||
from ..constants import RendererType
|
||||
from ..utils.file_ops import (
|
||||
from manim._config import config, logger
|
||||
from manim._config.logger_utils import set_file_logger
|
||||
from manim.file_writer.protocols import FileWriterProtocol
|
||||
from manim.file_writer.sections import DefaultSectionType, Section
|
||||
from manim.utils.file_ops import (
|
||||
add_extension_if_not_present,
|
||||
add_version_before_extension,
|
||||
guarantee_existence,
|
||||
|
|
@ -44,16 +44,13 @@ from ..utils.file_ops import (
|
|||
modify_atime,
|
||||
write_to_movie,
|
||||
)
|
||||
from ..utils.sounds import get_full_sound_file_path
|
||||
from .section import DefaultSectionType, Section
|
||||
from manim.utils.sounds import get_full_sound_file_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from av.container.output import OutputContainer
|
||||
from av.stream import Stream
|
||||
|
||||
from manim.renderer.cairo_renderer import CairoRenderer
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
from manim.typing import PixelArray, StrPath
|
||||
from manim.typing import PixelArray, StrOrBytesPath, StrPath
|
||||
|
||||
|
||||
def to_av_frame_rate(fps: float) -> Fraction:
|
||||
|
|
@ -74,7 +71,9 @@ def to_av_frame_rate(fps: float) -> Fraction:
|
|||
|
||||
|
||||
def convert_audio(
|
||||
input_path: Path, output_path: Path | _TemporaryFileWrapper[bytes], codec_name: str
|
||||
input_path: StrOrBytesPath,
|
||||
output_path: StrOrBytesPath,
|
||||
codec_name: str,
|
||||
) -> None:
|
||||
with (
|
||||
av.open(input_path) as input_audio,
|
||||
|
|
@ -90,8 +89,8 @@ def convert_audio(
|
|||
output_audio.mux(packet)
|
||||
|
||||
|
||||
class SceneFileWriter:
|
||||
"""SceneFileWriter is the object that actually writes the animations
|
||||
class FileWriter(FileWriterProtocol):
|
||||
"""FileWriter is the object that actually writes the animations
|
||||
played, into video files, using FFMPEG.
|
||||
This is mostly for Manim's internal use. You will rarely, if ever,
|
||||
have to use the methods for this class, unless tinkering with the very
|
||||
|
|
@ -120,16 +119,11 @@ class SceneFileWriter:
|
|||
|
||||
force_output_as_scene_name = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
renderer: CairoRenderer | OpenGLRenderer,
|
||||
scene_name: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.renderer = renderer
|
||||
def __init__(self, scene_name: str) -> None:
|
||||
self.init_output_directories(scene_name)
|
||||
self.init_audio()
|
||||
self.frame_count = 0
|
||||
self.num_plays = 0
|
||||
self.partial_movie_files: list[str | None] = []
|
||||
self.subcaptions: list[srt.Subtitle] = []
|
||||
self.sections: list[Section] = []
|
||||
|
|
@ -139,6 +133,10 @@ class SceneFileWriter:
|
|||
name="autocreated", type_=DefaultSectionType.NORMAL, skip_animations=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def use_output_as_scene_name(cls) -> None:
|
||||
cls.force_output_as_scene_name = True
|
||||
|
||||
def init_output_directories(self, scene_name: str) -> None:
|
||||
"""Initialise output directories.
|
||||
|
||||
|
|
@ -154,7 +152,7 @@ class SceneFileWriter:
|
|||
|
||||
module_name = config.get_dir("input_file").stem if config["input_file"] else ""
|
||||
|
||||
if SceneFileWriter.force_output_as_scene_name:
|
||||
if self.force_output_as_scene_name:
|
||||
self.output_name = Path(scene_name)
|
||||
elif config["output_file"] and not config["write_all"]:
|
||||
self.output_name = config.get_dir("output_file")
|
||||
|
|
@ -247,11 +245,8 @@ class SceneFileWriter:
|
|||
)
|
||||
|
||||
def add_partial_movie_file(self, hash_animation: str | None) -> None:
|
||||
"""Adds a new partial movie file path to ``scene.partial_movie_files``
|
||||
and current section from a hash.
|
||||
|
||||
This method will compute the path from the hash. In addition to that it
|
||||
adds the new animation to the current section.
|
||||
"""Adds a new partial movie file path to `scene.partial_movie_files` and current section from a hash.
|
||||
This method will compute the path from the hash. In addition to that it adds the new animation to the current section.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -269,7 +264,7 @@ class SceneFileWriter:
|
|||
else:
|
||||
new_partial_movie_file = str(
|
||||
self.partial_movie_directory
|
||||
/ f"{hash_animation}{config['movie_file_extension']}"
|
||||
/ f"{hash_animation}{config.movie_file_extension}"
|
||||
)
|
||||
self.partial_movie_files.append(new_partial_movie_file)
|
||||
self.sections[-1].partial_movie_files.append(new_partial_movie_file)
|
||||
|
|
@ -301,8 +296,8 @@ class SceneFileWriter:
|
|||
:class:`str`
|
||||
The name of the directory.
|
||||
"""
|
||||
pixel_height = config["pixel_height"]
|
||||
frame_rate = config["frame_rate"]
|
||||
pixel_height = config.pixel_height
|
||||
frame_rate = config.frame_rate
|
||||
return f"{pixel_height}p{frame_rate}"
|
||||
|
||||
# Sound
|
||||
|
|
@ -338,7 +333,7 @@ class SceneFileWriter:
|
|||
self.includes_sound = True
|
||||
self.create_audio_segment()
|
||||
segment = self.audio_segment
|
||||
curr_end = segment.duration_seconds
|
||||
curr_end: float = segment.duration_seconds
|
||||
if time is None:
|
||||
time = curr_end
|
||||
if time < 0:
|
||||
|
|
@ -425,6 +420,7 @@ class SceneFileWriter:
|
|||
"""
|
||||
if write_to_movie() and allow_write:
|
||||
self.close_partial_movie_stream()
|
||||
self.num_plays += 1
|
||||
|
||||
def listen_and_write(self) -> None:
|
||||
"""For internal use only: blocks until new frame is available on the queue."""
|
||||
|
|
@ -450,9 +446,7 @@ class SceneFileWriter:
|
|||
for packet in self.video_stream.encode(av_frame):
|
||||
self.video_container.mux(packet)
|
||||
|
||||
def write_frame(
|
||||
self, frame_or_renderer: PixelArray | OpenGLRenderer, num_frames: int = 1
|
||||
) -> None:
|
||||
def write_frame(self, frame: PixelArray, num_frames: int = 1) -> None:
|
||||
"""Used internally by Manim to write a frame to the FFMPEG input buffer.
|
||||
|
||||
Parameters
|
||||
|
|
@ -463,34 +457,18 @@ class SceneFileWriter:
|
|||
The number of times to write frame.
|
||||
"""
|
||||
if write_to_movie():
|
||||
if isinstance(frame_or_renderer, np.ndarray):
|
||||
frame = frame_or_renderer
|
||||
else:
|
||||
frame = (
|
||||
frame_or_renderer.get_frame()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else frame_or_renderer
|
||||
)
|
||||
|
||||
msg = (num_frames, frame)
|
||||
self.queue.put(msg)
|
||||
|
||||
if is_png_format() and not config["dry_run"]:
|
||||
if isinstance(frame_or_renderer, np.ndarray):
|
||||
image = Image.fromarray(frame_or_renderer)
|
||||
else:
|
||||
image = (
|
||||
frame_or_renderer.get_image()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else Image.fromarray(frame_or_renderer)
|
||||
)
|
||||
if is_png_format() and not config.dry_run:
|
||||
image = Image.fromarray(frame)
|
||||
target_dir = self.image_file_path.parent / self.image_file_path.stem
|
||||
extension = self.image_file_path.suffix
|
||||
self.output_image(
|
||||
image,
|
||||
target_dir,
|
||||
extension,
|
||||
config["zero_pad"],
|
||||
config.zero_pad,
|
||||
)
|
||||
|
||||
def output_image(
|
||||
|
|
@ -502,20 +480,19 @@ class SceneFileWriter:
|
|||
image.save(f"{target_dir}{self.frame_count}{ext}")
|
||||
self.frame_count += 1
|
||||
|
||||
def save_image(self, image: Image.Image) -> None:
|
||||
"""This method saves the image passed to it in the default image directory.
|
||||
def save_image(self, image: PixelArray) -> None:
|
||||
"""Saves an image in the default image directory.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image
|
||||
The pixel array of the image to save.
|
||||
"""
|
||||
if config["dry_run"]:
|
||||
return
|
||||
if not config["output_file"]:
|
||||
self.image_file_path = add_version_before_extension(self.image_file_path)
|
||||
|
||||
image.save(self.image_file_path)
|
||||
image_processed = Image.fromarray(image)
|
||||
image_processed.save(self.image_file_path)
|
||||
self.print_file_ready_message(self.image_file_path)
|
||||
|
||||
def finish(self) -> None:
|
||||
|
|
@ -544,7 +521,7 @@ class SceneFileWriter:
|
|||
the video stream of a partial movie file.
|
||||
"""
|
||||
if file_path is None:
|
||||
file_path = self.partial_movie_files[self.renderer.num_plays]
|
||||
file_path = self.partial_movie_files[self.num_plays]
|
||||
self.partial_movie_file_path = file_path
|
||||
|
||||
fps = to_av_frame_rate(config.frame_rate)
|
||||
|
|
@ -599,7 +576,7 @@ class SceneFileWriter:
|
|||
self.video_container.close()
|
||||
|
||||
logger.info(
|
||||
f"Animation {self.renderer.num_plays} : Partial movie file written in %(path)s",
|
||||
f"Animation {self.num_plays} : Partial movie file written in %(path)s",
|
||||
{"path": f"'{self.partial_movie_file_path}'"},
|
||||
)
|
||||
|
||||
|
|
@ -710,7 +687,7 @@ class SceneFileWriter:
|
|||
output_container.mux(packet)
|
||||
|
||||
else:
|
||||
output_stream = output_container.add_stream_from_template(
|
||||
output_stream = output_container.add_stream(
|
||||
template=partial_movies_stream,
|
||||
)
|
||||
if config.transparent and config.movie_file_extension == ".webm":
|
||||
|
|
@ -802,12 +779,8 @@ class SceneFileWriter:
|
|||
output_container = av.open(
|
||||
str(temp_file_path), mode="w", options=av_options
|
||||
)
|
||||
output_video_stream = output_container.add_stream_from_template(
|
||||
template=video_stream
|
||||
)
|
||||
output_audio_stream = output_container.add_stream_from_template(
|
||||
template=audio_stream
|
||||
)
|
||||
output_video_stream = output_container.add_stream(template=video_stream)
|
||||
output_audio_stream = output_container.add_stream(template=audio_stream)
|
||||
|
||||
for packet in video_input.demux(video_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
|
|
@ -861,9 +834,9 @@ class SceneFileWriter:
|
|||
for file_name in self.partial_movie_directory.iterdir()
|
||||
if file_name != "partial_movie_file_list.txt"
|
||||
]
|
||||
if len(cached_partial_movies) > config["max_files_cached"]:
|
||||
if len(cached_partial_movies) > config.max_files_cached:
|
||||
number_files_to_delete = (
|
||||
len(cached_partial_movies) - config["max_files_cached"]
|
||||
len(cached_partial_movies) - config.max_files_cached
|
||||
)
|
||||
oldest_files_to_delete = sorted(
|
||||
cached_partial_movies,
|
||||
|
|
@ -872,7 +845,7 @@ class SceneFileWriter:
|
|||
for file_to_delete in oldest_files_to_delete:
|
||||
file_to_delete.unlink()
|
||||
logger.info(
|
||||
f"The partial movie directory is full (> {config['max_files_cached']} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)."
|
||||
f"The partial movie directory is full (> {config.max_files_cached} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)."
|
||||
" You can change this behaviour by changing max_files_cached in config.",
|
||||
)
|
||||
|
||||
|
|
@ -900,5 +873,5 @@ class SceneFileWriter:
|
|||
|
||||
def print_file_ready_message(self, file_path: StrPath) -> None:
|
||||
"""Prints the "File Ready" message to STDOUT."""
|
||||
config["output_file"] = file_path
|
||||
logger.info("\nFile ready at %(file_path)s\n", {"file_path": f"'{file_path}'"})
|
||||
config.output_file = str(file_path)
|
||||
logger.info(f"\nFile ready at {str(file_path)!r}\n")
|
||||
33
manim/file_writer/protocols.py
Normal file
33
manim/file_writer/protocols.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from manim.typing import PixelArray
|
||||
|
||||
|
||||
class FileWriterProtocol(Protocol):
|
||||
"""Protocol for a file writer.
|
||||
|
||||
This is mainly useful for testing purposes, to create
|
||||
a mock file writer. However, it can be used in plugins.
|
||||
"""
|
||||
|
||||
num_plays: int
|
||||
|
||||
def __init__(self, scene_name: str) -> None: ...
|
||||
|
||||
def begin_animation(self, allow_write: bool = False) -> object: ...
|
||||
|
||||
def end_animation(self, allow_write: bool = False) -> object: ...
|
||||
|
||||
def is_already_cached(self, hash_invocation: str) -> bool: ...
|
||||
|
||||
def add_partial_movie_file(self, hash_animation: str) -> object: ...
|
||||
|
||||
def write_frame(self, frame: PixelArray) -> object: ...
|
||||
|
||||
def next_section(self, name: str, type_: str, skip_animations: bool) -> object: ...
|
||||
|
||||
def finish(self) -> None: ...
|
||||
|
||||
def save_image(self, image: PixelArray) -> object: ...
|
||||
482
manim/manager.py
Normal file
482
manim/manager.py
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
from __future__ import annotations
|
||||
|
||||
__all__ = ["Manager"]
|
||||
|
||||
import contextlib
|
||||
import platform
|
||||
import time
|
||||
import warnings
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim import config, logger
|
||||
from manim.animation.animation import Wait
|
||||
from manim.event_handler.window import WindowProtocol
|
||||
from manim.file_writer import FileWriter
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
from manim.renderer.opengl_renderer_window import Window
|
||||
from manim.scene.scene import Scene, SceneState
|
||||
from manim.utils.exceptions import EndSceneEarlyException
|
||||
from manim.utils.hashing import get_hash_from_play_call
|
||||
from manim.utils.progressbar import (
|
||||
ExperimentalProgressBarWarning,
|
||||
NullProgressBar,
|
||||
ProgressBar,
|
||||
ProgressBarProtocol,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
from typing import Any, Self
|
||||
|
||||
import numpy.typing as npt
|
||||
|
||||
from manim.animation.protocol import AnimationProtocol
|
||||
from manim.file_writer.protocols import FileWriterProtocol
|
||||
from manim.renderer.renderer import RendererProtocol
|
||||
|
||||
SceneT = TypeVar("SceneT", bound=Scene)
|
||||
|
||||
|
||||
class Manager(Generic[SceneT]):
|
||||
"""
|
||||
The Brain of Manim
|
||||
|
||||
.. admonition:: Warning for Developers
|
||||
|
||||
Only methods of this class that are not prefixed with an
|
||||
underscore (``_``) are stable. If you override any of the
|
||||
``_`` methods, consider pinning your version of Manim.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Manimation(Scene):
|
||||
def construct(self):
|
||||
self.play(FadeIn(Circle()))
|
||||
|
||||
|
||||
# make sure to use it as a context manager
|
||||
# to ensure proper resource cleanup
|
||||
with Manager(Manimation) as manager:
|
||||
manager.render()
|
||||
"""
|
||||
|
||||
def __init__(self, scene_cls: type[SceneT]) -> None:
|
||||
config._warn_about_config_options()
|
||||
self.scene: SceneT = scene_cls(manager=self)
|
||||
|
||||
if not isinstance(self.scene, Scene):
|
||||
raise ValueError(f"{self.scene!r} is not an instance of Scene")
|
||||
|
||||
self.time = 0.0
|
||||
|
||||
# Initialize window, if applicable
|
||||
self.window = self.create_window()
|
||||
|
||||
# this must be done AFTER instantiating a window
|
||||
self.renderer = self.create_renderer()
|
||||
|
||||
self.file_writer = self.create_file_writer()
|
||||
self._write_files = config.write_to_movie
|
||||
|
||||
# internal state
|
||||
self._skipping = config.save_last_frame
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.scene!r}) at time {self.time:.2f}s"
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.release()
|
||||
|
||||
# keep these as instance methods so subclasses
|
||||
# have access to everything
|
||||
def create_renderer(self) -> RendererProtocol:
|
||||
"""Create and return a renderer instance.
|
||||
|
||||
This can be overridden in subclasses (plugins), if more processing
|
||||
is needed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
An instance of a renderer
|
||||
"""
|
||||
renderer = OpenGLRenderer(
|
||||
background_color=config.background_color,
|
||||
background_opacity=config.background_opacity,
|
||||
)
|
||||
if config.preview:
|
||||
renderer.use_window()
|
||||
return renderer
|
||||
|
||||
def create_window(self) -> WindowProtocol | None:
|
||||
"""Create and return a window instance.
|
||||
|
||||
This can be overridden in subclasses (plugins), if more
|
||||
processing is needed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A window if previewing, else None
|
||||
"""
|
||||
return Window() if config.preview else None
|
||||
|
||||
def create_file_writer(self) -> FileWriterProtocol:
|
||||
"""Create and return a file writer instance.
|
||||
|
||||
This can be overridden in subclasses (plugins), if more
|
||||
processing is needed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A file writer satisfying :class:`.FileWriterProtocol`
|
||||
"""
|
||||
return FileWriter(scene_name=self.scene.get_default_scene_name())
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Set up processes and manager"""
|
||||
self.scene.setup()
|
||||
|
||||
# these are used for making sure it feels like the correct
|
||||
# amount of time has passed in the window instead of rendering
|
||||
# at full speed
|
||||
# See the docstring of :meth:`_wait_for_animation_time`
|
||||
self.virtual_animation_start_time = 0.0
|
||||
self.real_animation_start_time = time.perf_counter()
|
||||
|
||||
def render(self) -> None:
|
||||
"""
|
||||
Entry point to running a Manim class
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyScene(Scene):
|
||||
def construct(self):
|
||||
self.play(Create(Circle()))
|
||||
|
||||
|
||||
with tempconfig({"preview": True}), Manager(MyScene) as manager:
|
||||
manager.render()
|
||||
"""
|
||||
self._render_first_pass()
|
||||
self._render_second_pass()
|
||||
|
||||
def _render_first_pass(self) -> None:
|
||||
"""
|
||||
Temporarily use the normal single pass
|
||||
rendering system
|
||||
"""
|
||||
self.setup()
|
||||
|
||||
with contextlib.suppress(EndSceneEarlyException):
|
||||
self.construct()
|
||||
self.post_construct()
|
||||
self._interact()
|
||||
|
||||
self.tear_down()
|
||||
|
||||
def construct(self) -> None:
|
||||
if not self.scene.groups_api:
|
||||
self.scene.construct()
|
||||
return
|
||||
|
||||
for group in self.scene.find_groups():
|
||||
if not config.groups or group.name in config.groups:
|
||||
group()
|
||||
elif group.name not in config.groups:
|
||||
with self.no_render():
|
||||
group()
|
||||
|
||||
def _render_second_pass(self) -> None:
|
||||
"""
|
||||
In the future, this method could be used
|
||||
for two pass rendering
|
||||
"""
|
||||
...
|
||||
|
||||
def release(self) -> None:
|
||||
self.renderer.release()
|
||||
|
||||
def post_construct(self) -> None:
|
||||
"""Run post-construct hooks, and clean up the file writer."""
|
||||
should_write_image = config.save_last_frame or (
|
||||
config.write_to_movie and not self.file_writer.num_plays
|
||||
)
|
||||
if self.file_writer.num_plays:
|
||||
self.file_writer.finish()
|
||||
# otherwise no animations were played
|
||||
if should_write_image:
|
||||
self.render_state(write_frame=False)
|
||||
# FIXME: for some reason the OpenGLRenderer does not give out the
|
||||
# correct frame values here
|
||||
frame = self.renderer.get_pixels()
|
||||
self.file_writer.save_image(frame)
|
||||
|
||||
self._write_files = False
|
||||
|
||||
def tear_down(self) -> None:
|
||||
"""Tear down the scene and the window."""
|
||||
self.scene.tear_down()
|
||||
|
||||
if self.window is not None:
|
||||
self.window.close()
|
||||
self.window = None
|
||||
|
||||
def _interact(self) -> None:
|
||||
"""Live interaction with the Window"""
|
||||
if self.window is None:
|
||||
return
|
||||
logger.info(
|
||||
"\nTips: Using the keys `d`, `f`, or `z` "
|
||||
"you can interact with the scene. "
|
||||
"Press `command + q` or `esc` to quit"
|
||||
)
|
||||
last_time = time.perf_counter()
|
||||
while not self.window.is_closing:
|
||||
current_time = time.perf_counter()
|
||||
dt = current_time - last_time
|
||||
last_time = current_time
|
||||
self._update_frame(dt)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def no_render(self) -> Iterator[None]:
|
||||
"""Context manager to temporarily disable rendering.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with manager.no_render():
|
||||
manager._play(FadeIn(Circle()))
|
||||
"""
|
||||
self._skipping = True
|
||||
yield
|
||||
self._skipping = False
|
||||
|
||||
# ----------------------------------#
|
||||
# Animation Pipeline #
|
||||
# ----------------------------------#
|
||||
|
||||
def _update_frame(
|
||||
self, dt: float, *, write_frame: bool | None = None, run_updaters: bool = True
|
||||
) -> None:
|
||||
"""Update the current frame by ``dt``
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dt : the time in between frames
|
||||
write_frame : Whether to write the result to the output stream (videos ONLY).
|
||||
Default value checks :attr:`_write_files` to see if it should be written.
|
||||
"""
|
||||
self.time += dt
|
||||
if run_updaters:
|
||||
self.scene._update_mobjects(dt)
|
||||
self.scene.time = self.time
|
||||
|
||||
if self.window is not None:
|
||||
if not self._skipping:
|
||||
self.window.clear()
|
||||
|
||||
# if it's closing, then any subsequent methods will
|
||||
# raise an error because the internal C window pointer is nullptr.
|
||||
if self.window.is_closing:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
if not self._skipping:
|
||||
self.render_state(write_frame=write_frame)
|
||||
self._wait_for_animation_time()
|
||||
|
||||
def _wait_for_animation_time(self) -> None:
|
||||
"""Wait for the real time to catch up to the "virtual" animation time.
|
||||
|
||||
Animations can render faster than real time, so we have to
|
||||
slow the window down for the correct amount of time, such
|
||||
as during a wait animation.
|
||||
"""
|
||||
if self.window is None:
|
||||
return
|
||||
|
||||
self.window.swap_buffers()
|
||||
|
||||
if self._skipping:
|
||||
return
|
||||
|
||||
vt = self.time - self.virtual_animation_start_time
|
||||
rt = time.perf_counter() - self.real_animation_start_time
|
||||
# we can't sleep because we still need to poll for events,
|
||||
# e.g. hitting Escape or close
|
||||
while rt < vt:
|
||||
if self.window.is_closing:
|
||||
raise EndSceneEarlyException()
|
||||
# make sure to poll for events
|
||||
self.window.swap_buffers()
|
||||
rt = time.perf_counter() - self.real_animation_start_time
|
||||
|
||||
def _play(self, *animations: AnimationProtocol) -> None:
|
||||
"""Play a bunch of animations"""
|
||||
self.scene.pre_play()
|
||||
|
||||
if self.window is not None:
|
||||
self.real_animation_start_time = time.perf_counter()
|
||||
self.virtual_animation_start_time = self.time
|
||||
|
||||
self._write_hashed_movie_file(animations)
|
||||
|
||||
self.scene.begin_animations(animations)
|
||||
self._progress_through_animations(animations)
|
||||
self.scene.finish_animations(animations)
|
||||
|
||||
self.scene.post_play()
|
||||
|
||||
self.file_writer.end_animation(allow_write=self._write_files)
|
||||
|
||||
def _write_hashed_movie_file(self, animations: Sequence[AnimationProtocol]) -> None:
|
||||
"""Compute the hash of a self.play call, and write it to a file
|
||||
|
||||
Essentially, a series of methods that need to be called to successfully
|
||||
render a frame.
|
||||
"""
|
||||
if not config.write_to_movie or self._skipping:
|
||||
return
|
||||
|
||||
if config.disable_caching:
|
||||
if not config.disable_caching_warning:
|
||||
logger.info("Caching disabled...")
|
||||
hash_current_play = f"uncached_{self.file_writer.num_plays:05}"
|
||||
else:
|
||||
hash_current_play = get_hash_from_play_call(
|
||||
self.scene,
|
||||
self.scene.camera,
|
||||
animations,
|
||||
self.scene.mobjects,
|
||||
)
|
||||
if self.file_writer.is_already_cached(hash_current_play):
|
||||
logger.info(
|
||||
f"Animation {self.file_writer.num_plays} : Using cached data (hash : {hash_current_play})"
|
||||
)
|
||||
# TODO: think about how to skip
|
||||
raise NotImplementedError(
|
||||
"Skipping cached animations is not implemented yet"
|
||||
)
|
||||
|
||||
self.file_writer.add_partial_movie_file(hash_current_play)
|
||||
self.file_writer.begin_animation(allow_write=self._write_files)
|
||||
|
||||
def _create_progressbar(
|
||||
self, total: float, description: str, **kwargs: Any
|
||||
) -> contextlib.AbstractContextManager[ProgressBarProtocol]:
|
||||
"""Create a progressbar"""
|
||||
if not config.progress_bar:
|
||||
return contextlib.nullcontext(NullProgressBar())
|
||||
|
||||
with warnings.catch_warnings():
|
||||
if config.verbosity != "DEBUG":
|
||||
# Note: update when rich/notebook tqdm is no longer experimental
|
||||
warnings.simplefilter("ignore", category=ExperimentalProgressBarWarning)
|
||||
|
||||
return ProgressBar(
|
||||
total=total,
|
||||
unit="frames",
|
||||
desc=description % {"num": self.file_writer.num_plays},
|
||||
ascii=True if platform.system() == "Windows" else None,
|
||||
leave=config.progress_bar == "leave",
|
||||
disable=config.progress_bar == "none",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _progress_through_animations(
|
||||
self, animations: Sequence[AnimationProtocol]
|
||||
) -> None:
|
||||
last_t = 0.0
|
||||
run_time = _calc_runtime(animations)
|
||||
progression = _calc_time_progression(run_time)
|
||||
with self._create_progressbar(
|
||||
progression.shape[0],
|
||||
f"Animation %(num)d: {animations[0]}{', etc.' if len(animations) > 1 else ''}",
|
||||
) as progress:
|
||||
if self._skipping:
|
||||
self.scene._update_animations(animations, run_time, run_time)
|
||||
self._update_frame(run_time, run_updaters=False)
|
||||
return
|
||||
for t in progression:
|
||||
dt, last_t = t - last_t, t
|
||||
self.scene._update_animations(animations, t, dt)
|
||||
run_updaters = not self.scene.is_current_animation_frozen_frame(
|
||||
animations
|
||||
)
|
||||
self._update_frame(dt, run_updaters=run_updaters)
|
||||
for anim in animations:
|
||||
if (
|
||||
isinstance(anim, Wait)
|
||||
and anim.stop_condition is not None
|
||||
and anim.stop_condition()
|
||||
):
|
||||
return
|
||||
progress.update(1)
|
||||
|
||||
# -------------------------#
|
||||
# Rendering #
|
||||
# -------------------------#
|
||||
|
||||
def render_state(self, write_frame: bool | None = None) -> None:
|
||||
"""Render the current state of the scene.
|
||||
|
||||
Any extra kwargs are passed to :meth:`_render_frame`.
|
||||
"""
|
||||
state = self.scene.get_state()
|
||||
self._render_frame(state, write_frame=write_frame)
|
||||
|
||||
def _render_frame(
|
||||
self, state: SceneState, *, write_frame: bool | None = None
|
||||
) -> None:
|
||||
"""Renders a frame based on a state, and writes it to the file writers stream.
|
||||
|
||||
This is used for writing a single frame. Any extra kwargs are passed to :meth:`write_frame`.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method will not work if :meth:`.FileWriter.begin_animation` and
|
||||
:meth:`.FileWriter.add_partial_movie_file` have not been called. Do NOT
|
||||
use this to write a single frame!
|
||||
"""
|
||||
self.renderer.render(state)
|
||||
|
||||
should_write = write_frame if write_frame is not None else self._write_files
|
||||
if should_write:
|
||||
self.write_frame()
|
||||
|
||||
def write_frame(self) -> None:
|
||||
"""Take a frame from the renderer and write it in the file writer."""
|
||||
frame = self.renderer.get_pixels()
|
||||
self.file_writer.write_frame(frame)
|
||||
|
||||
|
||||
def _calc_time_progression(run_time: float) -> npt.NDArray[np.float64]:
|
||||
"""Compute the time values at which to evaluate the animation"""
|
||||
return np.arange(0, run_time, 1 / config.frame_rate)
|
||||
|
||||
|
||||
def _calc_runtime(animations: Iterable[AnimationProtocol]) -> float:
|
||||
"""Calculate the runtime of an iterable of animations.
|
||||
|
||||
.. warning::
|
||||
|
||||
If animations is a generator, this will consume the generator.
|
||||
"""
|
||||
return max(animation.get_run_time() for animation in animations)
|
||||
|
|
@ -50,8 +50,8 @@ from typing import TYPE_CHECKING, Any, Self, cast
|
|||
import numpy as np
|
||||
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.utils.color import BLACK, BLUE, RED, WHITE, ParsableManimColor
|
||||
from manim.utils.iterables import adjacent_pairs
|
||||
from manim.utils.space_ops import (
|
||||
|
|
@ -68,7 +68,7 @@ if TYPE_CHECKING:
|
|||
|
||||
import manim.mobject.geometry.tips as tips
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.text.tex_mobject import SingleStringMathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.typing import (
|
||||
|
|
@ -79,7 +79,7 @@ if TYPE_CHECKING:
|
|||
)
|
||||
|
||||
|
||||
class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
||||
class TipableVMobject(VMobject):
|
||||
"""Meant for shared functionality between Arc and Line.
|
||||
Functionality can be classified broadly into these groups:
|
||||
|
||||
|
|
@ -1093,7 +1093,7 @@ class Annulus(Circle):
|
|||
self.generate_points()
|
||||
|
||||
|
||||
class CubicBezier(VMobject, metaclass=ConvertToOpenGL):
|
||||
class CubicBezier(VMobject):
|
||||
"""A cubic Bézier curve.
|
||||
|
||||
Example
|
||||
|
|
@ -1128,7 +1128,7 @@ class CubicBezier(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.add_cubic_bezier_curve(start_anchor, start_handle, end_handle, end_anchor)
|
||||
|
||||
|
||||
class ArcPolygon(VMobject, metaclass=ConvertToOpenGL):
|
||||
class ArcPolygon(VMobject):
|
||||
"""A generalized polygon allowing for points to be connected with arcs.
|
||||
|
||||
This version tries to stick close to the way :class:`Polygon` is used. Points
|
||||
|
|
@ -1249,7 +1249,7 @@ class ArcPolygon(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.arcs = arcs
|
||||
|
||||
|
||||
class ArcPolygonFromArcs(VMobject, metaclass=ConvertToOpenGL):
|
||||
class ArcPolygonFromArcs(VMobject):
|
||||
"""A generalized polygon allowing for points to be connected with arcs.
|
||||
|
||||
This version takes in pre-defined arcs to generate the arcpolygon and introduces
|
||||
|
|
|
|||
|
|
@ -8,19 +8,16 @@ import numpy as np
|
|||
from pathops import Path as SkiaPath
|
||||
from pathops import PathVerb, difference, intersection, union, xor
|
||||
|
||||
from manim import config
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point2DLike_Array, Point3D_Array, Point3DLike_Array
|
||||
|
||||
from ...constants import RendererType
|
||||
|
||||
__all__ = ["Union", "Intersection", "Difference", "Exclusion"]
|
||||
|
||||
|
||||
class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
|
||||
class _BooleanOps(VMobject):
|
||||
"""This class contains some helper functions which
|
||||
helps to convert to and from skia objects and manim
|
||||
objects (:class:`~.VMobject`).
|
||||
|
|
@ -84,29 +81,15 @@ class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
|
|||
if len(points) == 0: # what? No points so return empty path
|
||||
return path
|
||||
|
||||
# In OpenGL it's quadratic beizer curves while on Cairo it's cubic...
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
subpaths = vmobject.get_subpaths_from_points(points)
|
||||
for subpath in subpaths:
|
||||
quads = vmobject.get_bezier_tuples_from_points(subpath)
|
||||
start = subpath[0]
|
||||
path.moveTo(*start[:2])
|
||||
for _p0, p1, p2 in quads:
|
||||
path.quadTo(*p1[:2], *p2[:2])
|
||||
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
subpaths = vmobject.gen_subpaths_from_points_2d(points) # type: ignore[assignment]
|
||||
for subpath in subpaths:
|
||||
quads = vmobject.gen_cubic_bezier_tuples_from_points(subpath)
|
||||
start = subpath[0]
|
||||
path.moveTo(*start[:2])
|
||||
for _p0, p1, p2, p3 in quads:
|
||||
path.cubicTo(*p1[:2], *p2[:2], *p3[:2])
|
||||
|
||||
if vmobject.consider_points_equals_2d(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
|
||||
subpaths = vmobject.get_subpaths_from_points(points)
|
||||
for subpath in subpaths:
|
||||
quads = vmobject.get_bezier_tuples_from_points(subpath)
|
||||
start = subpath[0]
|
||||
path.moveTo(*start[:2])
|
||||
for _p0, p1, p2 in quads:
|
||||
path.quadTo(*p1[:2], *p2[:2])
|
||||
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
return path
|
||||
|
||||
def _convert_skia_path_to_vmobject(self, path: SkiaPath) -> VMobject:
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ from manim.mobject.geometry.shape_matchers import (
|
|||
BackgroundRectangle,
|
||||
SurroundingRectangle,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.color import WHITE
|
||||
from manim.utils.polylabel import polylabel
|
||||
|
||||
|
|
|
|||
|
|
@ -18,14 +18,15 @@ from typing import TYPE_CHECKING, Any, Literal
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Arc, ArcBetweenPoints, Dot, TipableVMobject
|
||||
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.types.vectorized_mobject import DashedVMobject, VGroup, VMobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLDashedVMobject as DashedVMobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.utils.color import WHITE
|
||||
from manim.utils.space_ops import angle_of_vector, line_intersection, normalize
|
||||
|
||||
|
|
@ -187,7 +188,7 @@ class Line(TipableVMobject):
|
|||
direction
|
||||
The direction.
|
||||
"""
|
||||
if isinstance(mob_or_point, (Mobject, OpenGLMobject)):
|
||||
if isinstance(mob_or_point, Mobject):
|
||||
mob = mob_or_point
|
||||
if direction is None:
|
||||
return mob.get_center()
|
||||
|
|
@ -458,7 +459,7 @@ class TangentLine(Line):
|
|||
self.scale(self.length / self.get_length())
|
||||
|
||||
|
||||
class Elbow(VMobject, metaclass=ConvertToOpenGL):
|
||||
class Elbow(VMobject):
|
||||
"""Two lines that create a right angle about each other: L-shape.
|
||||
|
||||
Parameters
|
||||
|
|
@ -599,7 +600,7 @@ class Arrow(Line):
|
|||
super().__init__(*args, buff=buff, stroke_width=stroke_width, **kwargs) # type: ignore[misc]
|
||||
# TODO, should this be affected when
|
||||
# Arrow.set_stroke is called?
|
||||
self.initial_stroke_width = self.stroke_width
|
||||
self.initial_stroke_width = stroke_width
|
||||
self.add_tip(tip_shape=tip_shape)
|
||||
self._set_stroke_width_from_length()
|
||||
|
||||
|
|
@ -688,20 +689,13 @@ class Arrow(Line):
|
|||
def _set_stroke_width_from_length(self) -> Self:
|
||||
"""Sets stroke width based on length."""
|
||||
max_ratio = self.max_stroke_width_to_length_ratio
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
# Mypy does not recognize that the self object in this case
|
||||
# is a OpenGLVMobject and that the set_stroke method is
|
||||
# defined here:
|
||||
# mobject/opengl/opengl_vectorized_mobject.py#L248
|
||||
self.set_stroke( # type: ignore[call-arg]
|
||||
width=min(self.initial_stroke_width, max_ratio * self.get_length()),
|
||||
recurse=False,
|
||||
)
|
||||
else:
|
||||
self.set_stroke(
|
||||
width=min(self.initial_stroke_width, max_ratio * self.get_length()),
|
||||
family=False,
|
||||
)
|
||||
self.set_stroke(
|
||||
width=min(
|
||||
self.initial_stroke_width,
|
||||
[max_ratio * self.get_length()] * len(self.initial_stroke_width),
|
||||
),
|
||||
recurse=False,
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
|
|
@ -863,7 +857,7 @@ class DoubleArrow(Arrow):
|
|||
self.add_tip(at_start=True, tip_shape=tip_shape_start)
|
||||
|
||||
|
||||
class Angle(VMobject, metaclass=ConvertToOpenGL):
|
||||
class Angle(VMobject):
|
||||
"""A circular arc or elbow-type mobject representing an angle of two lines.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -24,8 +24,12 @@ import numpy as np
|
|||
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import ArcBetweenPoints
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.utils.color import BLUE, WHITE, ParsableManimColor
|
||||
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
|
||||
from manim.utils.qhull import QuickHull
|
||||
|
|
@ -45,7 +49,7 @@ if TYPE_CHECKING:
|
|||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
|
||||
class Polygram(VMobject, metaclass=ConvertToOpenGL):
|
||||
class Polygram(VMobject):
|
||||
"""A generalized :class:`Polygon`, allowing for disconnected sets of edges.
|
||||
|
||||
Parameters
|
||||
|
|
@ -743,7 +747,7 @@ class RoundedRectangle(Rectangle):
|
|||
self.round_corners(self.corner_radius)
|
||||
|
||||
|
||||
class Cutout(VMobject, metaclass=ConvertToOpenGL):
|
||||
class Cutout(VMobject):
|
||||
"""A shape with smaller cutouts.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -17,9 +17,8 @@ from manim.constants import (
|
|||
)
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import RoundedRectangle
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.utils.color import BLACK, PURE_YELLOW, RED, ParsableManimColor
|
||||
|
||||
|
||||
|
|
@ -55,12 +54,10 @@ class SurroundingRectangle(RoundedRectangle):
|
|||
corner_radius: float = 0.0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
from manim.mobject.mobject import Group
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup as Group
|
||||
|
||||
if not all(isinstance(mob, (Mobject, OpenGLMobject)) for mob in mobjects):
|
||||
raise TypeError(
|
||||
"Expected all inputs for parameter mobjects to be a Mobjects"
|
||||
)
|
||||
if not all(isinstance(mob, Mobject) for mob in mobjects):
|
||||
raise TypeError("Expected all inputs for parameter mobjects to be Mobjects")
|
||||
|
||||
if isinstance(buff, tuple):
|
||||
buff_x = buff[0]
|
||||
|
|
@ -127,7 +124,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
buff=buff,
|
||||
**kwargs,
|
||||
)
|
||||
self.original_fill_opacity: float = self.fill_opacity
|
||||
self.original_fill_opacity: float = self.get_fill_opacity()
|
||||
|
||||
def pointwise_become_partial(self, mobject: Mobject, a: Any, b: float) -> Self:
|
||||
self.set_fill(opacity=b * self.original_fill_opacity)
|
||||
|
|
|
|||
|
|
@ -20,15 +20,14 @@ import numpy as np
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square, Triangle
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.utils.space_ops import angle_of_vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point3D, Vector3D
|
||||
|
||||
|
||||
class ArrowTip(VMobject, metaclass=ConvertToOpenGL):
|
||||
class ArrowTip(VMobject):
|
||||
r"""Base class for arrow tips.
|
||||
|
||||
.. seealso::
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import numpy as np
|
|||
if TYPE_CHECKING:
|
||||
from typing import TypeAlias
|
||||
|
||||
from manim.scene.scene import Scene
|
||||
from manim.typing import Point3D, Point3DLike
|
||||
|
||||
NxGraph: TypeAlias = nx.classes.graph.Graph | nx.classes.digraph.DiGraph
|
||||
|
|
@ -27,11 +26,12 @@ from manim.animation.composition import AnimationGroup
|
|||
from manim.animation.creation import Create, Uncreate
|
||||
from manim.mobject.geometry.arc import Dot, LabeledDot
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.mobject import Mobject, override_animate
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
override_animate,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.utils.color import BLACK
|
||||
|
||||
|
||||
|
|
@ -476,7 +476,7 @@ def _determine_graph_layout(
|
|||
) from e
|
||||
|
||||
|
||||
class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
||||
class GenericGraph(VMobject):
|
||||
"""Abstract base class for graphs (that is, a collection of vertices
|
||||
connected with edges).
|
||||
|
||||
|
|
@ -569,10 +569,10 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
layout: LayoutName | dict[Hashable, Point3DLike] | LayoutFunction = "spring",
|
||||
layout_scale: float | tuple[float, float, float] = 2,
|
||||
layout_config: dict | None = None,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobjects: dict | None = None,
|
||||
edge_type: type[Mobject] = Line,
|
||||
edge_type: type[VMobject] = Line,
|
||||
partitions: Sequence[Sequence[Hashable]] | None = None,
|
||||
root_vertex: Hashable | None = None,
|
||||
edge_config: dict | None = None,
|
||||
|
|
@ -664,12 +664,12 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
raise NotImplementedError("To be implemented in concrete subclasses")
|
||||
|
||||
def _populate_edge_dict(
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[VMobject]
|
||||
):
|
||||
"""Helper method for populating the edges of the graph."""
|
||||
raise NotImplementedError("To be implemented in concrete subclasses")
|
||||
|
||||
def __getitem__(self: Graph, v: Hashable) -> Mobject:
|
||||
def __getitem__(self: Graph, v: Hashable) -> VMobject:
|
||||
return self.vertices[v]
|
||||
|
||||
def _create_vertex(
|
||||
|
|
@ -678,10 +678,10 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
position: Point3DLike | None = None,
|
||||
label: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobject: dict | None = None,
|
||||
) -> tuple[Hashable, Point3D, dict, Mobject]:
|
||||
) -> tuple[Hashable, Point3D, dict, VMobject]:
|
||||
np_position: Point3D = (
|
||||
self.get_center() if position is None else np.asarray(position)
|
||||
)
|
||||
|
|
@ -698,7 +698,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
label = MathTex(vertex, color=label_fill_color)
|
||||
elif vertex in self._labels:
|
||||
label = self._labels[vertex]
|
||||
elif not isinstance(label, (Mobject, OpenGLMobject)):
|
||||
elif not isinstance(label, VMobject):
|
||||
label = None
|
||||
|
||||
base_vertex_config = copy(self.default_vertex_config)
|
||||
|
|
@ -722,8 +722,8 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
vertex: Hashable,
|
||||
position: Point3DLike,
|
||||
vertex_config: dict,
|
||||
vertex_mobject: Mobject,
|
||||
) -> Mobject:
|
||||
vertex_mobject: VMobject,
|
||||
) -> VMobject:
|
||||
if vertex in self.vertices:
|
||||
raise ValueError(
|
||||
f"Vertex identifier '{vertex}' is already used for a vertex in this graph.",
|
||||
|
|
@ -749,10 +749,10 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
position: Point3DLike | None = None,
|
||||
label: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobject: dict | None = None,
|
||||
) -> Mobject:
|
||||
) -> VMobject:
|
||||
"""Add a vertex to the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -767,7 +767,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
Controls whether or not the vertex is labeled. If ``False`` (the default),
|
||||
the vertex is not labeled; if ``True`` it is labeled using its
|
||||
names (as specified in ``vertex``) via :class:`~.MathTex`. Alternatively,
|
||||
any :class:`~.Mobject` can be passed to be used as the label.
|
||||
any :class:`~.VMobject` can be passed to be used as the label.
|
||||
label_fill_color
|
||||
Sets the fill color of the default labels generated when ``labels``
|
||||
is set to ``True``. Has no effect for other values of ``label``.
|
||||
|
|
@ -798,10 +798,10 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
positions: dict | None = None,
|
||||
labels: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobjects: dict | None = None,
|
||||
) -> Iterable[tuple[Hashable, Point3D, dict, Mobject]]:
|
||||
) -> Iterable[tuple[Hashable, Point3D, dict, VMobject]]:
|
||||
if positions is None:
|
||||
positions = {}
|
||||
if vertex_mobjects is None:
|
||||
|
|
@ -852,10 +852,10 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
positions: dict | None = None,
|
||||
labels: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobjects: dict | None = None,
|
||||
):
|
||||
) -> VGroup:
|
||||
"""Add a list of vertices to the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -870,7 +870,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
Controls whether or not the vertex is labeled. If ``False`` (the default),
|
||||
the vertex is not labeled; if ``True`` it is labeled using its
|
||||
names (as specified in ``vertex``) via :class:`~.MathTex`. Alternatively,
|
||||
any :class:`~.Mobject` can be passed to be used as the label.
|
||||
any :class:`~.VMobject` can be passed to be used as the label.
|
||||
label_fill_color
|
||||
Sets the fill color of the default labels generated when ``labels``
|
||||
is set to ``True``. Has no effect for other values of ``labels``.
|
||||
|
|
@ -884,7 +884,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
values are mobjects that should be used as vertices. Overrides
|
||||
all other vertex customization options.
|
||||
"""
|
||||
return [
|
||||
return VGroup(
|
||||
self._add_created_vertex(*v)
|
||||
for v in self._create_vertices(
|
||||
*vertices,
|
||||
|
|
@ -895,29 +895,30 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
vertex_config=vertex_config,
|
||||
vertex_mobjects=vertex_mobjects,
|
||||
)
|
||||
]
|
||||
|
||||
@override_animate(add_vertices)
|
||||
def _add_vertices_animation(self, *args, anim_args=None, **kwargs):
|
||||
if anim_args is None:
|
||||
anim_args = {}
|
||||
|
||||
animation = anim_args.pop("animation", Create)
|
||||
|
||||
vertex_mobjects = self._create_vertices(*args, **kwargs)
|
||||
|
||||
def on_finish(scene: Scene):
|
||||
for v in vertex_mobjects:
|
||||
scene.remove(v[-1])
|
||||
self._add_created_vertex(*v)
|
||||
|
||||
return AnimationGroup(
|
||||
*(animation(v[-1], **anim_args) for v in vertex_mobjects),
|
||||
group=self,
|
||||
_on_finish=on_finish,
|
||||
)
|
||||
|
||||
def _remove_vertex(self, vertex):
|
||||
@override_animate(add_vertices)
|
||||
def _add_vertices_animation(
|
||||
self,
|
||||
*vertices: Hashable,
|
||||
anim_args: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AnimationGroup:
|
||||
# Use introducer=False to prevent re-adding the vertices when animating them
|
||||
base_anim_args = {"animation": Create, "introducer": False}
|
||||
if anim_args is not None:
|
||||
base_anim_args.update(anim_args)
|
||||
animation = base_anim_args.pop("animation")
|
||||
|
||||
vertex_mobjects = self.add_vertices(*vertices, **kwargs)
|
||||
return AnimationGroup(
|
||||
*(
|
||||
animation(vertex_mobject, **base_anim_args)
|
||||
for vertex_mobject in vertex_mobjects
|
||||
),
|
||||
)
|
||||
|
||||
def _remove_vertex(self, vertex: Hashable) -> VGroup:
|
||||
"""Remove a vertex (as well as all incident edges) from the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -951,9 +952,9 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
to_remove.append(self.vertices.pop(vertex))
|
||||
|
||||
self.remove(*to_remove)
|
||||
return self.get_group_class()(*to_remove)
|
||||
return VGroup(*to_remove)
|
||||
|
||||
def remove_vertices(self, *vertices):
|
||||
def remove_vertices(self, *vertices: Hashable) -> VGroup:
|
||||
"""Remove several vertices from the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -967,7 +968,8 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
::
|
||||
|
||||
>>> G = Graph([1, 2, 3], [(1, 2), (2, 3)])
|
||||
>>> removed = G.remove_vertices(2, 3); removed
|
||||
>>> removed = G.remove_vertices(2, 3)
|
||||
>>> removed
|
||||
VGroup(Line, Line, Dot, Dot)
|
||||
>>> G
|
||||
Undirected graph on 1 vertices and 0 edges
|
||||
|
|
@ -976,26 +978,32 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
mobjects = []
|
||||
for v in vertices:
|
||||
mobjects.extend(self._remove_vertex(v).submobjects)
|
||||
return self.get_group_class()(*mobjects)
|
||||
return VGroup(*mobjects)
|
||||
|
||||
@override_animate(remove_vertices)
|
||||
def _remove_vertices_animation(self, *vertices, anim_args=None):
|
||||
if anim_args is None:
|
||||
anim_args = {}
|
||||
def _remove_vertices_animation(
|
||||
self, *vertices: Hashable, anim_args: dict[str, Any] | None = None
|
||||
) -> AnimationGroup:
|
||||
base_anim_args = {"animation": Uncreate}
|
||||
if anim_args is not None:
|
||||
base_anim_args.update(anim_args)
|
||||
animation = base_anim_args.pop("animation")
|
||||
|
||||
animation = anim_args.pop("animation", Uncreate)
|
||||
|
||||
mobjects = self.remove_vertices(*vertices)
|
||||
vertex_and_edge_mobjects = self.remove_vertices(*vertices)
|
||||
return AnimationGroup(
|
||||
*(animation(mobj, **anim_args) for mobj in mobjects), group=self
|
||||
*(
|
||||
animation(vertex_or_edge_mobject, **anim_args)
|
||||
for vertex_or_edge_mobject in vertex_and_edge_mobjects
|
||||
),
|
||||
introducer=True, # Reintroduce vertices and edges temporarily to animate them
|
||||
)
|
||||
|
||||
def _add_edge(
|
||||
self,
|
||||
edge: tuple[Hashable, Hashable],
|
||||
edge_type: type[Mobject] = Line,
|
||||
edge_type: type[VMobject] = Line,
|
||||
edge_config: dict | None = None,
|
||||
):
|
||||
) -> VGroup:
|
||||
"""Add a new edge to the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1039,15 +1047,17 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
self.add(edge_mobject)
|
||||
added_mobjects.append(edge_mobject)
|
||||
return self.get_group_class()(*added_mobjects)
|
||||
return VGroup(*added_mobjects)
|
||||
|
||||
def add_edges(
|
||||
self,
|
||||
*edges: tuple[Hashable, Hashable],
|
||||
edge_type: type[Mobject] = Line,
|
||||
edge_config: dict | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
edge_type: type[VMobject] = Line,
|
||||
edge_config: dict[str, Any]
|
||||
| dict[tuple[Hashable, Hashable], dict[str, Any]]
|
||||
| None = None,
|
||||
**kwargs: Any,
|
||||
) -> VGroup:
|
||||
"""Add new edges to the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1100,20 +1110,33 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
),
|
||||
added_vertices,
|
||||
)
|
||||
return self.get_group_class()(*added_mobjects)
|
||||
return VGroup(*added_mobjects)
|
||||
|
||||
@override_animate(add_edges)
|
||||
def _add_edges_animation(self, *args, anim_args=None, **kwargs):
|
||||
if anim_args is None:
|
||||
anim_args = {}
|
||||
animation = anim_args.pop("animation", Create)
|
||||
def _add_edges_animation(
|
||||
self,
|
||||
*edges: tuple[Hashable, Hashable],
|
||||
anim_args: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AnimationGroup:
|
||||
# TODO: the animation is broken with introducer=False, but not passing it
|
||||
# disbands the graph upon re-adding the edges and vertices. Fix this
|
||||
|
||||
mobjects = self.add_edges(*args, **kwargs)
|
||||
# Use introducer=False to prevent re-adding the edges and vertices when animating
|
||||
base_anim_args = {"animation": Create, "introducer": False}
|
||||
if anim_args is not None:
|
||||
base_anim_args.update(anim_args)
|
||||
animation = base_anim_args.pop("animation")
|
||||
|
||||
edge_and_vertex_mobjects = self.add_edges(*edges, **kwargs)
|
||||
return AnimationGroup(
|
||||
*(animation(mobj, **anim_args) for mobj in mobjects), group=self
|
||||
*(
|
||||
animation(edge_or_vertex_mobject, **base_anim_args)
|
||||
for edge_or_vertex_mobject in edge_and_vertex_mobjects
|
||||
)
|
||||
)
|
||||
|
||||
def _remove_edge(self, edge: tuple[Hashable]):
|
||||
def _remove_edge(self, edge: tuple[Hashable]) -> VMobject:
|
||||
"""Remove an edge from the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1125,7 +1148,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
Returns
|
||||
-------
|
||||
|
||||
Mobject
|
||||
VMobject
|
||||
The removed edge.
|
||||
|
||||
"""
|
||||
|
|
@ -1140,7 +1163,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.remove(edge_mobject)
|
||||
return edge_mobject
|
||||
|
||||
def remove_edges(self, *edges: tuple[Hashable]):
|
||||
def remove_edges(self, *edges: tuple[Hashable]) -> VGroup:
|
||||
"""Remove several edges from the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1155,17 +1178,25 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
"""
|
||||
edge_mobjects = [self._remove_edge(edge) for edge in edges]
|
||||
return self.get_group_class()(*edge_mobjects)
|
||||
return VGroup(*edge_mobjects)
|
||||
|
||||
@override_animate(remove_edges)
|
||||
def _remove_edges_animation(self, *edges, anim_args=None):
|
||||
if anim_args is None:
|
||||
anim_args = {}
|
||||
def _remove_edges_animation(
|
||||
self, *edges: tuple[Hashable, Hashable], anim_args: dict[str, Any] | None = None
|
||||
) -> AnimationGroup:
|
||||
base_anim_args = {"animation": Uncreate}
|
||||
if anim_args is not None:
|
||||
base_anim_args.update(anim_args)
|
||||
animation = base_anim_args.pop("animation")
|
||||
|
||||
animation = anim_args.pop("animation", Uncreate)
|
||||
|
||||
mobjects = self.remove_edges(*edges)
|
||||
return AnimationGroup(*(animation(mobj, **anim_args) for mobj in mobjects))
|
||||
edge_and_vertex_mobjects = self.remove_edges(*edges)
|
||||
return AnimationGroup(
|
||||
*(
|
||||
animation(edge_or_vertex_mobject, **anim_args)
|
||||
for edge_or_vertex_mobject in edge_and_vertex_mobjects
|
||||
),
|
||||
introducer=True, # Reintroduce edges and vertices temporarily to animate them
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_networkx(
|
||||
|
|
@ -1537,7 +1568,7 @@ class Graph(GenericGraph):
|
|||
return nx.Graph()
|
||||
|
||||
def _populate_edge_dict(
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[VMobject]
|
||||
):
|
||||
self.edges = {
|
||||
(u, v): edge_type(
|
||||
|
|
@ -1562,6 +1593,9 @@ class Graph(GenericGraph):
|
|||
def __repr__(self: Graph) -> str:
|
||||
return f"Undirected graph on {len(self.vertices)} vertices and {len(self.edges)} edges"
|
||||
|
||||
def __str__(self: Graph) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class DiGraph(GenericGraph):
|
||||
"""A directed graph.
|
||||
|
|
@ -1744,7 +1778,7 @@ class DiGraph(GenericGraph):
|
|||
return nx.DiGraph()
|
||||
|
||||
def _populate_edge_dict(
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[VMobject]
|
||||
):
|
||||
self.edges = {
|
||||
(u, v): edge_type(
|
||||
|
|
@ -1767,7 +1801,7 @@ class DiGraph(GenericGraph):
|
|||
"""
|
||||
for (u, v), edge in graph.edges.items():
|
||||
tip = edge.pop_tips()[0]
|
||||
# Passing the Mobject instead of the vertex makes the tip
|
||||
# Passing the VMobject instead of the vertex makes the tip
|
||||
# stop on the bounding box of the vertex.
|
||||
edge.set_points_by_ends(
|
||||
graph[u],
|
||||
|
|
@ -1779,3 +1813,6 @@ class DiGraph(GenericGraph):
|
|||
|
||||
def __repr__(self: DiGraph) -> str:
|
||||
return f"Directed graph on {len(self.vertices)} vertices and {len(self.edges)} edges"
|
||||
|
||||
def __str__(self: DiGraph) -> str:
|
||||
return self.__repr__()
|
||||
|
|
|
|||
|
|
@ -26,17 +26,22 @@ from manim.mobject.geometry.polygram import Polygon, Rectangle, RegularPolygon
|
|||
from manim.mobject.graphing.functions import ImplicitFunction, ParametricFunction
|
||||
from manim.mobject.graphing.number_line import NumberLine
|
||||
from manim.mobject.graphing.scale import LinearBase
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_surface import OpenGLSurface
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVDict as VDict,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVectorizedPoint as VectorizedPoint,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.three_d.three_dimensions import Surface
|
||||
from manim.mobject.types.vectorized_mobject import (
|
||||
VDict,
|
||||
VectorizedPoint,
|
||||
VGroup,
|
||||
VMobject,
|
||||
)
|
||||
from manim.utils.color import (
|
||||
BLACK,
|
||||
BLUE,
|
||||
|
|
@ -55,7 +60,6 @@ from manim.utils.simple_functions import binary_search
|
|||
from manim.utils.space_ops import angle_of_vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.typing import (
|
||||
ManimFloat,
|
||||
Point2D,
|
||||
|
|
@ -975,10 +979,10 @@ class CoordinateSystem:
|
|||
.. manim:: PlotSurfaceExample
|
||||
:save_last_frame:
|
||||
|
||||
class PlotSurfaceExample(ThreeDScene):
|
||||
class PlotSurfaceExample(Scene):
|
||||
def construct(self):
|
||||
resolution_fa = 16
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=-60 * DEGREES)
|
||||
self.camera.set_orientation(theta=-60 * DEGREES, phi=75 * DEGREES)
|
||||
axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-5, 5, 1))
|
||||
def param_trig(u, v):
|
||||
x = u
|
||||
|
|
@ -994,21 +998,8 @@ class CoordinateSystem:
|
|||
)
|
||||
self.add(axes, trig_plane)
|
||||
"""
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
surface = Surface(
|
||||
lambda u, v: self.c2p(u, v, function(u, v)),
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
**kwargs,
|
||||
)
|
||||
if colorscale:
|
||||
surface.set_fill_by_value(
|
||||
axes=self.copy(),
|
||||
colorscale=colorscale,
|
||||
axis=colorscale_axis,
|
||||
)
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
surface = OpenGLSurface(
|
||||
lambda u, v: self.c2p(u, v, function(u, v)),
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
|
|
@ -1017,6 +1008,9 @@ class CoordinateSystem:
|
|||
colorscale_axis=colorscale_axis,
|
||||
**kwargs,
|
||||
)
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
# TODO: CairoSurface?
|
||||
raise NotImplementedError
|
||||
|
||||
return surface
|
||||
|
||||
|
|
@ -1873,7 +1867,7 @@ class CoordinateSystem:
|
|||
def _origin_shift(axis_range: Sequence[float]) -> float: ...
|
||||
|
||||
|
||||
class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
||||
class Axes(VGroup, CoordinateSystem):
|
||||
"""Creates a set of axes.
|
||||
|
||||
Parameters
|
||||
|
|
@ -2512,6 +2506,7 @@ class ThreeDAxes(Axes):
|
|||
self.z_axis = z_axis
|
||||
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
# TODO: check in how far these methods are supported by new VMobject class
|
||||
self._add_3d_pieces()
|
||||
self._set_axis_shading()
|
||||
|
||||
|
|
@ -2573,11 +2568,11 @@ class ThreeDAxes(Axes):
|
|||
.. manim:: GetYAxisLabelExample
|
||||
:save_last_frame:
|
||||
|
||||
class GetYAxisLabelExample(ThreeDScene):
|
||||
class GetYAxisLabelExample(Scene):
|
||||
def construct(self):
|
||||
ax = ThreeDAxes()
|
||||
lab = ax.get_y_axis_label(Tex("$y$-label"))
|
||||
self.set_camera_orientation(phi=2*PI/5, theta=PI/5)
|
||||
self.camera.set_orientation(theta=PI/5, phi=2*PI/5)
|
||||
self.add(ax, lab)
|
||||
"""
|
||||
positioned_label = self._get_axis_label(
|
||||
|
|
@ -2623,11 +2618,11 @@ class ThreeDAxes(Axes):
|
|||
.. manim:: GetZAxisLabelExample
|
||||
:save_last_frame:
|
||||
|
||||
class GetZAxisLabelExample(ThreeDScene):
|
||||
class GetZAxisLabelExample(Scene):
|
||||
def construct(self):
|
||||
ax = ThreeDAxes()
|
||||
lab = ax.get_z_axis_label(Tex("$z$-label"))
|
||||
self.set_camera_orientation(phi=2*PI/5, theta=PI/5)
|
||||
self.camera.set_orientation(theta=PI/5, phi=2*PI/5)
|
||||
self.add(ax, lab)
|
||||
"""
|
||||
positioned_label = self._get_axis_label(
|
||||
|
|
@ -2674,9 +2669,9 @@ class ThreeDAxes(Axes):
|
|||
.. manim:: GetAxisLabelsExample
|
||||
:save_last_frame:
|
||||
|
||||
class GetAxisLabelsExample(ThreeDScene):
|
||||
class GetAxisLabelsExample(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=2*PI/5, theta=PI/5)
|
||||
self.camera.set_orientation(theta=PI/5, phi=2*PI/5)
|
||||
axes = ThreeDAxes()
|
||||
labels = axes.get_axis_labels(
|
||||
Text("x-axis").scale(0.7), Text("y-axis").scale(0.45), Text("z-axis").scale(0.45)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ from isosurfaces import plot_isoline
|
|||
|
||||
from manim import config
|
||||
from manim.mobject.graphing.scale import LinearBase, _ScaleBase
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Self
|
||||
|
|
@ -25,7 +24,7 @@ if TYPE_CHECKING:
|
|||
from manim.utils.color import PURE_YELLOW
|
||||
|
||||
|
||||
class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
||||
class ParametricFunction(VMobject):
|
||||
"""A parametric curve.
|
||||
|
||||
Parameters
|
||||
|
|
@ -66,7 +65,7 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
.. manim:: ThreeDParametricSpring
|
||||
:save_last_frame:
|
||||
|
||||
class ThreeDParametricSpring(ThreeDScene):
|
||||
class ThreeDParametricSpring(Scene):
|
||||
def construct(self):
|
||||
curve1 = ParametricFunction(
|
||||
lambda u: (
|
||||
|
|
@ -77,7 +76,7 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
).set_shade_in_3d(True)
|
||||
axes = ThreeDAxes()
|
||||
self.add(axes, curve1)
|
||||
self.set_camera_orientation(phi=80 * DEGREES, theta=-60 * DEGREES)
|
||||
self.camera.set_orientation(theta=-60 * DEGREES, phi=80 * DEGREES)
|
||||
self.wait()
|
||||
|
||||
.. attention::
|
||||
|
|
@ -237,7 +236,7 @@ class FunctionGraph(ParametricFunction):
|
|||
return self.parametric_function(x)
|
||||
|
||||
|
||||
class ImplicitFunction(VMobject, metaclass=ConvertToOpenGL):
|
||||
class ImplicitFunction(VMobject):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], float],
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
|
||||
__all__ = ["NumberLine", "UnitInterval"]
|
||||
|
||||
|
||||
|
|
@ -23,10 +20,16 @@ from manim import config
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.graphing.scale import LinearBase, _ScaleBase
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.text.numbers import DecimalNumber, Integer
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.bezier import interpolate
|
||||
from manim.utils.config_ops import merge_dicts_recursively
|
||||
from manim.utils.space_ops import normalize
|
||||
|
|
@ -651,7 +654,7 @@ class NumberLine(Line):
|
|||
:class:`~.VMobject`
|
||||
The label.
|
||||
"""
|
||||
if isinstance(label_tex, (VMobject, OpenGLVMobject)):
|
||||
if isinstance(label_tex, VMobject):
|
||||
return label_tex
|
||||
if label_constructor is None:
|
||||
label_constructor = self.label_constructor
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@ from manim import config, logger
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
from manim.mobject.graphing.coordinate_systems import Axes
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.svg.brace import Brace
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.typing import Vector3D
|
||||
from manim.utils.color import (
|
||||
BLUE_E,
|
||||
|
|
@ -144,7 +148,7 @@ class SampleSpace(Rectangle):
|
|||
def get_subdivision_braces_and_labels(
|
||||
self,
|
||||
parts: VGroup,
|
||||
labels: list[str | VMobject | OpenGLVMobject],
|
||||
labels: list[str | VMobject],
|
||||
direction: Vector3D,
|
||||
buff: float = SMALL_BUFF,
|
||||
min_num_quads: int = 1,
|
||||
|
|
@ -153,7 +157,7 @@ class SampleSpace(Rectangle):
|
|||
braces = VGroup()
|
||||
for label, part in zip(labels, parts, strict=False):
|
||||
brace = Brace(part, direction, min_num_quads=min_num_quads, buff=buff)
|
||||
if isinstance(label, (VMobject, OpenGLVMobject)):
|
||||
if isinstance(label, VMobject):
|
||||
label_mob = label
|
||||
else:
|
||||
label_mob = MathTex(label)
|
||||
|
|
@ -174,7 +178,7 @@ class SampleSpace(Rectangle):
|
|||
|
||||
def get_side_braces_and_labels(
|
||||
self,
|
||||
labels: list[str | VMobject | OpenGLVMobject],
|
||||
labels: list[str | VMobject],
|
||||
direction: Vector3D = LEFT,
|
||||
**kwargs: Any,
|
||||
) -> VGroup:
|
||||
|
|
@ -185,14 +189,14 @@ class SampleSpace(Rectangle):
|
|||
)
|
||||
|
||||
def get_top_braces_and_labels(
|
||||
self, labels: list[str | VMobject | OpenGLVMobject], **kwargs: Any
|
||||
self, labels: list[str | VMobject], **kwargs: Any
|
||||
) -> VGroup:
|
||||
assert hasattr(self, "vertical_parts")
|
||||
parts = self.vertical_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, UP, **kwargs)
|
||||
|
||||
def get_bottom_braces_and_labels(
|
||||
self, labels: list[str | VMobject | OpenGLVMobject], **kwargs: Any
|
||||
self, labels: list[str | VMobject], **kwargs: Any
|
||||
) -> VGroup:
|
||||
assert hasattr(self, "vertical_parts")
|
||||
parts = self.vertical_parts
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ from manim.mobject.text.numbers import Integer
|
|||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
|
||||
|
||||
class _ScaleBase:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,12 @@ import svgelements as se
|
|||
from manim.animation.updaters.update import UpdateFromAlphaFunc
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square, Triangle
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.typing import Vector3D
|
||||
|
||||
from .. import constants as cst
|
||||
|
|
@ -20,7 +25,6 @@ from ..animation.composition import AnimationGroup, Succession
|
|||
from ..animation.creation import Create, SpiralIn
|
||||
from ..animation.fading import FadeIn
|
||||
from ..mobject.svg.svg_mobject import VMobjectFromSVGPath
|
||||
from ..mobject.types.vectorized_mobject import VGroup
|
||||
from ..utils.rate_functions import ease_in_out_cubic, smooth
|
||||
|
||||
MANIM_SVG_PATHS: list[se.Path] = [
|
||||
|
|
@ -153,7 +157,7 @@ class ManimBanner(VGroup):
|
|||
self.scale_factor = 1.0
|
||||
|
||||
self.M = VMobjectFromSVGPath(MANIM_SVG_PATHS[0]).flip(cst.RIGHT).center()
|
||||
self.M.set(stroke_width=0).scale(
|
||||
self.M.set_stroke(width=0).scale(
|
||||
7 * cst.DEFAULT_FONT_SIZE * cst.SCALE_FACTOR_PER_FONT_POINT
|
||||
)
|
||||
self.M.set_fill(color=self.font_color, opacity=1).shift(
|
||||
|
|
@ -170,7 +174,7 @@ class ManimBanner(VGroup):
|
|||
anim = VGroup()
|
||||
for ind, path in enumerate(MANIM_SVG_PATHS[1:]):
|
||||
tex = VMobjectFromSVGPath(path).flip(cst.RIGHT).center()
|
||||
tex.set(stroke_width=0).scale(
|
||||
tex.set_stroke(width=0).scale(
|
||||
cst.DEFAULT_FONT_SIZE * cst.SCALE_FACTOR_PER_FONT_POINT
|
||||
)
|
||||
if ind > 0:
|
||||
|
|
@ -265,7 +269,7 @@ class ManimBanner(VGroup):
|
|||
)
|
||||
|
||||
"""
|
||||
if direction not in ["left", "right", "center"]:
|
||||
if direction.lower() not in {"left", "right", "center"}:
|
||||
raise ValueError("direction must be 'left', 'right' or 'center'.")
|
||||
|
||||
m_shape_offset = 6.25 * self.scale_factor
|
||||
|
|
@ -292,7 +296,7 @@ class ManimBanner(VGroup):
|
|||
elif direction == "left":
|
||||
left_group.shift(-vector)
|
||||
|
||||
def slide_and_uncover(mob: Mobject, alpha: float) -> None:
|
||||
def slide_and_uncover(mob: VMobject, alpha: float) -> None:
|
||||
shift(alpha * (m_shape_offset + shape_sliding_overshoot) * cst.RIGHT)
|
||||
|
||||
# Add letters when they are covered
|
||||
|
|
@ -305,11 +309,11 @@ class ManimBanner(VGroup):
|
|||
if alpha == 1:
|
||||
self.remove(*[self.anim])
|
||||
self.add_to_back(self.anim)
|
||||
mob.shapes.set_z_index(0)
|
||||
mob.shapes.set_z(0)
|
||||
mob.shapes.save_state()
|
||||
mob.M.save_state()
|
||||
|
||||
def slide_back(mob: Mobject, alpha: float) -> None:
|
||||
def slide_back(mob: VMobject, alpha: float) -> None:
|
||||
if alpha == 0:
|
||||
m_clone.set_opacity(1)
|
||||
m_clone.move_to(mob.anim[-1])
|
||||
|
|
@ -327,11 +331,13 @@ class ManimBanner(VGroup):
|
|||
slide_and_uncover,
|
||||
run_time=run_time * 2 / 3,
|
||||
rate_func=ease_in_out_cubic,
|
||||
introducer=True,
|
||||
),
|
||||
UpdateFromAlphaFunc(
|
||||
self,
|
||||
slide_back,
|
||||
run_time=run_time * 1 / 3,
|
||||
rate_func=smooth,
|
||||
introducer=True,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,13 +45,17 @@ from typing import Any, Self
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.text.numbers import DecimalNumber, Integer
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
|
||||
from ..constants import *
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
|
||||
# TO DO : The following two functions are not used in this file.
|
||||
# Not sure if we should keep it or not.
|
||||
|
|
@ -72,7 +76,7 @@ def matrix_to_mobject(matrix: np.ndarray) -> MathTex:
|
|||
return MathTex(matrix_to_tex_string(matrix))
|
||||
|
||||
|
||||
class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
||||
class Matrix(VMobject):
|
||||
r"""A mobject that displays a matrix on the screen.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim import config, logger
|
||||
from manim.constants import *
|
||||
from manim.data_structures import MethodWithArgs
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
|
||||
from .. import config, logger
|
||||
from ..constants import *
|
||||
from ..utils.color import (
|
||||
from manim.mobject.opengl.opengl_mobject import InvisibleMobject
|
||||
from manim.utils.color import (
|
||||
BLACK,
|
||||
PURE_YELLOW,
|
||||
WHITE,
|
||||
|
|
@ -35,14 +35,15 @@ from ..utils.color import (
|
|||
color_gradient,
|
||||
interpolate_color,
|
||||
)
|
||||
from ..utils.exceptions import MultiAnimationOverrideException
|
||||
from ..utils.iterables import list_update, remove_list_redundancies
|
||||
from ..utils.paths import straight_path
|
||||
from ..utils.space_ops import angle_between_vectors, normalize, rotation_matrix
|
||||
from manim.utils.exceptions import MultiAnimationOverrideException
|
||||
from manim.utils.iterables import list_update, remove_list_redundancies
|
||||
from manim.utils.paths import straight_path
|
||||
from manim.utils.space_ops import angle_between_vectors, normalize, rotation_matrix
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self, TypeAlias
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
from manim.typing import (
|
||||
FunctionOverride,
|
||||
MappingFunction,
|
||||
|
|
@ -56,8 +57,6 @@ if TYPE_CHECKING:
|
|||
Vector3DLike,
|
||||
)
|
||||
|
||||
from ..animation.animation import Animation
|
||||
|
||||
TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object]
|
||||
NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object]
|
||||
Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater
|
||||
|
|
@ -151,7 +150,7 @@ class Mobject:
|
|||
return self._assert_valid_submobjects_internal(submobjects, Mobject)
|
||||
|
||||
def _assert_valid_submobjects_internal(
|
||||
self, submobjects: list[Mobject], mob_class: type[Mobject]
|
||||
self, submobjects: Iterable[Mobject], mob_class: type[Mobject]
|
||||
) -> Self:
|
||||
for i, submob in enumerate(submobjects):
|
||||
if not isinstance(submob, mob_class):
|
||||
|
|
@ -269,10 +268,12 @@ class Mobject:
|
|||
|
||||
>>> from manim import Square, GREEN
|
||||
>>> Square.set_default(color=GREEN, fill_opacity=0.25)
|
||||
>>> s = Square(); s.color, s.fill_opacity
|
||||
>>> s = Square()
|
||||
>>> s.color, s.fill_opacity
|
||||
(ManimColor('#83C167'), 0.25)
|
||||
>>> Square.set_default()
|
||||
>>> s = Square(); s.color, s.fill_opacity
|
||||
>>> s = Square()
|
||||
>>> s.color, s.fill_opacity
|
||||
(ManimColor('#FFFFFF'), 0.0)
|
||||
|
||||
.. manim:: ChangedDefaultTextcolor
|
||||
|
|
@ -716,13 +717,10 @@ class Mobject:
|
|||
# Add automatic compatibility layer
|
||||
# between properties and get_* and set_*
|
||||
# methods.
|
||||
#
|
||||
# In python 3.9+ we could change this
|
||||
# logic to use str.remove_prefix instead.
|
||||
|
||||
if attr.startswith("get_"):
|
||||
# Remove the "get_" prefix
|
||||
to_get = attr[4:]
|
||||
to_get = attr.removeprefix("get_")
|
||||
|
||||
def getter(self):
|
||||
warnings.warn(
|
||||
|
|
@ -864,7 +862,7 @@ class Mobject:
|
|||
|
||||
def get_image(self, camera=None) -> PixelArray:
|
||||
if camera is None:
|
||||
from ..camera.camera import Camera
|
||||
from manim.camera.cairo_camera import CairoCamera as Camera
|
||||
|
||||
camera = Camera()
|
||||
camera.capture_mobject(self)
|
||||
|
|
@ -981,6 +979,22 @@ class Mobject:
|
|||
"dt" in inspect.signature(updater).parameters for updater in self.updaters
|
||||
)
|
||||
|
||||
@property
|
||||
def has_updaters(self) -> bool:
|
||||
"""Test if ``self`` has an updater.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
``True`` if at least one updater is added, ``False`` otherwise.
|
||||
|
||||
See Also
|
||||
--------
|
||||
:meth:`get_updaters`
|
||||
|
||||
"""
|
||||
return len(self.updaters) > 0
|
||||
|
||||
def get_updaters(self) -> list[Updater]:
|
||||
"""Return all updaters.
|
||||
|
||||
|
|
@ -1454,6 +1468,7 @@ class Mobject:
|
|||
about_point,
|
||||
about_edge,
|
||||
)
|
||||
self.note_changed_family()
|
||||
return self
|
||||
|
||||
def apply_function_to_position(self, function: MappingFunction) -> Self:
|
||||
|
|
@ -2489,6 +2504,24 @@ class Mobject:
|
|||
all_mobjects = [self] + list(it.chain(*sub_families))
|
||||
return remove_list_redundancies(all_mobjects)
|
||||
|
||||
def get_ancestors(self, extended: bool = False) -> list[Self]:
|
||||
"""Returns parents, grandparents, etc.
|
||||
|
||||
Base Mobject implementation returns empty list as it doesn't
|
||||
track parent relationships. OpenGLMobject overrides this.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
extended
|
||||
If True, includes ancestors of all family members.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[Self]
|
||||
List of ancestor mobjects. Empty for base Mobject.
|
||||
"""
|
||||
return []
|
||||
|
||||
def family_members_with_points(self) -> list[Self]:
|
||||
"""Filters the list of family members (generated by :meth:`.get_family`) to include only mobjects with points.
|
||||
|
||||
|
|
@ -3297,7 +3330,7 @@ class Mobject:
|
|||
return self
|
||||
|
||||
|
||||
class Group(Mobject, metaclass=ConvertToOpenGL):
|
||||
class Group(Mobject, InvisibleMobject, metaclass=ConvertToOpenGL):
|
||||
"""Groups together multiple :class:`Mobjects <.Mobject>`.
|
||||
|
||||
Notes
|
||||
|
|
@ -3312,6 +3345,45 @@ class Group(Mobject, metaclass=ConvertToOpenGL):
|
|||
self.add(*mobjects)
|
||||
|
||||
|
||||
class Point(Mobject, InvisibleMobject, metaclass=ConvertToOpenGL):
|
||||
def __init__(
|
||||
self,
|
||||
location: np.ndarray = ORIGIN,
|
||||
artificial_width: float = 1e-6,
|
||||
artificial_height: float = 1e-6,
|
||||
**kwargs,
|
||||
):
|
||||
self.artificial_width = artificial_width
|
||||
self.artificial_height = artificial_height
|
||||
super().__init__(**kwargs)
|
||||
self.set_location(location)
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.artificial_width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.artificial_height
|
||||
|
||||
# TODO: properties vs. getter methods?
|
||||
|
||||
def get_width(self):
|
||||
return self.artificial_width
|
||||
|
||||
def get_height(self):
|
||||
return self.artificial_height
|
||||
|
||||
def get_location(self):
|
||||
return self.points[0].copy()
|
||||
|
||||
def get_bounding_box_point(self, *args, **kwargs):
|
||||
return self.get_location()
|
||||
|
||||
def set_location(self, new_loc):
|
||||
self.set_points(np.array(new_loc, ndmin=2, dtype=float))
|
||||
|
||||
|
||||
class _AnimationBuilder:
|
||||
def __init__(self, mobject) -> None:
|
||||
self.mobject = mobject
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,6 @@ __all__ = ["OpenGLPMobject", "OpenGLPGroup", "OpenGLPMPoint"]
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
|
||||
from manim.constants import *
|
||||
|
|
@ -18,7 +17,6 @@ from manim.utils.color import (
|
|||
color_gradient,
|
||||
color_to_rgba,
|
||||
)
|
||||
from manim.utils.config_ops import _Uniforms
|
||||
from manim.utils.iterables import resize_with_interpolation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -35,25 +33,17 @@ __all__ = ["OpenGLPMobject", "OpenGLPGroup", "OpenGLPMPoint"]
|
|||
|
||||
|
||||
class OpenGLPMobject(OpenGLMobject):
|
||||
shader_folder = "true_dot"
|
||||
# Scale for consistency with cairo units
|
||||
OPENGL_POINT_RADIUS_SCALE_FACTOR = 0.01
|
||||
shader_dtype = [
|
||||
("point", np.float32, (3,)),
|
||||
("color", np.float32, (4,)),
|
||||
]
|
||||
|
||||
point_radius = _Uniforms()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stroke_width: float = 2.0,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
render_primitive: int = moderngl.POINTS,
|
||||
**kwargs,
|
||||
):
|
||||
self.stroke_width = stroke_width
|
||||
super().__init__(color=color, render_primitive=render_primitive, **kwargs)
|
||||
super().__init__(color=color, **kwargs)
|
||||
self.point_radius = (
|
||||
self.stroke_width * OpenGLPMobject.OPENGL_POINT_RADIUS_SCALE_FACTOR
|
||||
)
|
||||
|
|
@ -148,21 +138,26 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
def filter_out(self, condition):
|
||||
for mob in self.family_members_with_points():
|
||||
to_keep = ~np.apply_along_axis(condition, 1, mob.points)
|
||||
for key in mob.data:
|
||||
mob.data[key] = mob.data[key][to_keep]
|
||||
for attr_name in mob.get_array_attrs():
|
||||
array = getattr(mob, attr_name)
|
||||
filtered_array = array[to_keep]
|
||||
setattr(mob, attr_name, filtered_array)
|
||||
return self
|
||||
|
||||
def sort_points(self, function=lambda p: p[0]):
|
||||
"""function is any map from R^3 to R"""
|
||||
for mob in self.family_members_with_points():
|
||||
indices = np.argsort(np.apply_along_axis(function, 1, mob.points))
|
||||
for key in mob.data:
|
||||
mob.data[key] = mob.data[key][indices]
|
||||
for attr_name in mob.get_array_attrs():
|
||||
array = getattr(mob, attr_name)
|
||||
sorted_array = array[indices]
|
||||
setattr(mob, attr_name, sorted_array)
|
||||
return self
|
||||
|
||||
def ingest_submobjects(self):
|
||||
for key in self.data:
|
||||
self.data[key] = np.vstack([sm.data[key] for sm in self.get_family()])
|
||||
for attr_name in self.get_array_attrs():
|
||||
submob_arrays = [getattr(sm, attr_name) for sm in self.get_family()]
|
||||
setattr(self, attr_name, np.vstack(submob_arrays))
|
||||
return self
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
|
|
@ -172,16 +167,12 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
def pointwise_become_partial(self, pmobject, a, b):
|
||||
lower_index = int(a * pmobject.get_num_points())
|
||||
upper_index = int(b * pmobject.get_num_points())
|
||||
for key in self.data:
|
||||
self.data[key] = pmobject.data[key][lower_index:upper_index]
|
||||
for attr_name in self.get_array_attrs():
|
||||
pmob_array = getattr(pmobject, attr_name)
|
||||
partial_pmob_array = pmob_array[lower_index:upper_index]
|
||||
setattr(self, attr_name, partial_pmob_array)
|
||||
return self
|
||||
|
||||
def get_shader_data(self):
|
||||
shader_data = np.zeros(len(self.points), dtype=self.shader_dtype)
|
||||
self.read_data_to_shader(shader_data, "point", "points")
|
||||
self.read_data_to_shader(shader_data, "color", "rgbas")
|
||||
return shader_data
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_type_class():
|
||||
return OpenGLPMobject
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from collections.abc import Iterable
|
|||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
|
@ -12,7 +11,6 @@ from manim.constants import *
|
|||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.utils.bezier import integer_interpolate, interpolate
|
||||
from manim.utils.color import *
|
||||
from manim.utils.config_ops import _Data, _Uniforms
|
||||
from manim.utils.images import change_to_rgba_array, get_full_raster_image_path
|
||||
from manim.utils.iterables import listify
|
||||
from manim.utils.space_ops import normalize_along_axis
|
||||
|
|
@ -25,6 +23,7 @@ if TYPE_CHECKING:
|
|||
__all__ = ["OpenGLSurface", "OpenGLTexturedSurface"]
|
||||
|
||||
|
||||
# TODO: Those will not work in the current state we will have to think about a different method to render these with shaders in our current pipeline
|
||||
class OpenGLSurface(OpenGLMobject):
|
||||
r"""Creates a Surface.
|
||||
|
||||
|
|
@ -57,14 +56,6 @@ class OpenGLSurface(OpenGLMobject):
|
|||
to 1 being fully opaque. Defaults to 1.
|
||||
"""
|
||||
|
||||
shader_dtype = [
|
||||
("point", np.float32, (3,)),
|
||||
("du_point", np.float32, (3,)),
|
||||
("dv_point", np.float32, (3,)),
|
||||
("color", np.float32, (4,)),
|
||||
]
|
||||
shader_folder = "surface"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uv_func=None,
|
||||
|
|
@ -85,9 +76,7 @@ class OpenGLSurface(OpenGLMobject):
|
|||
# For du and dv steps. Much smaller and numerical error
|
||||
# can crop up in the shaders.
|
||||
epsilon=1e-5,
|
||||
render_primitive=moderngl.TRIANGLES,
|
||||
depth_test=True,
|
||||
shader_folder=None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.passed_uv_func = uv_func
|
||||
|
|
@ -111,8 +100,6 @@ class OpenGLSurface(OpenGLMobject):
|
|||
opacity=opacity,
|
||||
gloss=gloss,
|
||||
shadow=shadow,
|
||||
shader_folder=shader_folder if shader_folder is not None else "surface",
|
||||
render_primitive=render_primitive,
|
||||
depth_test=depth_test,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -252,106 +239,6 @@ class OpenGLSurface(OpenGLMobject):
|
|||
tri_is[k::3] = tri_is[k::3][indices]
|
||||
return self
|
||||
|
||||
# For shaders
|
||||
def get_shader_data(self):
|
||||
"""Called by parent Mobject to calculate and return
|
||||
the shader data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
shader_dtype
|
||||
An array containing the shader data (vertices and
|
||||
color of each vertex)
|
||||
"""
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
shader_data = np.zeros(len(s_points), dtype=self.shader_dtype)
|
||||
if "points" not in self.locked_data_keys:
|
||||
shader_data["point"] = s_points
|
||||
shader_data["du_point"] = du_points
|
||||
shader_data["dv_point"] = dv_points
|
||||
if self.colorscale:
|
||||
if not hasattr(self, "color_by_val"):
|
||||
self.color_by_val = self._get_color_by_value(s_points)
|
||||
shader_data["color"] = self.color_by_val
|
||||
else:
|
||||
self.fill_in_shader_color_info(shader_data)
|
||||
return shader_data
|
||||
|
||||
def fill_in_shader_color_info(self, shader_data):
|
||||
"""Fills in the shader color data when the surface
|
||||
is all one color.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shader_data
|
||||
The vertices of the surface.
|
||||
|
||||
Returns
|
||||
-------
|
||||
shader_dtype
|
||||
An array containing the shader data (vertices and
|
||||
color of each vertex)
|
||||
"""
|
||||
self.read_data_to_shader(shader_data, "color", "rgbas")
|
||||
return shader_data
|
||||
|
||||
def _get_color_by_value(self, s_points):
|
||||
"""Matches each vertex to a color associated to it's z-value.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s_points
|
||||
The vertices of the surface.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List
|
||||
A list of colors matching the vertex inputs.
|
||||
"""
|
||||
if type(self.colorscale[0]) in (list, tuple):
|
||||
new_colors, pivots = [
|
||||
[i for i, j in self.colorscale],
|
||||
[j for i, j in self.colorscale],
|
||||
]
|
||||
else:
|
||||
new_colors = self.colorscale
|
||||
|
||||
pivot_min = self.axes.z_range[0]
|
||||
pivot_max = self.axes.z_range[1]
|
||||
pivot_frequency = (pivot_max - pivot_min) / (len(new_colors) - 1)
|
||||
pivots = np.arange(
|
||||
start=pivot_min,
|
||||
stop=pivot_max + pivot_frequency,
|
||||
step=pivot_frequency,
|
||||
)
|
||||
|
||||
return_colors = []
|
||||
for point in s_points:
|
||||
axis_value = self.axes.point_to_coords(point)[self.colorscale_axis]
|
||||
if axis_value <= pivots[0]:
|
||||
return_colors.append(color_to_rgba(new_colors[0], self.opacity))
|
||||
elif axis_value >= pivots[-1]:
|
||||
return_colors.append(color_to_rgba(new_colors[-1], self.opacity))
|
||||
else:
|
||||
for i, pivot in enumerate(pivots):
|
||||
if pivot > axis_value:
|
||||
color_index = (axis_value - pivots[i - 1]) / (
|
||||
pivots[i] - pivots[i - 1]
|
||||
)
|
||||
color_index = max(min(color_index, 1), 0)
|
||||
temp_color = interpolate_color(
|
||||
new_colors[i - 1],
|
||||
new_colors[i],
|
||||
color_index,
|
||||
)
|
||||
break
|
||||
return_colors.append(color_to_rgba(temp_color, self.opacity))
|
||||
|
||||
return return_colors
|
||||
|
||||
def get_shader_vert_indices(self):
|
||||
return self.get_triangle_indices()
|
||||
|
||||
|
||||
class OpenGLSurfaceGroup(OpenGLSurface):
|
||||
def __init__(self, *parametric_surfaces, resolution=None, **kwargs):
|
||||
|
|
@ -364,29 +251,14 @@ class OpenGLSurfaceGroup(OpenGLSurface):
|
|||
|
||||
|
||||
class OpenGLTexturedSurface(OpenGLSurface):
|
||||
shader_dtype = [
|
||||
("point", np.float32, (3,)),
|
||||
("du_point", np.float32, (3,)),
|
||||
("dv_point", np.float32, (3,)),
|
||||
("im_coords", np.float32, (2,)),
|
||||
("opacity", np.float32, (1,)),
|
||||
]
|
||||
shader_folder = "textured_surface"
|
||||
im_coords = _Data()
|
||||
opacity = _Data()
|
||||
num_textures = _Uniforms()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uv_surface: OpenGLSurface,
|
||||
image_file: str | Path | npt.NDArray,
|
||||
dark_image_file: str | Path = None,
|
||||
image_mode: str | Iterable[str] = "RGBA",
|
||||
shader_folder: str | Path = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.uniforms = {}
|
||||
|
||||
if not isinstance(uv_surface, OpenGLSurface):
|
||||
raise Exception("uv_surface must be of type OpenGLSurface")
|
||||
if isinstance(image_file, np.ndarray):
|
||||
|
|
@ -396,7 +268,8 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
if isinstance(image_mode, (str, Path)):
|
||||
image_mode = [image_mode] * 2
|
||||
image_mode_light, image_mode_dark = image_mode
|
||||
texture_paths = {
|
||||
# TODO: move to renderer
|
||||
_texture_paths = {
|
||||
"LightTexture": self.get_image_from_file(
|
||||
image_file,
|
||||
image_mode_light,
|
||||
|
|
@ -415,7 +288,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
self.v_range = uv_surface.v_range
|
||||
self.resolution = uv_surface.resolution
|
||||
self.gloss = self.uv_surface.gloss
|
||||
super().__init__(texture_paths=texture_paths, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_image_from_file(
|
||||
self,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -21,8 +21,8 @@ if TYPE_CHECKING:
|
|||
|
||||
from manim.typing import MatrixMN, Point3D
|
||||
|
||||
from .. import config
|
||||
from ..utils import opengl
|
||||
from ... import config
|
||||
from ...utils import opengl
|
||||
|
||||
SHADER_FOLDER = Path(__file__).parent / "shaders"
|
||||
shader_program_cache: dict[str, moderngl.Program] = {}
|
||||
|
|
@ -12,8 +12,8 @@ import svgelements as se
|
|||
from manim._config import config
|
||||
from manim.mobject.geometry.arc import Arc
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
|
||||
|
|
@ -22,7 +22,6 @@ from ...animation.composition import AnimationGroup
|
|||
from ...animation.fading import FadeIn
|
||||
from ...animation.growing import GrowFromCenter
|
||||
from ...constants import *
|
||||
from ...mobject.types.vectorized_mobject import VMobject
|
||||
from ...utils.color import BLACK
|
||||
from ..svg.svg_mobject import VMobjectFromSVGPath
|
||||
|
||||
|
|
@ -204,7 +203,7 @@ class Brace(VMobjectFromSVGPath):
|
|||
return vect / np.linalg.norm(vect)
|
||||
|
||||
|
||||
class BraceLabel(VMobject, metaclass=ConvertToOpenGL):
|
||||
class BraceLabel(VMobject):
|
||||
"""Create a brace with a label attached.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import numpy as np
|
|||
import svgelements as se
|
||||
|
||||
from manim import config, logger
|
||||
from manim.utils.color import ManimColor, ParsableManimColor
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
|
||||
from ...constants import RIGHT
|
||||
from ...utils.bezier import get_quadratic_approximation_of_cubic
|
||||
|
|
@ -20,8 +21,6 @@ from ...utils.iterables import hash_obj
|
|||
from ..geometry.arc import Circle
|
||||
from ..geometry.line import Line
|
||||
from ..geometry.polygram import Polygon, Rectangle, RoundedRectangle
|
||||
from ..opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from ..types.vectorized_mobject import VGroup, VMobject
|
||||
|
||||
__all__ = ["SVGMobject", "VMobjectFromSVGPath"]
|
||||
|
||||
|
|
@ -33,7 +32,7 @@ def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
|
|||
return np.array([x, y, 0.0])
|
||||
|
||||
|
||||
class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
||||
class SVGMobject(VMobject):
|
||||
"""A vectorized mobject created from importing an SVG file.
|
||||
|
||||
Parameters
|
||||
|
|
@ -100,36 +99,21 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
should_center: bool = True,
|
||||
height: float | None = 2,
|
||||
width: float | None = None,
|
||||
color: ParsableManimColor | None = None,
|
||||
opacity: float | None = None,
|
||||
fill_color: ParsableManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
stroke_color: ParsableManimColor | None = None,
|
||||
stroke_opacity: float | None = None,
|
||||
stroke_width: float | None = None,
|
||||
svg_default: dict | None = None,
|
||||
path_string_config: dict | None = None,
|
||||
use_svg_cache: bool = True,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(color=None, stroke_color=None, fill_color=None, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# process keyword arguments
|
||||
self.file_name = Path(file_name) if file_name is not None else None
|
||||
|
||||
self.should_center = should_center
|
||||
self.svg_height = height
|
||||
self.svg_width = width
|
||||
self.color = ManimColor(color)
|
||||
self.opacity = opacity
|
||||
self.fill_color = fill_color
|
||||
self.fill_opacity = fill_opacity # type: ignore[assignment]
|
||||
self.stroke_color = stroke_color
|
||||
self.stroke_opacity = stroke_opacity # type: ignore[assignment]
|
||||
self.stroke_width = stroke_width # type: ignore[assignment]
|
||||
self.id_to_vgroup_dict: dict[str, VGroup] = {}
|
||||
if self.stroke_width is None:
|
||||
self.stroke_width = 0
|
||||
|
||||
if svg_default is None:
|
||||
svg_default = {
|
||||
|
|
@ -137,24 +121,20 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
"opacity": None,
|
||||
"fill_color": None,
|
||||
"fill_opacity": None,
|
||||
"stroke_width": 0,
|
||||
"stroke_width": [0],
|
||||
"stroke_color": None,
|
||||
"stroke_opacity": None,
|
||||
}
|
||||
self.svg_default = svg_default
|
||||
|
||||
if path_string_config is None:
|
||||
path_string_config = {}
|
||||
self.path_string_config = path_string_config
|
||||
self.path_string_config = path_string_config or {}
|
||||
|
||||
self.init_svg_mobject(use_svg_cache=use_svg_cache)
|
||||
|
||||
self.set_style(
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_color=stroke_color,
|
||||
stroke_opacity=stroke_opacity,
|
||||
stroke_width=stroke_width,
|
||||
fill_color=self.fill_color,
|
||||
stroke_color=self.stroke_color,
|
||||
stroke_width=self.stroke_width,
|
||||
)
|
||||
self.move_into_position()
|
||||
|
||||
|
|
@ -497,7 +477,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.set(width=self.svg_width)
|
||||
|
||||
|
||||
class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
||||
class VMobjectFromSVGPath(VMobject):
|
||||
"""A vectorized mobject representing an SVG path.
|
||||
|
||||
.. note::
|
||||
|
|
@ -547,13 +527,12 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
self.handle_commands()
|
||||
|
||||
if config.renderer == "opengl":
|
||||
if self.should_subdivide_sharp_curves:
|
||||
# For a healthy triangulation later
|
||||
self.subdivide_sharp_curves()
|
||||
if self.should_remove_null_curves:
|
||||
# Get rid of any null curves
|
||||
self.set_points(self.get_points_without_null_curves())
|
||||
if self.should_subdivide_sharp_curves:
|
||||
# For a healthy triangulation later
|
||||
self.subdivide_sharp_curves()
|
||||
if self.should_remove_null_curves:
|
||||
# Get rid of any null curves
|
||||
self.set_points(self.get_points_without_null_curves())
|
||||
|
||||
def init_points(self) -> None:
|
||||
self.generate_points()
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@ from collections.abc import Callable, Iterable, Sequence
|
|||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import Polygon
|
||||
from manim.mobject.geometry.shape_matchers import BackgroundRectangle
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.text.numbers import DecimalNumber, Integer
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.text.text_mobject import Paragraph
|
||||
|
|
@ -78,9 +84,7 @@ from ..animation.animation import Animation
|
|||
from ..animation.composition import AnimationGroup
|
||||
from ..animation.creation import Create, Write
|
||||
from ..animation.fading import FadeIn
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from ..utils.color import BLACK, PURE_YELLOW, ManimColor, ParsableManimColor
|
||||
from .utils import get_vectorized_mobject_class
|
||||
|
||||
|
||||
class Table(VGroup):
|
||||
|
|
@ -323,8 +327,7 @@ class Table(VGroup):
|
|||
mob_table.insert(0, col_labels)
|
||||
else:
|
||||
# Placeholder to use arrange_in_grid if top_left_entry is not set.
|
||||
# Import OpenGLVMobject to work with --renderer=opengl
|
||||
dummy_mobject = get_vectorized_mobject_class()()
|
||||
dummy_mobject = VMobject()
|
||||
col_labels = [dummy_mobject] + self.col_labels
|
||||
mob_table.insert(0, col_labels)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -19,13 +19,17 @@ from pygments.styles import get_all_styles
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Dot
|
||||
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.typing import StrPath
|
||||
from manim.utils.color import WHITE, ManimColor
|
||||
|
||||
|
||||
class Code(VMobject, metaclass=ConvertToOpenGL):
|
||||
class Code(VMobject):
|
||||
"""A highlighted source code listing.
|
||||
|
||||
Examples
|
||||
|
|
|
|||
|
|
@ -10,17 +10,16 @@ import numpy as np
|
|||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.value_tracker import ValueTracker
|
||||
from manim.typing import Vector3DLike
|
||||
|
||||
string_to_mob_map: dict[str, SingleStringMathTex] = {}
|
||||
|
||||
|
||||
class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
||||
class DecimalNumber(VMobject):
|
||||
r"""An mobject representing a decimal number.
|
||||
|
||||
Parameters
|
||||
|
|
@ -154,6 +153,8 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def _set_submobjects_from_number(self, number: float) -> None:
|
||||
self.number = number
|
||||
# the self.add below will recalculate the family,
|
||||
# no need to do it here.
|
||||
self.submobjects = []
|
||||
|
||||
num_string = self._get_num_string(number)
|
||||
|
|
@ -341,7 +342,7 @@ class Integer(DecimalNumber):
|
|||
return int(np.round(super().get_value()))
|
||||
|
||||
|
||||
class Variable(VMobject, metaclass=ConvertToOpenGL):
|
||||
class Variable(VMobject):
|
||||
"""A class for displaying text that shows "label = value" with
|
||||
the value continuously updated from a :class:`~.ValueTracker`.
|
||||
|
||||
|
|
|
|||
|
|
@ -33,13 +33,12 @@ from typing import Any, Self
|
|||
from manim import config, logger
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.svg.svg_mobject import SVGMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.tex import TexTemplate
|
||||
from manim.utils.tex_file_writing import tex_to_svg_file
|
||||
|
||||
from ..opengl.opengl_compatibility import ConvertToOpenGL
|
||||
|
||||
MATHTEX_SUBSTRING = "substring"
|
||||
|
||||
|
||||
|
|
@ -67,15 +66,12 @@ class SingleStringMathTex(SVGMobject):
|
|||
color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if color is None:
|
||||
color = VMobject().color
|
||||
|
||||
self._font_size = font_size
|
||||
self.organize_left_to_right = organize_left_to_right
|
||||
self.tex_environment = tex_environment
|
||||
if tex_template is None:
|
||||
tex_template = config["tex_template"]
|
||||
self.tex_template: TexTemplate = tex_template
|
||||
tex_template = config.tex_template
|
||||
self.tex_template = tex_template
|
||||
|
||||
self.tex_string = tex_string
|
||||
file_name = tex_to_svg_file(
|
||||
|
|
@ -312,7 +308,7 @@ class MathTex(SingleStringMathTex):
|
|||
# Save the original tex_string
|
||||
self.tex_string = self.arg_separator.join(self.tex_strings)
|
||||
self._break_up_by_substrings()
|
||||
except ValueError as compilation_error:
|
||||
except ValueError:
|
||||
if self.brace_notation_split_occurred:
|
||||
logger.error(
|
||||
dedent(
|
||||
|
|
@ -326,7 +322,7 @@ class MathTex(SingleStringMathTex):
|
|||
""",
|
||||
),
|
||||
)
|
||||
raise compilation_error
|
||||
raise
|
||||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||
|
||||
if self.organize_left_to_right:
|
||||
|
|
@ -534,6 +530,12 @@ class MathTex(SingleStringMathTex):
|
|||
)
|
||||
new_submobjects.append(self.id_to_vgroup_dict["root"])
|
||||
self.submobjects = new_submobjects
|
||||
|
||||
# 5 hours of work went into this line
|
||||
# and it's still not perfect
|
||||
# July 18, 2024
|
||||
self.note_changed_family()
|
||||
|
||||
return self
|
||||
|
||||
def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None:
|
||||
|
|
@ -595,9 +597,10 @@ class MathTex(SingleStringMathTex):
|
|||
|
||||
def sort_alphabetically(self) -> None:
|
||||
self.submobjects.sort(key=lambda m: m.get_tex_string())
|
||||
self.note_changed_family()
|
||||
|
||||
|
||||
class MathTexPart(VMobject, metaclass=ConvertToOpenGL):
|
||||
class MathTexPart(VMobject):
|
||||
tex_string: str
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
|
|||
|
|
@ -70,14 +70,16 @@ from manimpango import MarkupUtils, PangoUtils, TextSetting
|
|||
from manim import config, logger
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Dot
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.svg.svg_mobject import SVGMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.typing import Point3D
|
||||
from manim.utils.color import ManimColor, ParsableManimColor, color_gradient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
from manim.typing import Point3D
|
||||
|
||||
TEXT_MOB_SCALE_FACTOR = 0.05
|
||||
|
|
@ -510,7 +512,7 @@ class Text(SVGMobject):
|
|||
else:
|
||||
self.line_spacing = self._font_size + self._font_size * self.line_spacing
|
||||
|
||||
parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
|
||||
parsed_color = ManimColor(color)
|
||||
file_name = self._text2svg(parsed_color.to_hex())
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
super().__init__(
|
||||
|
|
@ -526,6 +528,7 @@ class Text(SVGMobject):
|
|||
self.text = text
|
||||
if self.disable_ligatures:
|
||||
self.submobjects = [*self._gen_chars()]
|
||||
self.note_changed_family()
|
||||
self.chars = self.get_group_class()(*self.submobjects)
|
||||
self.text = text_without_tabs.replace(" ", "").replace("\n", "")
|
||||
nppc = self.n_points_per_curve
|
||||
|
|
@ -588,6 +591,11 @@ class Text(SVGMobject):
|
|||
# anti-aliasing
|
||||
if height is None and width is None:
|
||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||
|
||||
# Just a temporary hack to get better triangulation
|
||||
# See pr #1552 for details
|
||||
for i in self.submobjects:
|
||||
i.insert_n_curves(len(i.get_all_points()))
|
||||
self.initial_height = self.height
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
@ -827,13 +835,6 @@ class Text(SVGMobject):
|
|||
|
||||
return svg_file
|
||||
|
||||
def init_colors(self, propagate_colors: bool = True) -> Self:
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
super().init_colors()
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
super().init_colors(propagate_colors=propagate_colors)
|
||||
return self
|
||||
|
||||
|
||||
class MarkupText(SVGMobject):
|
||||
r"""Display (non-LaTeX) text rendered using `Pango <https://pango.org/>`_.
|
||||
|
|
@ -1208,7 +1209,7 @@ class MarkupText(SVGMobject):
|
|||
else:
|
||||
self.line_spacing = self._font_size + self._font_size * self.line_spacing
|
||||
|
||||
parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
|
||||
parsed_color = ManimColor(color)
|
||||
file_name = self._text2svg(parsed_color)
|
||||
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ import numpy as np
|
|||
|
||||
from manim.mobject.geometry.polygram import Polygon
|
||||
from manim.mobject.graph import Graph
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.three_d.three_dimensions import Dot3D
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.qhull import QuickHull
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.typing import Point3D, Point3DLike_Array
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -52,9 +52,9 @@ class Polyhedron(VGroup):
|
|||
.. manim:: SquarePyramidScene
|
||||
:save_last_frame:
|
||||
|
||||
class SquarePyramidScene(ThreeDScene):
|
||||
class SquarePyramidScene(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
vertex_coords = [
|
||||
[1, 1, 0],
|
||||
[1, -1, 0],
|
||||
|
|
@ -86,9 +86,9 @@ class Polyhedron(VGroup):
|
|||
.. manim:: PolyhedronSubMobjects
|
||||
:save_last_frame:
|
||||
|
||||
class PolyhedronSubMobjects(ThreeDScene):
|
||||
class PolyhedronSubMobjects(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
octahedron = Octahedron(edge_length = 3)
|
||||
octahedron.graph[0].set_color(RED)
|
||||
octahedron.faces[2].set_color(YELLOW)
|
||||
|
|
@ -173,9 +173,9 @@ class Tetrahedron(Polyhedron):
|
|||
.. manim:: TetrahedronScene
|
||||
:save_last_frame:
|
||||
|
||||
class TetrahedronScene(ThreeDScene):
|
||||
class TetrahedronScene(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
obj = Tetrahedron()
|
||||
self.add(obj)
|
||||
"""
|
||||
|
|
@ -208,9 +208,9 @@ class Octahedron(Polyhedron):
|
|||
.. manim:: OctahedronScene
|
||||
:save_last_frame:
|
||||
|
||||
class OctahedronScene(ThreeDScene):
|
||||
class OctahedronScene(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
obj = Octahedron()
|
||||
self.add(obj)
|
||||
"""
|
||||
|
|
@ -254,9 +254,9 @@ class Icosahedron(Polyhedron):
|
|||
.. manim:: IcosahedronScene
|
||||
:save_last_frame:
|
||||
|
||||
class IcosahedronScene(ThreeDScene):
|
||||
class IcosahedronScene(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
obj = Icosahedron()
|
||||
self.add(obj)
|
||||
"""
|
||||
|
|
@ -319,9 +319,9 @@ class Dodecahedron(Polyhedron):
|
|||
.. manim:: DodecahedronScene
|
||||
:save_last_frame:
|
||||
|
||||
class DodecahedronScene(ThreeDScene):
|
||||
class DodecahedronScene(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
obj = Dodecahedron()
|
||||
self.add(obj)
|
||||
"""
|
||||
|
|
@ -389,9 +389,9 @@ class ConvexHull3D(Polyhedron):
|
|||
:save_last_frame:
|
||||
:quality: high
|
||||
|
||||
class ConvexHull3DExample(ThreeDScene):
|
||||
class ConvexHull3DExample(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
points = [
|
||||
[ 1.93192757, 0.44134585, -1.52407061],
|
||||
[-0.93302521, 1.23206983, 0.64117067],
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ from manim.constants import ORIGIN, UP
|
|||
from manim.utils.space_ops import get_unit_normal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.typing import Point3D, Vector3D
|
||||
|
||||
from ..types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
def get_3d_vmob_gradient_start_and_end_points(
|
||||
vmob: VMobject,
|
||||
|
|
|
|||
|
|
@ -25,10 +25,16 @@ from manim import config, logger
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square
|
||||
from manim.mobject.mobject import *
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup, VMobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVectorizedPoint as VectorizedPoint,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.utils.color import (
|
||||
BLUE,
|
||||
BLUE_D,
|
||||
|
|
@ -46,7 +52,7 @@ if TYPE_CHECKING:
|
|||
from manim.typing import Point3D, Point3DLike, Vector3D, Vector3DLike
|
||||
|
||||
|
||||
class ThreeDVMobject(VMobject, metaclass=ConvertToOpenGL):
|
||||
class ThreeDVMobject(VMobject):
|
||||
u_index: int
|
||||
v_index: int
|
||||
u1: float
|
||||
|
|
@ -58,7 +64,7 @@ class ThreeDVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
super().__init__(shade_in_3d=shade_in_3d, **kwargs)
|
||||
|
||||
|
||||
class Surface(VGroup, metaclass=ConvertToOpenGL):
|
||||
class Surface(VGroup):
|
||||
"""Creates a Parametric Surface using a checkerboard pattern.
|
||||
|
||||
Parameters
|
||||
|
|
@ -94,7 +100,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
.. manim:: ParaSurface
|
||||
:save_last_frame:
|
||||
|
||||
class ParaSurface(ThreeDScene):
|
||||
class ParaSurface(Scene):
|
||||
def func(self, u, v):
|
||||
return np.array([np.cos(u) * np.cos(v), np.cos(u) * np.sin(v), u])
|
||||
|
||||
|
|
@ -106,7 +112,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
v_range=[0, TAU],
|
||||
resolution=8,
|
||||
)
|
||||
self.set_camera_orientation(theta=70 * DEGREES, phi=75 * DEGREES)
|
||||
self.camera.set_orientation(theta=70 * DEGREES, phi=75 * DEGREES)
|
||||
self.add(axes, surface)
|
||||
"""
|
||||
|
||||
|
|
@ -125,6 +131,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
],
|
||||
stroke_color: ParsableManimColor = LIGHT_GREY,
|
||||
stroke_width: float = 0.5,
|
||||
stroke_opacity: float = 1.0, # TODO: placed temporarily to have a stroke_opacity
|
||||
should_make_jagged: bool = False,
|
||||
pre_function_handle_to_anchor_scale_factor: float = 0.00001,
|
||||
**kwargs: Any,
|
||||
|
|
@ -136,6 +143,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
fill_opacity=fill_opacity,
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
stroke_opacity=stroke_opacity,
|
||||
**kwargs,
|
||||
)
|
||||
self.resolution = resolution
|
||||
|
|
@ -193,12 +201,8 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
face.u2 = u2
|
||||
face.v1 = v1
|
||||
face.v2 = v2
|
||||
faces.set_fill(color=self.fill_color, opacity=self.fill_opacity)
|
||||
faces.set_stroke(
|
||||
color=self.stroke_color,
|
||||
width=self.stroke_width,
|
||||
opacity=self.stroke_opacity,
|
||||
)
|
||||
faces.set_fill(color=self.fill_color)
|
||||
faces.set_stroke(color=self.stroke_color, width=self.stroke_width)
|
||||
self.add(*faces)
|
||||
if self.checkerboard_colors:
|
||||
self.set_fill_by_checkerboard(*self.checkerboard_colors)
|
||||
|
|
@ -262,16 +266,18 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
.. manim:: FillByValueExample
|
||||
:save_last_frame:
|
||||
|
||||
class FillByValueExample(ThreeDScene):
|
||||
class FillByValueExample(Scene):
|
||||
def construct(self):
|
||||
resolution_fa = 8
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=-160 * DEGREES)
|
||||
self.camera.set_orientation(theta=-160 * DEGREES, phi=75 * DEGREES)
|
||||
axes = ThreeDAxes(x_range=(0, 5, 1), y_range=(0, 5, 1), z_range=(-1, 1, 0.5))
|
||||
|
||||
def param_surface(u, v):
|
||||
x = u
|
||||
y = v
|
||||
z = np.sin(x) * np.cos(y)
|
||||
return z
|
||||
|
||||
surface_plane = Surface(
|
||||
lambda u, v: axes.c2p(u, v, param_surface(u, v)),
|
||||
resolution=(resolution_fa, resolution_fa),
|
||||
|
|
@ -337,11 +343,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
new_colors[i],
|
||||
color_index,
|
||||
)
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
assert isinstance(mob, OpenGLMobject)
|
||||
mob.set_color(mob_color, recurse=False)
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
mob.set_color(mob_color, family=False)
|
||||
mob.set_color(mob_color, recurse=False)
|
||||
break
|
||||
|
||||
return self
|
||||
|
|
@ -373,9 +375,9 @@ class Sphere(Surface):
|
|||
.. manim:: ExampleSphere
|
||||
:save_last_frame:
|
||||
|
||||
class ExampleSphere(ThreeDScene):
|
||||
class ExampleSphere(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=PI / 6, theta=PI / 6)
|
||||
self.camera.set_orientation(theta=PI / 6, phi=PI / 6)
|
||||
sphere1 = Sphere(
|
||||
center=(3, 0, 0),
|
||||
radius=1,
|
||||
|
|
@ -457,9 +459,9 @@ class Dot3D(Sphere):
|
|||
.. manim:: Dot3DExample
|
||||
:save_last_frame:
|
||||
|
||||
class Dot3DExample(ThreeDScene):
|
||||
class Dot3DExample(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75*DEGREES, theta=-45*DEGREES)
|
||||
self.camera.set_orientation(theta=-45*DEGREES, phi=75*DEGREES)
|
||||
|
||||
axes = ThreeDAxes()
|
||||
dot_1 = Dot3D(point=axes.coords_to_point(0, 0, 1), color=RED)
|
||||
|
|
@ -501,9 +503,9 @@ class Cube(VGroup):
|
|||
.. manim:: CubeExample
|
||||
:save_last_frame:
|
||||
|
||||
class CubeExample(ThreeDScene):
|
||||
class CubeExample(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75*DEGREES, theta=-45*DEGREES)
|
||||
self.camera.set_orientation(theta=-45*DEGREES, phi=75*DEGREES)
|
||||
|
||||
axes = ThreeDAxes()
|
||||
cube = Cube(side_length=3, fill_opacity=0.7, fill_color=BLUE)
|
||||
|
|
@ -559,9 +561,9 @@ class Prism(Cube):
|
|||
.. manim:: ExamplePrism
|
||||
:save_last_frame:
|
||||
|
||||
class ExamplePrism(ThreeDScene):
|
||||
class ExamplePrism(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=60 * DEGREES, theta=150 * DEGREES)
|
||||
self.camera.set_orientation(theta=150 * DEGREES, phi=60 * DEGREES)
|
||||
prismSmall = Prism(dimensions=[1, 2, 3]).rotate(PI / 2)
|
||||
prismLarge = Prism(dimensions=[1.5, 3, 4.5]).move_to([2, 0, 0])
|
||||
self.add(prismSmall, prismLarge)
|
||||
|
|
@ -612,11 +614,11 @@ class Cone(Surface):
|
|||
.. manim:: ExampleCone
|
||||
:save_last_frame:
|
||||
|
||||
class ExampleCone(ThreeDScene):
|
||||
class ExampleCone(Scene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
cone = Cone(direction=X_AXIS+Y_AXIS+2*Z_AXIS, resolution=8)
|
||||
self.set_camera_orientation(phi=5*PI/11, theta=PI/9)
|
||||
self.camera.set_orientation(theta=PI/9, phi=5*PI/11)
|
||||
self.add(axes, cone)
|
||||
"""
|
||||
|
||||
|
|
@ -647,8 +649,7 @@ class Cone(Surface):
|
|||
self._current_phi = 0
|
||||
self.base_circle = Circle(
|
||||
radius=base_radius,
|
||||
color=self.fill_color,
|
||||
fill_opacity=self.fill_opacity,
|
||||
color=self.get_fill_colors(),
|
||||
stroke_width=0,
|
||||
)
|
||||
self.base_circle.shift(height * IN)
|
||||
|
|
@ -774,11 +775,11 @@ class Cylinder(Surface):
|
|||
.. manim:: ExampleCylinder
|
||||
:save_last_frame:
|
||||
|
||||
class ExampleCylinder(ThreeDScene):
|
||||
class ExampleCylinder(Scene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
cylinder = Cylinder(radius=2, height=3)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.add(axes, cylinder)
|
||||
"""
|
||||
|
||||
|
|
@ -829,14 +830,14 @@ class Cylinder(Surface):
|
|||
|
||||
def add_bases(self) -> None:
|
||||
"""Adds the end caps of the cylinder."""
|
||||
opacity: float
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
assert isinstance(self, OpenGLMobject)
|
||||
color = self.color
|
||||
opacity = self.opacity
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
color = self.fill_color
|
||||
opacity = self.fill_opacity
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
# TODO: Surface should be made a separate mobject type
|
||||
# (like it is for OpenGL) for the Cairo renderer too,
|
||||
# to make them have the same interface.
|
||||
raise NotImplementedError
|
||||
|
||||
color = self.color
|
||||
opacity = self.opacity
|
||||
|
||||
self.base_top = Circle(
|
||||
radius=self.radius,
|
||||
|
|
@ -911,7 +912,7 @@ class Cylinder(Surface):
|
|||
|
||||
|
||||
class Line3D(Cylinder):
|
||||
"""A cylindrical line, for use in ThreeDScene.
|
||||
"""A cylindrical line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -935,11 +936,11 @@ class Line3D(Cylinder):
|
|||
.. manim:: ExampleLine3D
|
||||
:save_last_frame:
|
||||
|
||||
class ExampleLine3D(ThreeDScene):
|
||||
class ExampleLine3D(Scene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
line = Line3D(start=np.array([0, 0, 0]), end=np.array([2, 2, 2]))
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.add(axes, line)
|
||||
"""
|
||||
|
||||
|
|
@ -1017,7 +1018,7 @@ class Line3D(Cylinder):
|
|||
:class:`numpy.array`
|
||||
Center of the :class:`Mobjects <.Mobject>` or point, or edge if direction is given.
|
||||
"""
|
||||
if isinstance(mob_or_point, (Mobject, OpenGLMobject)):
|
||||
if isinstance(mob_or_point, Mobject):
|
||||
mob = mob_or_point
|
||||
if direction is None:
|
||||
return mob.get_center()
|
||||
|
|
@ -1077,9 +1078,9 @@ class Line3D(Cylinder):
|
|||
.. manim:: ParallelLineExample
|
||||
:save_last_frame:
|
||||
|
||||
class ParallelLineExample(ThreeDScene):
|
||||
class ParallelLineExample(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(PI / 3, -PI / 4)
|
||||
self.camera.set_orientation(theta=-PI / 4, phi=PI / 3)
|
||||
ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10)
|
||||
line1 = Line3D(RIGHT * 2, UP + OUT, color=RED)
|
||||
line2 = Line3D.parallel_to(line1, color=YELLOW)
|
||||
|
|
@ -1125,9 +1126,9 @@ class Line3D(Cylinder):
|
|||
.. manim:: PerpLineExample
|
||||
:save_last_frame:
|
||||
|
||||
class PerpLineExample(ThreeDScene):
|
||||
class PerpLineExample(Scene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(PI / 3, -PI / 4)
|
||||
self.camera.set_orientation(theta=-PI / 4, phi=PI / 3)
|
||||
ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10)
|
||||
line1 = Line3D(RIGHT * 2, UP + OUT, color=RED)
|
||||
line2 = Line3D.perpendicular_to(line1, color=BLUE)
|
||||
|
|
@ -1173,7 +1174,7 @@ class Arrow3D(Line3D):
|
|||
.. manim:: ExampleArrow3D
|
||||
:save_last_frame:
|
||||
|
||||
class ExampleArrow3D(ThreeDScene):
|
||||
class ExampleArrow3D(Scene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
arrow = Arrow3D(
|
||||
|
|
@ -1181,7 +1182,7 @@ class Arrow3D(Line3D):
|
|||
end=np.array([2, 2, 2]),
|
||||
resolution=8
|
||||
)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.add(axes, arrow)
|
||||
"""
|
||||
|
||||
|
|
@ -1249,11 +1250,11 @@ class Torus(Surface):
|
|||
.. manim :: ExampleTorus
|
||||
:save_last_frame:
|
||||
|
||||
class ExampleTorus(ThreeDScene):
|
||||
class ExampleTorus(Scene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
torus = Torus()
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.add(axes, torus)
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@ from PIL import Image
|
|||
from PIL.Image import Resampling
|
||||
|
||||
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
|
||||
from ... import config
|
||||
from ...camera.moving_camera import MovingCamera
|
||||
from ...constants import *
|
||||
from ...mobject.mobject import Mobject
|
||||
from ...utils.bezier import interpolate
|
||||
from ...utils.color import (
|
||||
WHITE,
|
||||
|
|
@ -27,8 +26,6 @@ from ...utils.color import (
|
|||
)
|
||||
from ...utils.images import change_to_rgba_array, get_full_raster_image_path
|
||||
|
||||
__all__ = ["ImageMobject", "ImageMobjectFromCamera"]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
|
|
@ -36,8 +33,6 @@ if TYPE_CHECKING:
|
|||
|
||||
from manim.typing import PixelArray, StrPath
|
||||
|
||||
from ...camera.moving_camera import MovingCamera
|
||||
|
||||
|
||||
class AbstractImageMobject(Mobject):
|
||||
"""
|
||||
|
|
@ -217,6 +212,10 @@ class ImageMobject(AbstractImageMobject):
|
|||
"""A simple getter method."""
|
||||
return self.pixel_array
|
||||
|
||||
def init_colors(self) -> None:
|
||||
"""Override base init_colors to avoid overwriting image pixels during init."""
|
||||
return None
|
||||
|
||||
def set_color( # type: ignore[override]
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW_C,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,10 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_point_cloud_mobject import OpenGLPMobject
|
||||
|
||||
from ...constants import *
|
||||
from ...mobject.mobject import Mobject
|
||||
from ...mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from ...utils.bezier import interpolate
|
||||
from ...utils.color import (
|
||||
BLACK,
|
||||
|
|
@ -27,7 +26,7 @@ from ...utils.color import (
|
|||
)
|
||||
from ...utils.iterables import stretch_array_to_length
|
||||
|
||||
__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot", "Point"]
|
||||
__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot"]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
|
@ -44,7 +43,7 @@ if TYPE_CHECKING:
|
|||
)
|
||||
|
||||
|
||||
class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
||||
class PMobject(Mobject):
|
||||
"""A disc made of a cloud of Dots
|
||||
|
||||
Examples
|
||||
|
|
@ -202,6 +201,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
for attr, array in zip(attrs, arrays, strict=True):
|
||||
setattr(self, attr, array)
|
||||
self.submobjects = []
|
||||
self.note_changed_family()
|
||||
return self
|
||||
|
||||
def get_color(self) -> ManimColor:
|
||||
|
|
@ -225,7 +225,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
def get_point_mobject(self, center: Point3DLike | None = None) -> Point:
|
||||
if center is None:
|
||||
center = self.get_center()
|
||||
return Point(center)
|
||||
return PMobject().set_points([center])
|
||||
|
||||
def interpolate_color(
|
||||
self, mobject1: Mobject, mobject2: Mobject, alpha: float
|
||||
|
|
@ -249,7 +249,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
|
||||
# TODO, Make the two implementations below non-redundant
|
||||
class Mobject1D(PMobject, metaclass=ConvertToOpenGL):
|
||||
class Mobject1D(PMobject):
|
||||
def __init__(self, density: int = DEFAULT_POINT_DENSITY_1D, **kwargs: Any) -> None:
|
||||
self.density = density
|
||||
self.epsilon = 1.0 / self.density
|
||||
|
|
@ -273,7 +273,7 @@ class Mobject1D(PMobject, metaclass=ConvertToOpenGL):
|
|||
self.add_points(points, color=color)
|
||||
|
||||
|
||||
class Mobject2D(PMobject, metaclass=ConvertToOpenGL):
|
||||
class Mobject2D(PMobject):
|
||||
def __init__(self, density: int = DEFAULT_POINT_DENSITY_2D, **kwargs: Any) -> None:
|
||||
self.density = density
|
||||
self.epsilon = 1.0 / self.density
|
||||
|
|
@ -388,39 +388,3 @@ class PointCloudDot(Mobject1D):
|
|||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Point(PMobject):
|
||||
"""A mobject representing a point.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: ExamplePoint
|
||||
:save_last_frame:
|
||||
|
||||
class ExamplePoint(Scene):
|
||||
def construct(self):
|
||||
colorList = [RED, GREEN, BLUE, YELLOW]
|
||||
for i in range(200):
|
||||
point = Point(location=[0.63 * np.random.randint(-4, 4), 0.37 * np.random.randint(-4, 4), 0], color=np.random.choice(colorList))
|
||||
self.add(point)
|
||||
for i in range(200):
|
||||
point = Point(location=[0.37 * np.random.randint(-4, 4), 0.63 * np.random.randint(-4, 4), 0], color=np.random.choice(colorList))
|
||||
self.add(point)
|
||||
self.add(point)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, location: Point3DLike = ORIGIN, color: ManimColor = BLACK, **kwargs: Any
|
||||
) -> None:
|
||||
self.location = location
|
||||
super().__init__(color=color, **kwargs)
|
||||
|
||||
def init_points(self) -> None:
|
||||
self.reset_points()
|
||||
self.generate_points()
|
||||
self.set_points([self.location])
|
||||
|
||||
def generate_points(self) -> None:
|
||||
self.add_points(np.array([self.location]))
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ from manim.utils.bezier import (
|
|||
proportions_along_bezier_curve_for_point,
|
||||
)
|
||||
from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor
|
||||
from manim.utils.deprecation import deprecated
|
||||
from manim.utils.iterables import (
|
||||
make_even,
|
||||
resize_array,
|
||||
|
|
@ -128,8 +129,6 @@ class VMobject(Mobject):
|
|||
cap_style: CapStyleType = CapStyleType.AUTO,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.fill_opacity = fill_opacity
|
||||
self.stroke_opacity = stroke_opacity
|
||||
self.stroke_width = stroke_width
|
||||
if background_stroke_color is not None:
|
||||
self.background_stroke_color: ManimColor = ManimColor(
|
||||
|
|
@ -161,14 +160,19 @@ class VMobject(Mobject):
|
|||
self.submobjects: list[VMobject]
|
||||
|
||||
# TODO: Find where color overwrites are happening and remove the color doubling
|
||||
# if "color" in kwargs:
|
||||
# fill_color = kwargs["color"]
|
||||
# stroke_color = kwargs["color"]
|
||||
if "color" in kwargs:
|
||||
fill_color = kwargs["color"]
|
||||
stroke_color = kwargs["color"]
|
||||
if fill_color is not None:
|
||||
self.fill_color = ManimColor.parse(fill_color)
|
||||
if stroke_color is not None:
|
||||
self.stroke_color = ManimColor.parse(stroke_color)
|
||||
|
||||
if fill_opacity is not None:
|
||||
self.fill_color = self.fill_color.opacity(fill_opacity)
|
||||
if stroke_opacity is not None:
|
||||
self.stroke_color = self.stroke_color.opacity(stroke_opacity)
|
||||
|
||||
def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
|
||||
return self._assert_valid_submobjects_internal(submobjects, VMobject)
|
||||
|
||||
|
|
@ -188,13 +192,11 @@ class VMobject(Mobject):
|
|||
def init_colors(self, propagate_colors: bool = True) -> Self:
|
||||
self.set_fill(
|
||||
color=self.fill_color,
|
||||
opacity=self.fill_opacity,
|
||||
family=propagate_colors,
|
||||
)
|
||||
self.set_stroke(
|
||||
color=self.stroke_color,
|
||||
width=self.stroke_width,
|
||||
opacity=self.stroke_opacity,
|
||||
family=propagate_colors,
|
||||
)
|
||||
self.set_background_stroke(
|
||||
|
|
@ -318,10 +320,8 @@ class VMobject(Mobject):
|
|||
if family:
|
||||
for submobject in self.submobjects:
|
||||
submobject.set_fill(color, opacity, family)
|
||||
self.update_rgbas_array("fill_rgbas", color, opacity)
|
||||
self.fill_rgbas: FloatRGBA_Array
|
||||
if opacity is not None:
|
||||
self.fill_opacity = opacity
|
||||
array_name = "fill_rgbas"
|
||||
self.update_rgbas_array(array_name, color, opacity)
|
||||
return self
|
||||
|
||||
def set_stroke(
|
||||
|
|
@ -773,6 +773,18 @@ class VMobject(Mobject):
|
|||
self.points: Point3D_Array = np.array(points)
|
||||
return self
|
||||
|
||||
def set_z(self, z: float) -> Self:
|
||||
self.points[..., -1] = z
|
||||
return self
|
||||
|
||||
@deprecated(
|
||||
since="0.18.2",
|
||||
until="0.19.0",
|
||||
message="OpenGL has no concept of z_index. Use set_z instead",
|
||||
)
|
||||
def set_z_index(self, z: float) -> Self:
|
||||
return self.set_z(z)
|
||||
|
||||
def resize_points(
|
||||
self,
|
||||
new_length: int,
|
||||
|
|
@ -2152,11 +2164,8 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
|
|||
f"submobject{'s' if len(self.submobjects) > 0 else ''}"
|
||||
)
|
||||
|
||||
def add(
|
||||
self,
|
||||
*vmobjects: VMobject | Iterable[VMobject],
|
||||
) -> Self:
|
||||
"""Checks if all passed elements are an instance, or iterables of VMobject and then adds them to submobjects
|
||||
def add(self, *vmobjects: OpenGLVMobject) -> Self:
|
||||
"""Checks if all passed elements are an instance of VMobject and then add them to submobjects
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -2474,7 +2483,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
|
|||
my_dict.remove("square")
|
||||
"""
|
||||
if key not in self.submob_dict:
|
||||
raise KeyError(f"The given key '{key!s}' is not present in the VDict")
|
||||
raise KeyError(f"The given key {key!r} is not present in the VDict")
|
||||
super().remove(self.submob_dict[key])
|
||||
del self.submob_dict[key]
|
||||
return self
|
||||
|
|
@ -2679,6 +2688,7 @@ class VectorizedPoint(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.set_points(np.array([new_loc]))
|
||||
|
||||
|
||||
# TODO: Move somewhere to match opengl
|
||||
class CurvesAsSubmobjects(VGroup):
|
||||
"""Convert a curve's elements to submobjects.
|
||||
|
||||
|
|
@ -2698,9 +2708,9 @@ class CurvesAsSubmobjects(VGroup):
|
|||
|
||||
def __init__(self, vmobject: VMobject, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
tuples = vmobject.get_cubic_bezier_tuples()
|
||||
tuples = vmobject.get_bezier_tuples()
|
||||
for tup in tuples:
|
||||
part = VMobject()
|
||||
part = OpenGLVMobject()
|
||||
part.set_points(tup)
|
||||
part.match_style(vmobject)
|
||||
self.add(part)
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
"""Utilities for working with mobjects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"get_mobject_class",
|
||||
"get_point_mobject_class",
|
||||
"get_vectorized_mobject_class",
|
||||
]
|
||||
|
||||
from .._config import config
|
||||
from ..constants import RendererType
|
||||
from .mobject import Mobject
|
||||
from .opengl.opengl_mobject import OpenGLMobject
|
||||
from .opengl.opengl_point_cloud_mobject import OpenGLPMobject
|
||||
from .opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from .types.point_cloud_mobject import PMobject
|
||||
from .types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
def get_mobject_class() -> type:
|
||||
"""Gets the base mobject class, depending on the currently active renderer.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
This method is intended to be used in the code base of Manim itself
|
||||
or in plugins where code should work independent of the selected
|
||||
renderer.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
The function has to be explicitly imported. We test that
|
||||
the name of the returned class is one of the known mobject
|
||||
base classes::
|
||||
|
||||
>>> from manim.mobject.utils import get_mobject_class
|
||||
>>> get_mobject_class().__name__ in ['Mobject', 'OpenGLMobject']
|
||||
True
|
||||
"""
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
return Mobject
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
return OpenGLMobject
|
||||
raise NotImplementedError(
|
||||
"Base mobjects are not implemented for the active renderer."
|
||||
)
|
||||
|
||||
|
||||
def get_vectorized_mobject_class() -> type:
|
||||
"""Gets the vectorized mobject class, depending on the currently
|
||||
active renderer.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
This method is intended to be used in the code base of Manim itself
|
||||
or in plugins where code should work independent of the selected
|
||||
renderer.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
The function has to be explicitly imported. We test that
|
||||
the name of the returned class is one of the known mobject
|
||||
base classes::
|
||||
|
||||
>>> from manim.mobject.utils import get_vectorized_mobject_class
|
||||
>>> get_vectorized_mobject_class().__name__ in ['VMobject', 'OpenGLVMobject']
|
||||
True
|
||||
"""
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
return VMobject
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
return OpenGLVMobject
|
||||
raise NotImplementedError(
|
||||
"Vectorized mobjects are not implemented for the active renderer."
|
||||
)
|
||||
|
||||
|
||||
def get_point_mobject_class() -> type:
|
||||
"""Gets the point cloud mobject class, depending on the currently
|
||||
active renderer.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
This method is intended to be used in the code base of Manim itself
|
||||
or in plugins where code should work independent of the selected
|
||||
renderer.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
The function has to be explicitly imported. We test that
|
||||
the name of the returned class is one of the known mobject
|
||||
base classes::
|
||||
|
||||
>>> from manim.mobject.utils import get_point_mobject_class
|
||||
>>> get_point_mobject_class().__name__ in ['PMobject', 'OpenGLPMobject']
|
||||
True
|
||||
"""
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
return PMobject
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
return OpenGLPMobject
|
||||
raise NotImplementedError(
|
||||
"Point cloud mobjects are not implemented for the active renderer."
|
||||
)
|
||||
|
|
@ -8,8 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.utils.paths import straight_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -18,7 +17,7 @@ if TYPE_CHECKING:
|
|||
from manim.typing import PathFuncType
|
||||
|
||||
|
||||
class ValueTracker(Mobject, metaclass=ConvertToOpenGL):
|
||||
class ValueTracker(Mobject):
|
||||
"""A mobject that can be used for tracking (real-valued) parameters.
|
||||
Useful for animating parameter changes.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,15 +20,19 @@ from PIL import Image
|
|||
from manim.animation.updaters.update import UpdateFromAlphaFunc
|
||||
from manim.mobject.geometry.line import Vector
|
||||
from manim.mobject.graphing.coordinate_systems import CoordinateSystem
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
|
||||
from .. import config
|
||||
from ..animation.composition import AnimationGroup, Succession
|
||||
from ..animation.creation import Create
|
||||
from ..animation.indication import ShowPassingFlash
|
||||
from ..constants import OUT, RIGHT, UP, RendererType
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.types.vectorized_mobject import VGroup
|
||||
from ..mobject.utils import get_vectorized_mobject_class
|
||||
from ..utils.bezier import interpolate, inverse_interpolate
|
||||
from ..utils.color import (
|
||||
BLUE_E,
|
||||
|
|
@ -835,7 +839,7 @@ class StreamLines(VectorField):
|
|||
step = max_steps
|
||||
if not step:
|
||||
continue
|
||||
line = get_vectorized_mobject_class()()
|
||||
line = VMobject()
|
||||
line.duration = step * dt
|
||||
step = max(1, int(len(points) / self.max_anchors_per_line))
|
||||
line.set_points_smoothly(points[::step])
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
from dearpygui import dearpygui as dpg
|
||||
|
||||
|
||||
from manim.mobject.opengl.dot_cloud import *
|
||||
from manim.mobject.opengl.opengl_image_mobject import *
|
||||
from manim.mobject.opengl.opengl_mobject import *
|
||||
from manim.mobject.opengl.opengl_point_cloud_mobject import *
|
||||
from manim.mobject.opengl.opengl_surface import *
|
||||
from manim.mobject.opengl.opengl_three_dimensions import *
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import *
|
||||
|
||||
from ..renderer.shader import *
|
||||
from ..utils.opengl import *
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue