mirror of
https://github.com/3b1b/manim.git
synced 2026-01-12 16:08:16 -05:00
Compare commits
231 Commits
v1.7.0
...
video-work
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9da66250ee | ||
|
|
bca82da8a8 | ||
|
|
e5298385ed | ||
|
|
30303e0ab1 | ||
|
|
b39b3e6256 | ||
|
|
b54f0659b7 | ||
|
|
15f9bfff6f | ||
|
|
cf168b415c | ||
|
|
13054d6c3c | ||
|
|
69f09615b1 | ||
|
|
f4c04244c7 | ||
|
|
043a0273ef | ||
|
|
e2cc6cd56e | ||
|
|
d301c5037f | ||
|
|
0c245d5e09 | ||
|
|
828cbec384 | ||
|
|
91bbd5566d | ||
|
|
75a789f37e | ||
|
|
db3bde18ba | ||
|
|
30763084f1 | ||
|
|
7839e1d6b3 | ||
|
|
22fa38fab9 | ||
|
|
057900ef8b | ||
|
|
60772ccfdc | ||
|
|
b13498ab31 | ||
|
|
41613db7ec | ||
|
|
c48c4b6a9f | ||
|
|
cc5fbe17b9 | ||
|
|
98faf7ed55 | ||
|
|
d330e1db7f | ||
|
|
e81dfab0e6 | ||
|
|
fd2a6a69e5 | ||
|
|
6fb1845f4a | ||
|
|
7787730743 | ||
|
|
a5a73cb2da | ||
|
|
bd8d2fbc99 | ||
|
|
53bc83d94a | ||
|
|
8b9ae95703 | ||
|
|
c667136060 | ||
|
|
66e8b04507 | ||
|
|
c7ef8404b7 | ||
|
|
f4737828f6 | ||
|
|
be7d93cf40 | ||
|
|
dbfe7ac75d | ||
|
|
7a61a13691 | ||
|
|
3e307926fd | ||
|
|
2ddec95ce5 | ||
|
|
db421e3981 | ||
|
|
7a7bf83f11 | ||
|
|
24eefef5bf | ||
|
|
96d44bd560 | ||
|
|
39fbb677dc | ||
|
|
c13d2a946b | ||
|
|
0c69ab6a32 | ||
|
|
f427fc67df | ||
|
|
3d9a0cd25e | ||
|
|
33dbf04985 | ||
|
|
744e695340 | ||
|
|
00b34f2020 | ||
|
|
bafea89ac9 | ||
|
|
eeb4fdf270 | ||
|
|
e2e785d6c9 | ||
|
|
c6c1a49ede | ||
|
|
6d753a297a | ||
|
|
f9fc543b07 | ||
|
|
bac0c0c9b9 | ||
|
|
9ae5b4dee3 | ||
|
|
0b350e248b | ||
|
|
7148d6bced | ||
|
|
b470a47da7 | ||
|
|
13fdc9629d | ||
|
|
fce92347fa | ||
|
|
185f642826 | ||
|
|
4a6a125739 | ||
|
|
8246d0da5d | ||
|
|
1794e4d0ba | ||
|
|
4d7f6093b4 | ||
|
|
37a05094ea | ||
|
|
76afc42e9a | ||
|
|
5fcb668f07 | ||
|
|
2d7b9d579a | ||
|
|
9ac16ab722 | ||
|
|
8744c878f4 | ||
|
|
9fcdd0de5f | ||
|
|
9f785a5fba | ||
|
|
a03accff9c | ||
|
|
7d3758c44c | ||
|
|
f9a44c9975 | ||
|
|
d5c36de3c5 | ||
|
|
c9b6ee57a8 | ||
|
|
2c43d293a5 | ||
|
|
3d3f8258f4 | ||
|
|
17f37ff02a | ||
|
|
2359ed9aa4 | ||
|
|
32d36a09f6 | ||
|
|
8cf95ec9a4 | ||
|
|
24697377db | ||
|
|
d21fbd02bc | ||
|
|
284c1d8f2c | ||
|
|
ae93d8fcc6 | ||
|
|
1d67768a13 | ||
|
|
07bb34793e | ||
|
|
cd744024ea | ||
|
|
667cfaf160 | ||
|
|
c61e0bcee5 | ||
|
|
d1080aa6fd | ||
|
|
f9fa8ac846 | ||
|
|
bcc4235e2f | ||
|
|
c51a84a6ee | ||
|
|
6b38011078 | ||
|
|
858d8c122b | ||
|
|
4b483b75ce | ||
|
|
4cc2e5ed17 | ||
|
|
d4c5c4736a | ||
|
|
178cca0ca5 | ||
|
|
c02259a39e | ||
|
|
1276724891 | ||
|
|
9e77b0dcdd | ||
|
|
1a14a6bd0d | ||
|
|
950ac31b9b | ||
|
|
8706ba1589 | ||
|
|
dd508b8cfc | ||
|
|
88bae476ce | ||
|
|
7a69807ce6 | ||
|
|
6d0b23f914 | ||
|
|
bf81d94362 | ||
|
|
5b315d5c70 | ||
|
|
cb3e115a6c | ||
|
|
40b5c7c1c1 | ||
|
|
636fb3a45b | ||
|
|
ea3f77e3f1 | ||
|
|
0692afdfec | ||
|
|
14c6fdc1d9 | ||
|
|
89bf0b1297 | ||
|
|
2e8a282cc7 | ||
|
|
5fa99b7723 | ||
|
|
df1e067480 | ||
|
|
0ef12ad7e4 | ||
|
|
09c27a654f | ||
|
|
90dfb02cc6 | ||
|
|
e270f5c3d3 | ||
|
|
fadd045fc1 | ||
|
|
dd0aa14442 | ||
|
|
d357e21c1d | ||
|
|
dd251ab8c2 | ||
|
|
2e49c60148 | ||
|
|
33c7f6d063 | ||
|
|
53b6c34ebe | ||
|
|
49c2b5cfe0 | ||
|
|
09fb8d324e | ||
|
|
6196daa5ec | ||
|
|
e05cae6775 | ||
|
|
94f6f0aa96 | ||
|
|
0e83c9c0d9 | ||
|
|
5a70d67b98 | ||
|
|
66862db9b2 | ||
|
|
5d3f730824 | ||
|
|
3cd3e8cedc | ||
|
|
08acfa6f1f | ||
|
|
75527563de | ||
|
|
c96734ace0 | ||
|
|
71e440be93 | ||
|
|
8098149006 | ||
|
|
4251ff436a | ||
|
|
85f8456228 | ||
|
|
e0031c63bc | ||
|
|
361d9d0652 | ||
|
|
1d14bae092 | ||
|
|
8dfd4c1c4e | ||
|
|
96a4a4b76f | ||
|
|
0496402c55 | ||
|
|
fc32f162a0 | ||
|
|
3b9ef57b22 | ||
|
|
b593cde317 | ||
|
|
34ad61d013 | ||
|
|
cfb7d2fa47 | ||
|
|
43821ab2ba | ||
|
|
89ddfadf6b | ||
|
|
0c385e820f | ||
|
|
ac01b144e8 | ||
|
|
129e512b0c | ||
|
|
88370d4d5d | ||
|
|
671a31b298 | ||
|
|
f8280a12be | ||
|
|
d78fe93743 | ||
|
|
8239f1bf35 | ||
|
|
1fa17030a2 | ||
|
|
530cb4f104 | ||
|
|
85638d88dc | ||
|
|
fbce0b132c | ||
|
|
dd51b696e5 | ||
|
|
9cd6a87ff8 | ||
|
|
54c8a9014b | ||
|
|
e19ceaaff0 | ||
|
|
5b88d2347c | ||
|
|
c6b9826f84 | ||
|
|
90ab2f64bb | ||
|
|
ed2f9f3305 | ||
|
|
1d0deb8a33 | ||
|
|
753a042dbe | ||
|
|
55b12c902c | ||
|
|
e80b9d0e47 | ||
|
|
1248abd922 | ||
|
|
314ca89a45 | ||
|
|
0ad5a0e76e | ||
|
|
64ae1364ca | ||
|
|
af923a2327 | ||
|
|
97b6e39abb | ||
|
|
b84376d6fd | ||
|
|
9475fcd19e | ||
|
|
003c4d8626 | ||
|
|
693a859caf | ||
|
|
52948f846e | ||
|
|
1738876f43 | ||
|
|
dc731f8bf2 | ||
|
|
e5cf0558d8 | ||
|
|
1139b545f9 | ||
|
|
0b65e4c7b6 | ||
|
|
371fca147b | ||
|
|
e1816c2ac5 | ||
|
|
199395b6e3 | ||
|
|
837bb14c03 | ||
|
|
eca370f5ce | ||
|
|
5505fc1d54 | ||
|
|
04295ec177 | ||
|
|
0c7c9dee93 | ||
|
|
1a65498f97 | ||
|
|
a34c4482f6 | ||
|
|
e3e87f6110 | ||
|
|
aaa28a2712 | ||
|
|
aa18373eb7 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -151,3 +151,5 @@ dmypy.json
|
||||
# For manim
|
||||
/videos
|
||||
/custom_config.yml
|
||||
test.py
|
||||
CLAUDE.md
|
||||
|
||||
17
README.md
17
README.md
@@ -15,14 +15,16 @@ Manim is an engine for precise programmatic animations, designed for creating ex
|
||||
Note, there are two versions of manim. This repository began as a personal project by the author of [3Blue1Brown](https://www.3blue1brown.com/) for the purpose of animating those videos, with video-specific code available [here](https://github.com/3b1b/videos). In 2020 a group of developers forked it into what is now the [community edition](https://github.com/ManimCommunity/manim/), with a goal of being more stable, better tested, quicker to respond to community contributions, and all around friendlier to get started with. See [this page](https://docs.manim.community/en/stable/faq/installation.html#different-versions) for more details.
|
||||
|
||||
## Installation
|
||||
> **WARNING:** These instructions are for ManimGL _only_. Trying to use these instructions to install [ManimCommunity/manim](https://github.com/ManimCommunity/manim) or instructions there to install this version will cause problems. You should first decide which version you wish to install, then only follow the instructions for your desired version.
|
||||
>
|
||||
> [!Warning]
|
||||
> **WARNING:** These instructions are for ManimGL _only_. Trying to use these instructions to install [Manim Community/manim](https://github.com/ManimCommunity/manim) or instructions there to install this version will cause problems. You should first decide which version you wish to install, then only follow the instructions for your desired version.
|
||||
|
||||
> [!Note]
|
||||
> **Note**: To install manim directly through pip, please pay attention to the name of the installed package. This repository is ManimGL of 3b1b. The package name is `manimgl` instead of `manim` or `manimlib`. Please use `pip install manimgl` to install the version in this repository.
|
||||
|
||||
Manim runs on Python 3.7 or higher.
|
||||
|
||||
System requirements are [FFmpeg](https://ffmpeg.org/), [OpenGL](https://www.opengl.org/) and [LaTeX](https://www.latex-project.org) (optional, if you want to use LaTeX).
|
||||
For Linux, [Pango](https://pango.gnome.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
|
||||
For Linux, [Pango](https://pango.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
|
||||
|
||||
|
||||
### Directly
|
||||
@@ -67,13 +69,18 @@ manim-render example_scenes.py OpeningManimExample
|
||||
```sh
|
||||
brew install ffmpeg mactex
|
||||
```
|
||||
|
||||
2. If you are using an ARM-based processor, install Cairo.
|
||||
```sh
|
||||
arch -arm64 brew install pkg-config cairo
|
||||
```
|
||||
|
||||
2. Install latest version of manim using these command.
|
||||
3. Install latest version of manim using these command.
|
||||
```sh
|
||||
git clone https://github.com/3b1b/manim.git
|
||||
cd manim
|
||||
pip install -e .
|
||||
manimgl example_scenes.py OpeningManimExample
|
||||
manimgl example_scenes.py OpeningManimExample (make sure to add manimgl to path first.)
|
||||
```
|
||||
|
||||
## Anaconda Install
|
||||
|
||||
@@ -8,38 +8,35 @@ they are only used inside manim.
|
||||
Frame and pixel shape
|
||||
---------------------
|
||||
|
||||
These values will be determined based on the ``camera`` configuration in default_config.yml or custom_config.yml
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ASPECT_RATIO = 16.0 / 9.0
|
||||
FRAME_HEIGHT = 8.0
|
||||
FRAME_WIDTH = FRAME_HEIGHT * ASPECT_RATIO
|
||||
FRAME_Y_RADIUS = FRAME_HEIGHT / 2
|
||||
FRAME_X_RADIUS = FRAME_WIDTH / 2
|
||||
ASPECT_RATIO
|
||||
FRAME_HEIGHT
|
||||
FRAME_WIDTH
|
||||
FRAME_Y_RADIUS
|
||||
FRAME_X_RADIUS
|
||||
|
||||
DEFAULT_PIXEL_HEIGHT = 1080
|
||||
DEFAULT_PIXEL_WIDTH = 1920
|
||||
DEFAULT_FPS = 30
|
||||
DEFAULT_PIXEL_HEIGHT
|
||||
DEFAULT_PIXEL_WIDTH
|
||||
DEFAULT_FPS
|
||||
|
||||
Buffs
|
||||
-----
|
||||
|
||||
.. code-block:: python
|
||||
These values will be determined based on the ``size`` configuration in default_config.yml or custom_config.yml
|
||||
|
||||
SMALL_BUFF = 0.1
|
||||
MED_SMALL_BUFF = 0.25
|
||||
MED_LARGE_BUFF = 0.5
|
||||
LARGE_BUFF = 1
|
||||
|
||||
DEFAULT_MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF # Distance between object and edge
|
||||
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF # Distance between objects
|
||||
|
||||
Run times
|
||||
---------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0
|
||||
DEFAULT_WAIT_TIME = 1.0
|
||||
SMALL_BUFF
|
||||
MED_SMALL_BUFF
|
||||
MED_LARGE_BUFF
|
||||
LARGE_BUFF
|
||||
|
||||
DEFAULT_MOBJECT_TO_EDGE_BUFF
|
||||
DEFAULT_MOBJECT_TO_MOBJECT_BUFF
|
||||
|
||||
Coordinates
|
||||
-----------
|
||||
@@ -77,7 +74,7 @@ Mathematical constant
|
||||
|
||||
PI = np.pi
|
||||
TAU = 2 * PI
|
||||
DEGREES = TAU / 360
|
||||
DEG = TAU / 360
|
||||
|
||||
Text
|
||||
----
|
||||
@@ -89,16 +86,11 @@ Text
|
||||
OBLIQUE = "OBLIQUE"
|
||||
BOLD = "BOLD"
|
||||
|
||||
Stroke width
|
||||
------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
DEFAULT_STROKE_WIDTH = 4
|
||||
|
||||
Colours
|
||||
-------
|
||||
|
||||
Color constants are determined based on the ``color`` configuration in default_config.yml or custom_config.yml
|
||||
|
||||
Here are the preview of default colours. (Modified from
|
||||
`elteoremadebeethoven <https://elteoremadebeethoven.github.io/manim_3feb_docs.github.io/html/_static/colors/colors.html>`_)
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ custom_config
|
||||
running file under the ``output`` path, and save the output (``images/``
|
||||
or ``videos/``) in it.
|
||||
|
||||
- ``base``
|
||||
The root directory that will hold files, such as video files manim renders,
|
||||
or image resources that it pulls from
|
||||
|
||||
- ``output``
|
||||
Output file path, the videos will be saved in the ``videos/`` folder under it,
|
||||
and the pictures will be saved in the ``images/`` folder under it.
|
||||
@@ -66,34 +70,62 @@ custom_config
|
||||
The directory for storing sound files to be used in ``Scene.add_sound()`` (
|
||||
including ``.wav`` and ``.mp3``).
|
||||
|
||||
- ``temporary_storage``
|
||||
- ``cache``
|
||||
The directory for storing temporarily generated cache files, including
|
||||
``Tex`` cache, ``Text`` cache and storage of object points.
|
||||
|
||||
``tex``
|
||||
|
||||
``window``
|
||||
----------
|
||||
|
||||
- ``position_string``
|
||||
The relative position of the playback window on the display (two characters,
|
||||
the first character means upper(U) / middle(O) / lower(D), the second character
|
||||
means left(L) / middle(O) / right(R)).
|
||||
|
||||
- ``monitor_index``
|
||||
If using multiple monitors, which one should the window show up in?
|
||||
|
||||
- ``full_screen``
|
||||
Should the preview window be full screen. If not, it defaults to half the screen
|
||||
|
||||
- ``position``
|
||||
This is an option to more manually set the default window position, in pixel
|
||||
coordinates, e.g. (500, 300)
|
||||
|
||||
- ``size``
|
||||
Option to more manually set the default window size, in pixel coordinates,
|
||||
e.g. (1920, 1080)
|
||||
|
||||
|
||||
``camera``
|
||||
----------
|
||||
|
||||
- ``resolution``
|
||||
Resolution to render at, e.g. (1920, 1080)
|
||||
|
||||
- ``background_color``
|
||||
Default background color of scenes
|
||||
|
||||
- ``fps``
|
||||
Framerate
|
||||
|
||||
- ``background_opacity``
|
||||
Opacity of the background
|
||||
|
||||
|
||||
``file_writer``
|
||||
---------------
|
||||
Configuration specifying how files are written, e.g. what ffmpeg parameters to use
|
||||
|
||||
|
||||
``scene``
|
||||
-------
|
||||
Some default configuration for the Scene class
|
||||
|
||||
- ``executable``
|
||||
The executable program used to compile LaTeX (``latex`` or ``xelatex -no-pdf``
|
||||
is recommended)
|
||||
|
||||
- ``template_file``
|
||||
LaTeX template used, in ``manimlib/tex_templates``
|
||||
|
||||
- ``intermediate_filetype``
|
||||
The type of intermediate vector file generated after compilation (``dvi`` if
|
||||
``latex`` is used, ``xdv`` if ``xelatex`` is used)
|
||||
|
||||
- ``text_to_replace``
|
||||
The text to be replaced in the template (needn't to change)
|
||||
|
||||
``universal_import_line``
|
||||
-------------------------
|
||||
|
||||
Import line that need to execute when entering interactive mode directly.
|
||||
|
||||
``style``
|
||||
---------
|
||||
``text``
|
||||
-------
|
||||
|
||||
- ``font``
|
||||
Default font of Text
|
||||
@@ -101,57 +133,44 @@ Import line that need to execute when entering interactive mode directly.
|
||||
- ``text_alignment``
|
||||
Default text alignment for LaTeX
|
||||
|
||||
- ``background_color``
|
||||
Default background color
|
||||
|
||||
``window_position``
|
||||
-------------------
|
||||
|
||||
The relative position of the playback window on the display (two characters,
|
||||
the first character means upper(U) / middle(O) / lower(D), the second character
|
||||
means left(L) / middle(O) / right(R)).
|
||||
|
||||
``window_monitor``
|
||||
------------------
|
||||
|
||||
The number of the monitor you want the preview window to pop up on. (default is 0)
|
||||
|
||||
``full_screen``
|
||||
---------------
|
||||
|
||||
Whether open the window in full screen. (default is false)
|
||||
|
||||
``break_into_partial_movies``
|
||||
-----------------------------
|
||||
|
||||
If this is set to ``True``, then many small files will be written corresponding
|
||||
to each ``Scene.play`` and ``Scene.wait`` call, and these files will then be combined
|
||||
to form the full scene.
|
||||
|
||||
Sometimes video-editing is made easier when working with the broken up scene, which
|
||||
effectively has cuts at all the places you might want.
|
||||
|
||||
``camera_resolutions``
|
||||
----------------------
|
||||
|
||||
Export resolutions
|
||||
|
||||
- ``low``
|
||||
Low resolutions (default is 480p)
|
||||
|
||||
- ``medium``
|
||||
Medium resolutions (default is 720p)
|
||||
|
||||
- ``high``
|
||||
High resolutions (default is 1080p)
|
||||
|
||||
- ``ultra_high``
|
||||
Ultra high resolutions (default is 4K)
|
||||
|
||||
- ``default_resolutions``
|
||||
Default resolutions (one of the above four, default is high)
|
||||
|
||||
``fps``
|
||||
``tex``
|
||||
-------
|
||||
|
||||
Export frame rate. (default is 30)
|
||||
- ``template``
|
||||
Which configuration from the manimlib/tex_template.yml file should be used
|
||||
to determine the latex compiler to use, and what preamble to include for
|
||||
rendering tex.
|
||||
|
||||
|
||||
``sizes``
|
||||
---------
|
||||
|
||||
Valuess for various constants used in manimm to specify distances, like the height
|
||||
of the frame, the value of SMALL_BUFF, LARGE_BUFF, etc.
|
||||
|
||||
|
||||
``colors``
|
||||
----------
|
||||
|
||||
Color pallete to use, determining values of color constants like RED, BLUE_E, TEAL, etc.
|
||||
|
||||
``loglevel``
|
||||
------------
|
||||
|
||||
Can be DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
|
||||
|
||||
``universal_import_line``
|
||||
-------------------------
|
||||
|
||||
Import line that need to execute when entering interactive mode directly.
|
||||
|
||||
|
||||
``ignore_manimlib_modules_on_reload``
|
||||
-------------------------------------
|
||||
|
||||
When calling ``reload`` during the interactive mode, imported modules are
|
||||
by default reloaded, in case the user writing a scene which pulls from various
|
||||
other files they have written. By default, modules withinn the manim library will
|
||||
be ignored, but one developing manim may want to set this to be False so that
|
||||
edits to the library are reloaded as well.
|
||||
|
||||
@@ -63,6 +63,7 @@ flag abbr function
|
||||
``--video_dir VIDEO_DIR`` Directory to write video
|
||||
``--config_file CONFIG_FILE`` Path to the custom configuration file
|
||||
``--log-level LOG_LEVEL`` Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
``--autoreload`` Automatically reload Python modules to pick up code changes across during an interactive embedding
|
||||
========================================================== ====== =====================================================================================================================================================================================================
|
||||
|
||||
custom_config
|
||||
|
||||
@@ -34,7 +34,7 @@ InteractiveDevlopment
|
||||
self.play(ReplacementTransform(square, circle))
|
||||
self.wait()
|
||||
self.play(circle.animate.stretch(4, 0))
|
||||
self.play(Rotate(circle, 90 * DEGREES))
|
||||
self.play(Rotate(circle, 90 * DEG))
|
||||
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
|
||||
|
||||
text = Text("""
|
||||
@@ -221,7 +221,7 @@ TexTransformExample
|
||||
self.play(
|
||||
TransformMatchingTex(
|
||||
lines[0].copy(), lines[1],
|
||||
path_arc=90 * DEGREES,
|
||||
path_arc=90 * DEG,
|
||||
),
|
||||
**play_kw
|
||||
)
|
||||
@@ -599,8 +599,8 @@ SurfaceExample
|
||||
# Set perspective
|
||||
frame = self.camera.frame
|
||||
frame.set_euler_angles(
|
||||
theta=-30 * DEGREES,
|
||||
phi=70 * DEGREES,
|
||||
theta=-30 * DEG,
|
||||
phi=70 * DEG,
|
||||
)
|
||||
|
||||
surface = surfaces[0]
|
||||
@@ -624,8 +624,8 @@ SurfaceExample
|
||||
self.play(
|
||||
Transform(surface, surfaces[2]),
|
||||
# Move camera frame during the transition
|
||||
frame.animate.increment_phi(-10 * DEGREES),
|
||||
frame.animate.increment_theta(-20 * DEGREES),
|
||||
frame.animate.increment_phi(-10 * DEG),
|
||||
frame.animate.increment_theta(-20 * DEG),
|
||||
run_time=3
|
||||
)
|
||||
# Add ambient rotation
|
||||
|
||||
@@ -103,7 +103,6 @@ Below is the directory structure of manim:
|
||||
├── family_ops.py # Process family members
|
||||
├── file_ops.py # Process files and directories
|
||||
├── images.py # Read image
|
||||
├── init_config.py # Configuration guide
|
||||
├── iterables.py # Functions related to list/dictionary processing
|
||||
├── paths.py # Curve path
|
||||
├── rate_functions.py # Some defined rate_functions
|
||||
|
||||
@@ -190,7 +190,7 @@ class TexTransformExample(Scene):
|
||||
# to go to a non-equal substring from the target,
|
||||
# use the key map.
|
||||
key_map={"+": "-"},
|
||||
path_arc=90 * DEGREES,
|
||||
path_arc=90 * DEG,
|
||||
),
|
||||
)
|
||||
self.wait()
|
||||
@@ -203,7 +203,7 @@ class TexTransformExample(Scene):
|
||||
TransformMatchingStrings(
|
||||
lines[2].copy(), lines[3],
|
||||
key_map={"2": R"\sqrt"},
|
||||
path_arc=-30 * DEGREES,
|
||||
path_arc=-30 * DEG,
|
||||
),
|
||||
)
|
||||
self.wait(2)
|
||||
@@ -326,7 +326,7 @@ class UpdatersExample(Scene):
|
||||
)
|
||||
self.wait()
|
||||
|
||||
# In general, you can alway call Mobject.add_updater, and pass in
|
||||
# In general, you can always call Mobject.add_updater, and pass in
|
||||
# a function that you want to be called on every frame. The function
|
||||
# should take in either one argument, the mobject, or two arguments,
|
||||
# the mobject and the amount of time since the last frame.
|
||||
@@ -534,7 +534,7 @@ class TexAndNumbersExample(Scene):
|
||||
rate_func=there_and_back,
|
||||
)
|
||||
|
||||
# By default, tex.make_number_changeable replaces the first occurance
|
||||
# By default, tex.make_number_changeable replaces the first occurrence
|
||||
# of the number,but by passing replace_all=True it replaces all and
|
||||
# returns a group of the results
|
||||
exponents = tex.make_number_changeable("2", replace_all=True)
|
||||
@@ -616,8 +616,8 @@ class SurfaceExample(ThreeDScene):
|
||||
self.play(
|
||||
Transform(surface, surfaces[2]),
|
||||
# Move camera frame during the transition
|
||||
self.frame.animate.increment_phi(-10 * DEGREES),
|
||||
self.frame.animate.increment_theta(-20 * DEGREES),
|
||||
self.frame.animate.increment_phi(-10 * DEG),
|
||||
self.frame.animate.increment_theta(-20 * DEG),
|
||||
run_time=3
|
||||
)
|
||||
# Add ambient rotation
|
||||
@@ -666,7 +666,7 @@ class InteractiveDevelopment(Scene):
|
||||
self.play(ReplacementTransform(square, circle))
|
||||
self.wait()
|
||||
self.play(circle.animate.stretch(4, 0))
|
||||
self.play(Rotate(circle, 90 * DEGREES))
|
||||
self.play(Rotate(circle, 90 * DEG))
|
||||
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
|
||||
|
||||
text = Text("""
|
||||
|
||||
@@ -61,9 +61,9 @@ from manimlib.scene.interactive_scene import *
|
||||
from manimlib.scene.scene import *
|
||||
|
||||
from manimlib.utils.bezier import *
|
||||
from manimlib.utils.cache import *
|
||||
from manimlib.utils.color import *
|
||||
from manimlib.utils.dict_ops import *
|
||||
from manimlib.utils.customization import *
|
||||
from manimlib.utils.debug import *
|
||||
from manimlib.utils.directories import *
|
||||
from manimlib.utils.file_ops import *
|
||||
|
||||
@@ -1,28 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
from addict import Dict
|
||||
|
||||
from manimlib import __version__
|
||||
import manimlib.config
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.config import parse_cli
|
||||
import manimlib.extract_scene
|
||||
import manimlib.logger
|
||||
import manimlib.utils.init_config
|
||||
from manimlib.utils.cache import clear_cache
|
||||
from manimlib.window import Window
|
||||
|
||||
|
||||
from IPython.terminal.embed import KillEmbedded
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
|
||||
|
||||
def run_scenes():
|
||||
"""
|
||||
Runs the scenes in a loop and detects when a scene reload is requested.
|
||||
"""
|
||||
# Create a new dict to be able to upate without
|
||||
# altering global configuration
|
||||
scene_config = Dict(manim_config.scene)
|
||||
run_config = manim_config.run
|
||||
|
||||
if run_config.show_in_window:
|
||||
# Create a reusable window
|
||||
window = Window(**manim_config.window)
|
||||
scene_config.update(window=window)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Blocking call since a scene may init an IPython shell()
|
||||
scenes = manimlib.extract_scene.main(scene_config, run_config)
|
||||
for scene in scenes:
|
||||
scene.run()
|
||||
return
|
||||
except KillEmbedded:
|
||||
# Requested via the `exit_raise` IPython runline magic
|
||||
# by means of the reload_scene() command
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for ManimGL.
|
||||
"""
|
||||
print(f"ManimGL \033[32mv{__version__}\033[0m")
|
||||
|
||||
args = manimlib.config.parse_cli()
|
||||
args = parse_cli()
|
||||
if args.version and args.file is None:
|
||||
return
|
||||
if args.log_level:
|
||||
manimlib.logger.log.setLevel(args.log_level)
|
||||
if args.clear_cache:
|
||||
clear_cache()
|
||||
|
||||
if args.config:
|
||||
manimlib.utils.init_config.init_customization()
|
||||
else:
|
||||
config = manimlib.config.get_configuration(args)
|
||||
scenes = manimlib.extract_scene.main(config)
|
||||
|
||||
for scene in scenes:
|
||||
scene.run()
|
||||
run_scenes()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -6,7 +6,6 @@ from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.rate_functions import squish_rate_func
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -34,7 +33,7 @@ class Animation(object):
|
||||
lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO,
|
||||
rate_func: Callable[[float], float] = smooth,
|
||||
name: str = "",
|
||||
# Does this animation add or remove a mobject form the screen
|
||||
# Does this animation add or remove a mobject from the screen
|
||||
remover: bool = False,
|
||||
# What to enter into the update function upon completion
|
||||
final_alpha_value: float = 1.0,
|
||||
@@ -43,6 +42,7 @@ class Animation(object):
|
||||
# updating, call mobject.suspend_updating() before the animation
|
||||
suspend_mobject_updating: bool = False,
|
||||
):
|
||||
self._validate_input_type(mobject)
|
||||
self.mobject = mobject
|
||||
self.run_time = run_time
|
||||
self.time_span = time_span
|
||||
@@ -53,7 +53,9 @@ class Animation(object):
|
||||
self.lag_ratio = lag_ratio
|
||||
self.suspend_mobject_updating = suspend_mobject_updating
|
||||
|
||||
assert isinstance(mobject, Mobject)
|
||||
def _validate_input_type(self, mobject: Mobject) -> None:
|
||||
if not isinstance(mobject, Mobject):
|
||||
raise TypeError("Animation only works for Mobjects.")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
@@ -45,7 +43,7 @@ class AnimationGroup(Animation):
|
||||
mobs = remove_list_redundancies([a.mobject for a in self.animations])
|
||||
if group is not None:
|
||||
self.group = group
|
||||
if group_type is not None:
|
||||
elif group_type is not None:
|
||||
self.group = group_type(*mobs)
|
||||
elif all(isinstance(anim.mobject, VMobject) for anim in animations):
|
||||
self.group = VGroup(*mobs)
|
||||
|
||||
@@ -5,7 +5,6 @@ from abc import ABC, abstractmethod
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
|
||||
@@ -5,9 +5,8 @@ import numpy as np
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.constants import ORIGIN
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.rate_functions import there_and_back
|
||||
|
||||
@@ -103,7 +102,7 @@ class FadeTransform(Transform):
|
||||
self.dim_to_match = dim_to_match
|
||||
|
||||
mobject.save_state()
|
||||
super().__init__(mobject.get_group_class()(mobject, target_mobject.copy()), **kwargs)
|
||||
super().__init__(Group(mobject, target_mobject.copy()), **kwargs)
|
||||
|
||||
def begin(self) -> None:
|
||||
self.ending_mobject = self.mobject.copy()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.constants import PI
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from os import remove
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
@@ -16,7 +14,7 @@ from manimlib.animation.transform import Transform
|
||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||
from manimlib.constants import ORIGIN, RIGHT, UP
|
||||
from manimlib.constants import SMALL_BUFF
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.constants import TAU
|
||||
from manimlib.constants import GREY, YELLOW
|
||||
from manimlib.mobject.geometry import Circle
|
||||
@@ -397,7 +395,7 @@ class WiggleOutThenIn(Animation):
|
||||
|
||||
|
||||
class TurnInsideOut(Transform):
|
||||
def __init__(self, mobject: Mobject, path_arc: float = 90 * DEGREES, **kwargs):
|
||||
def __init__(self, mobject: Mobject, path_arc: float = 90 * DEG, **kwargs):
|
||||
super().__init__(mobject, path_arc=path_arc, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
|
||||
@@ -29,9 +29,9 @@ class ChangingDecimal(Animation):
|
||||
self.mobject = decimal_mob
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.set_value(
|
||||
self.number_update_func(alpha)
|
||||
)
|
||||
true_alpha = self.time_spanned_alpha(alpha)
|
||||
new_value = self.number_update_func(true_alpha)
|
||||
self.mobject.set_value(new_value)
|
||||
|
||||
|
||||
class ChangeDecimalToValue(ChangingDecimal):
|
||||
|
||||
@@ -5,7 +5,7 @@ import inspect
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.constants import OUT
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -314,7 +314,7 @@ class ApplyComplexFunction(ApplyMethod):
|
||||
|
||||
|
||||
class CyclicReplace(Transform):
|
||||
def __init__(self, *mobjects: Mobject, path_arc=90 * DEGREES, **kwargs):
|
||||
def __init__(self, *mobjects: Mobject, path_arc=90 * DEG, **kwargs):
|
||||
super().__init__(Group(*mobjects), path_arc=path_arc, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
|
||||
@@ -88,7 +88,7 @@ class TransformMatchingParts(AnimationGroup):
|
||||
if not source_is_new or not target_is_new:
|
||||
return
|
||||
|
||||
transform_type = self.mismatch_animation
|
||||
transform_type = self.mismatch_animation
|
||||
if source.has_same_shape_as(target):
|
||||
transform_type = self.match_animation
|
||||
|
||||
@@ -154,16 +154,16 @@ class TransformMatchingStrings(TransformMatchingParts):
|
||||
counts2 = list(map(target.substr_to_path_count, syms2))
|
||||
|
||||
# Start with user specified matches
|
||||
blocks = [(source[key], target[key]) for key in matched_keys]
|
||||
blocks += [(source[key1], target[key2]) for key1, key2 in key_map.items()]
|
||||
blocks = [(source[key1], target[key2]) for key1, key2 in key_map.items()]
|
||||
blocks += [(source[key], target[key]) for key in matched_keys]
|
||||
|
||||
# Nullify any intersections with those matches in the two symbol lists
|
||||
for sub_source, sub_target in blocks:
|
||||
for i in range(len(syms1)):
|
||||
if source[i] in sub_source.family_members_with_points():
|
||||
if i < len(source) and source[i] in sub_source.family_members_with_points():
|
||||
syms1[i] = "Null1"
|
||||
for j in range(len(syms2)):
|
||||
if target[j] in sub_target.family_members_with_points():
|
||||
if j < len(target) and target[j] in sub_target.family_members_with_points():
|
||||
syms2[j] = "Null2"
|
||||
|
||||
# Group together longest matching substrings
|
||||
|
||||
@@ -46,7 +46,8 @@ class UpdateFromAlphaFunc(Animation):
|
||||
super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject, alpha)
|
||||
true_alpha = self.rate_func(self.time_spanned_alpha(alpha))
|
||||
self.update_function(self.mobject, true_alpha)
|
||||
|
||||
|
||||
class MaintainPositionRelativeTo(Animation):
|
||||
|
||||
@@ -6,10 +6,8 @@ import OpenGL.GL as gl
|
||||
from PIL import Image
|
||||
|
||||
from manimlib.camera.camera_frame import CameraFrame
|
||||
from manimlib.constants import ASPECT_RATIO
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.constants import DEFAULT_FPS
|
||||
from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH
|
||||
from manimlib.constants import DEFAULT_RESOLUTION
|
||||
from manimlib.constants import FRAME_HEIGHT
|
||||
from manimlib.constants import FRAME_WIDTH
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -30,10 +28,9 @@ class Camera(object):
|
||||
window: Optional[Window] = None,
|
||||
background_image: Optional[str] = None,
|
||||
frame_config: dict = dict(),
|
||||
pixel_width: int = DEFAULT_PIXEL_WIDTH,
|
||||
pixel_height: int = DEFAULT_PIXEL_HEIGHT,
|
||||
fps: int = DEFAULT_FPS,
|
||||
# Note: frame height and width will be resized to match the pixel aspect ratio
|
||||
# Note: frame height and width will be resized to match this resolution aspect ratio
|
||||
resolution=DEFAULT_RESOLUTION,
|
||||
fps: int = 30,
|
||||
background_color: ManimColor = BLACK,
|
||||
background_opacity: float = 1.0,
|
||||
# Points in vectorized mobjects with norm greater
|
||||
@@ -48,9 +45,9 @@ class Camera(object):
|
||||
# to set samples to be greater than 0.
|
||||
samples: int = 0,
|
||||
):
|
||||
self.background_image = background_image
|
||||
self.window = window
|
||||
self.default_pixel_shape = (pixel_width, pixel_height)
|
||||
self.background_image = background_image
|
||||
self.default_pixel_shape = resolution # Rename?
|
||||
self.fps = fps
|
||||
self.max_allowable_norm = max_allowable_norm
|
||||
self.image_mode = image_mode
|
||||
@@ -127,7 +124,7 @@ class Camera(object):
|
||||
def clear(self) -> None:
|
||||
self.fbo.clear(*self.background_rgba)
|
||||
if self.window:
|
||||
self.window.clear()
|
||||
self.window.clear(*self.background_rgba)
|
||||
|
||||
def blit(self, src_fbo, dst_fbo):
|
||||
"""
|
||||
@@ -221,8 +218,8 @@ class Camera(object):
|
||||
frame_height = frame_width / aspect_ratio
|
||||
else:
|
||||
frame_width = aspect_ratio * frame_height
|
||||
self.frame.set_height(frame_height, stretch=true)
|
||||
self.frame.set_width(frame_width, stretch=true)
|
||||
self.frame.set_height(frame_height, stretch=True)
|
||||
self.frame.set_width(frame_width, stretch=True)
|
||||
|
||||
# Rendering
|
||||
def capture(self, *mobjects: Mobject) -> None:
|
||||
|
||||
@@ -5,9 +5,8 @@ import warnings
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial.transform import Rotation
|
||||
from pyrr import Matrix44
|
||||
|
||||
from manimlib.constants import DEGREES, RADIANS
|
||||
from manimlib.constants import DEG, RADIANS
|
||||
from manimlib.constants import FRAME_SHAPE
|
||||
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||
from manimlib.constants import PI
|
||||
@@ -27,7 +26,7 @@ class CameraFrame(Mobject):
|
||||
frame_shape: tuple[float, float] = FRAME_SHAPE,
|
||||
center_point: Vect3 = ORIGIN,
|
||||
# Field of view in the y direction
|
||||
fovy: float = 45 * DEGREES,
|
||||
fovy: float = 45 * DEG,
|
||||
euler_axes: str = "zxz",
|
||||
# This keeps it ordered first in a scene
|
||||
z_index=-1,
|
||||
@@ -182,7 +181,7 @@ class CameraFrame(Mobject):
|
||||
Shortcut for set_euler_angles, defaulting to taking
|
||||
in angles in degrees
|
||||
"""
|
||||
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES)
|
||||
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEG)
|
||||
if center is not None:
|
||||
self.move_to(np.array(center))
|
||||
if height is not None:
|
||||
@@ -210,7 +209,7 @@ class CameraFrame(Mobject):
|
||||
self.increment_euler_angles(dgamma=dgamma, units=units)
|
||||
return self
|
||||
|
||||
def add_ambient_rotation(self, angular_speed=1 * DEGREES):
|
||||
def add_ambient_rotation(self, angular_speed=1 * DEG):
|
||||
self.add_updater(lambda m, dt: m.increment_theta(angular_speed * dt))
|
||||
return self
|
||||
|
||||
|
||||
@@ -1,25 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from argparse import Namespace
|
||||
import colour
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import screeninfo
|
||||
import sys
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from ast import literal_eval
|
||||
from addict import Dict
|
||||
|
||||
from manimlib.logger import log
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
from manimlib.utils.init_config import init_customization
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
Module = importlib.util.types.ModuleType
|
||||
from argparse import Namespace
|
||||
from typing import Optional
|
||||
|
||||
|
||||
__config_file__ = "custom_config.yml"
|
||||
def initialize_manim_config() -> Dict:
|
||||
"""
|
||||
Return default configuration for various classes in manim, such as
|
||||
Scene, Window, Camera, and SceneFileWriter, as well as configuration
|
||||
determining how the scene is run (e.g. written to file or previewed in window).
|
||||
|
||||
The result is initially on the contents of default_config.yml in the manimlib directory,
|
||||
which can be further updated by a custom configuration file custom_config.yml.
|
||||
It is further updated based on command line argument.
|
||||
"""
|
||||
args = parse_cli()
|
||||
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
config = Dict(merge_dicts_recursively(
|
||||
load_yaml(global_defaults_file),
|
||||
load_yaml("custom_config.yml"), # From current working directory
|
||||
load_yaml(args.config_file) if args.config_file else dict(),
|
||||
))
|
||||
|
||||
log.setLevel(args.log_level or config["log_level"])
|
||||
|
||||
update_directory_config(config)
|
||||
update_window_config(config, args)
|
||||
update_camera_config(config, args)
|
||||
update_file_writer_config(config, args)
|
||||
update_scene_config(config, args)
|
||||
update_run_config(config, args)
|
||||
update_embed_config(config, args)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def parse_cli():
|
||||
@@ -49,12 +78,12 @@ def parse_cli():
|
||||
parser.add_argument(
|
||||
"-l", "--low_quality",
|
||||
action="store_true",
|
||||
help="Render at a low quality (for faster rendering)",
|
||||
help="Render at 480p",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m", "--medium_quality",
|
||||
action="store_true",
|
||||
help="Render at a medium quality",
|
||||
help="Render at 720p",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hd",
|
||||
@@ -77,11 +106,6 @@ def parse_cli():
|
||||
help="Scene will stay paused during wait calls until " + \
|
||||
"space bar or right arrow is hit, like a slide show"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-g", "--save_pngs",
|
||||
action="store_true",
|
||||
help="Save each frame as a png",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i", "--gif",
|
||||
action="store_true",
|
||||
@@ -121,9 +145,10 @@ def parse_cli():
|
||||
help="Show the output file in finder",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
"--subdivide",
|
||||
action="store_true",
|
||||
help="Guide for automatic configuration",
|
||||
help="Divide the output animation into individual movie files " +
|
||||
"for each animation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file_name",
|
||||
@@ -138,12 +163,9 @@ def parse_cli():
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e", "--embed",
|
||||
nargs="?",
|
||||
const="",
|
||||
help="Creates a new file where the line `self.embed` is inserted " + \
|
||||
"into the Scenes construct method. " + \
|
||||
"If a string is passed in, the line will be inserted below the " + \
|
||||
"last line of code including that string."
|
||||
metavar="LINE_NUMBER",
|
||||
help="Adds a breakpoint at the inputted file dropping into an " + \
|
||||
"interactive iPython session at that point of the code."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--resolution",
|
||||
@@ -152,6 +174,7 @@ def parse_cli():
|
||||
parser.add_argument(
|
||||
"--fps",
|
||||
help="Frame rate, as an integer",
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c", "--color",
|
||||
@@ -190,6 +213,17 @@ def parse_cli():
|
||||
"--log-level",
|
||||
help="Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clear-cache",
|
||||
action="store_true",
|
||||
help="Erase the cache used for Tex and Text Mobjects"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--autoreload",
|
||||
action="store_true",
|
||||
help="Automatically reload Python modules to pick up code changes " +
|
||||
"across different files",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
args.write_file = any([args.write_file, args.open, args.finder])
|
||||
return args
|
||||
@@ -198,171 +232,133 @@ def parse_cli():
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def update_directory_config(config: Dict):
|
||||
dir_config = config.directories
|
||||
base = dir_config.base
|
||||
for key, subdir in dir_config.subdirs.items():
|
||||
dir_config[key] = os.path.join(base, subdir)
|
||||
|
||||
|
||||
def update_window_config(config: Dict, args: Namespace):
|
||||
window_config = config.window
|
||||
for key in "position", "size":
|
||||
if window_config.get(key):
|
||||
window_config[key] = literal_eval(window_config[key])
|
||||
if args.full_screen:
|
||||
window_config.full_screen = True
|
||||
|
||||
|
||||
def update_camera_config(config: Dict, args: Namespace):
|
||||
camera_config = config.camera
|
||||
arg_resolution = get_resolution_from_args(args, config.resolution_options)
|
||||
camera_config.resolution = arg_resolution or literal_eval(camera_config.resolution)
|
||||
if args.fps:
|
||||
camera_config.fps = args.fps
|
||||
if args.color:
|
||||
try:
|
||||
camera_config.background_color = colour.Color(args.color)
|
||||
except Exception as err:
|
||||
log.error("Please use a valid color")
|
||||
log.error(err)
|
||||
sys.exit(2)
|
||||
if args.transparent:
|
||||
camera_config.background_opacity = 0.0
|
||||
|
||||
|
||||
def update_file_writer_config(config: Dict, args: Namespace):
|
||||
file_writer_config = config.file_writer
|
||||
file_writer_config.update(
|
||||
write_to_movie=(not args.skip_animations and args.write_file),
|
||||
subdivide_output=args.subdivide,
|
||||
save_last_frame=(args.skip_animations and args.write_file),
|
||||
png_mode=("RGBA" if args.transparent else "RGB"),
|
||||
movie_file_extension=(get_file_ext(args)),
|
||||
output_directory=get_output_directory(args, config),
|
||||
file_name=args.file_name,
|
||||
open_file_upon_completion=args.open,
|
||||
show_file_location_upon_completion=args.finder,
|
||||
quiet=args.quiet,
|
||||
)
|
||||
|
||||
if args.vcodec:
|
||||
file_writer_config.video_codec = args.vcodec
|
||||
elif args.transparent:
|
||||
file_writer_config.video_codec = 'prores_ks'
|
||||
file_writer_config.pixel_format = ''
|
||||
elif args.gif:
|
||||
file_writer_config.video_codec = ''
|
||||
|
||||
if args.pix_fmt:
|
||||
file_writer_config.pixel_format = args.pix_fmt
|
||||
|
||||
|
||||
def update_scene_config(config: Dict, args: Namespace):
|
||||
scene_config = config.scene
|
||||
start, end = get_animations_numbers(args)
|
||||
scene_config.update(
|
||||
# Note, Scene.__init__ makes use of both manimlib.camera and
|
||||
# manimlib.file_writer below, so the arguments here are just for
|
||||
# any future specifications beyond what the global configuration holds
|
||||
camera_config=dict(),
|
||||
file_writer_config=dict(),
|
||||
skip_animations=args.skip_animations,
|
||||
start_at_animation_number=start,
|
||||
end_at_animation_number=end,
|
||||
presenter_mode=args.presenter_mode,
|
||||
)
|
||||
if args.leave_progress_bars:
|
||||
scene_config.leave_progress_bars = True
|
||||
if args.show_animation_progress:
|
||||
scene_config.show_animation_progress = True
|
||||
|
||||
|
||||
def update_run_config(config: Dict, args: Namespace):
|
||||
config.run = Dict(
|
||||
file_name=args.file,
|
||||
embed_line=(int(args.embed) if args.embed is not None else None),
|
||||
is_reload=False,
|
||||
prerun=args.prerun,
|
||||
scene_names=args.scene_names,
|
||||
quiet=args.quiet or args.write_all,
|
||||
write_all=args.write_all,
|
||||
show_in_window=not args.write_file
|
||||
)
|
||||
|
||||
|
||||
def update_embed_config(config: Dict, args: Namespace):
|
||||
if args.autoreload:
|
||||
config.embed.autoreload = True
|
||||
|
||||
|
||||
# Helpers for the functions above
|
||||
|
||||
|
||||
def load_yaml(file_path: str):
|
||||
try:
|
||||
with open(file_path, "r") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
|
||||
def get_manim_dir():
|
||||
manimlib_module = importlib.import_module("manimlib")
|
||||
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
|
||||
return os.path.abspath(os.path.join(manimlib_dir, ".."))
|
||||
|
||||
|
||||
def get_module(file_name: str | None) -> Module:
|
||||
if file_name is None:
|
||||
return None
|
||||
module_name = file_name.replace(os.sep, ".").replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def get_indent(line: str):
|
||||
return len(line) - len(line.lstrip())
|
||||
|
||||
|
||||
def get_module_with_inserted_embed_line(
|
||||
file_name: str, scene_name: str, line_marker: str
|
||||
):
|
||||
"""
|
||||
This is hacky, but convenient. When user includes the argument "-e", it will try
|
||||
to recreate a file that inserts the line `self.embed()` into the end of the scene's
|
||||
construct method. If there is an argument passed in, it will insert the line after
|
||||
the last line in the sourcefile which includes that string.
|
||||
"""
|
||||
with open(file_name, 'r') as fp:
|
||||
lines = fp.readlines()
|
||||
|
||||
try:
|
||||
scene_line_number = next(
|
||||
i for i, line in enumerate(lines)
|
||||
if line.startswith(f"class {scene_name}")
|
||||
)
|
||||
except StopIteration:
|
||||
log.error(f"No scene {scene_name}")
|
||||
return
|
||||
|
||||
prev_line_num = -1
|
||||
n_spaces = None
|
||||
if len(line_marker) == 0:
|
||||
# Find the end of the construct method
|
||||
in_construct = False
|
||||
for index in range(scene_line_number, len(lines) - 1):
|
||||
line = lines[index]
|
||||
if line.lstrip().startswith("def construct"):
|
||||
in_construct = True
|
||||
n_spaces = get_indent(line) + 4
|
||||
elif in_construct:
|
||||
if len(line.strip()) > 0 and get_indent(line) < (n_spaces or 0):
|
||||
prev_line_num = index - 1
|
||||
break
|
||||
if prev_line_num < 0:
|
||||
prev_line_num = len(lines) - 1
|
||||
elif line_marker.isdigit():
|
||||
# Treat the argument as a line number
|
||||
prev_line_num = int(line_marker) - 1
|
||||
elif len(line_marker) > 0:
|
||||
# Treat the argument as a string
|
||||
try:
|
||||
prev_line_num = next(
|
||||
i
|
||||
for i in range(scene_line_number, len(lines) - 1)
|
||||
if line_marker in lines[i]
|
||||
)
|
||||
except StopIteration:
|
||||
log.error(f"No lines matching {line_marker}")
|
||||
sys.exit(2)
|
||||
|
||||
# Insert the embed line, rewrite file, then write it back when done
|
||||
if n_spaces is None:
|
||||
n_spaces = get_indent(lines[prev_line_num])
|
||||
inserted_line = " " * n_spaces + "self.embed()\n"
|
||||
new_lines = list(lines)
|
||||
new_lines.insert(prev_line_num + 1, inserted_line)
|
||||
new_file = file_name.replace(".py", "_insert_embed.py")
|
||||
|
||||
with open(new_file, 'w') as fp:
|
||||
fp.writelines(new_lines)
|
||||
|
||||
module = get_module(new_file)
|
||||
# This is to pretend the module imported from the edited lines
|
||||
# of code actually comes from the original file.
|
||||
module.__file__ = file_name
|
||||
|
||||
os.remove(new_file)
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def get_scene_module(args: Namespace) -> Module:
|
||||
if args.embed is None:
|
||||
return get_module(args.file)
|
||||
else:
|
||||
return get_module_with_inserted_embed_line(
|
||||
args.file, args.scene_names[0], args.embed
|
||||
)
|
||||
|
||||
|
||||
def get_custom_config():
|
||||
global __config_file__
|
||||
|
||||
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
|
||||
if os.path.exists(global_defaults_file):
|
||||
with open(global_defaults_file, "r") as file:
|
||||
custom_config = yaml.safe_load(file)
|
||||
|
||||
if os.path.exists(__config_file__):
|
||||
with open(__config_file__, "r") as file:
|
||||
local_defaults = yaml.safe_load(file)
|
||||
if local_defaults:
|
||||
custom_config = merge_dicts_recursively(
|
||||
custom_config,
|
||||
local_defaults,
|
||||
)
|
||||
else:
|
||||
with open(__config_file__, "r") as file:
|
||||
custom_config = yaml.safe_load(file)
|
||||
|
||||
# Check temporary storage(custom_config)
|
||||
if custom_config["directories"]["temporary_storage"] == "" and sys.platform == "win32":
|
||||
log.warning(
|
||||
"You may be using Windows platform and have not specified the path of" + \
|
||||
" `temporary_storage`, which may cause OSError. So it is recommended" + \
|
||||
" to specify the `temporary_storage` in the config file (.yml)"
|
||||
)
|
||||
|
||||
return custom_config
|
||||
|
||||
|
||||
def init_global_config(config_file):
|
||||
global __config_file__
|
||||
|
||||
# ensure __config_file__ always exists
|
||||
if config_file is not None:
|
||||
if not os.path.exists(config_file):
|
||||
log.error(f"Can't find {config_file}.")
|
||||
if sys.platform == 'win32':
|
||||
log.info(f"Copying default configuration file to {config_file}...")
|
||||
os.system(f"copy default_config.yml {config_file}")
|
||||
elif sys.platform in ["linux2", "darwin"]:
|
||||
log.info(f"Copying default configuration file to {config_file}...")
|
||||
os.system(f"cp default_config.yml {config_file}")
|
||||
else:
|
||||
log.info("Please create the configuration file manually.")
|
||||
log.info("Read configuration from default_config.yml.")
|
||||
else:
|
||||
__config_file__ = config_file
|
||||
|
||||
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
|
||||
if not (os.path.exists(global_defaults_file) or os.path.exists(__config_file__)):
|
||||
log.info("There is no configuration file detected. Switch to the config file initializer:")
|
||||
init_customization()
|
||||
|
||||
elif not os.path.exists(__config_file__):
|
||||
log.info(f"Using the default configuration file, which you can modify in `{global_defaults_file}`")
|
||||
log.info(
|
||||
"If you want to create a local configuration file, you can create a file named" + \
|
||||
f" `{__config_file__}`, or run `manimgl --config`"
|
||||
)
|
||||
def get_resolution_from_args(args: Optional[Namespace], resolution_options: dict) -> Optional[tuple[int, int]]:
|
||||
if args.resolution:
|
||||
return tuple(map(int, args.resolution.split("x")))
|
||||
if args.low_quality:
|
||||
return literal_eval(resolution_options["low"])
|
||||
if args.medium_quality:
|
||||
return literal_eval(resolution_options["med"])
|
||||
if args.hd:
|
||||
return literal_eval(resolution_options["high"])
|
||||
if args.uhd:
|
||||
return literal_eval(resolution_options["4k"])
|
||||
return None
|
||||
|
||||
|
||||
def get_file_ext(args: Namespace) -> str:
|
||||
@@ -385,159 +381,19 @@ def get_animations_numbers(args: Namespace) -> tuple[int | None, int | None]:
|
||||
return int(stan), None
|
||||
|
||||
|
||||
def get_output_directory(args: Namespace, custom_config: dict) -> str:
|
||||
dir_config = custom_config["directories"]
|
||||
output_directory = args.video_dir or dir_config["output"]
|
||||
if dir_config["mirror_module_path"] and args.file:
|
||||
to_cut = dir_config["removed_mirror_prefix"]
|
||||
ext = os.path.abspath(args.file)
|
||||
ext = ext.replace(to_cut, "").replace(".py", "")
|
||||
if ext.startswith("_"):
|
||||
ext = ext[1:]
|
||||
output_directory = os.path.join(output_directory, ext)
|
||||
return output_directory
|
||||
def get_output_directory(args: Namespace, config: Dict) -> str:
|
||||
dir_config = config.directories
|
||||
out_dir = args.video_dir or dir_config.output
|
||||
if dir_config.mirror_module_path and args.file:
|
||||
file_path = Path(args.file).absolute()
|
||||
if str(file_path).startswith(dir_config.removed_mirror_prefix):
|
||||
rel_path = file_path.relative_to(dir_config.removed_mirror_prefix)
|
||||
rel_path = Path(str(rel_path).lstrip("_"))
|
||||
else:
|
||||
rel_path = file_path.stem
|
||||
out_dir = Path(out_dir, rel_path).with_suffix("")
|
||||
return out_dir
|
||||
|
||||
|
||||
def get_file_writer_config(args: Namespace, custom_config: dict) -> dict:
|
||||
result = {
|
||||
"write_to_movie": not args.skip_animations and args.write_file,
|
||||
"save_last_frame": args.skip_animations and args.write_file,
|
||||
"save_pngs": args.save_pngs,
|
||||
# If -t is passed in (for transparent), this will be RGBA
|
||||
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||
"movie_file_extension": get_file_ext(args),
|
||||
"output_directory": get_output_directory(args, custom_config),
|
||||
"file_name": args.file_name,
|
||||
"input_file_path": args.file or "",
|
||||
"open_file_upon_completion": args.open,
|
||||
"show_file_location_upon_completion": args.finder,
|
||||
"quiet": args.quiet,
|
||||
**custom_config["file_writer_config"],
|
||||
}
|
||||
|
||||
if args.vcodec:
|
||||
result["video_codec"] = args.vcodec
|
||||
elif args.transparent:
|
||||
result["video_codec"] = 'prores_ks'
|
||||
result["pixel_format"] = ''
|
||||
elif args.gif:
|
||||
result["video_codec"] = ''
|
||||
|
||||
if args.pix_fmt:
|
||||
result["pixel_format"] = args.pix_fmt
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) -> dict:
|
||||
# Default to making window half the screen size
|
||||
# but make it full screen if -f is passed in
|
||||
try:
|
||||
monitors = screeninfo.get_monitors()
|
||||
except screeninfo.ScreenInfoError:
|
||||
pass
|
||||
mon_index = custom_config["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
aspect_ratio = camera_config["pixel_width"] / camera_config["pixel_height"]
|
||||
window_width = monitor.width
|
||||
if not (args.full_screen or custom_config["full_screen"]):
|
||||
window_width //= 2
|
||||
window_height = int(window_width / aspect_ratio)
|
||||
return dict(size=(window_width, window_height))
|
||||
|
||||
|
||||
def get_camera_config(args: Namespace, custom_config: dict) -> dict:
|
||||
camera_config = {}
|
||||
camera_resolutions = custom_config["camera_resolutions"]
|
||||
if args.resolution:
|
||||
resolution = args.resolution
|
||||
elif args.low_quality:
|
||||
resolution = camera_resolutions["low"]
|
||||
elif args.medium_quality:
|
||||
resolution = camera_resolutions["med"]
|
||||
elif args.hd:
|
||||
resolution = camera_resolutions["high"]
|
||||
elif args.uhd:
|
||||
resolution = camera_resolutions["4k"]
|
||||
else:
|
||||
resolution = camera_resolutions[camera_resolutions["default_resolution"]]
|
||||
|
||||
if args.fps:
|
||||
fps = int(args.fps)
|
||||
else:
|
||||
fps = custom_config["fps"]
|
||||
|
||||
width_str, height_str = resolution.split("x")
|
||||
width = int(width_str)
|
||||
height = int(height_str)
|
||||
|
||||
camera_config.update({
|
||||
"pixel_width": width,
|
||||
"pixel_height": height,
|
||||
"frame_config": {
|
||||
"frame_shape": ((width / height) * get_frame_height(), get_frame_height()),
|
||||
},
|
||||
"fps": fps,
|
||||
})
|
||||
|
||||
try:
|
||||
bg_color = args.color or custom_config["style"]["background_color"]
|
||||
camera_config["background_color"] = colour.Color(bg_color)
|
||||
except ValueError as err:
|
||||
log.error("Please use a valid color")
|
||||
log.error(err)
|
||||
sys.exit(2)
|
||||
|
||||
# If rendering a transparent image/move, make sure the
|
||||
# scene has a background opacity of 0
|
||||
if args.transparent:
|
||||
camera_config["background_opacity"] = 0
|
||||
|
||||
return camera_config
|
||||
|
||||
|
||||
def get_configuration(args: Namespace) -> dict:
|
||||
init_global_config(args.config_file)
|
||||
custom_config = get_custom_config()
|
||||
camera_config = get_camera_config(args, custom_config)
|
||||
window_config = get_window_config(args, custom_config, camera_config)
|
||||
start, end = get_animations_numbers(args)
|
||||
|
||||
return {
|
||||
"module": get_scene_module(args),
|
||||
"scene_names": args.scene_names,
|
||||
"file_writer_config": get_file_writer_config(args, custom_config),
|
||||
"camera_config": camera_config,
|
||||
"window_config": window_config,
|
||||
"quiet": args.quiet or args.write_all,
|
||||
"write_all": args.write_all,
|
||||
"skip_animations": args.skip_animations,
|
||||
"start_at_animation_number": start,
|
||||
"end_at_animation_number": end,
|
||||
"preview": not args.write_file,
|
||||
"presenter_mode": args.presenter_mode,
|
||||
"leave_progress_bars": args.leave_progress_bars,
|
||||
"show_animation_progress": args.show_animation_progress,
|
||||
"prerun": args.prerun,
|
||||
"embed_exception_mode": custom_config["embed_exception_mode"],
|
||||
"embed_error_sound": custom_config["embed_error_sound"],
|
||||
}
|
||||
|
||||
|
||||
def get_frame_height():
|
||||
return 8.0
|
||||
|
||||
|
||||
def get_aspect_ratio():
|
||||
cam_config = get_camera_config(parse_cli(), get_custom_config())
|
||||
return cam_config['pixel_width'] / cam_config['pixel_height']
|
||||
|
||||
|
||||
def get_default_pixel_width():
|
||||
cam_config = get_camera_config(parse_cli(), get_custom_config())
|
||||
return cam_config['pixel_width']
|
||||
|
||||
|
||||
def get_default_pixel_height():
|
||||
cam_config = get_camera_config(parse_cli(), get_custom_config())
|
||||
return cam_config['pixel_height']
|
||||
# Create global configuration
|
||||
manim_config: Dict = initialize_manim_config()
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
from manimlib.config import get_aspect_ratio
|
||||
from manimlib.config import get_default_pixel_width
|
||||
from manimlib.config import get_default_pixel_height
|
||||
from manimlib.config import get_frame_height
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from typing import List
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
|
||||
# See manimlib/default_config.yml
|
||||
from manimlib.config import manim_config
|
||||
|
||||
|
||||
DEFAULT_RESOLUTION: tuple[int, int] = manim_config.camera.resolution
|
||||
DEFAULT_PIXEL_WIDTH: int = DEFAULT_RESOLUTION[0]
|
||||
DEFAULT_PIXEL_HEIGHT: int = DEFAULT_RESOLUTION[1]
|
||||
|
||||
# Sizes relevant to default camera frame
|
||||
ASPECT_RATIO: float = get_aspect_ratio()
|
||||
FRAME_HEIGHT: float = get_frame_height()
|
||||
ASPECT_RATIO: float = DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT
|
||||
FRAME_HEIGHT: float = manim_config.sizes.frame_height
|
||||
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
|
||||
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
|
||||
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
|
||||
FRAME_X_RADIUS: float = FRAME_WIDTH / 2
|
||||
|
||||
DEFAULT_PIXEL_HEIGHT: int = get_default_pixel_height()
|
||||
DEFAULT_PIXEL_WIDTH: int = get_default_pixel_width()
|
||||
DEFAULT_FPS: int = 30
|
||||
|
||||
SMALL_BUFF: float = 0.1
|
||||
MED_SMALL_BUFF: float = 0.25
|
||||
MED_LARGE_BUFF: float = 0.5
|
||||
LARGE_BUFF: float = 1
|
||||
# Helpful values for positioning mobjects
|
||||
SMALL_BUFF: float = manim_config.sizes.small_buff
|
||||
MED_SMALL_BUFF: float = manim_config.sizes.med_small_buff
|
||||
MED_LARGE_BUFF: float = manim_config.sizes.med_large_buff
|
||||
LARGE_BUFF: float = manim_config.sizes.large_buff
|
||||
|
||||
DEFAULT_MOBJECT_TO_EDGE_BUFFER: float = MED_LARGE_BUFF
|
||||
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER: float = MED_SMALL_BUFF
|
||||
|
||||
|
||||
# In seconds
|
||||
DEFAULT_WAIT_TIME: float = 1.0
|
||||
DEFAULT_MOBJECT_TO_EDGE_BUFF: float = manim_config.sizes.default_mobject_to_edge_buff
|
||||
DEFAULT_MOBJECT_TO_MOBJECT_BUFF: float = manim_config.sizes.default_mobject_to_mobject_buff
|
||||
|
||||
|
||||
# Standard vectors
|
||||
ORIGIN: Vect3 = np.array([0., 0., 0.])
|
||||
UP: Vect3 = np.array([0., 1., 0.])
|
||||
DOWN: Vect3 = np.array([0., -1., 0.])
|
||||
@@ -61,111 +58,83 @@ BOTTOM: Vect3 = FRAME_Y_RADIUS * DOWN
|
||||
LEFT_SIDE: Vect3 = FRAME_X_RADIUS * LEFT
|
||||
RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT
|
||||
|
||||
# Angles
|
||||
PI: float = np.pi
|
||||
TAU: float = 2 * PI
|
||||
DEGREES: float = TAU / 360
|
||||
DEG: float = TAU / 360
|
||||
DEGREES = DEG # Many older animations use the full name
|
||||
# Nice to have a constant for readability
|
||||
# when juxtaposed with expressions like 30 * DEGREES
|
||||
# when juxtaposed with expressions like 30 * DEG
|
||||
RADIANS: float = 1
|
||||
|
||||
FFMPEG_BIN: str = "ffmpeg"
|
||||
|
||||
JOINT_TYPE_MAP: dict = {
|
||||
"no_joint": 0,
|
||||
"auto": 1,
|
||||
"bevel": 2,
|
||||
"miter": 3,
|
||||
}
|
||||
|
||||
# Related to Text
|
||||
NORMAL: str = "NORMAL"
|
||||
ITALIC: str = "ITALIC"
|
||||
OBLIQUE: str = "OBLIQUE"
|
||||
BOLD: str = "BOLD"
|
||||
|
||||
DEFAULT_STROKE_WIDTH: float = 4
|
||||
|
||||
# For keyboard interactions
|
||||
CTRL_SYMBOL: int = 65508
|
||||
SHIFT_SYMBOL: int = 65505
|
||||
COMMAND_SYMBOL: int = 65517
|
||||
DELETE_SYMBOL: int = 65288
|
||||
ARROW_SYMBOLS: list[int] = list(range(65361, 65365))
|
||||
|
||||
SHIFT_MODIFIER: int = 1
|
||||
CTRL_MODIFIER: int = 2
|
||||
COMMAND_MODIFIER: int = 64
|
||||
DEFAULT_STROKE_WIDTH: float = manim_config.vmobject.default_stroke_width
|
||||
|
||||
# Colors
|
||||
BLUE_E: ManimColor = manim_config.colors.blue_e
|
||||
BLUE_D: ManimColor = manim_config.colors.blue_d
|
||||
BLUE_C: ManimColor = manim_config.colors.blue_c
|
||||
BLUE_B: ManimColor = manim_config.colors.blue_b
|
||||
BLUE_A: ManimColor = manim_config.colors.blue_a
|
||||
TEAL_E: ManimColor = manim_config.colors.teal_e
|
||||
TEAL_D: ManimColor = manim_config.colors.teal_d
|
||||
TEAL_C: ManimColor = manim_config.colors.teal_c
|
||||
TEAL_B: ManimColor = manim_config.colors.teal_b
|
||||
TEAL_A: ManimColor = manim_config.colors.teal_a
|
||||
GREEN_E: ManimColor = manim_config.colors.green_e
|
||||
GREEN_D: ManimColor = manim_config.colors.green_d
|
||||
GREEN_C: ManimColor = manim_config.colors.green_c
|
||||
GREEN_B: ManimColor = manim_config.colors.green_b
|
||||
GREEN_A: ManimColor = manim_config.colors.green_a
|
||||
YELLOW_E: ManimColor = manim_config.colors.yellow_e
|
||||
YELLOW_D: ManimColor = manim_config.colors.yellow_d
|
||||
YELLOW_C: ManimColor = manim_config.colors.yellow_c
|
||||
YELLOW_B: ManimColor = manim_config.colors.yellow_b
|
||||
YELLOW_A: ManimColor = manim_config.colors.yellow_a
|
||||
GOLD_E: ManimColor = manim_config.colors.gold_e
|
||||
GOLD_D: ManimColor = manim_config.colors.gold_d
|
||||
GOLD_C: ManimColor = manim_config.colors.gold_c
|
||||
GOLD_B: ManimColor = manim_config.colors.gold_b
|
||||
GOLD_A: ManimColor = manim_config.colors.gold_a
|
||||
RED_E: ManimColor = manim_config.colors.red_e
|
||||
RED_D: ManimColor = manim_config.colors.red_d
|
||||
RED_C: ManimColor = manim_config.colors.red_c
|
||||
RED_B: ManimColor = manim_config.colors.red_b
|
||||
RED_A: ManimColor = manim_config.colors.red_a
|
||||
MAROON_E: ManimColor = manim_config.colors.maroon_e
|
||||
MAROON_D: ManimColor = manim_config.colors.maroon_d
|
||||
MAROON_C: ManimColor = manim_config.colors.maroon_c
|
||||
MAROON_B: ManimColor = manim_config.colors.maroon_b
|
||||
MAROON_A: ManimColor = manim_config.colors.maroon_a
|
||||
PURPLE_E: ManimColor = manim_config.colors.purple_e
|
||||
PURPLE_D: ManimColor = manim_config.colors.purple_d
|
||||
PURPLE_C: ManimColor = manim_config.colors.purple_c
|
||||
PURPLE_B: ManimColor = manim_config.colors.purple_b
|
||||
PURPLE_A: ManimColor = manim_config.colors.purple_a
|
||||
GREY_E: ManimColor = manim_config.colors.grey_e
|
||||
GREY_D: ManimColor = manim_config.colors.grey_d
|
||||
GREY_C: ManimColor = manim_config.colors.grey_c
|
||||
GREY_B: ManimColor = manim_config.colors.grey_b
|
||||
GREY_A: ManimColor = manim_config.colors.grey_a
|
||||
WHITE: ManimColor = manim_config.colors.white
|
||||
BLACK: ManimColor = manim_config.colors.black
|
||||
GREY_BROWN: ManimColor = manim_config.colors.grey_brown
|
||||
DARK_BROWN: ManimColor = manim_config.colors.dark_brown
|
||||
LIGHT_BROWN: ManimColor = manim_config.colors.light_brown
|
||||
PINK: ManimColor = manim_config.colors.pink
|
||||
LIGHT_PINK: ManimColor = manim_config.colors.light_pink
|
||||
GREEN_SCREEN: ManimColor = manim_config.colors.green_screen
|
||||
ORANGE: ManimColor = manim_config.colors.orange
|
||||
PURE_RED: ManimColor = manim_config.colors.pure_red
|
||||
PURE_GREEN: ManimColor = manim_config.colors.pure_green
|
||||
PURE_BLUE: ManimColor = manim_config.colors.pure_blue
|
||||
|
||||
BLUE_E: ManimColor = "#1C758A"
|
||||
BLUE_D: ManimColor = "#29ABCA"
|
||||
BLUE_C: ManimColor = "#58C4DD"
|
||||
BLUE_B: ManimColor = "#9CDCEB"
|
||||
BLUE_A: ManimColor = "#C7E9F1"
|
||||
TEAL_E: ManimColor = "#49A88F"
|
||||
TEAL_D: ManimColor = "#55C1A7"
|
||||
TEAL_C: ManimColor = "#5CD0B3"
|
||||
TEAL_B: ManimColor = "#76DDC0"
|
||||
TEAL_A: ManimColor = "#ACEAD7"
|
||||
GREEN_E: ManimColor = "#699C52"
|
||||
GREEN_D: ManimColor = "#77B05D"
|
||||
GREEN_C: ManimColor = "#83C167"
|
||||
GREEN_B: ManimColor = "#A6CF8C"
|
||||
GREEN_A: ManimColor = "#C9E2AE"
|
||||
YELLOW_E: ManimColor = "#E8C11C"
|
||||
YELLOW_D: ManimColor = "#F4D345"
|
||||
YELLOW_C: ManimColor = "#FFFF00"
|
||||
YELLOW_B: ManimColor = "#FFEA94"
|
||||
YELLOW_A: ManimColor = "#FFF1B6"
|
||||
GOLD_E: ManimColor = "#C78D46"
|
||||
GOLD_D: ManimColor = "#E1A158"
|
||||
GOLD_C: ManimColor = "#F0AC5F"
|
||||
GOLD_B: ManimColor = "#F9B775"
|
||||
GOLD_A: ManimColor = "#F7C797"
|
||||
RED_E: ManimColor = "#CF5044"
|
||||
RED_D: ManimColor = "#E65A4C"
|
||||
RED_C: ManimColor = "#FC6255"
|
||||
RED_B: ManimColor = "#FF8080"
|
||||
RED_A: ManimColor = "#F7A1A3"
|
||||
MAROON_E: ManimColor = "#94424F"
|
||||
MAROON_D: ManimColor = "#A24D61"
|
||||
MAROON_C: ManimColor = "#C55F73"
|
||||
MAROON_B: ManimColor = "#EC92AB"
|
||||
MAROON_A: ManimColor = "#ECABC1"
|
||||
PURPLE_E: ManimColor = "#644172"
|
||||
PURPLE_D: ManimColor = "#715582"
|
||||
PURPLE_C: ManimColor = "#9A72AC"
|
||||
PURPLE_B: ManimColor = "#B189C6"
|
||||
PURPLE_A: ManimColor = "#CAA3E8"
|
||||
GREY_E: ManimColor = "#222222"
|
||||
GREY_D: ManimColor = "#444444"
|
||||
GREY_C: ManimColor = "#888888"
|
||||
GREY_B: ManimColor = "#BBBBBB"
|
||||
GREY_A: ManimColor = "#DDDDDD"
|
||||
WHITE: ManimColor = "#FFFFFF"
|
||||
BLACK: ManimColor = "#000000"
|
||||
GREY_BROWN: ManimColor = "#736357"
|
||||
DARK_BROWN: ManimColor = "#8B4513"
|
||||
LIGHT_BROWN: ManimColor = "#CD853F"
|
||||
PINK: ManimColor = "#D147BD"
|
||||
LIGHT_PINK: ManimColor = "#DC75CD"
|
||||
GREEN_SCREEN: ManimColor = "#00FF00"
|
||||
ORANGE: ManimColor = "#FF862F"
|
||||
|
||||
MANIM_COLORS: List[ManimColor] = [
|
||||
BLACK, GREY_E, GREY_D, GREY_C, GREY_B, GREY_A, WHITE,
|
||||
BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A,
|
||||
TEAL_E, TEAL_D, TEAL_C, TEAL_B, TEAL_A,
|
||||
GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A,
|
||||
YELLOW_E, YELLOW_D, YELLOW_C, YELLOW_B, YELLOW_A,
|
||||
GOLD_E, GOLD_D, GOLD_C, GOLD_B, GOLD_A,
|
||||
RED_E, RED_D, RED_C, RED_B, RED_A,
|
||||
MAROON_E, MAROON_D, MAROON_C, MAROON_B, MAROON_A,
|
||||
PURPLE_E, PURPLE_D, PURPLE_C, PURPLE_B, PURPLE_A,
|
||||
GREY_BROWN, DARK_BROWN, LIGHT_BROWN,
|
||||
PINK, LIGHT_PINK,
|
||||
]
|
||||
MANIM_COLORS: List[ManimColor] = list(manim_config.colors.values())
|
||||
|
||||
# Abbreviated names for the "median" colors
|
||||
BLUE: ManimColor = BLUE_C
|
||||
@@ -179,3 +148,12 @@ PURPLE: ManimColor = PURPLE_C
|
||||
GREY: ManimColor = GREY_C
|
||||
|
||||
COLORMAP_3B1B: List[ManimColor] = [BLUE_E, GREEN, YELLOW, RED]
|
||||
|
||||
# Default mobject colors should be configurable just like background color
|
||||
# DEFAULT_MOBJECT_COLOR is mainly for text, tex, line, etc... mobjects. Default is WHITE
|
||||
# DEFAULT_LIGHT_COLOR is mainly for things like axes, arrows, annulus and other lightly colored mobjects. Default is GREY_B
|
||||
DEFAULT_MOBJECT_COLOR: ManimColor = manim_config.mobject.default_mobject_color or WHITE
|
||||
DEFAULT_LIGHT_COLOR: ManimColor = manim_config.mobject.default_light_color or GREY_B
|
||||
|
||||
DEFAULT_VMOBJECT_STROKE_COLOR : ManimColor = manim_config.vmobject.default_stroke_color or GREY_A
|
||||
DEFAULT_VMOBJECT_FILL_COLOR : ManimColor = manim_config.vmobject.default_fill_color or GREY_C
|
||||
|
||||
@@ -1,51 +1,184 @@
|
||||
# This file determines the default configuration for how manim is
|
||||
# run, including names for directories it will write to, default
|
||||
# parameters for various classes, style choices, etc. To customize
|
||||
# your own, create a custom_config.yml file in whatever directory
|
||||
# you are running manim. For 3blue1brown, for instance, mind is
|
||||
# here: https://github.com/3b1b/videos/blob/master/custom_config.yml
|
||||
|
||||
# Alternatively, you can create it wherever you like, and on running
|
||||
# manim, pass in `--config_file /path/to/custom/config/file.yml`
|
||||
|
||||
directories:
|
||||
# Set this to true if you want the path to video files
|
||||
# to match the directory structure of the path to the
|
||||
# sourcecode generating that video
|
||||
# source code generating that video
|
||||
mirror_module_path: False
|
||||
# Where should manim output video and image files?
|
||||
output: ""
|
||||
# If you want to use images, manim will look to these folders to find them
|
||||
raster_images: ""
|
||||
vector_images: ""
|
||||
# If you want to use sounds, manim will look here to find it.
|
||||
sounds: ""
|
||||
# Manim often generates tex_files or other kinds of serialized data
|
||||
# to keep from having to generate the same thing too many times. By
|
||||
# default, these will be stored at tempfile.gettempdir(), e.g. this might
|
||||
# return whatever is at to the TMPDIR environment variable. If you want to
|
||||
# specify them elsewhere,
|
||||
temporary_storage: ""
|
||||
universal_import_line: "from manimlib import *"
|
||||
style:
|
||||
tex_template: "default"
|
||||
font: "Consolas"
|
||||
text_alignment: "LEFT"
|
||||
# Manim may write to and read from the file system, e.g.
|
||||
# to render videos and to look for svg/png assets. This
|
||||
# will specify where those assets live, with a base directory,
|
||||
# and various subdirectory names within it
|
||||
base: ""
|
||||
subdirs:
|
||||
# Where should manim output video and image files?
|
||||
output: "videos"
|
||||
# If you want to use images, manim will look to these folders to find them
|
||||
raster_images: "raster_images"
|
||||
vector_images: "vector_images"
|
||||
# If you want to use sounds, manim will look here to find it.
|
||||
sounds: "sounds"
|
||||
# Place for other forms of data relevant to any projects, like csv's
|
||||
data: "data"
|
||||
# When downloading, say an image, where will it go?
|
||||
downloads: "downloads"
|
||||
# For certain object types, especially Tex and Text, manim will save information
|
||||
# to file to prevent the need to re-compute, e.g. recompiling the latex. By default,
|
||||
# it stores this saved data to whatever directory appdirs.user_cache_dir("manim") returns,
|
||||
# but here a user can specify a different cache location
|
||||
cache: ""
|
||||
window:
|
||||
# The position of window on screen. UR -> Upper Right, and likewise DL -> Down and Left,
|
||||
# UO would be upper middle, etc.
|
||||
position_string: UR
|
||||
# If using multiple monitors, which one should show the window
|
||||
monitor_index: 0
|
||||
# If not full screen, the default to give it half the screen width
|
||||
full_screen: False
|
||||
# Other optional specifications that override the above include:
|
||||
# position: (500, 500) # Specific position, in pixel coordinates, for upper right corner
|
||||
# size: (1920, 1080) # Specific size, in pixels
|
||||
camera:
|
||||
resolution: (1920, 1080)
|
||||
background_color: "#333333"
|
||||
# Set the position of preview window, you can use directions, e.g. UL/DR/OL/OO/...
|
||||
# also, you can also specify the position(pixel) of the upper left corner of
|
||||
# the window on the monitor, e.g. "960,540"
|
||||
window_position: UR
|
||||
window_monitor: 0
|
||||
full_screen: False
|
||||
file_writer_config:
|
||||
# If break_into_partial_movies is set to True, then many small
|
||||
# files will be written corresponding to each Scene.play and
|
||||
# Scene.wait call, and these files will then be combined
|
||||
# to form the full scene. Sometimes video-editing is made
|
||||
# easier when working with the broken up scene, which
|
||||
# effectively has cuts at all the places you might want.
|
||||
break_into_partial_movies: False
|
||||
fps: 30
|
||||
background_opacity: 1.0
|
||||
file_writer:
|
||||
# What command to use for ffmpeg
|
||||
ffmpeg_bin: "ffmpeg"
|
||||
# Parameters to pass into ffmpeg
|
||||
video_codec: "libx264"
|
||||
pixel_format: "yuv420p"
|
||||
saturation: 1.0
|
||||
gamma: 1.0
|
||||
camera_resolutions:
|
||||
low: "854x480"
|
||||
med: "1280x720"
|
||||
high: "1920x1080"
|
||||
4k: "3840x2160"
|
||||
default_resolution: "high"
|
||||
fps: 30
|
||||
embed_exception_mode: "Verbose"
|
||||
embed_error_sound: False
|
||||
# Most of the scene configuration will come from CLI arguments,
|
||||
# but defaults can be set here
|
||||
scene:
|
||||
show_animation_progress: False
|
||||
leave_progress_bars: False
|
||||
# When skipping animations, should a single frame be rendered
|
||||
# at the end of each play call?
|
||||
preview_while_skipping: True
|
||||
# How long does a scene pause on Scene.wait calls
|
||||
default_wait_time: 1.0
|
||||
vmobject:
|
||||
default_stroke_width: 4.0
|
||||
default_stroke_color: "#DDDDDD" # Default is GREY_A
|
||||
default_fill_color: "#888888" # Default is GREY_C
|
||||
mobject:
|
||||
default_mobject_color: "#FFFFFF" # Default is WHITE
|
||||
default_light_color: "#BBBBBB" # Default is GREY_B
|
||||
tex:
|
||||
# See tex_templates.yml
|
||||
template: "default"
|
||||
text:
|
||||
# font: "Cambria Math"
|
||||
font: "Consolas"
|
||||
alignment: "LEFT"
|
||||
embed:
|
||||
exception_mode: "Verbose"
|
||||
autoreload: False
|
||||
resolution_options:
|
||||
# When the user passes in -l, -m, --hd or --uhd, these are the corresponding
|
||||
# resolutions
|
||||
low: (854, 480)
|
||||
med: (1280, 720)
|
||||
high: (1920, 1080)
|
||||
4k: (3840, 2160)
|
||||
sizes:
|
||||
# This determines the scale of the manim coordinate system with respect to
|
||||
# the viewing frame
|
||||
frame_height: 8.0
|
||||
# These determine the constants SMALL_BUFF, MED_SMALL_BUFF, etc., useful
|
||||
# for nudging things around and having default spacing values
|
||||
small_buff: 0.1
|
||||
med_small_buff: 0.25
|
||||
med_large_buff: 0.5
|
||||
large_buff: 1.0
|
||||
# Default buffers used in Mobject.next_to or Mobject.to_edge
|
||||
default_mobject_to_edge_buff: 0.5
|
||||
default_mobject_to_mobject_buff: 0.25
|
||||
key_bindings:
|
||||
pan_3d: "d"
|
||||
pan: "f"
|
||||
reset: "r"
|
||||
quit: "q" # Together with command
|
||||
select: "s"
|
||||
unselect: "u"
|
||||
grab: "g"
|
||||
x_grab: "h"
|
||||
y_grab: "v"
|
||||
resize: "t"
|
||||
color: "c"
|
||||
information: "i"
|
||||
cursor: "k"
|
||||
colors:
|
||||
blue_e: "#1C758A"
|
||||
blue_d: "#29ABCA"
|
||||
blue_c: "#58C4DD"
|
||||
blue_b: "#9CDCEB"
|
||||
blue_a: "#C7E9F1"
|
||||
teal_e: "#49A88F"
|
||||
teal_d: "#55C1A7"
|
||||
teal_c: "#5CD0B3"
|
||||
teal_b: "#76DDC0"
|
||||
teal_a: "#ACEAD7"
|
||||
green_e: "#699C52"
|
||||
green_d: "#77B05D"
|
||||
green_c: "#83C167"
|
||||
green_b: "#A6CF8C"
|
||||
green_a: "#C9E2AE"
|
||||
yellow_e: "#E8C11C"
|
||||
yellow_d: "#F4D345"
|
||||
yellow_c: "#FFFF00"
|
||||
yellow_b: "#FFEA94"
|
||||
yellow_a: "#FFF1B6"
|
||||
gold_e: "#C78D46"
|
||||
gold_d: "#E1A158"
|
||||
gold_c: "#F0AC5F"
|
||||
gold_b: "#F9B775"
|
||||
gold_a: "#F7C797"
|
||||
red_e: "#CF5044"
|
||||
red_d: "#E65A4C"
|
||||
red_c: "#FC6255"
|
||||
red_b: "#FF8080"
|
||||
red_a: "#F7A1A3"
|
||||
maroon_e: "#94424F"
|
||||
maroon_d: "#A24D61"
|
||||
maroon_c: "#C55F73"
|
||||
maroon_b: "#EC92AB"
|
||||
maroon_a: "#ECABC1"
|
||||
purple_e: "#644172"
|
||||
purple_d: "#715582"
|
||||
purple_c: "#9A72AC"
|
||||
purple_b: "#B189C6"
|
||||
purple_a: "#CAA3E8"
|
||||
grey_e: "#222222"
|
||||
grey_d: "#444444"
|
||||
grey_c: "#888888"
|
||||
grey_b: "#BBBBBB"
|
||||
grey_a: "#DDDDDD"
|
||||
white: "#FFFFFF"
|
||||
black: "#000000"
|
||||
grey_brown: "#736357"
|
||||
dark_brown: "#8B4513"
|
||||
light_brown: "#CD853F"
|
||||
pink: "#D147BD"
|
||||
light_pink: "#DC75CD"
|
||||
green_screen: "#00FF00"
|
||||
orange: "#FF862F"
|
||||
pure_red: "#FF0000"
|
||||
pure_green: "#00FF00"
|
||||
pure_blue: "#0000FF"
|
||||
# Can be DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
log_level: "INFO"
|
||||
universal_import_line: "from manimlib import *"
|
||||
ignore_manimlib_modules_on_reload: True
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.module_loader import ModuleLoader
|
||||
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.logger import log
|
||||
from manimlib.scene.interactive_scene import InteractiveScene
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
Module = importlib.util.types.ModuleType
|
||||
from typing import Optional
|
||||
from addict import Dict
|
||||
|
||||
|
||||
class BlankScene(InteractiveScene):
|
||||
def construct(self):
|
||||
exec(get_custom_config()["universal_import_line"])
|
||||
exec(manim_config.universal_import_line)
|
||||
self.embed()
|
||||
|
||||
|
||||
@@ -34,11 +45,7 @@ def prompt_user_for_choice(scene_classes):
|
||||
print(f"{str(idx).zfill(max_digits)}: {name}")
|
||||
name_to_class[name] = scene_class
|
||||
try:
|
||||
user_input = input(
|
||||
"\nThat module has multiple scenes, " + \
|
||||
"which ones would you like to render?" + \
|
||||
"\nScene Name or Number: "
|
||||
)
|
||||
user_input = input("\nSelect which scene to render (by name or number): ")
|
||||
return [
|
||||
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str) - 1]
|
||||
for split_str in user_input.replace(" ", "").split(",")
|
||||
@@ -53,14 +60,6 @@ def prompt_user_for_choice(scene_classes):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_scene_config(config):
|
||||
scene_parameters = inspect.signature(Scene).parameters.keys()
|
||||
return {
|
||||
key: config[key]
|
||||
for key in set(scene_parameters).intersection(config.keys())
|
||||
}
|
||||
|
||||
|
||||
def compute_total_frames(scene_class, scene_config):
|
||||
"""
|
||||
When a scene is being written to file, a copy of the scene is run with
|
||||
@@ -76,41 +75,44 @@ def compute_total_frames(scene_class, scene_config):
|
||||
pre_scene = scene_class(**pre_config)
|
||||
pre_scene.run()
|
||||
total_time = pre_scene.time - pre_scene.skip_time
|
||||
return int(total_time * scene_config["camera_config"]["fps"])
|
||||
return int(total_time * manim_config.camera.fps)
|
||||
|
||||
|
||||
def scene_from_class(scene_class, scene_config, config):
|
||||
fw_config = scene_config["file_writer_config"]
|
||||
if fw_config["write_to_movie"] and config["prerun"]:
|
||||
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
|
||||
def scene_from_class(scene_class, scene_config: Dict, run_config: Dict):
|
||||
fw_config = manim_config.file_writer
|
||||
if fw_config.write_to_movie and run_config.prerun:
|
||||
scene_config.file_writer_config.total_frames = compute_total_frames(scene_class, scene_config)
|
||||
return scene_class(**scene_config)
|
||||
|
||||
|
||||
def get_scenes_to_render(all_scene_classes, scene_config, config):
|
||||
if config["write_all"]:
|
||||
return [sc(**scene_config) for sc in all_scene_classes]
|
||||
def note_missing_scenes(arg_names, module_names):
|
||||
for name in arg_names:
|
||||
if name not in module_names:
|
||||
log.error(f"No scene named {name} found")
|
||||
|
||||
names_to_classes = {sc.__name__ : sc for sc in all_scene_classes}
|
||||
scene_names = config["scene_names"]
|
||||
|
||||
for name in set.difference(set(scene_names), names_to_classes):
|
||||
log.error(f"No scene named {name} found")
|
||||
scene_names.remove(name)
|
||||
|
||||
if scene_names:
|
||||
classes_to_run = [names_to_classes[name] for name in scene_names]
|
||||
elif len(all_scene_classes) == 1:
|
||||
classes_to_run = [all_scene_classes[0]]
|
||||
def get_scenes_to_render(all_scene_classes: list, scene_config: Dict, run_config: Dict):
|
||||
if run_config["write_all"] or len(all_scene_classes) == 1:
|
||||
classes_to_run = all_scene_classes
|
||||
else:
|
||||
name_to_class = {sc.__name__: sc for sc in all_scene_classes}
|
||||
classes_to_run = [name_to_class.get(name) for name in run_config.scene_names]
|
||||
classes_to_run = list(filter(lambda x: x, classes_to_run)) # Remove Nones
|
||||
note_missing_scenes(run_config.scene_names, name_to_class.keys())
|
||||
|
||||
if len(classes_to_run) == 0:
|
||||
classes_to_run = prompt_user_for_choice(all_scene_classes)
|
||||
|
||||
return [
|
||||
scene_from_class(scene_class, scene_config, config)
|
||||
scene_from_class(scene_class, scene_config, run_config)
|
||||
for scene_class in classes_to_run
|
||||
]
|
||||
|
||||
|
||||
def get_scene_classes_from_module(module):
|
||||
def get_scene_classes(module: Optional[Module]):
|
||||
if module is None:
|
||||
# If no module was passed in, just play the blank scene
|
||||
return [BlankScene]
|
||||
if hasattr(module, "SCENES_IN_ORDER"):
|
||||
return module.SCENES_IN_ORDER
|
||||
else:
|
||||
@@ -123,13 +125,70 @@ def get_scene_classes_from_module(module):
|
||||
]
|
||||
|
||||
|
||||
def main(config):
|
||||
module = config["module"]
|
||||
scene_config = get_scene_config(config)
|
||||
if module is None:
|
||||
# If no module was passed in, just play the blank scene
|
||||
return [BlankScene(**scene_config)]
|
||||
def get_indent(code_lines: list[str], line_number: int) -> str:
|
||||
"""
|
||||
Find the indent associated with a given line of python code,
|
||||
as a string of spaces
|
||||
"""
|
||||
# Find most recent non-empty line
|
||||
try:
|
||||
line = next(filter(lambda line: line.strip(), code_lines[line_number - 1::-1]))
|
||||
except StopIteration:
|
||||
return ""
|
||||
|
||||
all_scene_classes = get_scene_classes_from_module(module)
|
||||
scenes = get_scenes_to_render(all_scene_classes, scene_config, config)
|
||||
# Either return its leading spaces, or add for if it ends with colon
|
||||
n_spaces = len(line) - len(line.lstrip())
|
||||
if line.endswith(":"):
|
||||
n_spaces += 4
|
||||
return n_spaces * " "
|
||||
|
||||
|
||||
def insert_embed_line_to_module(module: Module, run_config: Dict) -> None:
|
||||
"""
|
||||
This is hacky, but convenient. When user includes the argument "-e", it will try
|
||||
to recreate a file that inserts the line `self.embed()` into the end of the scene's
|
||||
construct method. If there is an argument passed in, it will insert the line after
|
||||
the last line in the sourcefile which includes that string.
|
||||
"""
|
||||
lines = inspect.getsource(module).splitlines()
|
||||
line_number = run_config.embed_line
|
||||
|
||||
# Add the relevant embed line to the code
|
||||
indent = get_indent(lines, line_number)
|
||||
lines.insert(line_number, indent + "self.embed()")
|
||||
new_code = "\n".join(lines)
|
||||
|
||||
# When the user executes the `-e <line_number>` command
|
||||
# without specifying scene_names, the nearest class name above
|
||||
# `<line_number>` will be automatically used as 'scene_names'.
|
||||
|
||||
if not run_config.scene_names:
|
||||
classes = list(filter(lambda line: line.startswith("class"), lines[:line_number]))
|
||||
if classes:
|
||||
from re import search
|
||||
|
||||
scene_name = search(r"(\w+)\(", classes[-1])
|
||||
run_config.update(scene_names=[scene_name.group(1)])
|
||||
else:
|
||||
log.error(f"No 'class' found above {line_number}!")
|
||||
|
||||
# Execute the code, which presumably redefines the user's
|
||||
# scene to include this embed line, within the relevant module.
|
||||
code_object = compile(new_code, module.__name__, 'exec')
|
||||
exec(code_object, module.__dict__)
|
||||
|
||||
|
||||
def get_module(run_config: Dict) -> Module:
|
||||
module = ModuleLoader.get_module(run_config.file_name, run_config.is_reload)
|
||||
if run_config.embed_line:
|
||||
insert_embed_line_to_module(module, run_config)
|
||||
return module
|
||||
|
||||
|
||||
def main(scene_config: Dict, run_config: Dict):
|
||||
module = get_module(run_config)
|
||||
all_scene_classes = get_scene_classes(module)
|
||||
scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config)
|
||||
if len(scenes) == 0:
|
||||
print("No scenes found to run")
|
||||
return scenes
|
||||
|
||||
@@ -11,4 +11,3 @@ logging.basicConfig(
|
||||
)
|
||||
|
||||
log = logging.getLogger("manimgl")
|
||||
log.setLevel("DEBUG")
|
||||
|
||||
@@ -7,7 +7,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
# Boolean operations between 2D mobjects
|
||||
# Borrowed from from https://github.com/ManimCommunity/manim/
|
||||
# Borrowed from https://github.com/ManimCommunity/manim/
|
||||
|
||||
def _convert_vmobject_to_skia_path(vmobject: VMobject) -> pathops.Path:
|
||||
path = pathops.Path()
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import BLUE_B, BLUE_D, BLUE_E, GREY_BROWN, WHITE
|
||||
from manimlib.constants import BLUE_B, BLUE_D, BLUE_E, GREY_BROWN, DEFAULT_MOBJECT_COLOR
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
@@ -101,10 +101,17 @@ class TracedPath(VMobject):
|
||||
traced_point_func: Callable[[], Vect3],
|
||||
time_traced: float = np.inf,
|
||||
time_per_anchor: float = 1.0 / 15,
|
||||
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
stroke_width: float | Iterable[float] = 2.0,
|
||||
stroke_color: ManimColor = WHITE,
|
||||
stroke_opacity: float = 1.0,
|
||||
**kwargs
|
||||
):
|
||||
self.stroke_config = dict(
|
||||
color=stroke_color,
|
||||
width=stroke_width,
|
||||
opacity=stroke_opacity,
|
||||
)
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.time_traced = time_traced
|
||||
@@ -112,7 +119,6 @@ class TracedPath(VMobject):
|
||||
self.time: float = 0
|
||||
self.traced_points: list[np.ndarray] = []
|
||||
self.add_updater(lambda m, dt: m.update_path(dt))
|
||||
self.set_stroke(stroke_color, stroke_width)
|
||||
|
||||
def update_path(self, dt: float) -> Self:
|
||||
if dt == 0:
|
||||
@@ -136,6 +142,8 @@ class TracedPath(VMobject):
|
||||
if points:
|
||||
self.set_points_smoothly(points)
|
||||
|
||||
self.set_stroke(**self.stroke_config)
|
||||
|
||||
self.time += dt
|
||||
return self
|
||||
|
||||
@@ -145,21 +153,24 @@ class TracingTail(TracedPath):
|
||||
self,
|
||||
mobject_or_func: Mobject | Callable[[], np.ndarray],
|
||||
time_traced: float = 1.0,
|
||||
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
stroke_width: float | Iterable[float] = (0, 3),
|
||||
stroke_opacity: float | Iterable[float] = (0, 1),
|
||||
stroke_color: ManimColor = WHITE,
|
||||
**kwargs
|
||||
):
|
||||
if isinstance(mobject_or_func, Mobject):
|
||||
func = mobject_or_func.get_center
|
||||
else:
|
||||
func = mobject_or_func
|
||||
|
||||
super().__init__(
|
||||
func,
|
||||
time_traced=time_traced,
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
stroke_opacity=stroke_opacity,
|
||||
stroke_color=stroke_color,
|
||||
**kwargs
|
||||
)
|
||||
self.add_updater(lambda m: m.set_stroke(width=stroke_width, opacity=stroke_opacity))
|
||||
curr_point = self.traced_point_func()
|
||||
n_points = int(self.time_traced / self.time_per_anchor)
|
||||
self.traced_points: list[np.ndarray] = n_points * [curr_point]
|
||||
|
||||
@@ -6,8 +6,8 @@ import numbers
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
|
||||
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
|
||||
from manimlib.constants import DEGREES, PI
|
||||
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, RED, DEFAULT_MOBJECT_COLOR
|
||||
from manimlib.constants import DEG, PI
|
||||
from manimlib.constants import DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
|
||||
@@ -45,6 +45,12 @@ DEFAULT_X_RANGE = (-8.0, 8.0, 1.0)
|
||||
DEFAULT_Y_RANGE = (-4.0, 4.0, 1.0)
|
||||
|
||||
|
||||
def full_range_specifier(range_args):
|
||||
if len(range_args) == 2:
|
||||
return (*range_args, 1)
|
||||
return range_args
|
||||
|
||||
|
||||
class CoordinateSystem(ABC):
|
||||
"""
|
||||
Abstract class for Axes and NumberPlane
|
||||
@@ -57,8 +63,8 @@ class CoordinateSystem(ABC):
|
||||
y_range: RangeSpecifier = DEFAULT_Y_RANGE,
|
||||
num_sampled_graph_points_per_tick: int = 5,
|
||||
):
|
||||
self.x_range = x_range
|
||||
self.y_range = y_range
|
||||
self.x_range = full_range_specifier(x_range)
|
||||
self.y_range = full_range_specifier(y_range)
|
||||
self.num_sampled_graph_points_per_tick = num_sampled_graph_points_per_tick
|
||||
|
||||
@abstractmethod
|
||||
@@ -301,7 +307,7 @@ class CoordinateSystem(ABC):
|
||||
|
||||
point = self.input_to_graph_point(x, graph)
|
||||
angle = self.angle_of_tangent(x, graph)
|
||||
normal = rotate_vector(RIGHT, angle + 90 * DEGREES)
|
||||
normal = rotate_vector(RIGHT, angle + 90 * DEG)
|
||||
if normal[1] < 0:
|
||||
normal *= -1
|
||||
label.next_to(point, normal, buff=buff)
|
||||
@@ -399,22 +405,26 @@ class CoordinateSystem(ABC):
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_background=stroke_background
|
||||
stroke_behind=stroke_background
|
||||
)
|
||||
for rect in result:
|
||||
if not rect.positive:
|
||||
rect.set_fill(negative_color)
|
||||
return result
|
||||
|
||||
def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=0.5):
|
||||
if not hasattr(graph, "x_range"):
|
||||
raise Exception("Argument `graph` must have attribute `x_range`")
|
||||
def get_area_under_graph(self, graph, x_range=None, fill_color=BLUE, fill_opacity=0.5):
|
||||
if x_range is None:
|
||||
x_range = [
|
||||
self.x_axis.p2n(graph.get_start()),
|
||||
self.x_axis.p2n(graph.get_end()),
|
||||
]
|
||||
|
||||
alpha_bounds = [
|
||||
inverse_interpolate(*graph.x_range, x)
|
||||
inverse_interpolate(*graph.x_range[:2], x)
|
||||
for x in x_range
|
||||
]
|
||||
sub_graph = graph.copy()
|
||||
sub_graph.clear_updaters()
|
||||
sub_graph.pointwise_become_partial(graph, *alpha_bounds)
|
||||
sub_graph.add_line_to(self.c2p(x_range[1], 0))
|
||||
sub_graph.add_line_to(self.c2p(x_range[0], 0))
|
||||
@@ -468,7 +478,7 @@ class Axes(VGroup, CoordinateSystem):
|
||||
),
|
||||
length=height,
|
||||
)
|
||||
self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN)
|
||||
self.y_axis.rotate(90 * DEG, about_point=ORIGIN)
|
||||
# Add as a separate group in case various other
|
||||
# mobjects are added to self, as for example in
|
||||
# NumberPlane below
|
||||
@@ -536,7 +546,7 @@ class ThreeDAxes(Axes):
|
||||
):
|
||||
Axes.__init__(self, x_range, y_range, **kwargs)
|
||||
|
||||
self.z_range = z_range
|
||||
self.z_range = full_range_specifier(z_range)
|
||||
self.z_axis = self.create_axis(
|
||||
self.z_range,
|
||||
axis_config=merge_dicts_recursively(
|
||||
@@ -611,7 +621,7 @@ class ThreeDAxes(Axes):
|
||||
|
||||
class NumberPlane(Axes):
|
||||
default_axis_config: dict = dict(
|
||||
stroke_color=WHITE,
|
||||
stroke_color=DEFAULT_MOBJECT_COLOR,
|
||||
stroke_width=2,
|
||||
include_ticks=False,
|
||||
include_tip=False,
|
||||
@@ -632,7 +642,10 @@ class NumberPlane(Axes):
|
||||
stroke_opacity=1,
|
||||
),
|
||||
# Defaults to a faded version of line_config
|
||||
faded_line_style: dict = dict(),
|
||||
faded_line_style: dict = dict(
|
||||
stroke_width=1,
|
||||
stroke_opacity=0.25,
|
||||
),
|
||||
faded_line_ratio: int = 4,
|
||||
make_smooth_after_applying_functions: bool = True,
|
||||
**kwargs
|
||||
@@ -645,14 +658,8 @@ class NumberPlane(Axes):
|
||||
self.init_background_lines()
|
||||
|
||||
def init_background_lines(self) -> None:
|
||||
if not self.faded_line_style:
|
||||
style = dict(self.background_line_style)
|
||||
# For anything numerical, like stroke_width
|
||||
# and stroke_opacity, chop it in half
|
||||
for key in style:
|
||||
if isinstance(style[key], numbers.Number):
|
||||
style[key] *= 0.5
|
||||
self.faded_line_style = style
|
||||
if "stroke_color" not in self.faded_line_style:
|
||||
self.faded_line_style["stroke_color"] = self.background_line_style["stroke_color"]
|
||||
|
||||
self.background_lines, self.faded_lines = self.get_lines()
|
||||
self.background_lines.set_style(**self.background_line_style)
|
||||
@@ -720,11 +727,10 @@ class NumberPlane(Axes):
|
||||
|
||||
|
||||
class ComplexPlane(NumberPlane):
|
||||
def number_to_point(self, number: complex | float) -> Vect3:
|
||||
number = complex(number)
|
||||
return self.coords_to_point(number.real, number.imag)
|
||||
def number_to_point(self, number: complex | float | np.array) -> Vect3:
|
||||
return self.coords_to_point(np.real(number), np.imag(number))
|
||||
|
||||
def n2p(self, number: complex | float) -> Vect3:
|
||||
def n2p(self, number: complex | float | np.array) -> Vect3:
|
||||
return self.number_to_point(number)
|
||||
|
||||
def point_to_number(self, point: Vect3) -> complex:
|
||||
@@ -764,13 +770,6 @@ class ComplexPlane(NumberPlane):
|
||||
axis = self.get_x_axis()
|
||||
value = z.real
|
||||
number_mob = axis.get_number_mobject(value, font_size=font_size, **kwargs)
|
||||
# For -i, remove the "1"
|
||||
if z.imag == -1:
|
||||
number_mob.remove(number_mob[1])
|
||||
number_mob[0].next_to(
|
||||
number_mob[1], LEFT,
|
||||
buff=number_mob[0].get_width() / 4
|
||||
)
|
||||
self.coordinate_labels.add(number_mob)
|
||||
self.add(self.coordinate_labels)
|
||||
return self
|
||||
|
||||
@@ -37,6 +37,7 @@ class FullScreenRectangle(ScreenRectangle):
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_width=stroke_width,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import numbers
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UL, UP, UR
|
||||
from manimlib.constants import GREY_A, RED, WHITE, BLACK
|
||||
from manimlib.constants import RED, BLACK, DEFAULT_MOBJECT_COLOR, DEFAULT_LIGHT_COLOR
|
||||
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
|
||||
from manimlib.constants import DEGREES, PI, TAU
|
||||
from manimlib.constants import DEG, PI, TAU
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.types.vectorized_mobject import DashedVMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.bezier import bezier
|
||||
from manimlib.utils.bezier import quadratic_bezier_points_for_arc
|
||||
from manimlib.utils.bezier import partial_quadratic_bezier_points
|
||||
from manimlib.utils.iterables import adjacent_n_tuples
|
||||
from manimlib.utils.iterables import adjacent_pairs
|
||||
from manimlib.utils.simple_functions import clip
|
||||
@@ -30,7 +27,6 @@ from manimlib.utils.space_ops import normalize
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
from manimlib.utils.space_ops import rotation_matrix_transpose
|
||||
from manimlib.utils.space_ops import rotation_between_vectors
|
||||
from manimlib.utils.space_ops import rotation_about_z
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -207,17 +203,42 @@ class TipableVMobject(VMobject):
|
||||
|
||||
|
||||
class Arc(TipableVMobject):
|
||||
'''
|
||||
Creates an arc.
|
||||
Parameters
|
||||
-----
|
||||
start_angle : float
|
||||
Starting angle of the arc in radians. (Angles are measured counter-clockwise)
|
||||
angle : float
|
||||
Angle subtended by the arc at its center in radians. (Angles are measured counter-clockwise)
|
||||
radius : float
|
||||
Radius of the arc
|
||||
arc_center : array_like
|
||||
Center of the arc
|
||||
Examples :
|
||||
arc = Arc(start_angle=TAU/4, angle=TAU/2, radius=3, arc_center=ORIGIN)
|
||||
arc = Arc(angle=TAU/4, radius=4.5, arc_center=(1,2,0), color=BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : Arc object
|
||||
An Arc object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_angle: float = 0,
|
||||
angle: float = TAU / 4,
|
||||
radius: float = 1.0,
|
||||
n_components: int = 8,
|
||||
n_components: Optional[int] = None,
|
||||
arc_center: Vect3 = ORIGIN,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if n_components is None:
|
||||
# 16 components for a full circle
|
||||
n_components = int(15 * (abs(angle) / TAU)) + 1
|
||||
|
||||
self.set_points(quadratic_bezier_points_for_arc(angle, n_components))
|
||||
self.rotate(start_angle, about_point=ORIGIN)
|
||||
self.scale(radius, about_point=ORIGIN)
|
||||
@@ -252,6 +273,26 @@ class Arc(TipableVMobject):
|
||||
|
||||
|
||||
class ArcBetweenPoints(Arc):
|
||||
'''
|
||||
Creates an arc passing through the specified points with "angle" as the
|
||||
angle subtended at its center.
|
||||
Parameters
|
||||
-----
|
||||
start : array_like
|
||||
Starting point of the arc
|
||||
end : array_like
|
||||
Ending point of the arc
|
||||
angle : float
|
||||
Angle subtended by the arc at its center in radians. (Angles are measured counter-clockwise)
|
||||
Examples :
|
||||
arc = ArcBetweenPoints(start=(0, 0, 0), end=(1, 2, 0), angle=TAU / 2)
|
||||
arc = ArcBetweenPoints(start=(-2, 3, 0), end=(1, 2, 0), angle=-TAU / 12, color=BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : ArcBetweenPoints object
|
||||
An ArcBetweenPoints object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start: Vect3,
|
||||
@@ -266,6 +307,26 @@ class ArcBetweenPoints(Arc):
|
||||
|
||||
|
||||
class CurvedArrow(ArcBetweenPoints):
|
||||
'''
|
||||
Creates a curved arrow passing through the specified points with "angle" as the
|
||||
angle subtended at its center.
|
||||
Parameters
|
||||
-----
|
||||
start_point : array_like
|
||||
Starting point of the curved arrow
|
||||
end_point : array_like
|
||||
Ending point of the curved arrow
|
||||
angle : float
|
||||
Angle subtended by the curved arrow at its center in radians. (Angles are measured counter-clockwise)
|
||||
Examples :
|
||||
curvedArrow = CurvedArrow(start_point=(0, 0, 0), end_point=(1, 2, 0), angle=TAU/2)
|
||||
curvedArrow = CurvedArrow(start_point=(-2, 3, 0), end_point=(1, 2, 0), angle=-TAU/12, color=BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : CurvedArrow object
|
||||
A CurvedArrow object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_point: Vect3,
|
||||
@@ -277,6 +338,26 @@ class CurvedArrow(ArcBetweenPoints):
|
||||
|
||||
|
||||
class CurvedDoubleArrow(CurvedArrow):
|
||||
'''
|
||||
Creates a curved double arrow passing through the specified points with "angle" as the
|
||||
angle subtended at its center.
|
||||
Parameters
|
||||
-----
|
||||
start_point : array_like
|
||||
Starting point of the curved double arrow
|
||||
end_point : array_like
|
||||
Ending point of the curved double arrow
|
||||
angle : float
|
||||
Angle subtended by the curved double arrow at its center in radians. (Angles are measured counter-clockwise)
|
||||
Examples :
|
||||
curvedDoubleArrow = CurvedDoubleArrow(start_point = (0, 0, 0), end_point = (1, 2, 0), angle = TAU/2)
|
||||
curvedDoubleArrow = CurvedDoubleArrow(start_point = (-2, 3, 0), end_point = (1, 2, 0), angle = -TAU/12, color = BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : CurvedDoubleArrow object
|
||||
A CurvedDoubleArrow object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_point: Vect3,
|
||||
@@ -288,6 +369,23 @@ class CurvedDoubleArrow(CurvedArrow):
|
||||
|
||||
|
||||
class Circle(Arc):
|
||||
'''
|
||||
Creates a circle.
|
||||
Parameters
|
||||
-----
|
||||
radius : float
|
||||
Radius of the circle
|
||||
arc_center : array_like
|
||||
Center of the circle
|
||||
Examples :
|
||||
circle = Circle(radius=2, arc_center=(1,2,0))
|
||||
circle = Circle(radius=3.14, arc_center=2 * LEFT + UP, color=DARK_BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : Circle object
|
||||
A Circle object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_angle: float = 0,
|
||||
@@ -323,6 +421,21 @@ class Circle(Arc):
|
||||
|
||||
|
||||
class Dot(Circle):
|
||||
'''
|
||||
Creates a dot. Dot is a filled white circle with no bounary and DEFAULT_DOT_RADIUS.
|
||||
Parameters
|
||||
-----
|
||||
point : array_like
|
||||
Coordinates of center of the dot.
|
||||
Examples :
|
||||
dot = Dot(point=(1, 2, 0))
|
||||
|
||||
Returns
|
||||
-----
|
||||
out : Dot object
|
||||
A Dot object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
point: Vect3 = ORIGIN,
|
||||
@@ -330,7 +443,7 @@ class Dot(Circle):
|
||||
stroke_color: ManimColor = BLACK,
|
||||
stroke_width: float = 0.0,
|
||||
fill_opacity: float = 1.0,
|
||||
fill_color: ManimColor = WHITE,
|
||||
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
@@ -345,6 +458,21 @@ class Dot(Circle):
|
||||
|
||||
|
||||
class SmallDot(Dot):
|
||||
'''
|
||||
Creates a small dot. Small dot is a filled white circle with no bounary and DEFAULT_SMALL_DOT_RADIUS.
|
||||
Parameters
|
||||
-----
|
||||
point : array_like
|
||||
Coordinates of center of the small dot.
|
||||
Examples :
|
||||
smallDot = SmallDot(point=(1, 2, 0))
|
||||
|
||||
Returns
|
||||
-----
|
||||
out : SmallDot object
|
||||
A SmallDot object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
point: Vect3 = ORIGIN,
|
||||
@@ -355,6 +483,25 @@ class SmallDot(Dot):
|
||||
|
||||
|
||||
class Ellipse(Circle):
|
||||
'''
|
||||
Creates an ellipse.
|
||||
Parameters
|
||||
-----
|
||||
width : float
|
||||
Width of the ellipse
|
||||
height : float
|
||||
Height of the ellipse
|
||||
arc_center : array_like
|
||||
Coordinates of center of the ellipse
|
||||
Examples :
|
||||
ellipse = Ellipse(width=4, height=1, arc_center=(3, 3, 0))
|
||||
ellipse = Ellipse(width=2, height=5, arc_center=ORIGIN, color=BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : Ellipse object
|
||||
An Ellipse object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: float = 2.0,
|
||||
@@ -367,6 +514,28 @@ class Ellipse(Circle):
|
||||
|
||||
|
||||
class AnnularSector(VMobject):
|
||||
'''
|
||||
Creates an annular sector.
|
||||
Parameters
|
||||
-----
|
||||
inner_radius : float
|
||||
Inner radius of the annular sector
|
||||
outer_radius : float
|
||||
Outer radius of the annular sector
|
||||
start_angle : float
|
||||
Starting angle of the annular sector (Angles are measured counter-clockwise)
|
||||
angle : float
|
||||
Angle subtended at the center of the annular sector (Angles are measured counter-clockwise)
|
||||
arc_center : array_like
|
||||
Coordinates of center of the annular sector
|
||||
Examples :
|
||||
annularSector = AnnularSector(inner_radius=1, outer_radius=2, angle=TAU/2, start_angle=TAU*3/4, arc_center=(1,-2,0))
|
||||
Returns
|
||||
-----
|
||||
out : AnnularSector object
|
||||
An AnnularSector object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
angle: float = TAU / 4,
|
||||
@@ -374,7 +543,7 @@ class AnnularSector(VMobject):
|
||||
inner_radius: float = 1.0,
|
||||
outer_radius: float = 2.0,
|
||||
arc_center: Vect3 = ORIGIN,
|
||||
fill_color: ManimColor = GREY_A,
|
||||
fill_color: ManimColor = DEFAULT_LIGHT_COLOR,
|
||||
fill_opacity: float = 1.0,
|
||||
stroke_width: float = 0.0,
|
||||
**kwargs,
|
||||
@@ -403,6 +572,27 @@ class AnnularSector(VMobject):
|
||||
|
||||
|
||||
class Sector(AnnularSector):
|
||||
'''
|
||||
Creates a sector.
|
||||
Parameters
|
||||
-----
|
||||
outer_radius : float
|
||||
Radius of the sector
|
||||
start_angle : float
|
||||
Starting angle of the sector in radians. (Angles are measured counter-clockwise)
|
||||
angle : float
|
||||
Angle subtended by the sector at its center in radians. (Angles are measured counter-clockwise)
|
||||
arc_center : array_like
|
||||
Coordinates of center of the sector
|
||||
Examples :
|
||||
sector = Sector(outer_radius=1, start_angle=TAU/3, angle=TAU/2, arc_center=[0,3,0])
|
||||
sector = Sector(outer_radius=3, start_angle=TAU/4, angle=TAU/4, arc_center=ORIGIN, color=PINK)
|
||||
Returns
|
||||
-----
|
||||
out : Sector object
|
||||
An Sector object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
angle: float = TAU / 4,
|
||||
@@ -418,13 +608,32 @@ class Sector(AnnularSector):
|
||||
|
||||
|
||||
class Annulus(VMobject):
|
||||
'''
|
||||
Creates an annulus.
|
||||
Parameters
|
||||
-----
|
||||
inner_radius : float
|
||||
Inner radius of the annulus
|
||||
outer_radius : float
|
||||
Outer radius of the annulus
|
||||
arc_center : array_like
|
||||
Coordinates of center of the annulus
|
||||
Examples :
|
||||
annulus = Annulus(inner_radius=2, outer_radius=3, arc_center=(1, -1, 0))
|
||||
annulus = Annulus(inner_radius=2, outer_radius=3, stroke_width=20, stroke_color=RED, fill_color=BLUE, arc_center=ORIGIN)
|
||||
Returns
|
||||
-----
|
||||
out : Annulus object
|
||||
An Annulus object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
inner_radius: float = 1.0,
|
||||
outer_radius: float = 2.0,
|
||||
fill_opacity: float = 1.0,
|
||||
stroke_width: float = 0.0,
|
||||
fill_color: ManimColor = GREY_A,
|
||||
fill_color: ManimColor = DEFAULT_LIGHT_COLOR,
|
||||
center: Vect3 = ORIGIN,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -444,6 +653,23 @@ class Annulus(VMobject):
|
||||
|
||||
|
||||
class Line(TipableVMobject):
|
||||
'''
|
||||
Creates a line joining the points "start" and "end".
|
||||
Parameters
|
||||
-----
|
||||
start : array_like
|
||||
Starting point of the line
|
||||
end : array_like
|
||||
Ending point of the line
|
||||
Examples :
|
||||
line = Line((0, 0, 0), (3, 0, 0))
|
||||
line = Line((1, 2, 0), (-2, -3, 0), color=BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : Line object
|
||||
A Line object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start: Vect3 | Mobject = LEFT,
|
||||
@@ -563,6 +789,25 @@ class Line(TipableVMobject):
|
||||
|
||||
|
||||
class DashedLine(Line):
|
||||
'''
|
||||
Creates a dashed line joining the points "start" and "end".
|
||||
Parameters
|
||||
-----
|
||||
start : array_like
|
||||
Starting point of the dashed line
|
||||
end : array_like
|
||||
Ending point of the dashed line
|
||||
dash_length : float
|
||||
length of each dash
|
||||
Examples :
|
||||
line = DashedLine((0, 0, 0), (3, 0, 0))
|
||||
line = DashedLine((1, 2, 3), (4, 5, 6), dash_length=0.01)
|
||||
Returns
|
||||
-----
|
||||
out : DashedLine object
|
||||
A DashedLine object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start: Vect3 = LEFT,
|
||||
@@ -601,6 +846,9 @@ class DashedLine(Line):
|
||||
else:
|
||||
return Line.get_end(self)
|
||||
|
||||
def get_start_and_end(self) -> Tuple[Vect3, Vect3]:
|
||||
return self.get_start(), self.get_end()
|
||||
|
||||
def get_first_handle(self) -> Vect3:
|
||||
return self.submobjects[0].get_points()[1]
|
||||
|
||||
@@ -609,6 +857,26 @@ class DashedLine(Line):
|
||||
|
||||
|
||||
class TangentLine(Line):
|
||||
'''
|
||||
Creates a tangent line to the specified vectorized math object.
|
||||
Parameters
|
||||
-----
|
||||
vmob : VMobject object
|
||||
Vectorized math object which the line will be tangent to
|
||||
alpha : float
|
||||
Point on the perimeter of the vectorized math object. It takes value between 0 and 1
|
||||
both inclusive.
|
||||
length : float
|
||||
Length of the tangent line
|
||||
Examples :
|
||||
circle = Circle(arc_center=ORIGIN, radius=3, color=GREEN)
|
||||
tangentLine = TangentLine(vmob=circle, alpha=1/3, length=6, color=BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : TangentLine object
|
||||
A TangentLine object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vmob: VMobject,
|
||||
@@ -624,6 +892,22 @@ class TangentLine(Line):
|
||||
|
||||
|
||||
class Elbow(VMobject):
|
||||
'''
|
||||
Creates an elbow. Elbow is an L-shaped shaped object.
|
||||
Parameters
|
||||
-----
|
||||
width : float
|
||||
Width of the elbow
|
||||
angle : float
|
||||
Angle of the elbow in radians with the horizontal. (Angles are measured counter-clockwise)
|
||||
Examples :
|
||||
line = Elbow(width=2, angle=TAU/16)
|
||||
Returns
|
||||
-----
|
||||
out : Elbow object
|
||||
A Elbow object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: float = 0.2,
|
||||
@@ -641,7 +925,7 @@ class StrokeArrow(Line):
|
||||
self,
|
||||
start: Vect3 | Mobject,
|
||||
end: Vect3 | Mobject,
|
||||
stroke_color: ManimColor = GREY_A,
|
||||
stroke_color: ManimColor = DEFAULT_LIGHT_COLOR,
|
||||
stroke_width: float = 5,
|
||||
buff: float = 0.25,
|
||||
tip_width_ratio: float = 5,
|
||||
@@ -733,6 +1017,47 @@ class StrokeArrow(Line):
|
||||
|
||||
|
||||
class Arrow(Line):
|
||||
'''
|
||||
Creates an arrow.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start : array_like
|
||||
Starting point of the arrow
|
||||
end : array_like
|
||||
Ending point of the arrow
|
||||
buff : float, optional
|
||||
Buffer distance from the start and end points. Default is MED_SMALL_BUFF.
|
||||
path_arc : float, optional
|
||||
If set to a non-zero value, the arrow will be curved to subtend a circle by this angle.
|
||||
Default is 0 (straight arrow).
|
||||
thickness : float, optional
|
||||
How wide should the base of the arrow be. This affects the shaft width. Default is 3.0.
|
||||
tip_width_ratio : float, optional
|
||||
Ratio of the tip width to the shaft width. Default is 5.
|
||||
tip_angle : float, optional
|
||||
Angle of the arrow tip in radians. Default is PI/3 (60 degrees).
|
||||
max_tip_length_to_length_ratio : float, optional
|
||||
Maximum ratio of tip length to total arrow length. Prevents tips from being too large
|
||||
relative to the arrow. Default is 0.5.
|
||||
max_width_to_length_ratio : float, optional
|
||||
Maximum ratio of arrow width to total arrow length. Prevents arrows from being too wide
|
||||
relative to their length. Default is 0.1.
|
||||
**kwargs
|
||||
Additional keyword arguments passed to the parent Line class.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> arrow = Arrow((0, 0, 0), (3, 0, 0))
|
||||
>>> curved_arrow = Arrow(LEFT, RIGHT, path_arc=PI/4)
|
||||
>>> thick_arrow = Arrow(UP, DOWN, thickness=5.0, tip_width_ratio=3)
|
||||
|
||||
Returns
|
||||
-------
|
||||
Arrow
|
||||
An Arrow object satisfying the specified parameters.
|
||||
'''
|
||||
|
||||
tickness_multiplier = 0.015
|
||||
|
||||
def __init__(
|
||||
@@ -741,7 +1066,7 @@ class Arrow(Line):
|
||||
end: Vect3 | Mobject = LEFT,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
path_arc: float = 0,
|
||||
fill_color: ManimColor = GREY_A,
|
||||
fill_color: ManimColor = DEFAULT_LIGHT_COLOR,
|
||||
fill_opacity: float = 1.0,
|
||||
stroke_width: float = 0.0,
|
||||
thickness: float = 3.0,
|
||||
@@ -895,6 +1220,20 @@ class Arrow(Line):
|
||||
|
||||
|
||||
class Vector(Arrow):
|
||||
'''
|
||||
Creates a vector. Vector is an arrow with start point as ORIGIN
|
||||
Parameters
|
||||
-----
|
||||
direction : array_like
|
||||
Coordinates of direction of the arrow
|
||||
Examples :
|
||||
arrow = Vector(direction=LEFT)
|
||||
Returns
|
||||
-----
|
||||
out : Vector object
|
||||
A Vector object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
direction: Vect3 = RIGHT,
|
||||
@@ -907,6 +1246,33 @@ class Vector(Arrow):
|
||||
|
||||
|
||||
class CubicBezier(VMobject):
|
||||
'''
|
||||
Creates a cubic Bézier curve.
|
||||
|
||||
A cubic Bézier curve is defined by four control points: two anchor points (start and end)
|
||||
and two handle points that control the curvature. The curve starts at the first anchor
|
||||
point, is "pulled" toward the handle points, and ends at the second anchor point.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
a0 : array_like
|
||||
First anchor point (starting point of the curve).
|
||||
h0 : array_like
|
||||
First handle point (controls the initial direction and curvature from a0).
|
||||
h1 : array_like
|
||||
Second handle point (controls the final direction and curvature toward a1).
|
||||
a1 : array_like
|
||||
Second anchor point (ending point of the curve).
|
||||
**kwargs
|
||||
Additional keyword arguments passed to the parent VMobject class, such as
|
||||
stroke_color, stroke_width, fill_color, fill_opacity, etc.
|
||||
Returns
|
||||
-------
|
||||
CubicBezier
|
||||
A CubicBezier object representing the specified cubic Bézier curve.
|
||||
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
a0: Vect3,
|
||||
@@ -920,6 +1286,20 @@ class CubicBezier(VMobject):
|
||||
|
||||
|
||||
class Polygon(VMobject):
|
||||
'''
|
||||
Creates a polygon by joining the specified vertices.
|
||||
Parameters
|
||||
-----
|
||||
*vertices : array_like
|
||||
Vertex of the polygon
|
||||
Examples :
|
||||
triangle = Polygon((-3,0,0), (3,0,0), (0,3,0))
|
||||
Returns
|
||||
-----
|
||||
out : Polygon object
|
||||
A Polygon object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*vertices: Vect3,
|
||||
@@ -978,6 +1358,22 @@ class Polyline(VMobject):
|
||||
|
||||
|
||||
class RegularPolygon(Polygon):
|
||||
'''
|
||||
Creates a regular polygon of edge length 1 at the center of the screen.
|
||||
Parameters
|
||||
-----
|
||||
n : int
|
||||
Number of vertices of the regular polygon
|
||||
start_angle : float
|
||||
Starting angle of the regular polygon in radians. (Angles are measured counter-clockwise)
|
||||
Examples :
|
||||
pentagon = RegularPolygon(n=5, start_angle=30 * DEGREES)
|
||||
Returns
|
||||
-----
|
||||
out : RegularPolygon object
|
||||
A RegularPolygon object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
n: int = 6,
|
||||
@@ -987,13 +1383,27 @@ class RegularPolygon(Polygon):
|
||||
):
|
||||
# Defaults to 0 for odd, 90 for even
|
||||
if start_angle is None:
|
||||
start_angle = (n % 2) * 90 * DEGREES
|
||||
start_angle = (n % 2) * 90 * DEG
|
||||
start_vect = rotate_vector(radius * RIGHT, start_angle)
|
||||
vertices = compass_directions(n, start_vect)
|
||||
super().__init__(*vertices, **kwargs)
|
||||
|
||||
|
||||
class Triangle(RegularPolygon):
|
||||
'''
|
||||
Creates a triangle of edge length 1 at the center of the screen.
|
||||
Parameters
|
||||
-----
|
||||
start_angle : float
|
||||
Starting angle of the triangle in radians. (Angles are measured counter-clockwise)
|
||||
Examples :
|
||||
triangle = Triangle(start_angle=45 * DEGREES)
|
||||
Returns
|
||||
-----
|
||||
out : Triangle object
|
||||
A Triangle object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(n=3, **kwargs)
|
||||
|
||||
@@ -1005,7 +1415,7 @@ class ArrowTip(Triangle):
|
||||
width: float = DEFAULT_ARROW_TIP_WIDTH,
|
||||
length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
fill_opacity: float = 1.0,
|
||||
fill_color: ManimColor = WHITE,
|
||||
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
stroke_width: float = 0.0,
|
||||
tip_style: int = 0, # triangle=0, inner_smooth=1, dot=2
|
||||
**kwargs
|
||||
@@ -1044,6 +1454,22 @@ class ArrowTip(Triangle):
|
||||
|
||||
|
||||
class Rectangle(Polygon):
|
||||
'''
|
||||
Creates a rectangle at the center of the screen.
|
||||
Parameters
|
||||
-----
|
||||
width : float
|
||||
Width of the rectangle
|
||||
height : float
|
||||
Height of the rectangle
|
||||
Examples :
|
||||
rectangle = Rectangle(width=3, height=4, color=BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : Rectangle object
|
||||
A Rectangle object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: float = 4.0,
|
||||
@@ -1062,11 +1488,43 @@ class Rectangle(Polygon):
|
||||
|
||||
|
||||
class Square(Rectangle):
|
||||
'''
|
||||
Creates a square at the center of the screen.
|
||||
Parameters
|
||||
-----
|
||||
side_length : float
|
||||
Edge length of the square
|
||||
Examples :
|
||||
square = Square(side_length=5, color=PINK)
|
||||
Returns
|
||||
-----
|
||||
out : Square object
|
||||
A Square object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(self, side_length: float = 2.0, **kwargs):
|
||||
super().__init__(side_length, side_length, **kwargs)
|
||||
|
||||
|
||||
class RoundedRectangle(Rectangle):
|
||||
'''
|
||||
Creates a rectangle with round edges at the center of the screen.
|
||||
Parameters
|
||||
-----
|
||||
width : float
|
||||
Width of the rounded rectangle
|
||||
height : float
|
||||
Height of the rounded rectangle
|
||||
corner_radius : float
|
||||
Corner radius of the rectangle
|
||||
Examples :
|
||||
rRectangle = RoundedRectangle(width=3, height=4, corner_radius=1, color=BLUE)
|
||||
Returns
|
||||
-----
|
||||
out : RoundedRectangle object
|
||||
A RoundedRectangle object satisfying the specified parameters
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: float = 4.0,
|
||||
|
||||
@@ -6,7 +6,7 @@ from pyglet.window import key as PygletWindowKeys
|
||||
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
||||
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, UP
|
||||
from manimlib.constants import MED_LARGE_BUFF, MED_SMALL_BUFF, SMALL_BUFF
|
||||
from manimlib.constants import BLACK, BLUE, GREEN, GREY_A, GREY_C, RED, WHITE
|
||||
from manimlib.constants import BLACK, BLUE, GREEN, GREY_A, GREY_C, RED, WHITE, DEFAULT_MOBJECT_COLOR
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.geometry import Circle
|
||||
@@ -387,7 +387,7 @@ class Textbox(ControlMobject):
|
||||
box_kwargs: dict = {
|
||||
"width": 2.0,
|
||||
"height": 1.0,
|
||||
"fill_color": WHITE,
|
||||
"fill_color": DEFAULT_MOBJECT_COLOR,
|
||||
"fill_opacity": 1.0,
|
||||
},
|
||||
text_kwargs: dict = {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DOWN, LEFT, RIGHT, ORIGIN
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
@@ -14,7 +12,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence, Union, Tuple, Optional
|
||||
from typing import Sequence, Union, Optional
|
||||
from manimlib.typing import ManimColor, Vect3, VectNArray, Self
|
||||
|
||||
StringMatrixType = Union[Sequence[Sequence[str]], np.ndarray[int, np.dtype[np.str_]]]
|
||||
@@ -198,7 +196,7 @@ class Matrix(VMobject):
|
||||
dots.set_width(hdots_width)
|
||||
self.swap_entry_for_dots(row[col_index], dots)
|
||||
if use_vdots and use_hdots:
|
||||
rows[row_index][col_index].rotate(-45 * DEGREES)
|
||||
rows[row_index][col_index].rotate(-45 * DEG)
|
||||
return self
|
||||
|
||||
def get_mob_matrix(self) -> VMobjectMatrixType:
|
||||
|
||||
@@ -12,13 +12,13 @@ import moderngl
|
||||
import numbers
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER
|
||||
from manimlib.constants import DEFAULT_MOBJECT_TO_EDGE_BUFF
|
||||
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF
|
||||
from manimlib.constants import DOWN, IN, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||
from manimlib.constants import MED_SMALL_BUFF
|
||||
from manimlib.constants import TAU
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import DEFAULT_MOBJECT_COLOR
|
||||
from manimlib.event_handler import EVENT_DISPATCHER
|
||||
from manimlib.event_handler.event_listner import EventListener
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
@@ -52,7 +52,7 @@ SubmobjectType = TypeVar('SubmobjectType', bound='Mobject')
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterator, Union, Tuple, Optional, Any
|
||||
import numpy.typing as npt
|
||||
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, UniformDict, Self
|
||||
from manimlib.typing import ManimColor, Vect3, Vect4Array, Vect3Array, UniformDict, Self
|
||||
from moderngl.context import Context
|
||||
|
||||
T = TypeVar('T')
|
||||
@@ -78,7 +78,7 @@ class Mobject(object):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: ManimColor = WHITE,
|
||||
color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
opacity: float = 1.0,
|
||||
shading: Tuple[float, float, float] = (0.0, 0.0, 0.0),
|
||||
# For shaders
|
||||
@@ -160,10 +160,10 @@ class Mobject(object):
|
||||
return self
|
||||
|
||||
@property
|
||||
def animate(self) -> _AnimationBuilder:
|
||||
def animate(self) -> _AnimationBuilder | Self:
|
||||
"""
|
||||
Methods called with Mobject.animate.method() can be passed
|
||||
into a Scene.play call, as if you were calling
|
||||
into a Scene.play call, as if you were calling
|
||||
ApplyMethod(mobject.method)
|
||||
|
||||
Borrowed from https://github.com/ManimCommunity/manim/
|
||||
@@ -287,10 +287,7 @@ class Mobject(object):
|
||||
about_point = self.get_bounding_box_point(about_edge)
|
||||
|
||||
for mob in self.get_family():
|
||||
arrs = []
|
||||
if mob.has_points():
|
||||
for key in mob.pointlike_data_keys:
|
||||
arrs.append(mob.data[key])
|
||||
arrs = [mob.data[key] for key in mob.pointlike_data_keys if mob.has_points()]
|
||||
if works_on_bounding_box:
|
||||
arrs.append(mob.get_bounding_box())
|
||||
|
||||
@@ -307,12 +304,15 @@ class Mobject(object):
|
||||
parent.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
# Others related to points
|
||||
|
||||
@affects_data
|
||||
def match_points(self, mobject: Mobject) -> Self:
|
||||
self.set_points(mobject.get_points())
|
||||
self.resize_points(len(mobject.data), resize_func=resize_preserving_order)
|
||||
for key in self.pointlike_data_keys:
|
||||
self.data[key][:] = mobject.data[key]
|
||||
return self
|
||||
|
||||
# Others related to points
|
||||
|
||||
def get_points(self) -> Vect3Array:
|
||||
return self.data["point"]
|
||||
|
||||
@@ -715,21 +715,6 @@ class Mobject(object):
|
||||
self.become(self.saved_state)
|
||||
return self
|
||||
|
||||
def save_to_file(self, file_path: str) -> Self:
|
||||
with open(file_path, "wb") as fp:
|
||||
fp.write(self.serialize())
|
||||
log.info(f"Saved mobject to {file_path}")
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def load(file_path) -> Mobject:
|
||||
if not os.path.exists(file_path):
|
||||
log.error(f"No file found at {file_path}")
|
||||
sys.exit(2)
|
||||
with open(file_path, "rb") as fp:
|
||||
mobject = pickle.load(fp)
|
||||
return mobject
|
||||
|
||||
def become(self, mobject: Mobject, match_updaters=False) -> Self:
|
||||
"""
|
||||
Edit all data and submobjects to be idential
|
||||
@@ -857,6 +842,7 @@ class Mobject(object):
|
||||
if call:
|
||||
self.update(dt=0)
|
||||
self.refresh_has_updater_status()
|
||||
self.update()
|
||||
return self
|
||||
|
||||
def insert_updater(self, update_func: Updater, index=0):
|
||||
@@ -1070,7 +1056,7 @@ class Mobject(object):
|
||||
def align_on_border(
|
||||
self,
|
||||
direction: Vect3,
|
||||
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF
|
||||
) -> Self:
|
||||
"""
|
||||
Direction just needs to be a vector pointing towards side or
|
||||
@@ -1086,14 +1072,14 @@ class Mobject(object):
|
||||
def to_corner(
|
||||
self,
|
||||
corner: Vect3 = LEFT + DOWN,
|
||||
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF
|
||||
) -> Self:
|
||||
return self.align_on_border(corner, buff)
|
||||
|
||||
def to_edge(
|
||||
self,
|
||||
edge: Vect3 = LEFT,
|
||||
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF
|
||||
) -> Self:
|
||||
return self.align_on_border(edge, buff)
|
||||
|
||||
@@ -1101,7 +1087,7 @@ class Mobject(object):
|
||||
self,
|
||||
mobject_or_point: Mobject | Vect3,
|
||||
direction: Vect3 = RIGHT,
|
||||
buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
|
||||
buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF,
|
||||
aligned_edge: Vect3 = ORIGIN,
|
||||
submobject_to_align: Mobject | None = None,
|
||||
index_of_submobject_to_align: int | slice | None = None,
|
||||
@@ -1132,7 +1118,7 @@ class Mobject(object):
|
||||
space_lengths = [FRAME_X_RADIUS, FRAME_Y_RADIUS]
|
||||
for vect in UP, DOWN, LEFT, RIGHT:
|
||||
dim = np.argmax(np.abs(vect))
|
||||
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFFER)
|
||||
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFF)
|
||||
max_val = space_lengths[dim] - buff
|
||||
edge_center = self.get_edge_center(vect)
|
||||
if np.dot(edge_center, vect) > max_val:
|
||||
@@ -1246,8 +1232,9 @@ class Mobject(object):
|
||||
def set_z(self, z: float, direction: Vect3 = ORIGIN) -> Self:
|
||||
return self.set_coord(z, 2, direction)
|
||||
|
||||
def set_z_index(self, z_index: int) -> Self:
|
||||
self.z_index = z_index
|
||||
def set_z_index(self, z_index: int, recurse=True) -> Self:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.z_index = z_index
|
||||
return self
|
||||
|
||||
def space_out_submobjects(self, factor: float = 1.5, **kwargs) -> Self:
|
||||
@@ -1298,6 +1285,14 @@ class Mobject(object):
|
||||
self.scale((length + buff) / length)
|
||||
return self
|
||||
|
||||
def put_start_on(self, point: Vect3) -> Self:
|
||||
self.shift(point - self.get_start())
|
||||
return self
|
||||
|
||||
def put_end_on(self, point: Vect3) -> Self:
|
||||
self.shift(point - self.get_end())
|
||||
return self
|
||||
|
||||
def put_start_and_end_on(self, start: Vect3, end: Vect3) -> Self:
|
||||
curr_start, curr_end = self.get_start_and_end()
|
||||
curr_vect = curr_end - curr_start
|
||||
@@ -1334,20 +1329,19 @@ class Mobject(object):
|
||||
|
||||
def set_color_by_rgba_func(
|
||||
self,
|
||||
func: Callable[[Vect3], Vect4],
|
||||
func: Callable[[Vect3Array], Vect4Array],
|
||||
recurse: bool = True
|
||||
) -> Self:
|
||||
"""
|
||||
Func should take in a point in R3 and output an rgba value
|
||||
"""
|
||||
for mob in self.get_family(recurse):
|
||||
rgba_array = [func(point) for point in mob.get_points()]
|
||||
mob.set_rgba_array(rgba_array)
|
||||
mob.set_rgba_array(func(mob.get_points()))
|
||||
return self
|
||||
|
||||
def set_color_by_rgb_func(
|
||||
self,
|
||||
func: Callable[[Vect3], Vect3],
|
||||
func: Callable[[Vect3Array], Vect3Array],
|
||||
opacity: float = 1,
|
||||
recurse: bool = True
|
||||
) -> Self:
|
||||
@@ -1355,8 +1349,9 @@ class Mobject(object):
|
||||
Func should take in a point in R3 and output an rgb value
|
||||
"""
|
||||
for mob in self.get_family(recurse):
|
||||
rgba_array = [[*func(point), opacity] for point in mob.get_points()]
|
||||
mob.set_rgba_array(rgba_array)
|
||||
points = mob.get_points()
|
||||
opacity = np.ones((points.shape[0], 1)) * opacity
|
||||
mob.set_rgba_array(np.hstack((func(points), opacity)))
|
||||
return self
|
||||
|
||||
@affects_family_data
|
||||
@@ -1375,7 +1370,7 @@ class Mobject(object):
|
||||
rgbs = resize_with_interpolation(rgbs, len(data))
|
||||
data[name][:, :3] = rgbs
|
||||
if opacity is not None:
|
||||
if not isinstance(opacity, (float, int)):
|
||||
if not isinstance(opacity, (float, int, np.floating)):
|
||||
opacity = resize_with_interpolation(np.array(opacity), len(data))
|
||||
data[name][:, 3] = opacity
|
||||
return self
|
||||
@@ -1950,12 +1945,14 @@ class Mobject(object):
|
||||
def set_clip_plane(
|
||||
self,
|
||||
vect: Vect3 | None = None,
|
||||
threshold: float | None = None
|
||||
threshold: float | None = None,
|
||||
recurse=True
|
||||
) -> Self:
|
||||
if vect is not None:
|
||||
self.uniforms["clip_plane"][:3] = vect
|
||||
if threshold is not None:
|
||||
self.uniforms["clip_plane"][3] = threshold
|
||||
for submob in self.get_family(recurse):
|
||||
if vect is not None:
|
||||
submob.uniforms["clip_plane"][:3] = vect
|
||||
if threshold is not None:
|
||||
submob.uniforms["clip_plane"][3] = threshold
|
||||
return self
|
||||
|
||||
def deactivate_clip_plane(self) -> Self:
|
||||
@@ -2268,6 +2265,18 @@ class _AnimationBuilder:
|
||||
def __call__(self, **kwargs):
|
||||
return self.set_anim_args(**kwargs)
|
||||
|
||||
def __dir__(self) -> list[str]:
|
||||
"""
|
||||
Extend attribute list of _AnimationBuilder object to include mobject attributes
|
||||
for better autocompletion in the IPython terminal when using interactive mode.
|
||||
"""
|
||||
methods = super().__dir__()
|
||||
mobject_methods = [
|
||||
attr for attr in dir(self.mobject)
|
||||
if not attr.startswith('_')
|
||||
]
|
||||
return sorted(set(methods+mobject_methods))
|
||||
|
||||
def set_anim_args(self, **kwargs):
|
||||
'''
|
||||
You can change the args of :class:`~manimlib.animation.transform.Transform`, such as
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.simple_functions import clip
|
||||
@@ -71,7 +71,7 @@ def always_shift(
|
||||
|
||||
def always_rotate(
|
||||
mobject: Mobject,
|
||||
rate: float = 20 * DEGREES,
|
||||
rate: float = 20 * DEG,
|
||||
**kwargs
|
||||
) -> Mobject:
|
||||
mobject.add_updater(
|
||||
|
||||
@@ -3,20 +3,24 @@ from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DOWN, LEFT, RIGHT, UP
|
||||
from manimlib.constants import GREY_B
|
||||
from manimlib.constants import MED_SMALL_BUFF
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.constants import DEFAULT_LIGHT_COLOR
|
||||
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
|
||||
from manimlib.constants import YELLOW, DEG
|
||||
from manimlib.mobject.geometry import Line, ArrowTip
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.value_tracker import ValueTracker
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import outer_interpolate
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
from manimlib.utils.simple_functions import fdiv
|
||||
from manimlib.utils.space_ops import rotate_vector, angle_of_vector
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable, Optional
|
||||
from typing import Iterable, Optional, Tuple, Dict, Any
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier
|
||||
|
||||
|
||||
@@ -24,7 +28,7 @@ class NumberLine(Line):
|
||||
def __init__(
|
||||
self,
|
||||
x_range: RangeSpecifier = (-8, 8, 1),
|
||||
color: ManimColor = GREY_B,
|
||||
color: ManimColor = DEFAULT_LIGHT_COLOR,
|
||||
stroke_width: float = 2.0,
|
||||
# How big is one one unit of this number line in terms of absolute spacial distance
|
||||
unit_size: float = 1.0,
|
||||
@@ -182,9 +186,13 @@ class NumberLine(Line):
|
||||
if x < 0 and direction[0] == 0:
|
||||
# Align without the minus sign
|
||||
num_mob.shift(num_mob[0].get_width() * LEFT / 2)
|
||||
if x == unit and unit_tex:
|
||||
if abs(x) == unit and unit_tex:
|
||||
center = num_mob.get_center()
|
||||
num_mob.remove(num_mob[0])
|
||||
if x > 0:
|
||||
num_mob.remove(num_mob[0])
|
||||
else:
|
||||
num_mob.remove(num_mob[1])
|
||||
num_mob[0].next_to(num_mob[1], LEFT, buff=num_mob[0].get_width() / 4)
|
||||
num_mob.move_to(center)
|
||||
return num_mob
|
||||
|
||||
@@ -221,11 +229,77 @@ class UnitInterval(NumberLine):
|
||||
big_tick_numbers: list[float] = [0, 1],
|
||||
decimal_number_config: dict = dict(
|
||||
num_decimal_places=1,
|
||||
)
|
||||
),
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
x_range=x_range,
|
||||
unit_size=unit_size,
|
||||
big_tick_numbers=big_tick_numbers,
|
||||
decimal_number_config=decimal_number_config,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class Slider(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
value_tracker: ValueTracker,
|
||||
x_range: Tuple[float, float] = (-5, 5),
|
||||
var_name: Optional[str] = None,
|
||||
width: float = 3,
|
||||
unit_size: float = 1,
|
||||
arrow_width: float = 0.15,
|
||||
arrow_length: float = 0.15,
|
||||
arrow_color: ManimColor = YELLOW,
|
||||
font_size: int = 24,
|
||||
label_buff: float = SMALL_BUFF,
|
||||
num_decimal_places: int = 2,
|
||||
tick_size: float = 0.05,
|
||||
number_line_config: Dict[str, Any] = dict(),
|
||||
arrow_tip_config: Dict[str, Any] = dict(),
|
||||
decimal_config: Dict[str, Any] = dict(),
|
||||
angle: float = 0,
|
||||
label_direction: Optional[np.ndarray] = None,
|
||||
add_tick_labels: bool = True,
|
||||
tick_label_font_size: int = 16,
|
||||
):
|
||||
get_value = value_tracker.get_value
|
||||
if label_direction is None:
|
||||
label_direction = np.round(rotate_vector(UP, angle), 2)
|
||||
|
||||
# Initialize number line
|
||||
number_line_kw = dict(x_range=x_range, width=width, tick_size=tick_size)
|
||||
number_line_kw.update(number_line_config)
|
||||
number_line = NumberLine(**number_line_kw)
|
||||
number_line.rotate(angle)
|
||||
if add_tick_labels:
|
||||
number_line.add_numbers(
|
||||
font_size=tick_label_font_size,
|
||||
buff=2 * tick_size,
|
||||
direction=-label_direction
|
||||
)
|
||||
|
||||
# Initialize arrow tip
|
||||
arrow_tip_kw = dict(
|
||||
width=arrow_width,
|
||||
length=arrow_length,
|
||||
fill_color=arrow_color,
|
||||
angle=-180 * DEG + angle_of_vector(label_direction),
|
||||
)
|
||||
arrow_tip_kw.update(arrow_tip_config)
|
||||
tip = ArrowTip(**arrow_tip_kw)
|
||||
tip.add_updater(lambda m: m.move_to(number_line.n2p(get_value()), -label_direction))
|
||||
|
||||
# Initialize label
|
||||
dec_string = f"{{:.{num_decimal_places}f}}".format(0)
|
||||
lhs = f"{var_name} = " if var_name is not None else ""
|
||||
label = Tex(lhs + dec_string, font_size=font_size)
|
||||
label[var_name].set_fill(arrow_color)
|
||||
decimal = label.make_number_changeable(dec_string)
|
||||
decimal.add_updater(lambda m: m.set_value(get_value()))
|
||||
label.add_updater(lambda m: m.next_to(tip, label_direction, label_buff))
|
||||
|
||||
# Assemble group
|
||||
super().__init__(number_line, tip, label)
|
||||
self.set_stroke(behind=True)
|
||||
|
||||
@@ -4,7 +4,7 @@ from functools import lru_cache
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DOWN, LEFT, RIGHT, UP
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import DEFAULT_MOBJECT_COLOR
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
@@ -14,7 +14,8 @@ from manimlib.utils.bezier import interpolate
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypeVar
|
||||
from typing import TypeVar, Callable
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.typing import ManimColor, Vect3, Self
|
||||
|
||||
T = TypeVar("T", bound=VMobject)
|
||||
@@ -22,24 +23,31 @@ if TYPE_CHECKING:
|
||||
|
||||
@lru_cache()
|
||||
def char_to_cahced_mob(char: str, **text_config):
|
||||
return Text(char, **text_config)
|
||||
if "\\" in char:
|
||||
# This is for when the "character" is a LaTeX command
|
||||
# like ^\circ or \dots
|
||||
return Tex(char, **text_config)
|
||||
else:
|
||||
return Text(char, **text_config)
|
||||
|
||||
|
||||
class DecimalNumber(VMobject):
|
||||
def __init__(
|
||||
self,
|
||||
number: float | complex = 0,
|
||||
color: ManimColor = WHITE,
|
||||
color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
stroke_width: float = 0,
|
||||
fill_opacity: float = 1.0,
|
||||
fill_border_width: float = 0.5,
|
||||
num_decimal_places: int = 2,
|
||||
min_total_width: Optional[int] = 0,
|
||||
include_sign: bool = False,
|
||||
group_with_commas: bool = True,
|
||||
digit_buff_per_font_unit: float = 0.001,
|
||||
show_ellipsis: bool = False,
|
||||
unit: str | None = None, # Aligned to bottom unless it starts with "^"
|
||||
include_background_rectangle: bool = False,
|
||||
hide_zero_components_on_complex: bool = True,
|
||||
edge_to_fix: Vect3 = LEFT,
|
||||
font_size: float = 48,
|
||||
text_config: dict = dict(), # Do not pass in font_size here
|
||||
@@ -48,10 +56,12 @@ class DecimalNumber(VMobject):
|
||||
self.num_decimal_places = num_decimal_places
|
||||
self.include_sign = include_sign
|
||||
self.group_with_commas = group_with_commas
|
||||
self.min_total_width = min_total_width
|
||||
self.digit_buff_per_font_unit = digit_buff_per_font_unit
|
||||
self.show_ellipsis = show_ellipsis
|
||||
self.unit = unit
|
||||
self.include_background_rectangle = include_background_rectangle
|
||||
self.hide_zero_components_on_complex = hide_zero_components_on_complex
|
||||
self.edge_to_fix = edge_to_fix
|
||||
self.font_size = font_size
|
||||
self.text_config = dict(text_config)
|
||||
@@ -112,7 +122,14 @@ class DecimalNumber(VMobject):
|
||||
|
||||
def get_num_string(self, number: float | complex) -> str:
|
||||
if isinstance(number, complex):
|
||||
formatter = self.get_complex_formatter()
|
||||
if self.hide_zero_components_on_complex and number.imag == 0:
|
||||
number = number.real
|
||||
formatter = self.get_formatter()
|
||||
elif self.hide_zero_components_on_complex and number.real == 0:
|
||||
number = number.imag
|
||||
formatter = self.get_formatter() + "i"
|
||||
else:
|
||||
formatter = self.get_complex_formatter()
|
||||
else:
|
||||
formatter = self.get_formatter()
|
||||
if self.num_decimal_places == 0 and isinstance(number, float):
|
||||
@@ -161,6 +178,7 @@ class DecimalNumber(VMobject):
|
||||
"include_sign",
|
||||
"group_with_commas",
|
||||
"num_decimal_places",
|
||||
"min_total_width",
|
||||
]
|
||||
])
|
||||
config.update(kwargs)
|
||||
@@ -170,6 +188,7 @@ class DecimalNumber(VMobject):
|
||||
config.get("field_name", ""),
|
||||
":",
|
||||
"+" if config["include_sign"] else "",
|
||||
"0" + str(config.get("min_total_width", "")) if config.get("min_total_width") else "",
|
||||
"," if config["group_with_commas"] else "",
|
||||
f".{ndp}f" if ndp > 0 else "d",
|
||||
"}",
|
||||
|
||||
@@ -43,6 +43,7 @@ class SampleSpace(Rectangle):
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
**kwargs
|
||||
)
|
||||
self.default_label_scale_val = default_label_scale_val
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ from __future__ import annotations
|
||||
|
||||
from colour import Color
|
||||
|
||||
from manimlib.constants import BLACK, RED, YELLOW, WHITE
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.constants import BLACK, RED, YELLOW, DEFAULT_MOBJECT_COLOR
|
||||
from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR
|
||||
from manimlib.constants import SMALL_BUFF
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.customization import get_customization
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -57,7 +57,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||
**kwargs
|
||||
):
|
||||
if color is None:
|
||||
color = get_customization()['style']['background_color']
|
||||
color = manim_config.camera.background_color
|
||||
super().__init__(
|
||||
mobject,
|
||||
color=color,
|
||||
@@ -79,7 +79,8 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||
stroke_width: float | None = None,
|
||||
fill_color: ManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
family: bool = True
|
||||
family: bool = True,
|
||||
**kwargs
|
||||
) -> Self:
|
||||
# Unchangeable style, except for fill_opacity
|
||||
VMobject.set_style(
|
||||
@@ -117,7 +118,7 @@ class Underline(Line):
|
||||
self,
|
||||
mobject: Mobject,
|
||||
buff: float = SMALL_BUFF,
|
||||
stroke_color=WHITE,
|
||||
stroke_color=DEFAULT_MOBJECT_COLOR,
|
||||
stroke_width: float | Sequence[float] = [0, 3, 3, 0],
|
||||
stretch_factor=1.2,
|
||||
**kwargs
|
||||
|
||||
@@ -5,8 +5,8 @@ import copy
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, SMALL_BUFF
|
||||
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, UP, DL, DR, UL
|
||||
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF, SMALL_BUFF
|
||||
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL, UP
|
||||
from manimlib.constants import PI
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.animation.fading import FadeIn
|
||||
@@ -79,7 +79,7 @@ class Brace(Tex):
|
||||
)
|
||||
else:
|
||||
mob.move_to(self.get_tip())
|
||||
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFFER)
|
||||
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFF)
|
||||
shift_distance = mob.get_width() / 2.0 + buff
|
||||
mob.shift(self.get_direction() * shift_distance)
|
||||
return self
|
||||
@@ -116,7 +116,7 @@ class BraceLabel(VMobject):
|
||||
text: str | Iterable[str],
|
||||
brace_direction: np.ndarray = DOWN,
|
||||
label_scale: float = 1.0,
|
||||
label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
|
||||
label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF,
|
||||
**kwargs
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
@@ -174,3 +174,12 @@ class BraceLabel(VMobject):
|
||||
|
||||
class BraceText(BraceLabel):
|
||||
label_constructor: type = TexText
|
||||
|
||||
|
||||
class LineBrace(Brace):
|
||||
def __init__(self, line: Line, direction=UP, **kwargs):
|
||||
angle = line.get_angle()
|
||||
line.rotate(-angle)
|
||||
super().__init__(line, direction, **kwargs)
|
||||
line.rotate(angle)
|
||||
self.rotate(angle, about_point=line.get_center())
|
||||
|
||||
@@ -25,7 +25,6 @@ from manimlib.constants import LEFT
|
||||
from manimlib.constants import LEFT
|
||||
from manimlib.constants import MED_LARGE_BUFF
|
||||
from manimlib.constants import MED_SMALL_BUFF
|
||||
from manimlib.constants import LARGE_BUFF
|
||||
from manimlib.constants import ORIGIN
|
||||
from manimlib.constants import OUT
|
||||
from manimlib.constants import PI
|
||||
@@ -52,12 +51,9 @@ from manimlib.mobject.geometry import Polygon
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import Square
|
||||
from manimlib.mobject.geometry import AnnularSector
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.numbers import Integer
|
||||
from manimlib.mobject.shape_matchers import SurroundingRectangle
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.svg.special_tex import TexTextFromPresetString
|
||||
from manimlib.mobject.three_dimensions import Prismify
|
||||
from manimlib.mobject.three_dimensions import VCube
|
||||
@@ -348,6 +344,8 @@ class ClockPassesTime(AnimationGroup):
|
||||
angle=12 * hour_radians,
|
||||
**rot_kwargs
|
||||
),
|
||||
group=clock,
|
||||
run_time=run_time,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -421,7 +419,7 @@ class Bubble(VGroup):
|
||||
super().flip(axis=axis, **kwargs)
|
||||
if only_body:
|
||||
# Flip in place, don't use kwargs
|
||||
self.content.flip(axis=axis)
|
||||
self.content.flip(axis=axis)
|
||||
if abs(axis[1]) > 0:
|
||||
self.direction = -np.array(self.direction)
|
||||
return self
|
||||
|
||||
@@ -4,10 +4,10 @@ from functools import reduce
|
||||
import operator as op
|
||||
import re
|
||||
|
||||
from manimlib.constants import BLACK, WHITE
|
||||
from manimlib.constants import BLACK, DEFAULT_MOBJECT_COLOR
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
|
||||
from manimlib.utils.tex_file_writing import latex_to_svg
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -26,10 +26,10 @@ class SingleStringTex(SVGMobject):
|
||||
self,
|
||||
tex_string: str,
|
||||
height: float | None = None,
|
||||
fill_color: ManimColor = WHITE,
|
||||
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
fill_opacity: float = 1.0,
|
||||
stroke_width: float = 0,
|
||||
svg_default: dict = dict(fill_color=WHITE),
|
||||
svg_default: dict = dict(fill_color=DEFAULT_MOBJECT_COLOR),
|
||||
path_string_config: dict = dict(),
|
||||
font_size: int = 48,
|
||||
alignment: str = R"\centering",
|
||||
@@ -76,12 +76,8 @@ class SingleStringTex(SVGMobject):
|
||||
self.additional_preamble
|
||||
)
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
content = self.get_tex_file_body(self.tex_string)
|
||||
file_path = tex_content_to_svg_file(
|
||||
content, self.template, self.additional_preamble, self.tex_string
|
||||
)
|
||||
return file_path
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
return latex_to_svg(content, self.template, self.additional_preamble)
|
||||
|
||||
def get_tex_file_body(self, tex_string: str) -> str:
|
||||
new_tex = self.get_modified_expression(tex_string)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import MED_SMALL_BUFF, WHITE, GREY_C
|
||||
from manimlib.constants import MED_SMALL_BUFF, DEFAULT_MOBJECT_COLOR, GREY_C
|
||||
from manimlib.constants import DOWN, LEFT, RIGHT, UP
|
||||
from manimlib.constants import FRAME_WIDTH
|
||||
from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF
|
||||
@@ -21,13 +21,15 @@ class BulletedList(VGroup):
|
||||
*items: str,
|
||||
buff: float = MED_LARGE_BUFF,
|
||||
aligned_edge: Vect3 = LEFT,
|
||||
numbered: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
):
|
||||
labelled_content = [R"\item " + item for item in items]
|
||||
enum_str = "enumerate" if numbered else "itemize"
|
||||
tex_string = "\n".join([
|
||||
R"\begin{itemize}",
|
||||
fR"\begin{{{enum_str}}}",
|
||||
*labelled_content,
|
||||
R"\end{itemize}"
|
||||
fR"\end{{{enum_str}}}"
|
||||
])
|
||||
tex_text = TexText(tex_string, isolate=labelled_content, **kwargs)
|
||||
lines = (tex_text.select_part(part) for part in labelled_content)
|
||||
@@ -36,14 +38,17 @@ class BulletedList(VGroup):
|
||||
|
||||
self.arrange(DOWN, buff=buff, aligned_edge=aligned_edge)
|
||||
|
||||
def fade_all_but(self, index: int, opacity: float = 0.25) -> None:
|
||||
def fade_all_but(self, index: int, opacity: float = 0.25, scale_factor=0.7) -> None:
|
||||
max_dot_height = max([item[0].get_height() for item in self.submobjects])
|
||||
for i, part in enumerate(self.submobjects):
|
||||
trg_dot_height = (1.0 if i == index else scale_factor) * max_dot_height
|
||||
part.set_fill(opacity=(1.0 if i == index else opacity))
|
||||
part.scale(trg_dot_height / part[0].get_height(), about_edge=LEFT)
|
||||
|
||||
|
||||
class TexTextFromPresetString(TexText):
|
||||
tex: str = ""
|
||||
default_color: ManimColor = WHITE
|
||||
default_color: ManimColor = DEFAULT_MOBJECT_COLOR
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
|
||||
@@ -6,7 +6,7 @@ import re
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
from scipy.spatial.distance import cdist
|
||||
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import DEFAULT_MOBJECT_COLOR
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
@@ -46,11 +46,11 @@ class StringMobject(SVGMobject, ABC):
|
||||
def __init__(
|
||||
self,
|
||||
string: str,
|
||||
fill_color: ManimColor = WHITE,
|
||||
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
fill_border_width: float = 0.5,
|
||||
stroke_color: ManimColor = WHITE,
|
||||
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
stroke_width: float = 0,
|
||||
base_color: ManimColor = WHITE,
|
||||
base_color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
isolate: Selector = (),
|
||||
protect: Selector = (),
|
||||
# When set to true, only the labelled svg is
|
||||
@@ -60,23 +60,24 @@ class StringMobject(SVGMobject, ABC):
|
||||
**kwargs
|
||||
):
|
||||
self.string = string
|
||||
self.base_color = base_color or WHITE
|
||||
self.base_color = base_color or DEFAULT_MOBJECT_COLOR
|
||||
self.isolate = isolate
|
||||
self.protect = protect
|
||||
self.use_labelled_svg = use_labelled_svg
|
||||
|
||||
self.parse()
|
||||
super().__init__(**kwargs)
|
||||
svg_string = self.get_svg_string()
|
||||
super().__init__(svg_string=svg_string, **kwargs)
|
||||
self.set_stroke(stroke_color, stroke_width)
|
||||
self.set_fill(fill_color, border_width=fill_border_width)
|
||||
self.labels = [submob.label for submob in self.submobjects]
|
||||
|
||||
def get_file_path(self, is_labelled: bool = False) -> str:
|
||||
is_labelled = is_labelled or self.use_labelled_svg
|
||||
return self.get_file_path_by_content(self.get_content(is_labelled))
|
||||
def get_svg_string(self, is_labelled: bool = False) -> str:
|
||||
content = self.get_content(is_labelled or self.use_labelled_svg)
|
||||
return self.get_svg_string_by_content(content)
|
||||
|
||||
@abstractmethod
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
return ""
|
||||
|
||||
def assign_labels_by_color(self, mobjects: list[VMobject]) -> None:
|
||||
@@ -109,8 +110,8 @@ class StringMobject(SVGMobject, ABC):
|
||||
)
|
||||
)
|
||||
|
||||
def mobjects_from_file(self, file_path: str) -> list[VMobject]:
|
||||
submobs = super().mobjects_from_file(file_path)
|
||||
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
|
||||
submobs = super().mobjects_from_svg_string(svg_string)
|
||||
|
||||
if self.use_labelled_svg:
|
||||
# This means submobjects are colored according to spans
|
||||
@@ -121,8 +122,8 @@ class StringMobject(SVGMobject, ABC):
|
||||
# of submobject which are and use those for labels
|
||||
unlabelled_submobs = submobs
|
||||
labelled_content = self.get_content(is_labelled=True)
|
||||
labelled_file = self.get_file_path_by_content(labelled_content)
|
||||
labelled_submobs = super().mobjects_from_file(labelled_file)
|
||||
labelled_file = self.get_svg_string_by_content(labelled_content)
|
||||
labelled_submobs = super().mobjects_from_svg_string(labelled_file)
|
||||
self.labelled_submobs = labelled_submobs
|
||||
self.unlabelled_submobs = unlabelled_submobs
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import numpy as np
|
||||
import svgelements as se
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.constants import TAU
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Line
|
||||
@@ -16,14 +17,13 @@ from manimlib.mobject.geometry import Polyline
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import RoundedRectangle
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.directories import get_mobject_data_dir
|
||||
from manimlib.utils.bezier import quadratic_bezier_points_for_arc
|
||||
from manimlib.utils.images import get_full_vector_image_path
|
||||
from manimlib.utils.iterables import hash_obj
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
from manimlib.utils.space_ops import rotation_about_z
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple
|
||||
from manimlib.typing import ManimColor, Vect3Array
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ class SVGMobject(VMobject):
|
||||
def __init__(
|
||||
self,
|
||||
file_name: str = "",
|
||||
svg_string: str = "",
|
||||
should_center: bool = True,
|
||||
height: float | None = None,
|
||||
width: float | None = None,
|
||||
@@ -67,11 +68,19 @@ class SVGMobject(VMobject):
|
||||
path_string_config: dict = dict(),
|
||||
**kwargs
|
||||
):
|
||||
self.file_name = file_name or self.file_name
|
||||
if svg_string != "":
|
||||
self.svg_string = svg_string
|
||||
elif file_name != "":
|
||||
self.svg_string = self.file_name_to_svg_string(file_name)
|
||||
elif self.file_name != "":
|
||||
self.svg_string = self.file_name_to_svg_string(self.file_name)
|
||||
else:
|
||||
raise Exception("Must specify either a file_name or svg_string SVGMobject")
|
||||
|
||||
self.svg_default = dict(svg_default)
|
||||
self.path_string_config = dict(path_string_config)
|
||||
|
||||
super().__init__(**kwargs )
|
||||
super().__init__(**kwargs)
|
||||
self.init_svg_mobject()
|
||||
self.ensure_positive_orientation()
|
||||
|
||||
@@ -101,7 +110,7 @@ class SVGMobject(VMobject):
|
||||
if hash_val in SVG_HASH_TO_MOB_MAP:
|
||||
submobs = [sm.copy() for sm in SVG_HASH_TO_MOB_MAP[hash_val]]
|
||||
else:
|
||||
submobs = self.mobjects_from_file(self.get_file_path())
|
||||
submobs = self.mobjects_from_svg_string(self.svg_string)
|
||||
SVG_HASH_TO_MOB_MAP[hash_val] = [sm.copy() for sm in submobs]
|
||||
|
||||
self.add(*submobs)
|
||||
@@ -115,11 +124,11 @@ class SVGMobject(VMobject):
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.file_name
|
||||
self.svg_string
|
||||
)
|
||||
|
||||
def mobjects_from_file(self, file_path: str) -> list[VMobject]:
|
||||
element_tree = ET.parse(file_path)
|
||||
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
|
||||
element_tree = ET.ElementTree(ET.fromstring(svg_string))
|
||||
new_tree = self.modify_xml_tree(element_tree)
|
||||
|
||||
# New svg based on tree contents
|
||||
@@ -131,10 +140,8 @@ class SVGMobject(VMobject):
|
||||
|
||||
return self.mobjects_from_svg(svg)
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
if self.file_name is None:
|
||||
raise Exception("Must specify file for SVGMobject")
|
||||
return get_full_vector_image_path(self.file_name)
|
||||
def file_name_to_svg_string(self, file_name: str) -> str:
|
||||
return Path(get_full_vector_image_path(file_name)).read_text()
|
||||
|
||||
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
|
||||
config_style_attrs = self.generate_config_style_dict()
|
||||
@@ -296,8 +303,9 @@ class VMobjectFromSVGPath(VMobject):
|
||||
path_obj: se.Path,
|
||||
**kwargs
|
||||
):
|
||||
# Get rid of arcs
|
||||
path_obj.approximate_arcs_with_quads()
|
||||
# caches (transform.inverse(), rot, shift)
|
||||
self.transform_cache: tuple[se.Matrix, np.ndarray, np.ndarray] | None = None
|
||||
|
||||
self.path_obj = path_obj
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -324,13 +332,55 @@ class VMobjectFromSVGPath(VMobject):
|
||||
}
|
||||
for segment in self.path_obj:
|
||||
segment_class = segment.__class__
|
||||
func, attr_names = segment_class_to_func_map[segment_class]
|
||||
points = [
|
||||
_convert_point_to_3d(*segment.__getattribute__(attr_name))
|
||||
for attr_name in attr_names
|
||||
]
|
||||
func(*points)
|
||||
if segment_class is se.Arc:
|
||||
self.handle_arc(segment)
|
||||
else:
|
||||
func, attr_names = segment_class_to_func_map[segment_class]
|
||||
points = [
|
||||
_convert_point_to_3d(*segment.__getattribute__(attr_name))
|
||||
for attr_name in attr_names
|
||||
]
|
||||
func(*points)
|
||||
|
||||
# Get rid of the side effect of trailing "Z M" commands.
|
||||
if self.has_new_path_started():
|
||||
self.resize_points(self.get_num_points() - 2)
|
||||
|
||||
def handle_arc(self, arc: se.Arc) -> None:
|
||||
if self.transform_cache is not None:
|
||||
transform, rot, shift = self.transform_cache
|
||||
else:
|
||||
# The transform obtained in this way considers the combined effect
|
||||
# of all parent group transforms in the SVG.
|
||||
# Therefore, the arc can be transformed inversely using this transform
|
||||
# to correctly compute the arc path before transforming it back.
|
||||
transform = se.Matrix(self.path_obj.values.get('transform', ''))
|
||||
rot = np.array([
|
||||
[transform.a, transform.c],
|
||||
[transform.b, transform.d]
|
||||
])
|
||||
shift = np.array([transform.e, transform.f, 0])
|
||||
transform.inverse()
|
||||
self.transform_cache = (transform, rot, shift)
|
||||
|
||||
# Apply inverse transformation to the arc so that its path can be correctly computed
|
||||
arc *= transform
|
||||
|
||||
# The value of n_components is chosen based on the implementation of VMobject.arc_to
|
||||
n_components = int(np.ceil(8 * abs(arc.sweep) / TAU))
|
||||
|
||||
# Obtain the required angular segments on the unit circle
|
||||
arc_points = quadratic_bezier_points_for_arc(arc.sweep, n_components)
|
||||
arc_points @= np.array(rotation_about_z(arc.get_start_t())).T
|
||||
|
||||
# Transform to an ellipse, considering rotation and translating the ellipse center
|
||||
arc_points[:, 0] *= arc.rx
|
||||
arc_points[:, 1] *= arc.ry
|
||||
arc_points @= np.array(rotation_about_z(arc.get_rotation().as_radians)).T
|
||||
arc_points += [*arc.center, 0]
|
||||
|
||||
# Transform back
|
||||
arc_points[:, :2] @= rot.T
|
||||
arc_points += shift
|
||||
|
||||
self.append_points(arc_points[1:])
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.color import color_to_hex
|
||||
from manimlib.utils.color import hex_to_int
|
||||
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
|
||||
from manimlib.utils.tex_file_writing import latex_to_svg
|
||||
from manimlib.utils.tex import num_tex_symbols
|
||||
from manimlib.logger import log
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import ManimColor, Span, Selector
|
||||
from manimlib.typing import ManimColor, Span, Selector, Self
|
||||
|
||||
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
TEX_MOB_SCALE_FACTOR = 0.001
|
||||
|
||||
|
||||
class Tex(StringMobject):
|
||||
@@ -62,29 +63,17 @@ class Tex(StringMobject):
|
||||
)
|
||||
|
||||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
|
||||
self.scale(TEX_MOB_SCALE_FACTOR * font_size)
|
||||
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.base_color,
|
||||
self.isolate,
|
||||
self.protect,
|
||||
self.tex_string,
|
||||
self.alignment,
|
||||
self.tex_environment,
|
||||
self.tex_to_color_map,
|
||||
self.template,
|
||||
self.additional_preamble
|
||||
)
|
||||
self.font_size = font_size # Important for this to go after the scale call
|
||||
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
return tex_content_to_svg_file(
|
||||
content, self.template, self.additional_preamble, self.tex_string
|
||||
)
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
return latex_to_svg(content, self.template, self.additional_preamble, short_tex=self.tex_string)
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
|
||||
if hasattr(self, "font_size"):
|
||||
self.font_size *= scale_factor
|
||||
return self
|
||||
|
||||
# Parsing
|
||||
|
||||
@@ -253,15 +242,10 @@ class Tex(StringMobject):
|
||||
|
||||
decimal_mobs = []
|
||||
for part in parts:
|
||||
if "." in substr:
|
||||
num_decimal_places = len(substr.split(".")[1])
|
||||
else:
|
||||
num_decimal_places = 0
|
||||
decimal_mob = DecimalNumber(
|
||||
float(value),
|
||||
num_decimal_places=num_decimal_places,
|
||||
**config,
|
||||
)
|
||||
if "num_decimal_places" not in config:
|
||||
ndp = len(substr.split(".")[1]) if "." in substr else 0
|
||||
config["num_decimal_places"] = ndp
|
||||
decimal_mob = DecimalNumber(float(value), **config)
|
||||
decimal_mob.replace(part)
|
||||
decimal_mob.match_style(part)
|
||||
if len(part) > 1:
|
||||
|
||||
@@ -4,21 +4,22 @@ from contextlib import contextmanager
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
from functools import lru_cache
|
||||
|
||||
import manimpango
|
||||
import pygments
|
||||
import pygments.formatters
|
||||
import pygments.lexers
|
||||
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.constants import DEFAULT_PIXEL_WIDTH, FRAME_WIDTH
|
||||
from manimlib.constants import NORMAL
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||
from manimlib.utils.customization import get_customization
|
||||
from manimlib.utils.cache import cache_on_disk
|
||||
from manimlib.utils.color import color_to_hex
|
||||
from manimlib.utils.color import int_to_hex
|
||||
from manimlib.utils.directories import get_downloads_dir
|
||||
from manimlib.utils.directories import get_text_dir
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -49,6 +50,56 @@ class _Alignment:
|
||||
self.value = _Alignment.VAL_DICT[s.upper()]
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
@cache_on_disk
|
||||
def markup_to_svg(
|
||||
markup_str: str,
|
||||
justify: bool = False,
|
||||
indent: float = 0,
|
||||
alignment: str = "CENTER",
|
||||
line_width: float | None = None,
|
||||
) -> str:
|
||||
validate_error = manimpango.MarkupUtils.validate(markup_str)
|
||||
if validate_error:
|
||||
raise ValueError(
|
||||
f"Invalid markup string \"{markup_str}\"\n" + \
|
||||
f"{validate_error}"
|
||||
)
|
||||
|
||||
# `manimpango` is under construction,
|
||||
# so the following code is intended to suit its interface
|
||||
alignment = _Alignment(alignment)
|
||||
if line_width is None:
|
||||
pango_width = -1
|
||||
else:
|
||||
pango_width = line_width / FRAME_WIDTH * DEFAULT_PIXEL_WIDTH
|
||||
|
||||
# Write the result to a temporary svg file, and return it's contents.
|
||||
temp_file = Path(tempfile.gettempdir(), hash_string(markup_str)).with_suffix(".svg")
|
||||
manimpango.MarkupUtils.text2svg(
|
||||
text=markup_str,
|
||||
font="", # Already handled
|
||||
slant="NORMAL", # Already handled
|
||||
weight="NORMAL", # Already handled
|
||||
size=1, # Already handled
|
||||
_=0, # Empty parameter
|
||||
disable_liga=False,
|
||||
file_name=str(temp_file),
|
||||
START_X=0,
|
||||
START_Y=0,
|
||||
width=DEFAULT_CANVAS_WIDTH,
|
||||
height=DEFAULT_CANVAS_HEIGHT,
|
||||
justify=justify,
|
||||
indent=indent,
|
||||
line_spacing=None, # Already handled
|
||||
alignment=alignment,
|
||||
pango_width=pango_width
|
||||
)
|
||||
result = temp_file.read_text()
|
||||
os.remove(temp_file)
|
||||
return result
|
||||
|
||||
|
||||
class MarkupText(StringMobject):
|
||||
# See https://docs.gtk.org/Pango/pango_markup.html
|
||||
MARKUP_TAGS = {
|
||||
@@ -102,13 +153,14 @@ class MarkupText(StringMobject):
|
||||
isolate: Selector = re.compile(r"\w+", re.U),
|
||||
**kwargs
|
||||
):
|
||||
text_config = manim_config.text
|
||||
self.text = text
|
||||
self.font_size = font_size
|
||||
self.justify = justify
|
||||
self.indent = indent
|
||||
self.alignment = alignment or get_customization()["style"]["text_alignment"]
|
||||
self.alignment = alignment or text_config.alignment
|
||||
self.line_width = line_width
|
||||
self.font = font or get_customization()["style"]["font"]
|
||||
self.font = font or text_config.font
|
||||
self.slant = slant
|
||||
self.weight = weight
|
||||
|
||||
@@ -124,9 +176,6 @@ class MarkupText(StringMobject):
|
||||
self.disable_ligatures = disable_ligatures
|
||||
self.isolate = isolate
|
||||
|
||||
if not isinstance(self, Text):
|
||||
self.validate_markup_string(text)
|
||||
|
||||
super().__init__(text, height=height, **kwargs)
|
||||
|
||||
if self.t2g:
|
||||
@@ -141,88 +190,14 @@ class MarkupText(StringMobject):
|
||||
if height is None:
|
||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.base_color,
|
||||
self.isolate,
|
||||
self.protect,
|
||||
self.text,
|
||||
self.font_size,
|
||||
self.lsh,
|
||||
self.justify,
|
||||
self.indent,
|
||||
self.alignment,
|
||||
self.line_width,
|
||||
self.font,
|
||||
self.slant,
|
||||
self.weight,
|
||||
self.t2c,
|
||||
self.t2f,
|
||||
self.t2s,
|
||||
self.t2w,
|
||||
self.global_config,
|
||||
self.local_configs,
|
||||
self.disable_ligatures
|
||||
)
|
||||
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
hash_content = str((
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
self.content = content
|
||||
return markup_to_svg(
|
||||
content,
|
||||
self.justify,
|
||||
self.indent,
|
||||
self.alignment,
|
||||
self.line_width
|
||||
))
|
||||
svg_file = os.path.join(
|
||||
get_text_dir(), hash_string(hash_content) + ".svg"
|
||||
)
|
||||
if not os.path.exists(svg_file):
|
||||
self.markup_to_svg(content, svg_file)
|
||||
return svg_file
|
||||
|
||||
def markup_to_svg(self, markup_str: str, file_name: str) -> str:
|
||||
self.validate_markup_string(markup_str)
|
||||
|
||||
# `manimpango` is under construction,
|
||||
# so the following code is intended to suit its interface
|
||||
alignment = _Alignment(self.alignment)
|
||||
if self.line_width is None:
|
||||
pango_width = -1
|
||||
else:
|
||||
pango_width = self.line_width / FRAME_WIDTH * DEFAULT_PIXEL_WIDTH
|
||||
|
||||
return manimpango.MarkupUtils.text2svg(
|
||||
text=markup_str,
|
||||
font="", # Already handled
|
||||
slant="NORMAL", # Already handled
|
||||
weight="NORMAL", # Already handled
|
||||
size=1, # Already handled
|
||||
_=0, # Empty parameter
|
||||
disable_liga=False,
|
||||
file_name=file_name,
|
||||
START_X=0,
|
||||
START_Y=0,
|
||||
width=DEFAULT_CANVAS_WIDTH,
|
||||
height=DEFAULT_CANVAS_HEIGHT,
|
||||
justify=self.justify,
|
||||
indent=self.indent,
|
||||
line_spacing=None, # Already handled
|
||||
alignment=alignment,
|
||||
pango_width=pango_width
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_markup_string(markup_str: str) -> None:
|
||||
validate_error = manimpango.MarkupUtils.validate(markup_str)
|
||||
if not validate_error:
|
||||
return
|
||||
raise ValueError(
|
||||
f"Invalid markup string \"{markup_str}\"\n" + \
|
||||
f"{validate_error}"
|
||||
alignment=self.alignment,
|
||||
line_width=self.line_width
|
||||
)
|
||||
|
||||
# Toolkits
|
||||
@@ -511,20 +486,10 @@ def register_font(font_file: str | Path):
|
||||
method with previous releases will raise an :class:`AttributeError` on macOS.
|
||||
"""
|
||||
|
||||
input_folder = Path(get_downloads_dir()).parent.resolve()
|
||||
possible_paths = [
|
||||
Path(font_file),
|
||||
input_folder / font_file,
|
||||
]
|
||||
for path in possible_paths:
|
||||
path = path.resolve()
|
||||
if path.exists():
|
||||
file_path = path
|
||||
break
|
||||
else:
|
||||
error = f"Can't find {font_file}." f"Tried these : {possible_paths}"
|
||||
file_path = Path(font_file).resolve()
|
||||
if not file_path.exists():
|
||||
error = f"Can't find {font_file}."
|
||||
raise FileNotFoundError(error)
|
||||
|
||||
try:
|
||||
assert manimpango.register_font(str(file_path))
|
||||
yield
|
||||
|
||||
@@ -94,23 +94,30 @@ class Sphere(Surface):
|
||||
def __init__(
|
||||
self,
|
||||
u_range: Tuple[float, float] = (0, TAU),
|
||||
v_range: Tuple[float, float] = (1e-5, PI - 1e-5),
|
||||
v_range: Tuple[float, float] = (0, PI),
|
||||
resolution: Tuple[int, int] = (101, 51),
|
||||
radius: float = 1.0,
|
||||
true_normals: bool = True,
|
||||
clockwise=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.radius = radius
|
||||
self.clockwise = clockwise
|
||||
super().__init__(
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
resolution=resolution,
|
||||
**kwargs
|
||||
)
|
||||
# Add bespoke normal specification to avoid issue at poles
|
||||
if true_normals:
|
||||
self.data['d_normal_point'] = self.data['point'] * ((radius + self.normal_nudge) / radius)
|
||||
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
sign = -1 if self.clockwise else +1
|
||||
return self.radius * np.array([
|
||||
math.cos(u) * math.sin(v),
|
||||
math.sin(u) * math.sin(v),
|
||||
math.cos(sign * u) * math.sin(v),
|
||||
math.sin(sign * u) * math.sin(v),
|
||||
-math.cos(v)
|
||||
])
|
||||
|
||||
@@ -158,7 +165,6 @@ class Cylinder(Surface):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def init_points(self):
|
||||
super().init_points()
|
||||
self.scale(self.radius)
|
||||
@@ -169,6 +175,20 @@ class Cylinder(Surface):
|
||||
return np.array([np.cos(u), np.sin(u), v])
|
||||
|
||||
|
||||
class Cone(Cylinder):
|
||||
def __init__(
|
||||
self,
|
||||
u_range: Tuple[float, float] = (0, TAU),
|
||||
v_range: Tuple[float, float] = (0, 1),
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(u_range=u_range, v_range=v_range, *args, **kwargs)
|
||||
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([(1 - v) * np.cos(u), (1 - v) * np.sin(u), v])
|
||||
|
||||
|
||||
class Line3D(Cylinder):
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -8,9 +8,11 @@ from manimlib.constants import OUT
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import inverse_interpolate
|
||||
from manimlib.utils.images import get_full_raster_image_path
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.simple_functions import clip
|
||||
from manimlib.utils.space_ops import normalize_along_axis
|
||||
from manimlib.utils.space_ops import cross
|
||||
|
||||
@@ -28,11 +30,10 @@ class Surface(Mobject):
|
||||
shader_folder: str = "surface"
|
||||
data_dtype: np.dtype = np.dtype([
|
||||
('point', np.float32, (3,)),
|
||||
('du_point', np.float32, (3,)),
|
||||
('dv_point', np.float32, (3,)),
|
||||
('d_normal_point', np.float32, (3,)),
|
||||
('rgba', np.float32, (4,)),
|
||||
])
|
||||
pointlike_data_keys = ['point', 'du_point', 'dv_point']
|
||||
pointlike_data_keys = ['point', 'd_normal_point']
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -46,9 +47,11 @@ class Surface(Mobject):
|
||||
# rows/columns of approximating squares
|
||||
resolution: Tuple[int, int] = (101, 101),
|
||||
prefered_creation_axis: int = 1,
|
||||
# For du and dv steps. Much smaller and numerical error
|
||||
# can crop up in the shaders.
|
||||
epsilon: float = 1e-4,
|
||||
# For du and dv steps.
|
||||
epsilon: float = 1e-3,
|
||||
# Step off the surface to a new point which will
|
||||
# be used to determine the normal direction
|
||||
normal_nudge: float = 1e-3,
|
||||
**kwargs
|
||||
):
|
||||
self.u_range = u_range
|
||||
@@ -56,6 +59,7 @@ class Surface(Mobject):
|
||||
self.resolution = resolution
|
||||
self.prefered_creation_axis = prefered_creation_axis
|
||||
self.epsilon = epsilon
|
||||
self.normal_nudge = normal_nudge
|
||||
|
||||
super().__init__(
|
||||
**kwargs,
|
||||
@@ -71,16 +75,12 @@ class Surface(Mobject):
|
||||
|
||||
@Mobject.affects_data
|
||||
def init_points(self):
|
||||
dim = self.dim
|
||||
nu, nv = self.resolution
|
||||
u_range = np.linspace(*self.u_range, nu)
|
||||
v_range = np.linspace(*self.v_range, nv)
|
||||
|
||||
# Get three lists:
|
||||
# - Points generated by pure uv values
|
||||
# - Those generated by values nudged by du
|
||||
# - Those generated by values nudged by dv
|
||||
uv_grid = np.array([[[u, v] for v in v_range] for u in u_range])
|
||||
nu, nv = self.resolution
|
||||
uv_grid = self.get_uv_grid()
|
||||
uv_plus_du = uv_grid.copy()
|
||||
uv_plus_du[:, :, 0] += self.epsilon
|
||||
uv_plus_dv = uv_grid.copy()
|
||||
@@ -89,12 +89,51 @@ class Surface(Mobject):
|
||||
points, du_points, dv_points = [
|
||||
np.apply_along_axis(
|
||||
lambda p: self.uv_func(*p), 2, grid
|
||||
).reshape((nu * nv, dim))
|
||||
).reshape((nu * nv, self.dim))
|
||||
for grid in (uv_grid, uv_plus_du, uv_plus_dv)
|
||||
]
|
||||
crosses = cross(du_points - points, dv_points - points)
|
||||
normals = normalize_along_axis(crosses, 1)
|
||||
|
||||
self.set_points(points)
|
||||
self.data['du_point'][:] = du_points
|
||||
self.data['dv_point'][:] = dv_points
|
||||
self.data['d_normal_point'] = points + self.normal_nudge * normals
|
||||
|
||||
def get_uv_grid(self) -> np.array:
|
||||
"""
|
||||
Returns an (nu, nv, 2) array of all pairs of u, v values, where
|
||||
(nu, nv) is the resolution
|
||||
"""
|
||||
nu, nv = self.resolution
|
||||
u_range = np.linspace(*self.u_range, nu)
|
||||
v_range = np.linspace(*self.v_range, nv)
|
||||
U, V = np.meshgrid(u_range, v_range, indexing='ij')
|
||||
return np.stack([U, V], axis=-1)
|
||||
|
||||
def uv_to_point(self, u, v):
|
||||
nu, nv = self.resolution
|
||||
verts_by_uv = np.reshape(self.get_points(), (nu, nv, self.dim))
|
||||
|
||||
alpha1 = clip(inverse_interpolate(*self.u_range[:2], u), 0, 1)
|
||||
alpha2 = clip(inverse_interpolate(*self.v_range[:2], v), 0, 1)
|
||||
scaled_u = alpha1 * (nu - 1)
|
||||
scaled_v = alpha2 * (nv - 1)
|
||||
u_int = int(scaled_u)
|
||||
v_int = int(scaled_v)
|
||||
u_int_plus = min(u_int + 1, nu - 1)
|
||||
v_int_plus = min(v_int + 1, nv - 1)
|
||||
|
||||
a = verts_by_uv[u_int, v_int, :]
|
||||
b = verts_by_uv[u_int, v_int_plus, :]
|
||||
c = verts_by_uv[u_int_plus, v_int, :]
|
||||
d = verts_by_uv[u_int_plus, v_int_plus, :]
|
||||
|
||||
u_res = scaled_u % 1
|
||||
v_res = scaled_v % 1
|
||||
return interpolate(
|
||||
interpolate(a, b, v_res),
|
||||
interpolate(c, d, v_res),
|
||||
u_res
|
||||
)
|
||||
|
||||
def apply_points_function(self, *args, **kwargs) -> Self:
|
||||
super().apply_points_function(*args, **kwargs)
|
||||
@@ -124,12 +163,8 @@ class Surface(Mobject):
|
||||
return self.triangle_indices
|
||||
|
||||
def get_unit_normals(self) -> Vect3Array:
|
||||
points = self.get_points()
|
||||
crosses = cross(
|
||||
self.data['du_point'] - points,
|
||||
self.data['dv_point'] - points,
|
||||
)
|
||||
return normalize_along_axis(crosses, 1)
|
||||
# TOOD, I could try a more resiliant way to compute this using the neighboring grid values
|
||||
return normalize_along_axis(self.data['d_normal_point'] - self.data['point'], 1)
|
||||
|
||||
@Mobject.affects_data
|
||||
def pointwise_become_partial(
|
||||
@@ -212,6 +247,14 @@ class Surface(Mobject):
|
||||
self.add_updater(updater)
|
||||
return self
|
||||
|
||||
def color_by_uv_function(self, uv_to_color: Callable[[Vect2], Color]):
|
||||
uv_grid = self.get_uv_grid()
|
||||
self.set_rgba_array_by_color([
|
||||
uv_to_color(u, v)
|
||||
for u, v in uv_grid.reshape(-1, 2)
|
||||
])
|
||||
return self
|
||||
|
||||
def get_shader_vert_indices(self) -> np.ndarray:
|
||||
return self.get_triangle_indices()
|
||||
|
||||
@@ -248,8 +291,7 @@ class TexturedSurface(Surface):
|
||||
shader_folder: str = "textured_surface"
|
||||
data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
|
||||
('point', np.float32, (3,)),
|
||||
('du_point', np.float32, (3,)),
|
||||
('dv_point', np.float32, (3,)),
|
||||
('d_normal_point', np.float32, (3,)),
|
||||
('im_coords', np.float32, (2,)),
|
||||
('opacity', np.float32, (1,)),
|
||||
]
|
||||
@@ -293,8 +335,7 @@ class TexturedSurface(Surface):
|
||||
self.resize_points(surf.get_num_points())
|
||||
self.resolution = surf.resolution
|
||||
self.data['point'][:] = surf.data['point']
|
||||
self.data['du_point'][:] = surf.data['du_point']
|
||||
self.data['dv_point'][:] = surf.data['dv_point']
|
||||
self.data['d_normal_point'][:] = surf.data['d_normal_point']
|
||||
self.data['opacity'][:, 0] = surf.data["rgba"][:, 3]
|
||||
self.data["im_coords"] = np.array([
|
||||
[u, v]
|
||||
@@ -307,7 +348,7 @@ class TexturedSurface(Surface):
|
||||
self.uniforms["num_textures"] = self.num_textures
|
||||
|
||||
@Mobject.affects_data
|
||||
def set_opacity(self, opacity: float | Iterable[float]) -> Self:
|
||||
def set_opacity(self, opacity: float | Iterable[float], recurse=True) -> Self:
|
||||
op_arr = np.array(listify(opacity))
|
||||
self.data["opacity"][:, 0] = resize_with_interpolation(op_arr, len(self.data))
|
||||
return self
|
||||
|
||||
@@ -2,16 +2,13 @@ from __future__ import annotations
|
||||
|
||||
from functools import wraps
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
import operator as op
|
||||
import itertools as it
|
||||
|
||||
from manimlib.constants import GREY_A, GREY_C, GREY_E
|
||||
from manimlib.constants import DEFAULT_VMOBJECT_FILL_COLOR, DEFAULT_VMOBJECT_STROKE_COLOR
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.constants import DEFAULT_STROKE_WIDTH
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import JOINT_TYPE_MAP
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.constants import ORIGIN, OUT
|
||||
from manimlib.constants import PI
|
||||
from manimlib.constants import TAU
|
||||
@@ -35,10 +32,7 @@ from manimlib.utils.iterables import make_even
|
||||
from manimlib.utils.iterables import resize_array
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.iterables import resize_preserving_order
|
||||
from manimlib.utils.iterables import arrays_match
|
||||
from manimlib.utils.simple_functions import fdiv
|
||||
from manimlib.utils.space_ops import angle_between_vectors
|
||||
from manimlib.utils.space_ops import cross
|
||||
from manimlib.utils.space_ops import cross2d
|
||||
from manimlib.utils.space_ops import earclip_triangulation
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
@@ -49,7 +43,6 @@ from manimlib.utils.space_ops import rotation_between_vectors
|
||||
from manimlib.utils.space_ops import rotation_matrix_transpose
|
||||
from manimlib.utils.space_ops import poly_line_length
|
||||
from manimlib.utils.space_ops import z_to_vector
|
||||
from manimlib.shader_wrapper import ShaderWrapper
|
||||
from manimlib.shader_wrapper import VShaderWrapper
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -57,13 +50,10 @@ from typing import Generic, TypeVar, Iterable
|
||||
SubVmobjectType = TypeVar('SubVmobjectType', bound='VMobject')
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Tuple, Any
|
||||
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array, Self
|
||||
from typing import Callable, Tuple, Any, Optional
|
||||
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Self
|
||||
from moderngl.context import Context
|
||||
|
||||
DEFAULT_STROKE_COLOR = GREY_A
|
||||
DEFAULT_FILL_COLOR = GREY_C
|
||||
|
||||
|
||||
class VMobject(Mobject):
|
||||
data_dtype: np.dtype = np.dtype([
|
||||
@@ -79,6 +69,12 @@ class VMobject(Mobject):
|
||||
make_smooth_after_applying_functions: bool = False
|
||||
# TODO, do we care about accounting for varying zoom levels?
|
||||
tolerance_for_point_equality: float = 1e-8
|
||||
joint_type_map: dict = {
|
||||
"no_joint": 0,
|
||||
"auto": 1,
|
||||
"bevel": 2,
|
||||
"miter": 3,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -94,15 +90,16 @@ class VMobject(Mobject):
|
||||
# Could also be "no_joint", "bevel", "miter"
|
||||
joint_type: str = "auto",
|
||||
flat_stroke: bool = False,
|
||||
scale_stroke_with_zoom: bool = False,
|
||||
use_simple_quadratic_approx: bool = False,
|
||||
# Measured in pixel widths
|
||||
anti_alias_width: float = 1.5,
|
||||
fill_border_width: float = 0.0,
|
||||
**kwargs
|
||||
):
|
||||
self.fill_color = fill_color or color or DEFAULT_FILL_COLOR
|
||||
self.fill_color = fill_color or color or DEFAULT_VMOBJECT_FILL_COLOR
|
||||
self.fill_opacity = fill_opacity
|
||||
self.stroke_color = stroke_color or color or DEFAULT_STROKE_COLOR
|
||||
self.stroke_color = stroke_color or color or DEFAULT_VMOBJECT_STROKE_COLOR
|
||||
self.stroke_opacity = stroke_opacity
|
||||
self.stroke_width = stroke_width
|
||||
self.stroke_behind = stroke_behind
|
||||
@@ -110,6 +107,7 @@ class VMobject(Mobject):
|
||||
self.long_lines = long_lines
|
||||
self.joint_type = joint_type
|
||||
self.flat_stroke = flat_stroke
|
||||
self.scale_stroke_with_zoom = scale_stroke_with_zoom
|
||||
self.use_simple_quadratic_approx = use_simple_quadratic_approx
|
||||
self.anti_alias_width = anti_alias_width
|
||||
self.fill_border_width = fill_border_width
|
||||
@@ -126,9 +124,12 @@ class VMobject(Mobject):
|
||||
|
||||
def init_uniforms(self):
|
||||
super().init_uniforms()
|
||||
self.uniforms["anti_alias_width"] = self.anti_alias_width
|
||||
self.uniforms["joint_type"] = JOINT_TYPE_MAP[self.joint_type]
|
||||
self.uniforms["flat_stroke"] = float(self.flat_stroke)
|
||||
self.uniforms.update(
|
||||
anti_alias_width=self.anti_alias_width,
|
||||
joint_type=self.joint_type_map[self.joint_type],
|
||||
flat_stroke=float(self.flat_stroke),
|
||||
scale_stroke_with_zoom=float(self.scale_stroke_with_zoom)
|
||||
)
|
||||
|
||||
def add(self, *vmobjects: VMobject) -> Self:
|
||||
if not all((isinstance(m, VMobject) for m in vmobjects)):
|
||||
@@ -182,7 +183,7 @@ class VMobject(Mobject):
|
||||
if width is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
data = mob.data if mob.get_num_points() > 0 else mob._data_defaults
|
||||
if isinstance(width, (float, int)):
|
||||
if isinstance(width, (float, int, np.floating)):
|
||||
data['stroke_width'][:, 0] = width
|
||||
else:
|
||||
data['stroke_width'][:, 0] = resize_with_interpolation(
|
||||
@@ -302,6 +303,11 @@ class VMobject(Mobject):
|
||||
self.set_stroke(opacity=opacity, recurse=recurse)
|
||||
return self
|
||||
|
||||
def set_color_by_proportion(self, prop_to_color: Callable[[float], Color]) -> Self:
|
||||
colors = list(map(prop_to_color, np.linspace(0, 1, self.get_num_points())))
|
||||
self.set_stroke(color=colors)
|
||||
return self
|
||||
|
||||
def set_anti_alias_width(self, anti_alias_width: float, recurse: bool = True) -> Self:
|
||||
self.set_uniform(recurse, anti_alias_width=anti_alias_width)
|
||||
return self
|
||||
@@ -399,9 +405,16 @@ class VMobject(Mobject):
|
||||
def get_flat_stroke(self) -> bool:
|
||||
return self.uniforms["flat_stroke"] == 1.0
|
||||
|
||||
def set_scale_stroke_with_zoom(self, scale_stroke_with_zoom: bool = True, recurse: bool = True) -> Self:
|
||||
self.set_uniform(recurse, scale_stroke_with_zoom=float(scale_stroke_with_zoom))
|
||||
pass
|
||||
|
||||
def get_scale_stroke_with_zoom(self) -> bool:
|
||||
return self.uniforms["flat_stroke"] == 1.0
|
||||
|
||||
def set_joint_type(self, joint_type: str, recurse: bool = True) -> Self:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.uniforms["joint_type"] = JOINT_TYPE_MAP[joint_type]
|
||||
mob.uniforms["joint_type"] = self.joint_type_map[joint_type]
|
||||
return self
|
||||
|
||||
def get_joint_type(self) -> float:
|
||||
@@ -480,7 +493,7 @@ class VMobject(Mobject):
|
||||
v1 = handle1 - last
|
||||
v2 = anchor - handle2
|
||||
angle = angle_between_vectors(v1, v2)
|
||||
if self.use_simple_quadratic_approx and angle < 45 * DEGREES:
|
||||
if self.use_simple_quadratic_approx and angle < 45 * DEG:
|
||||
quad_approx = [last, find_intersection(last, v1, anchor, -v2), anchor]
|
||||
else:
|
||||
quad_approx = get_quadratic_approximation_of_cubic(
|
||||
@@ -606,7 +619,7 @@ class VMobject(Mobject):
|
||||
|
||||
def subdivide_sharp_curves(
|
||||
self,
|
||||
angle_threshold: float = 30 * DEGREES,
|
||||
angle_threshold: float = 30 * DEG,
|
||||
recurse: bool = True
|
||||
) -> Self:
|
||||
def tuple_to_subdivisions(b0, b1, b2):
|
||||
@@ -646,7 +659,7 @@ class VMobject(Mobject):
|
||||
self.make_smooth(approx=approx)
|
||||
return self
|
||||
|
||||
def is_smooth(self, angle_tol=1 * DEGREES) -> bool:
|
||||
def is_smooth(self, angle_tol=1 * DEG) -> bool:
|
||||
angles = np.abs(self.get_joint_angles()[0::2])
|
||||
return (angles < angle_tol).all()
|
||||
|
||||
|
||||
@@ -3,29 +3,26 @@ from __future__ import annotations
|
||||
import itertools as it
|
||||
|
||||
import numpy as np
|
||||
from scipy.integrate import solve_ivp
|
||||
|
||||
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
||||
from manimlib.constants import BLUE, WHITE
|
||||
from manimlib.constants import ORIGIN
|
||||
from manimlib.constants import DEFAULT_MOBJECT_COLOR
|
||||
from manimlib.animation.indication import VShowPassingFlash
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import inverse_interpolate
|
||||
from manimlib.utils.color import get_colormap_list
|
||||
from manimlib.utils.color import rgb_to_color
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
from manimlib.utils.color import get_color_map
|
||||
from manimlib.utils.iterables import cartesian_product
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.simple_functions import sigmoid
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, Sequence, TypeVar, Tuple
|
||||
from manimlib.typing import ManimColor, Vect3, VectN, Vect3Array
|
||||
from typing import Callable, Iterable, Sequence, TypeVar, Tuple, Optional
|
||||
from manimlib.typing import ManimColor, Vect3, VectN, VectArray, Vect3Array, Vect4Array
|
||||
|
||||
from manimlib.mobject.coordinate_systems import CoordinateSystem
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -33,6 +30,7 @@ if TYPE_CHECKING:
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
#### Delete these two ###
|
||||
def get_vectorized_rgb_gradient_function(
|
||||
min_value: T,
|
||||
max_value: T,
|
||||
@@ -52,6 +50,7 @@ def get_vectorized_rgb_gradient_function(
|
||||
inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3))
|
||||
result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas)
|
||||
return result
|
||||
|
||||
return func
|
||||
|
||||
|
||||
@@ -62,6 +61,17 @@ def get_rgb_gradient_function(
|
||||
) -> Callable[[float], Vect3]:
|
||||
vectorized_func = get_vectorized_rgb_gradient_function(min_value, max_value, color_map)
|
||||
return lambda value: vectorized_func(np.array([value]))[0]
|
||||
####
|
||||
|
||||
|
||||
def ode_solution_points(function, state0, time, dt=0.01):
|
||||
solution = solve_ivp(
|
||||
lambda t, state: function(state),
|
||||
t_span=(0, time),
|
||||
y0=state0,
|
||||
t_eval=np.arange(0, time, dt)
|
||||
)
|
||||
return solution.y.T
|
||||
|
||||
|
||||
def move_along_vector_field(
|
||||
@@ -98,24 +108,31 @@ def move_points_along_vector_field(
|
||||
cs = coordinate_system
|
||||
origin = cs.get_origin()
|
||||
|
||||
def apply_nudge(self, dt):
|
||||
mobject.apply_function(
|
||||
def apply_nudge(mob, dt):
|
||||
mob.apply_function(
|
||||
lambda p: p + (cs.c2p(*func(*cs.p2c(p))) - origin) * dt
|
||||
)
|
||||
mobject.add_updater(apply_nudge)
|
||||
return mobject
|
||||
|
||||
|
||||
def get_sample_points_from_coordinate_system(
|
||||
def get_sample_coords(
|
||||
coordinate_system: CoordinateSystem,
|
||||
step_multiple: float
|
||||
density: float = 1.0
|
||||
) -> it.product[tuple[Vect3, ...]]:
|
||||
ranges = []
|
||||
for range_args in coordinate_system.get_all_ranges():
|
||||
_min, _max, step = range_args
|
||||
step *= step_multiple
|
||||
step /= density
|
||||
ranges.append(np.arange(_min, _max + step, step))
|
||||
return it.product(*ranges)
|
||||
return np.array(list(it.product(*ranges)))
|
||||
|
||||
|
||||
def vectorize(pointwise_function: Callable[[Tuple], Tuple]):
|
||||
def v_func(coords_array: VectArray) -> VectArray:
|
||||
return np.array([pointwise_function(*coords) for coords in coords_array])
|
||||
|
||||
return v_func
|
||||
|
||||
|
||||
# Mobjects
|
||||
@@ -124,65 +141,73 @@ def get_sample_points_from_coordinate_system(
|
||||
class VectorField(VMobject):
|
||||
def __init__(
|
||||
self,
|
||||
func,
|
||||
stroke_color: ManimColor = BLUE,
|
||||
# Vectorized function: Takes in an array of coordinates, returns an array of outputs.
|
||||
func: Callable[[VectArray], VectArray],
|
||||
# Typically a set of Axes or NumberPlane
|
||||
coordinate_system: CoordinateSystem,
|
||||
sample_coords: Optional[VectArray] = None,
|
||||
density: float = 2.0,
|
||||
magnitude_range: Optional[Tuple[float, float]] = None,
|
||||
color: Optional[ManimColor] = None,
|
||||
color_map_name: Optional[str] = "3b1b_colormap",
|
||||
color_map: Optional[Callable[[Sequence[float]], Vect4Array]] = None,
|
||||
stroke_opacity: float = 1.0,
|
||||
center: Vect3 = ORIGIN,
|
||||
sample_points: Optional[Vect3Array] = None,
|
||||
x_density: float = 2.0,
|
||||
y_density: float = 2.0,
|
||||
z_density: float = 2.0,
|
||||
width: float = 14.0,
|
||||
height: float = 8.0,
|
||||
depth: float = 0.0,
|
||||
stroke_width: float = 2,
|
||||
stroke_width: float = 3,
|
||||
tip_width_ratio: float = 4,
|
||||
tip_len_to_width: float = 0.01,
|
||||
max_vect_len: float | None = None,
|
||||
min_drawn_norm: float = 1e-2,
|
||||
max_vect_len_to_step_size: float = 0.8,
|
||||
flat_stroke: bool = False,
|
||||
norm_to_opacity_func=None,
|
||||
norm_to_rgb_func=None,
|
||||
norm_to_opacity_func=None, # TODO, check on this
|
||||
**kwargs
|
||||
):
|
||||
self.func = func
|
||||
self.coordinate_system = coordinate_system
|
||||
self.stroke_width = stroke_width
|
||||
self.tip_width_ratio = tip_width_ratio
|
||||
self.tip_len_to_width = tip_len_to_width
|
||||
self.min_drawn_norm = min_drawn_norm
|
||||
self.norm_to_opacity_func = norm_to_opacity_func
|
||||
self.norm_to_rgb_func = norm_to_rgb_func
|
||||
|
||||
if max_vect_len is not None:
|
||||
self.max_vect_len = max_vect_len
|
||||
# Search for sample_points
|
||||
if sample_coords is not None:
|
||||
self.sample_coords = sample_coords
|
||||
else:
|
||||
densities = np.array([x_density, y_density, z_density])
|
||||
dims = np.array([width, height, depth])
|
||||
self.max_vect_len = 1.0 / densities[dims > 0].mean()
|
||||
self.sample_coords = get_sample_coords(coordinate_system, density)
|
||||
self.update_sample_points()
|
||||
|
||||
if sample_points is None:
|
||||
self.sample_points = self.get_sample_points(
|
||||
center, width, height, depth,
|
||||
x_density, y_density, z_density
|
||||
)
|
||||
if max_vect_len is None:
|
||||
step_size = get_norm(self.sample_points[1] - self.sample_points[0])
|
||||
self.max_displayed_vect_len = max_vect_len_to_step_size * step_size
|
||||
else:
|
||||
self.sample_points = sample_points
|
||||
self.max_displayed_vect_len = max_vect_len * coordinate_system.x_axis.get_unit_size()
|
||||
|
||||
self.init_base_stroke_width_array(len(self.sample_points))
|
||||
# Prepare the color map
|
||||
if magnitude_range is None:
|
||||
max_value = max(map(get_norm, func(self.sample_coords)))
|
||||
magnitude_range = (0, max_value)
|
||||
|
||||
self.magnitude_range = magnitude_range
|
||||
|
||||
if color is not None:
|
||||
self.color_map = None
|
||||
else:
|
||||
self.color_map = color_map or get_color_map(color_map_name)
|
||||
|
||||
self.init_base_stroke_width_array(len(self.sample_coords))
|
||||
|
||||
super().__init__(
|
||||
stroke_color=stroke_color,
|
||||
stroke_opacity=stroke_opacity,
|
||||
flat_stroke=flat_stroke,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
n_samples = len(self.sample_points)
|
||||
self.set_points(np.zeros((8 * n_samples - 1, 3)))
|
||||
self.set_stroke(width=stroke_width)
|
||||
self.set_joint_type('no_joint')
|
||||
self.set_stroke(color, stroke_width)
|
||||
self.update_vectors()
|
||||
|
||||
def init_points(self):
|
||||
n_samples = len(self.sample_coords)
|
||||
self.set_points(np.zeros((8 * n_samples - 1, 3)))
|
||||
self.set_joint_type('no_joint')
|
||||
|
||||
def get_sample_points(
|
||||
self,
|
||||
center: np.ndarray,
|
||||
@@ -211,8 +236,8 @@ class VectorField(VMobject):
|
||||
arr[7::8] = 0
|
||||
self.base_stroke_width_array = arr
|
||||
|
||||
def set_sample_points(self, sample_points: Vect3Array):
|
||||
self.sample_points = sample_points
|
||||
def set_sample_coords(self, sample_coords: VectArray):
|
||||
self.sample_coords = sample_coords
|
||||
return self
|
||||
|
||||
def set_stroke(self, color=None, width=None, opacity=None, behind=None, flat=None, recurse=True):
|
||||
@@ -227,35 +252,40 @@ class VectorField(VMobject):
|
||||
self.stroke_width = width
|
||||
return self
|
||||
|
||||
def update_sample_points(self):
|
||||
self.sample_points = self.coordinate_system.c2p(*self.sample_coords.T)
|
||||
|
||||
def update_vectors(self):
|
||||
tip_width = self.tip_width_ratio * self.stroke_width
|
||||
tip_len = self.tip_len_to_width * tip_width
|
||||
samples = self.sample_points
|
||||
|
||||
# Get raw outputs and lengths
|
||||
outputs = self.func(samples)
|
||||
norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis]
|
||||
# Outputs in the coordinate system
|
||||
outputs = self.func(self.sample_coords)
|
||||
output_norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis]
|
||||
|
||||
# How long should the arrows be drawn?
|
||||
max_len = self.max_vect_len
|
||||
# Corresponding vector values in global coordinates
|
||||
out_vects = self.coordinate_system.c2p(*outputs.T) - self.coordinate_system.get_origin()
|
||||
out_vect_norms = np.linalg.norm(out_vects, axis=1)[:, np.newaxis]
|
||||
unit_outputs = np.zeros_like(out_vects)
|
||||
np.true_divide(out_vects, out_vect_norms, out=unit_outputs, where=(out_vect_norms > 0))
|
||||
|
||||
# How long should the arrows be drawn, in global coordinates
|
||||
max_len = self.max_displayed_vect_len
|
||||
if max_len < np.inf:
|
||||
drawn_norms = max_len * np.tanh(norms / max_len)
|
||||
drawn_norms = max_len * np.tanh(out_vect_norms / max_len)
|
||||
else:
|
||||
drawn_norms = norms
|
||||
drawn_norms = out_vect_norms
|
||||
|
||||
# What's the distance from the base of an arrow to
|
||||
# the base of its head?
|
||||
dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf)
|
||||
dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf) # Mixing units!
|
||||
|
||||
# Set all points
|
||||
unit_outputs = np.zeros_like(outputs)
|
||||
np.true_divide(outputs, norms, out=unit_outputs, where=(norms > self.min_drawn_norm))
|
||||
|
||||
points = self.get_points()
|
||||
points[0::8] = samples
|
||||
points[2::8] = samples + dist_to_head_base * unit_outputs
|
||||
points[0::8] = self.sample_points
|
||||
points[2::8] = self.sample_points + dist_to_head_base * unit_outputs
|
||||
points[4::8] = points[2::8]
|
||||
points[6::8] = samples + drawn_norms * unit_outputs
|
||||
points[6::8] = self.sample_points + drawn_norms * unit_outputs
|
||||
for i in (1, 3, 5):
|
||||
points[i::8] = 0.5 * (points[i - 1::8] + points[i + 1::8])
|
||||
points[7::8] = points[6:-1:8]
|
||||
@@ -267,14 +297,16 @@ class VectorField(VMobject):
|
||||
self.get_stroke_widths()[:] = width_scalars * width_arr
|
||||
|
||||
# Potentially adjust opacity and color
|
||||
if self.color_map is not None:
|
||||
self.get_stroke_colors() # Ensures the array is updated to appropriate length
|
||||
low, high = self.magnitude_range
|
||||
self.data['stroke_rgba'][:, :3] = self.color_map(
|
||||
inverse_interpolate(low, high, np.repeat(output_norms, 8)[:-1])
|
||||
)[:, :3]
|
||||
|
||||
if self.norm_to_opacity_func is not None:
|
||||
self.get_stroke_opacities()[:] = self.norm_to_opacity_func(
|
||||
np.repeat(norms, 8)[:-1]
|
||||
)
|
||||
if self.norm_to_rgb_func is not None:
|
||||
self.get_stroke_colors()
|
||||
self.data['stroke_rgba'][:, :3] = self.norm_to_rgb_func(
|
||||
np.repeat(norms, 8)[:-1]
|
||||
np.repeat(output_norms, 8)[:-1]
|
||||
)
|
||||
|
||||
self.note_changed_data()
|
||||
@@ -285,90 +317,33 @@ class TimeVaryingVectorField(VectorField):
|
||||
def __init__(
|
||||
self,
|
||||
# Takes in an array of points and a float for time
|
||||
time_func,
|
||||
time_func: Callable[[VectArray, float], VectArray],
|
||||
coordinate_system: CoordinateSystem,
|
||||
**kwargs
|
||||
):
|
||||
self.time = 0
|
||||
super().__init__(func=lambda p: time_func(p, self.time), **kwargs)
|
||||
|
||||
def func(coords):
|
||||
return time_func(coords, self.time)
|
||||
|
||||
super().__init__(func, coordinate_system, **kwargs)
|
||||
self.add_updater(lambda m, dt: m.increment_time(dt))
|
||||
always(self.update_vectors)
|
||||
self.always.update_vectors()
|
||||
|
||||
def increment_time(self, dt):
|
||||
self.time += dt
|
||||
|
||||
|
||||
class OldVectorField(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], Sequence[float]],
|
||||
coordinate_system: CoordinateSystem,
|
||||
step_multiple: float = 0.5,
|
||||
magnitude_range: Tuple[float, float] = (0, 2),
|
||||
color_map: str = "3b1b_colormap",
|
||||
# Takes in actual norm, spits out displayed norm
|
||||
length_func: Callable[[float], float] = lambda norm: 0.45 * sigmoid(norm),
|
||||
opacity: float = 1.0,
|
||||
vector_config: dict = dict(),
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.func = func
|
||||
self.coordinate_system = coordinate_system
|
||||
self.step_multiple = step_multiple
|
||||
self.magnitude_range = magnitude_range
|
||||
self.color_map = color_map
|
||||
self.length_func = length_func
|
||||
self.opacity = opacity
|
||||
self.vector_config = dict(vector_config)
|
||||
|
||||
self.value_to_rgb = get_rgb_gradient_function(
|
||||
*self.magnitude_range, self.color_map,
|
||||
)
|
||||
|
||||
samples = get_sample_points_from_coordinate_system(
|
||||
coordinate_system, self.step_multiple
|
||||
)
|
||||
self.add(*(
|
||||
self.get_vector(coords)
|
||||
for coords in samples
|
||||
))
|
||||
|
||||
def get_vector(self, coords: Iterable[float], **kwargs) -> Arrow:
|
||||
vector_config = merge_dicts_recursively(
|
||||
self.vector_config,
|
||||
kwargs
|
||||
)
|
||||
|
||||
output = np.array(self.func(*coords))
|
||||
norm = get_norm(output)
|
||||
if norm > 0:
|
||||
output *= self.length_func(norm) / norm
|
||||
|
||||
origin = self.coordinate_system.get_origin()
|
||||
_input = self.coordinate_system.c2p(*coords)
|
||||
_output = self.coordinate_system.c2p(*output)
|
||||
|
||||
vect = Arrow(
|
||||
origin, _output, buff=0,
|
||||
**vector_config
|
||||
)
|
||||
vect.shift(_input - origin)
|
||||
vect.set_color(
|
||||
rgb_to_color(self.value_to_rgb(norm)),
|
||||
opacity=self.opacity,
|
||||
)
|
||||
return vect
|
||||
|
||||
|
||||
class StreamLines(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], Sequence[float]],
|
||||
func: Callable[[VectArray], VectArray],
|
||||
coordinate_system: CoordinateSystem,
|
||||
step_multiple: float = 0.5,
|
||||
density: float = 1.0,
|
||||
n_repeats: int = 1,
|
||||
noise_factor: float | None = None,
|
||||
# Config for drawing lines
|
||||
solution_time: float = 3,
|
||||
dt: float = 0.05,
|
||||
arc_len: float = 3,
|
||||
max_time_steps: int = 200,
|
||||
@@ -376,7 +351,7 @@ class StreamLines(VGroup):
|
||||
cutoff_norm: float = 15,
|
||||
# Style info
|
||||
stroke_width: float = 1.0,
|
||||
stroke_color: ManimColor = WHITE,
|
||||
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
|
||||
stroke_opacity: float = 1,
|
||||
color_by_magnitude: bool = True,
|
||||
magnitude_range: Tuple[float, float] = (0, 2.0),
|
||||
@@ -387,9 +362,10 @@ class StreamLines(VGroup):
|
||||
super().__init__(**kwargs)
|
||||
self.func = func
|
||||
self.coordinate_system = coordinate_system
|
||||
self.step_multiple = step_multiple
|
||||
self.density = density
|
||||
self.n_repeats = n_repeats
|
||||
self.noise_factor = noise_factor
|
||||
self.solution_time = solution_time
|
||||
self.dt = dt
|
||||
self.arc_len = arc_len
|
||||
self.max_time_steps = max_time_steps
|
||||
@@ -406,48 +382,37 @@ class StreamLines(VGroup):
|
||||
self.draw_lines()
|
||||
self.init_style()
|
||||
|
||||
def point_func(self, point: Vect3) -> Vect3:
|
||||
in_coords = self.coordinate_system.p2c(point)
|
||||
out_coords = self.func(*in_coords)
|
||||
return self.coordinate_system.c2p(*out_coords)
|
||||
def point_func(self, points: Vect3Array) -> Vect3:
|
||||
in_coords = np.array(self.coordinate_system.p2c(points)).T
|
||||
out_coords = self.func(in_coords)
|
||||
origin = self.coordinate_system.get_origin()
|
||||
return self.coordinate_system.c2p(*out_coords.T) - origin
|
||||
|
||||
def draw_lines(self) -> None:
|
||||
lines = []
|
||||
origin = self.coordinate_system.get_origin()
|
||||
for point in self.get_start_points():
|
||||
points = [point]
|
||||
total_arc_len = 0
|
||||
time = 0
|
||||
for x in range(self.max_time_steps):
|
||||
time += self.dt
|
||||
last_point = points[-1]
|
||||
new_point = last_point + self.dt * (self.point_func(last_point) - origin)
|
||||
points.append(new_point)
|
||||
total_arc_len += get_norm(new_point - last_point)
|
||||
if get_norm(last_point) > self.cutoff_norm:
|
||||
break
|
||||
if total_arc_len > self.arc_len:
|
||||
break
|
||||
|
||||
# Todo, it feels like coordinate system should just have
|
||||
# the ODE solver built into it, no?
|
||||
lines = []
|
||||
for coords in self.get_sample_coords():
|
||||
solution_coords = ode_solution_points(self.func, coords, self.solution_time, self.dt)
|
||||
line = VMobject()
|
||||
line.virtual_time = time
|
||||
step = max(1, int(len(points) / self.n_samples_per_line))
|
||||
line.set_points_as_corners(points[::step])
|
||||
line.make_smooth(approx=True)
|
||||
line.set_points_smoothly(self.coordinate_system.c2p(*solution_coords.T))
|
||||
# TODO, account for arc length somehow?
|
||||
line.virtual_time = self.solution_time
|
||||
lines.append(line)
|
||||
self.set_submobjects(lines)
|
||||
|
||||
def get_start_points(self) -> Vect3Array:
|
||||
def get_sample_coords(self):
|
||||
cs = self.coordinate_system
|
||||
sample_coords = get_sample_points_from_coordinate_system(
|
||||
cs, self.step_multiple,
|
||||
)
|
||||
sample_coords = get_sample_coords(cs, self.density)
|
||||
|
||||
noise_factor = self.noise_factor
|
||||
if noise_factor is None:
|
||||
noise_factor = cs.x_range[2] * self.step_multiple * 0.5
|
||||
noise_factor = (cs.x_axis.get_unit_size() / self.density) * 0.5
|
||||
|
||||
return np.array([
|
||||
cs.c2p(*coords) + noise_factor * np.random.random(3)
|
||||
coords + noise_factor * np.random.random(coords.shape)
|
||||
for n in range(self.n_repeats)
|
||||
for coords in sample_coords
|
||||
])
|
||||
@@ -460,7 +425,7 @@ class StreamLines(VGroup):
|
||||
cs = self.coordinate_system
|
||||
for line in self.submobjects:
|
||||
norms = [
|
||||
get_norm(self.func(*cs.p2c(point)))
|
||||
get_norm(self.func(cs.p2c(point)))
|
||||
for point in line.get_points()
|
||||
]
|
||||
rgbs = values_to_rgbs(norms)
|
||||
@@ -483,6 +448,7 @@ class AnimatedStreamLines(VGroup):
|
||||
self,
|
||||
stream_lines: StreamLines,
|
||||
lag_range: float = 4,
|
||||
rate_multiple: float = 1.0,
|
||||
line_anim_config: dict = dict(
|
||||
rate_func=linear,
|
||||
time_width=1.0,
|
||||
@@ -495,7 +461,7 @@ class AnimatedStreamLines(VGroup):
|
||||
for line in stream_lines:
|
||||
line.anim = VShowPassingFlash(
|
||||
line,
|
||||
run_time=line.virtual_time,
|
||||
run_time=line.virtual_time / rate_multiple,
|
||||
**line_anim_config,
|
||||
)
|
||||
line.anim.begin()
|
||||
@@ -504,7 +470,7 @@ class AnimatedStreamLines(VGroup):
|
||||
|
||||
self.add_updater(lambda m, dt: m.update(dt))
|
||||
|
||||
def update(self, dt: float) -> None:
|
||||
def update(self, dt: float = 0) -> None:
|
||||
stream_lines = self.stream_lines
|
||||
for line in stream_lines:
|
||||
line.time += dt
|
||||
|
||||
176
manimlib/module_loader.py
Normal file
176
manimlib/module_loader.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.logger import log
|
||||
|
||||
Module = importlib.util.types.ModuleType
|
||||
|
||||
|
||||
class ModuleLoader:
|
||||
"""
|
||||
Utility class to load a module from a file and handle its imports.
|
||||
|
||||
Most parts of this class are only needed for the reload functionality,
|
||||
while the `get_module` method is the main entry point to import a module.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_module(file_name: str | None, is_during_reload=False) -> Module | None:
|
||||
"""
|
||||
Imports a module from a file and returns it.
|
||||
|
||||
During reload (when the user calls `reload()` in the IPython shell), we
|
||||
also track the imported modules and reload them as well (they would be
|
||||
cached otherwise). See the reload_manager where the reload parameter is set.
|
||||
|
||||
Note that `exec_module()` is called twice when reloading a module:
|
||||
1. In exec_module_and_track_imports to track the imports
|
||||
2. Here to actually execute the module again with the respective
|
||||
imported modules reloaded.
|
||||
"""
|
||||
if file_name is None:
|
||||
return None
|
||||
|
||||
module_name = file_name.replace(os.sep, ".").replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
|
||||
if is_during_reload:
|
||||
imported_modules = ModuleLoader._exec_module_and_track_imports(spec, module)
|
||||
reloaded_modules_tracker = set()
|
||||
ModuleLoader._reload_modules(imported_modules, reloaded_modules_tracker)
|
||||
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
@staticmethod
|
||||
def _exec_module_and_track_imports(spec, module: Module) -> set[str]:
|
||||
"""
|
||||
Executes the given module (imports it) and returns all the modules that
|
||||
are imported during its execution.
|
||||
|
||||
This is achieved by replacing the __import__ function with a custom one
|
||||
that tracks the imported modules. At the end, the original __import__
|
||||
built-in function is restored.
|
||||
"""
|
||||
imported_modules: set[str] = set()
|
||||
original_import = builtins.__import__
|
||||
|
||||
def tracked_import(name, globals=None, locals=None, fromlist=(), level=0):
|
||||
"""
|
||||
Custom __import__ function that does exactly the same as the original
|
||||
one, but also tracks the imported modules by means of adding their
|
||||
names to a set.
|
||||
"""
|
||||
result = original_import(name, globals, locals, fromlist, level)
|
||||
imported_modules.add(name)
|
||||
return result
|
||||
|
||||
builtins.__import__ = tracked_import
|
||||
|
||||
try:
|
||||
module_name = module.__name__
|
||||
log.debug('Reloading module "%s"', module_name)
|
||||
|
||||
spec.loader.exec_module(module)
|
||||
finally:
|
||||
builtins.__import__ = original_import
|
||||
|
||||
return imported_modules
|
||||
|
||||
@staticmethod
|
||||
def _reload_modules(modules: set[str], reloaded_modules_tracker: set[str]):
|
||||
"""
|
||||
Out of the given modules, reloads the ones that were not already imported.
|
||||
|
||||
We skip modules that are not user-defined (see `is_user_defined_module()`).
|
||||
"""
|
||||
for mod in modules:
|
||||
if mod in reloaded_modules_tracker:
|
||||
continue
|
||||
|
||||
if not ModuleLoader._is_user_defined_module(mod):
|
||||
continue
|
||||
|
||||
module = sys.modules[mod]
|
||||
ModuleLoader._deep_reload(module, reloaded_modules_tracker)
|
||||
|
||||
reloaded_modules_tracker.add(mod)
|
||||
|
||||
@staticmethod
|
||||
def _is_user_defined_module(mod: str) -> bool:
|
||||
"""
|
||||
Returns whether the given module is user-defined or not.
|
||||
|
||||
A module is considered user-defined if
|
||||
- it is not part of the standard library
|
||||
- AND it is not an external library (site-packages or dist-packages)
|
||||
"""
|
||||
if mod not in sys.modules:
|
||||
return False
|
||||
|
||||
if mod in sys.builtin_module_names:
|
||||
return False
|
||||
|
||||
module = sys.modules[mod]
|
||||
module_path = getattr(module, "__file__", None)
|
||||
if module_path is None:
|
||||
return False
|
||||
module_path = os.path.abspath(module_path)
|
||||
|
||||
# External libraries (site-packages or dist-packages), e.g. numpy
|
||||
if "site-packages" in module_path or "dist-packages" in module_path:
|
||||
return False
|
||||
|
||||
# Standard lib
|
||||
standard_lib_path = sysconfig.get_path("stdlib")
|
||||
if module_path.startswith(standard_lib_path):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _deep_reload(module: Module, reloaded_modules_tracker: set[str]):
|
||||
"""
|
||||
Recursively reloads modules imported by the given module.
|
||||
|
||||
Only user-defined modules are reloaded, see `is_user_defined_module()`.
|
||||
"""
|
||||
ignore_manimlib_modules = manim_config.ignore_manimlib_modules_on_reload
|
||||
if ignore_manimlib_modules and module.__name__.startswith("manimlib"):
|
||||
return
|
||||
if module.__name__.startswith("manimlib.config"):
|
||||
# We don't want to reload global manim_config
|
||||
return
|
||||
|
||||
if not hasattr(module, "__dict__"):
|
||||
return
|
||||
|
||||
# Prevent reloading the same module multiple times
|
||||
if module.__name__ in reloaded_modules_tracker:
|
||||
return
|
||||
reloaded_modules_tracker.add(module.__name__)
|
||||
|
||||
# Recurse for all imported modules
|
||||
for _attr_name, attr_value in module.__dict__.items():
|
||||
if isinstance(attr_value, Module):
|
||||
if ModuleLoader._is_user_defined_module(attr_value.__name__):
|
||||
ModuleLoader._deep_reload(attr_value, reloaded_modules_tracker)
|
||||
|
||||
# Also reload modules that are part of a class or function
|
||||
# e.g. when importing `from custom_module import CustomClass`
|
||||
elif hasattr(attr_value, "__module__"):
|
||||
attr_module_name = attr_value.__module__
|
||||
if ModuleLoader._is_user_defined_module(attr_module_name):
|
||||
attr_module = sys.modules[attr_module_name]
|
||||
ModuleLoader._deep_reload(attr_module, reloaded_modules_tracker)
|
||||
|
||||
# Reload
|
||||
log.debug('Reloading module "%s"', module.__name__)
|
||||
importlib.reload(module)
|
||||
@@ -4,14 +4,14 @@ import itertools as it
|
||||
import numpy as np
|
||||
import pyperclip
|
||||
from IPython.core.getipython import get_ipython
|
||||
from pyglet.window import key as PygletWindowKeys
|
||||
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL
|
||||
from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
|
||||
from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF
|
||||
from manimlib.constants import PI
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
@@ -27,7 +27,6 @@ from manimlib.mobject.types.vectorized_mobject import VHighlight
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.scene.scene import SceneState
|
||||
from manimlib.scene.scene import PAN_3D_KEY
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.tex_file_writing import LatexError
|
||||
@@ -38,18 +37,27 @@ if TYPE_CHECKING:
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
|
||||
SELECT_KEY = 's'
|
||||
UNSELECT_KEY = 'u'
|
||||
GRAB_KEY = 'g'
|
||||
X_GRAB_KEY = 'h'
|
||||
Y_GRAB_KEY = 'v'
|
||||
SELECT_KEY = manim_config.key_bindings.select
|
||||
UNSELECT_KEY = manim_config.key_bindings.unselect
|
||||
GRAB_KEY = manim_config.key_bindings.grab
|
||||
X_GRAB_KEY = manim_config.key_bindings.x_grab
|
||||
Y_GRAB_KEY = manim_config.key_bindings.y_grab
|
||||
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
|
||||
RESIZE_KEY = 't'
|
||||
COLOR_KEY = 'c'
|
||||
INFORMATION_KEY = 'i'
|
||||
CURSOR_KEY = 'k'
|
||||
COPY_FRAME_POSITION_KEY = 'p'
|
||||
RESIZE_KEY = manim_config.key_bindings.resize # TODO
|
||||
COLOR_KEY = manim_config.key_bindings.color
|
||||
INFORMATION_KEY = manim_config.key_bindings.information
|
||||
CURSOR_KEY = manim_config.key_bindings.cursor
|
||||
|
||||
# For keyboard interactions
|
||||
|
||||
ARROW_SYMBOLS: list[int] = [
|
||||
PygletWindowKeys.LEFT,
|
||||
PygletWindowKeys.UP,
|
||||
PygletWindowKeys.RIGHT,
|
||||
PygletWindowKeys.DOWN,
|
||||
]
|
||||
|
||||
ALL_MODIFIERS = PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_SHIFT
|
||||
|
||||
# Note, a lot of the functionality here is still buggy and very much a work in progress.
|
||||
|
||||
@@ -237,6 +245,10 @@ class InteractiveScene(Scene):
|
||||
super().remove(*mobjects)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
def remove_all_except(self, *mobjects_to_keep : Mobject):
|
||||
super().remove_all_except(*mobjects_to_keep)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
# Related to selection
|
||||
|
||||
def toggle_selection_mode(self):
|
||||
@@ -460,59 +472,51 @@ class InteractiveScene(Scene):
|
||||
nudge *= 10
|
||||
self.selection.shift(nudge * vect)
|
||||
|
||||
def save_selection_to_file(self):
|
||||
if len(self.selection) == 1:
|
||||
self.save_mobject_to_file(self.selection[0])
|
||||
else:
|
||||
self.save_mobject_to_file(self.selection)
|
||||
|
||||
# Key actions
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
super().on_key_press(symbol, modifiers)
|
||||
char = chr(symbol)
|
||||
if char == SELECT_KEY and modifiers == 0:
|
||||
if char == SELECT_KEY and (modifiers & ALL_MODIFIERS) == 0:
|
||||
self.enable_selection()
|
||||
if char == UNSELECT_KEY:
|
||||
self.clear_selection()
|
||||
elif char in GRAB_KEYS and modifiers == 0:
|
||||
elif char in GRAB_KEYS and (modifiers & ALL_MODIFIERS) == 0:
|
||||
self.prepare_grab()
|
||||
elif char == RESIZE_KEY and modifiers in [0, SHIFT_MODIFIER]:
|
||||
self.prepare_resizing(about_corner=(modifiers == SHIFT_MODIFIER))
|
||||
elif symbol == SHIFT_SYMBOL:
|
||||
elif char == RESIZE_KEY and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||
self.prepare_resizing(about_corner=((modifiers & PygletWindowKeys.MOD_SHIFT) > 0))
|
||||
elif symbol == PygletWindowKeys.LSHIFT:
|
||||
if self.window.is_key_pressed(ord("t")):
|
||||
self.prepare_resizing(about_corner=True)
|
||||
elif char == COLOR_KEY and modifiers == 0:
|
||||
elif char == COLOR_KEY and (modifiers & ALL_MODIFIERS) == 0:
|
||||
self.toggle_color_palette()
|
||||
elif char == INFORMATION_KEY and modifiers == 0:
|
||||
elif char == INFORMATION_KEY and (modifiers & ALL_MODIFIERS) == 0:
|
||||
self.display_information()
|
||||
elif char == "c" and modifiers == COMMAND_MODIFIER:
|
||||
elif char == "c" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.copy_selection()
|
||||
elif char == "v" and modifiers == COMMAND_MODIFIER:
|
||||
elif char == "v" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.paste_selection()
|
||||
elif char == "x" and modifiers == COMMAND_MODIFIER:
|
||||
elif char == "x" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.copy_selection()
|
||||
self.delete_selection()
|
||||
elif symbol == DELETE_SYMBOL:
|
||||
elif symbol == PygletWindowKeys.BACKSPACE:
|
||||
self.delete_selection()
|
||||
elif char == "a" and modifiers == COMMAND_MODIFIER:
|
||||
elif char == "a" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.clear_selection()
|
||||
self.add_to_selection(*self.mobjects)
|
||||
elif char == "g" and modifiers == COMMAND_MODIFIER:
|
||||
elif char == "g" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.group_selection()
|
||||
elif char == "g" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
|
||||
elif char == "g" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_SHIFT)):
|
||||
self.ungroup_selection()
|
||||
elif char == "t" and modifiers == COMMAND_MODIFIER:
|
||||
elif char == "t" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.toggle_selection_mode()
|
||||
elif char == "s" and modifiers == COMMAND_MODIFIER:
|
||||
self.save_selection_to_file()
|
||||
elif char == "d" and modifiers == SHIFT_MODIFIER:
|
||||
elif char == "d" and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||
self.copy_frame_positioning()
|
||||
elif char == "c" and modifiers == SHIFT_MODIFIER:
|
||||
elif char == "c" and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||
self.copy_cursor_position()
|
||||
elif symbol in ARROW_SYMBOLS:
|
||||
self.nudge_selection(
|
||||
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
|
||||
large=(modifiers & SHIFT_MODIFIER),
|
||||
large=(modifiers & PygletWindowKeys.MOD_SHIFT),
|
||||
)
|
||||
# Adding crosshair
|
||||
if char == CURSOR_KEY:
|
||||
@@ -535,7 +539,7 @@ class InteractiveScene(Scene):
|
||||
self.is_grabbing = False
|
||||
elif chr(symbol) == INFORMATION_KEY:
|
||||
self.display_information(False)
|
||||
elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
elif symbol == PygletWindowKeys.LSHIFT and self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
self.prepare_resizing(about_corner=False)
|
||||
|
||||
# Mouse actions
|
||||
@@ -552,7 +556,7 @@ class InteractiveScene(Scene):
|
||||
if not hasattr(self, "scale_about_point"):
|
||||
return
|
||||
vect = point - self.scale_about_point
|
||||
if self.window.is_key_pressed(CTRL_SYMBOL):
|
||||
if self.window.is_key_pressed(PygletWindowKeys.LCTRL):
|
||||
for i in (0, 1):
|
||||
scalar = vect[i] / self.scale_ref_vect[i]
|
||||
self.selection.rescale_to_fit(
|
||||
@@ -597,7 +601,7 @@ class InteractiveScene(Scene):
|
||||
self.handle_grabbing(point)
|
||||
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
self.handle_resizing(point)
|
||||
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL):
|
||||
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(PygletWindowKeys.LSHIFT):
|
||||
self.handle_sweeping_selection(point)
|
||||
|
||||
def on_mouse_drag(
|
||||
@@ -625,7 +629,7 @@ class InteractiveScene(Scene):
|
||||
angles = frame.get_euler_angles()
|
||||
|
||||
call = f"reorient("
|
||||
theta, phi, gamma = (angles / DEGREES).astype(int)
|
||||
theta, phi, gamma = (angles / DEG).astype(int)
|
||||
call += f"{theta}, {phi}, {gamma}"
|
||||
if any(center != 0):
|
||||
call += f", {tuple(np.round(center, 2))}"
|
||||
|
||||
@@ -1,51 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
import pyperclip
|
||||
import random
|
||||
import time
|
||||
import re
|
||||
from functools import wraps
|
||||
|
||||
from IPython.terminal import pt_inputhooks
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
from IPython.core.getipython import get_ipython
|
||||
from contextlib import contextmanager
|
||||
from contextlib import ExitStack
|
||||
|
||||
import numpy as np
|
||||
from tqdm.auto import tqdm as ProgressDisplay
|
||||
from pyglet.window import key as PygletWindowKeys
|
||||
|
||||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.animation.fading import VFadeInThenOut
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.camera.camera_frame import CameraFrame
|
||||
from manimlib.config import get_module
|
||||
from manimlib.constants import ARROW_SYMBOLS
|
||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||
from manimlib.constants import COMMAND_MODIFIER
|
||||
from manimlib.constants import SHIFT_MODIFIER
|
||||
from manimlib.constants import RED
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.event_handler import EVENT_DISPATCHER
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.frame import FullScreenRectangle
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.scene.scene_embed import InteractiveSceneEmbed
|
||||
from manimlib.scene.scene_embed import CheckpointManager
|
||||
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
from manimlib.utils.family_ops import recursive_mobject_remove
|
||||
from manimlib.utils.iterables import batch_by_property
|
||||
from manimlib.utils.sounds import play_sound
|
||||
from manimlib.utils.color import color_to_rgba
|
||||
from manimlib.window import Window
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, TypeVar
|
||||
from typing import Callable, Iterable, TypeVar, Optional
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
T = TypeVar('T')
|
||||
@@ -55,12 +49,6 @@ if TYPE_CHECKING:
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
|
||||
PAN_3D_KEY = 'd'
|
||||
FRAME_SHIFT_KEY = 'f'
|
||||
RESET_FRAME_KEY = 'r'
|
||||
QUIT_KEY = 'q'
|
||||
|
||||
|
||||
class Scene(object):
|
||||
random_seed: int = 0
|
||||
pan_sensitivity: float = 0.5
|
||||
@@ -68,7 +56,6 @@ class Scene(object):
|
||||
drag_to_pan: bool = True
|
||||
max_num_saved_states: int = 50
|
||||
default_camera_config: dict = dict()
|
||||
default_window_config: dict = dict()
|
||||
default_file_writer_config: dict = dict()
|
||||
samples = 0
|
||||
# Euler angles, in degrees
|
||||
@@ -76,48 +63,52 @@ class Scene(object):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
window_config: dict = dict(),
|
||||
window: Optional[Window] = None,
|
||||
camera_config: dict = dict(),
|
||||
file_writer_config: dict = dict(),
|
||||
skip_animations: bool = False,
|
||||
always_update_mobjects: bool = False,
|
||||
start_at_animation_number: int | None = None,
|
||||
end_at_animation_number: int | None = None,
|
||||
leave_progress_bars: bool = False,
|
||||
preview: bool = True,
|
||||
presenter_mode: bool = False,
|
||||
show_animation_progress: bool = False,
|
||||
embed_exception_mode: str = "",
|
||||
embed_error_sound: bool = False,
|
||||
leave_progress_bars: bool = False,
|
||||
preview_while_skipping: bool = True,
|
||||
presenter_mode: bool = False,
|
||||
default_wait_time: float = 1.0,
|
||||
):
|
||||
self.skip_animations = skip_animations
|
||||
self.always_update_mobjects = always_update_mobjects
|
||||
self.start_at_animation_number = start_at_animation_number
|
||||
self.end_at_animation_number = end_at_animation_number
|
||||
self.leave_progress_bars = leave_progress_bars
|
||||
self.preview = preview
|
||||
self.presenter_mode = presenter_mode
|
||||
self.show_animation_progress = show_animation_progress
|
||||
self.embed_exception_mode = embed_exception_mode
|
||||
self.embed_error_sound = embed_error_sound
|
||||
self.leave_progress_bars = leave_progress_bars
|
||||
self.preview_while_skipping = preview_while_skipping
|
||||
self.presenter_mode = presenter_mode
|
||||
self.default_wait_time = default_wait_time
|
||||
|
||||
self.camera_config = {**self.default_camera_config, **camera_config}
|
||||
self.window_config = {**self.default_window_config, **window_config}
|
||||
for config in self.camera_config, self.window_config:
|
||||
config["samples"] = self.samples
|
||||
self.file_writer_config = {**self.default_file_writer_config, **file_writer_config}
|
||||
self.camera_config = merge_dicts_recursively(
|
||||
manim_config.camera, # Global default
|
||||
self.default_camera_config, # Updated configuration that subclasses may specify
|
||||
camera_config, # Updated configuration from instantiation
|
||||
)
|
||||
self.file_writer_config = merge_dicts_recursively(
|
||||
manim_config.file_writer,
|
||||
self.default_file_writer_config,
|
||||
file_writer_config,
|
||||
)
|
||||
|
||||
# Initialize window, if applicable
|
||||
if self.preview:
|
||||
from manimlib.window import Window
|
||||
self.window = Window(scene=self, **self.window_config)
|
||||
self.camera_config["window"] = self.window
|
||||
self.camera_config["fps"] = 30 # Where's that 30 from?
|
||||
else:
|
||||
self.window = None
|
||||
self.window = window
|
||||
if self.window:
|
||||
self.window.init_for_scene(self)
|
||||
# Make sure camera and Pyglet window sync
|
||||
self.camera_config["fps"] = 30
|
||||
|
||||
# Core state of the scene
|
||||
self.camera: Camera = Camera(**self.camera_config)
|
||||
self.camera: Camera = Camera(
|
||||
window=self.window,
|
||||
samples=self.samples,
|
||||
**self.camera_config
|
||||
)
|
||||
self.frame: CameraFrame = self.camera.frame
|
||||
self.frame.reorient(*self.default_frame_orientation)
|
||||
self.frame.make_orientation_default()
|
||||
@@ -130,7 +121,6 @@ class Scene(object):
|
||||
self.time: float = 0
|
||||
self.skip_time: float = 0
|
||||
self.original_skipping_status: bool = self.skip_animations
|
||||
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
|
||||
self.undo_stack = []
|
||||
self.redo_stack = []
|
||||
|
||||
@@ -153,6 +143,9 @@ class Scene(object):
|
||||
def __str__(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
def get_window(self) -> Window | None:
|
||||
return self.window
|
||||
|
||||
def run(self) -> None:
|
||||
self.virtual_animation_start_time: float = 0
|
||||
self.real_animation_start_time: float = time.time()
|
||||
@@ -212,76 +205,14 @@ class Scene(object):
|
||||
close_scene_on_exit: bool = True,
|
||||
show_animation_progress: bool = False,
|
||||
) -> None:
|
||||
if not self.preview:
|
||||
# Embed is only relevant with a preview
|
||||
if not self.window:
|
||||
# Embed is only relevant for interactive development with a Window
|
||||
return
|
||||
self.show_animation_progress = show_animation_progress
|
||||
self.stop_skipping()
|
||||
self.update_frame(force_draw=True)
|
||||
self.save_state()
|
||||
self.show_animation_progress = show_animation_progress
|
||||
|
||||
# Create embedded IPython terminal configured to have access to
|
||||
# the local namespace of the caller
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
module = get_module(caller_frame.f_globals["__file__"])
|
||||
shell = InteractiveShellEmbed(user_module=module)
|
||||
|
||||
# Add a few custom shortcuts to that local namespace
|
||||
local_ns = dict(caller_frame.f_locals)
|
||||
local_ns.update(
|
||||
play=self.play,
|
||||
wait=self.wait,
|
||||
add=self.add,
|
||||
remove=self.remove,
|
||||
clear=self.clear,
|
||||
save_state=self.save_state,
|
||||
undo=self.undo,
|
||||
redo=self.redo,
|
||||
i2g=self.i2g,
|
||||
i2m=self.i2m,
|
||||
checkpoint_paste=self.checkpoint_paste,
|
||||
touch=lambda: shell.enable_gui("manim"),
|
||||
notouch=lambda: shell.enable_gui(None),
|
||||
)
|
||||
|
||||
# Update the shell module with the caller's locals + shortcuts
|
||||
module.__dict__.update(local_ns)
|
||||
|
||||
# Enables gui interactions during the embed
|
||||
def inputhook(context):
|
||||
while not context.input_is_ready():
|
||||
if not self.is_window_closing():
|
||||
self.update_frame(dt=0)
|
||||
if self.is_window_closing():
|
||||
shell.ask_exit()
|
||||
|
||||
pt_inputhooks.register("manim", inputhook)
|
||||
shell.enable_gui("manim")
|
||||
|
||||
# Operation to run after each ipython command
|
||||
def post_cell_func(*args, **kwargs):
|
||||
if not self.is_window_closing():
|
||||
self.update_frame(dt=0, force_draw=True)
|
||||
|
||||
shell.events.register("post_run_cell", post_cell_func)
|
||||
|
||||
# Flash border, and potentially play sound, on exceptions
|
||||
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
|
||||
# Show the error don't just swallow it
|
||||
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
|
||||
if self.embed_error_sound:
|
||||
os.system("printf '\a'")
|
||||
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
|
||||
rect.fix_in_frame()
|
||||
self.play(VFadeInThenOut(rect, run_time=0.5))
|
||||
|
||||
shell.set_custom_exc((Exception,), custom_exc)
|
||||
|
||||
# Set desired exception mode
|
||||
shell.magic(f"xmode {self.embed_exception_mode}")
|
||||
|
||||
# Launch shell
|
||||
shell()
|
||||
InteractiveSceneEmbed(self).launch()
|
||||
|
||||
# End scene when exiting an embed
|
||||
if close_scene_on_exit:
|
||||
@@ -374,7 +305,7 @@ class Scene(object):
|
||||
"""
|
||||
batches = batch_by_property(
|
||||
self.mobjects,
|
||||
lambda m: str(type(m)) + str(m.get_shader_wrapper(self.camera.ctx).get_id())
|
||||
lambda m: str(type(m)) + str(m.get_shader_wrapper(self.camera.ctx).get_id()) + str(m.z_index)
|
||||
)
|
||||
|
||||
for group in self.render_groups:
|
||||
@@ -450,6 +381,11 @@ class Scene(object):
|
||||
new_mobjects, _ = recursive_mobject_remove(self.mobjects, to_remove)
|
||||
self.mobjects = new_mobjects
|
||||
|
||||
@affects_mobject_list
|
||||
def remove_all_except(self, *mobjects_to_keep : Mobject):
|
||||
self.clear()
|
||||
self.add(*mobjects_to_keep)
|
||||
|
||||
def bring_to_front(self, *mobjects: Mobject):
|
||||
self.add(*mobjects)
|
||||
return self
|
||||
@@ -597,13 +533,14 @@ class Scene(object):
|
||||
if not self.skip_animations:
|
||||
self.file_writer.end_animation()
|
||||
|
||||
if self.skip_animations and self.window is not None:
|
||||
if self.preview_while_skipping and self.skip_animations and self.window is not None:
|
||||
# Show some quick frames along the way
|
||||
self.update_frame(dt=0, force_draw=True)
|
||||
|
||||
self.num_plays += 1
|
||||
|
||||
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
||||
all_mobjects = set(self.get_mobject_family_members())
|
||||
for animation in animations:
|
||||
animation.begin()
|
||||
# Anything animated that's not already in the
|
||||
@@ -611,8 +548,9 @@ class Scene(object):
|
||||
# animated mobjects that are in the family of
|
||||
# those on screen, this can result in a restructuring
|
||||
# of the scene.mobjects list, which is usually desired.
|
||||
if animation.mobject not in self.mobjects:
|
||||
if animation.mobject not in all_mobjects:
|
||||
self.add(animation.mobject)
|
||||
all_mobjects = all_mobjects.union(animation.mobject.get_family())
|
||||
|
||||
def progress_through_animations(self, animations: Iterable[Animation]) -> None:
|
||||
last_t = 0
|
||||
@@ -657,11 +595,13 @@ class Scene(object):
|
||||
|
||||
def wait(
|
||||
self,
|
||||
duration: float = DEFAULT_WAIT_TIME,
|
||||
duration: Optional[float] = None,
|
||||
stop_condition: Callable[[], bool] = None,
|
||||
note: str = None,
|
||||
ignore_presenter_mode: bool = False
|
||||
):
|
||||
if duration is None:
|
||||
duration = self.default_wait_time
|
||||
self.pre_play()
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
||||
@@ -724,8 +664,6 @@ class Scene(object):
|
||||
scene_state.restore_scene(self)
|
||||
|
||||
def save_state(self) -> None:
|
||||
if not self.preview:
|
||||
return
|
||||
state = self.get_state()
|
||||
if self.undo_stack and state.mobjects_match(self.undo_stack[-1]):
|
||||
return
|
||||
@@ -744,102 +682,44 @@ class Scene(object):
|
||||
self.undo_stack.append(self.get_state())
|
||||
self.restore_state(self.redo_stack.pop())
|
||||
|
||||
def checkpoint_paste(
|
||||
self,
|
||||
skip: bool = False,
|
||||
record: bool = False,
|
||||
progress_bar: bool = True
|
||||
):
|
||||
"""
|
||||
Used during interactive development to run (or re-run)
|
||||
a block of scene code.
|
||||
|
||||
If the copied selection starts with a comment, this will
|
||||
revert to the state of the scene the first time this function
|
||||
was called on a block of code starting with that comment.
|
||||
"""
|
||||
shell = get_ipython()
|
||||
if shell is None or self.window is None:
|
||||
raise Exception(
|
||||
"Scene.checkpoint_paste cannot be called outside of " +
|
||||
"an ipython shell"
|
||||
)
|
||||
|
||||
pasted = pyperclip.paste()
|
||||
lines = pasted.split("\n")
|
||||
|
||||
# Commented lines trigger saved checkpoints
|
||||
if lines[0].lstrip().startswith("#"):
|
||||
if lines[0] not in self.checkpoint_states:
|
||||
self.checkpoint(lines[0])
|
||||
else:
|
||||
self.revert_to_checkpoint(lines[0])
|
||||
|
||||
# Copied methods of a scene are handled specially
|
||||
# A bit hacky, yes, but convenient
|
||||
method_pattern = r"^def\s+([a-zA-Z_]\w*)\s*\(self.*\):"
|
||||
method_names = re.findall(method_pattern ,lines[0].strip())
|
||||
if method_names:
|
||||
method_name = method_names[0]
|
||||
indent = " " * lines[0].index(lines[0].strip())
|
||||
pasted = "\n".join([
|
||||
# Remove self from function signature
|
||||
re.sub(r"self(,\s*)?", "", lines[0]),
|
||||
*lines[1:],
|
||||
# Attach to scene via self.func_name = func_name
|
||||
f"{indent}self.{method_name} = {method_name}"
|
||||
])
|
||||
|
||||
# Keep track of skipping and progress bar status
|
||||
self.skip_animations = skip
|
||||
@contextmanager
|
||||
def temp_skip(self):
|
||||
prev_status = self.skip_animations
|
||||
self.skip_animations = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if not prev_status:
|
||||
self.stop_skipping()
|
||||
|
||||
@contextmanager
|
||||
def temp_progress_bar(self):
|
||||
prev_progress = self.show_animation_progress
|
||||
self.show_animation_progress = progress_bar
|
||||
self.show_animation_progress = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.show_animation_progress = prev_progress
|
||||
|
||||
if record:
|
||||
self.camera.use_window_fbo(False)
|
||||
self.file_writer.begin_insert()
|
||||
|
||||
shell.run_cell(pasted)
|
||||
|
||||
if record:
|
||||
@contextmanager
|
||||
def temp_record(self):
|
||||
self.camera.use_window_fbo(False)
|
||||
self.file_writer.begin_insert()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.file_writer.end_insert()
|
||||
self.camera.use_window_fbo(True)
|
||||
|
||||
self.stop_skipping()
|
||||
self.show_animation_progress = prev_progress
|
||||
|
||||
def checkpoint(self, key: str):
|
||||
self.checkpoint_states[key] = self.get_state()
|
||||
|
||||
def revert_to_checkpoint(self, key: str):
|
||||
if key not in self.checkpoint_states:
|
||||
log.error(f"No checkpoint at {key}")
|
||||
return
|
||||
all_keys = list(self.checkpoint_states.keys())
|
||||
index = all_keys.index(key)
|
||||
for later_key in all_keys[index + 1:]:
|
||||
self.checkpoint_states.pop(later_key)
|
||||
|
||||
self.restore_state(self.checkpoint_states[key])
|
||||
|
||||
def clear_checkpoints(self):
|
||||
self.checkpoint_states = dict()
|
||||
|
||||
def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None:
|
||||
if file_path is None:
|
||||
file_path = self.file_writer.get_saved_mobject_path(mobject)
|
||||
if file_path is None:
|
||||
return
|
||||
mobject.save_to_file(file_path)
|
||||
|
||||
def load_mobject(self, file_name):
|
||||
if os.path.exists(file_name):
|
||||
path = file_name
|
||||
else:
|
||||
directory = self.file_writer.get_saved_mobject_directory()
|
||||
path = os.path.join(directory, file_name)
|
||||
return Mobject.load(path)
|
||||
def temp_config_change(self, skip=False, record=False, progress_bar=False):
|
||||
stack = ExitStack()
|
||||
if skip:
|
||||
stack.enter_context(self.temp_skip())
|
||||
if record:
|
||||
stack.enter_context(self.temp_record())
|
||||
if progress_bar:
|
||||
stack.enter_context(self.temp_progress_bar())
|
||||
return stack
|
||||
|
||||
def is_window_closing(self):
|
||||
return self.window and (self.window.is_closing or self.quit_interaction)
|
||||
@@ -868,13 +748,13 @@ class Scene(object):
|
||||
|
||||
frame = self.camera.frame
|
||||
# Handle perspective changes
|
||||
if self.window.is_key_pressed(ord(PAN_3D_KEY)):
|
||||
if self.window.is_key_pressed(ord(manim_config.key_bindings.pan_3d)):
|
||||
ff_d_point = frame.to_fixed_frame_point(d_point, relative=True)
|
||||
ff_d_point *= self.pan_sensitivity
|
||||
frame.increment_theta(-ff_d_point[0])
|
||||
frame.increment_phi(ff_d_point[1])
|
||||
# Handle frame movements
|
||||
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
|
||||
elif self.window.is_key_pressed(ord(manim_config.key_bindings.pan)):
|
||||
frame.shift(-d_point)
|
||||
|
||||
def on_mouse_drag(
|
||||
@@ -960,17 +840,17 @@ class Scene(object):
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
if char == RESET_FRAME_KEY:
|
||||
if char == manim_config.key_bindings.reset:
|
||||
self.play(self.camera.frame.animate.to_default_state())
|
||||
elif char == "z" and modifiers == COMMAND_MODIFIER:
|
||||
elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.undo()
|
||||
elif char == "z" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
|
||||
elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_SHIFT)):
|
||||
self.redo()
|
||||
# command + q
|
||||
elif char == QUIT_KEY and modifiers == COMMAND_MODIFIER:
|
||||
elif char == manim_config.key_bindings.quit and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.quit_interaction = True
|
||||
# Space or right arrow
|
||||
elif char == " " or symbol == ARROW_SYMBOLS[2]:
|
||||
elif char == " " or symbol == PygletWindowKeys.RIGHT:
|
||||
self.hold_on_wait = False
|
||||
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
@@ -985,6 +865,19 @@ class Scene(object):
|
||||
def on_close(self) -> None:
|
||||
pass
|
||||
|
||||
def focus(self) -> None:
|
||||
"""
|
||||
Puts focus on the ManimGL window.
|
||||
"""
|
||||
if not self.window:
|
||||
return
|
||||
self.window.focus()
|
||||
|
||||
def set_background_color(self, background_color, background_opacity=1) -> None:
|
||||
self.camera.background_rgba = list(color_to_rgba(
|
||||
background_color, background_opacity
|
||||
))
|
||||
|
||||
|
||||
class SceneState():
|
||||
def __init__(self, scene: Scene, ignore: list[Mobject] | None = None):
|
||||
|
||||
236
manimlib/scene/scene_embed.py
Normal file
236
manimlib/scene/scene_embed.py
Normal file
@@ -0,0 +1,236 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import pyperclip
|
||||
import traceback
|
||||
|
||||
from IPython.terminal import pt_inputhooks
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
|
||||
from manimlib.animation.fading import VFadeInThenOut
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.constants import RED
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.frame import FullScreenRectangle
|
||||
from manimlib.module_loader import ModuleLoader
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class InteractiveSceneEmbed:
|
||||
def __init__(self, scene: Scene):
|
||||
self.scene = scene
|
||||
self.checkpoint_manager = CheckpointManager()
|
||||
|
||||
self.shell = self.get_ipython_shell_for_embedded_scene()
|
||||
self.enable_gui()
|
||||
self.ensure_frame_update_post_cell()
|
||||
self.ensure_flash_on_error()
|
||||
if manim_config.embed.autoreload:
|
||||
self.auto_reload()
|
||||
|
||||
def launch(self):
|
||||
self.shell()
|
||||
|
||||
def get_ipython_shell_for_embedded_scene(self) -> InteractiveShellEmbed:
|
||||
"""
|
||||
Create embedded IPython terminal configured to have access to
|
||||
the local namespace of the caller
|
||||
"""
|
||||
# Triple back should take us to the context in a user's scene definition
|
||||
# which is calling "self.embed"
|
||||
caller_frame = inspect.currentframe().f_back.f_back.f_back
|
||||
|
||||
# Update the module's namespace to include local variables
|
||||
module = ModuleLoader.get_module(caller_frame.f_globals["__file__"])
|
||||
module.__dict__.update(caller_frame.f_locals)
|
||||
module.__dict__.update(self.get_shortcuts())
|
||||
exception_mode = manim_config.embed.exception_mode
|
||||
|
||||
return InteractiveShellEmbed(
|
||||
user_module=module,
|
||||
display_banner=False,
|
||||
xmode=exception_mode
|
||||
)
|
||||
|
||||
def get_shortcuts(self):
|
||||
"""
|
||||
A few custom shortcuts useful to have in the interactive shell namespace
|
||||
"""
|
||||
scene = self.scene
|
||||
return dict(
|
||||
play=scene.play,
|
||||
wait=scene.wait,
|
||||
add=scene.add,
|
||||
remove=scene.remove,
|
||||
clear=scene.clear,
|
||||
focus=scene.focus,
|
||||
save_state=scene.save_state,
|
||||
undo=scene.undo,
|
||||
redo=scene.redo,
|
||||
i2g=scene.i2g,
|
||||
i2m=scene.i2m,
|
||||
checkpoint_paste=self.checkpoint_paste,
|
||||
clear_checkpoints=self.checkpoint_manager.clear_checkpoints,
|
||||
reload=self.reload_scene # Defined below
|
||||
)
|
||||
|
||||
def enable_gui(self):
|
||||
"""Enables gui interactions during the embed"""
|
||||
def inputhook(context):
|
||||
while not context.input_is_ready():
|
||||
if not self.scene.is_window_closing():
|
||||
self.scene.update_frame(dt=0)
|
||||
if self.scene.is_window_closing():
|
||||
self.shell.ask_exit()
|
||||
|
||||
pt_inputhooks.register("manim", inputhook)
|
||||
self.shell.enable_gui("manim")
|
||||
|
||||
def ensure_frame_update_post_cell(self):
|
||||
"""Ensure the scene updates its frame after each ipython cell"""
|
||||
def post_cell_func(*args, **kwargs):
|
||||
if not self.scene.is_window_closing():
|
||||
self.scene.update_frame(dt=0, force_draw=True)
|
||||
|
||||
self.shell.events.register("post_run_cell", post_cell_func)
|
||||
|
||||
def ensure_flash_on_error(self):
|
||||
"""Flash border, and potentially play sound, on exceptions"""
|
||||
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
|
||||
# Show the error don't just swallow it
|
||||
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
|
||||
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
|
||||
rect.fix_in_frame()
|
||||
self.scene.play(VFadeInThenOut(rect, run_time=0.5))
|
||||
|
||||
self.shell.set_custom_exc((Exception,), custom_exc)
|
||||
|
||||
def validate_syntax(self, file_path: str) -> bool:
|
||||
"""
|
||||
Validates the syntax of a Python file without executing it.
|
||||
Returns True if syntax is valid, False otherwise.
|
||||
Prints syntax errors to the console if found.
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
source_code = f.read()
|
||||
|
||||
# Use compile() to check for syntax errors without executing
|
||||
compile(source_code, file_path, 'exec')
|
||||
return True
|
||||
|
||||
except SyntaxError as e:
|
||||
print(f"\nSyntax Error in {file_path}:")
|
||||
print(f" Line {e.lineno}: {e.text.strip() if e.text else ''}")
|
||||
print(f" {' ' * (e.offset - 1 if e.offset else 0)}^")
|
||||
print(f" {e.msg}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError reading {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def reload_scene(self, embed_line: int | None = None) -> None:
|
||||
"""
|
||||
Reloads the scene just like the `manimgl` command would do with the
|
||||
same arguments that were provided for the initial startup. This allows
|
||||
for quick iteration during scene development since we don't have to exit
|
||||
the IPython kernel and re-run the `manimgl` command again. The GUI stays
|
||||
open during the reload.
|
||||
|
||||
If `embed_line` is provided, the scene will be reloaded at that line
|
||||
number. This corresponds to the `linemarker` param of the
|
||||
`extract_scene.insert_embed_line_to_module()` method.
|
||||
|
||||
Before reload, the scene is cleared and the entire state is reset, such
|
||||
that we can start from a clean slate. This is taken care of by the
|
||||
run_scenes function in __main__.py, which will catch the error raised by the
|
||||
`exit_raise` magic command that we invoke here.
|
||||
|
||||
Note that we cannot define a custom exception class for this error,
|
||||
since the IPython kernel will swallow any exception. While we can catch
|
||||
such an exception in our custom exception handler registered with the
|
||||
`set_custom_exc` method, we cannot break out of the IPython shell by
|
||||
this means.
|
||||
"""
|
||||
# Get the current file path for syntax validation
|
||||
current_file = self.shell.user_module.__file__
|
||||
|
||||
# Validate syntax before attempting reload
|
||||
if not self.validate_syntax(current_file):
|
||||
print("[ERROR] Reload cancelled due to syntax errors. Fix the errors and try again.")
|
||||
return
|
||||
|
||||
# Update the global run configuration.
|
||||
run_config = manim_config.run
|
||||
run_config.is_reload = True
|
||||
if embed_line:
|
||||
run_config.embed_line = embed_line
|
||||
|
||||
print("Reloading...")
|
||||
self.shell.run_line_magic("exit_raise", "")
|
||||
|
||||
def auto_reload(self):
|
||||
"""Enables reload the shell's module before all calls"""
|
||||
def pre_cell_func(*args, **kwargs):
|
||||
new_mod = ModuleLoader.get_module(self.shell.user_module.__file__, is_during_reload=True)
|
||||
self.shell.user_ns.update(vars(new_mod))
|
||||
|
||||
self.shell.events.register("pre_run_cell", pre_cell_func)
|
||||
|
||||
def checkpoint_paste(
|
||||
self,
|
||||
skip: bool = False,
|
||||
record: bool = False,
|
||||
progress_bar: bool = True
|
||||
):
|
||||
with self.scene.temp_config_change(skip, record, progress_bar):
|
||||
self.checkpoint_manager.checkpoint_paste(self.shell, self.scene)
|
||||
|
||||
|
||||
class CheckpointManager:
|
||||
def __init__(self):
|
||||
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
|
||||
|
||||
def checkpoint_paste(self, shell, scene):
|
||||
"""
|
||||
Used during interactive development to run (or re-run)
|
||||
a block of scene code.
|
||||
|
||||
If the copied selection starts with a comment, this will
|
||||
revert to the state of the scene the first time this function
|
||||
was called on a block of code starting with that comment.
|
||||
"""
|
||||
code_string = pyperclip.paste()
|
||||
checkpoint_key = self.get_leading_comment(code_string)
|
||||
self.handle_checkpoint_key(scene, checkpoint_key)
|
||||
shell.run_cell(code_string)
|
||||
|
||||
@staticmethod
|
||||
def get_leading_comment(code_string: str) -> str:
|
||||
leading_line = code_string.partition("\n")[0].lstrip()
|
||||
if leading_line.startswith("#"):
|
||||
return leading_line
|
||||
return ""
|
||||
|
||||
def handle_checkpoint_key(self, scene, key: str):
|
||||
if not key:
|
||||
return
|
||||
elif key in self.checkpoint_states:
|
||||
# Revert to checkpoint
|
||||
scene.restore_state(self.checkpoint_states[key])
|
||||
|
||||
# Clear out any saved states that show up later
|
||||
all_keys = list(self.checkpoint_states.keys())
|
||||
index = all_keys.index(key)
|
||||
for later_key in all_keys[index + 1:]:
|
||||
self.checkpoint_states.pop(later_key)
|
||||
else:
|
||||
self.checkpoint_states[key] = scene.get_state()
|
||||
|
||||
def clear_checkpoints(self):
|
||||
self.checkpoint_states = dict()
|
||||
@@ -11,11 +11,8 @@ from pydub import AudioSegment
|
||||
from tqdm.auto import tqdm as ProgressDisplay
|
||||
from pathlib import Path
|
||||
|
||||
from manimlib.constants import FFMPEG_BIN
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.file_ops import add_extension_if_not_present
|
||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
from manimlib.utils.file_ops import guarantee_existence
|
||||
from manimlib.utils.sounds import get_full_sound_file_path
|
||||
|
||||
@@ -33,22 +30,20 @@ class SceneFileWriter(object):
|
||||
self,
|
||||
scene: Scene,
|
||||
write_to_movie: bool = False,
|
||||
break_into_partial_movies: bool = False,
|
||||
save_pngs: bool = False, # TODO, this currently does nothing
|
||||
subdivide_output: bool = False,
|
||||
png_mode: str = "RGBA",
|
||||
save_last_frame: bool = False,
|
||||
movie_file_extension: str = ".mp4",
|
||||
# What python file is generating this scene
|
||||
input_file_path: str = "",
|
||||
# Where should this be written
|
||||
output_directory: str | None = None,
|
||||
output_directory: str = ".",
|
||||
file_name: str | None = None,
|
||||
subdirectory_for_videos: bool = False,
|
||||
open_file_upon_completion: bool = False,
|
||||
show_file_location_upon_completion: bool = False,
|
||||
quiet: bool = False,
|
||||
total_frames: int = 0,
|
||||
progress_description_len: int = 40,
|
||||
# Name of the binary used for ffmpeg
|
||||
ffmpeg_bin: str = "ffmpeg",
|
||||
video_codec: str = "libx264",
|
||||
pixel_format: str = "yuv420p",
|
||||
saturation: float = 1.0,
|
||||
@@ -56,20 +51,18 @@ class SceneFileWriter(object):
|
||||
):
|
||||
self.scene: Scene = scene
|
||||
self.write_to_movie = write_to_movie
|
||||
self.break_into_partial_movies = break_into_partial_movies
|
||||
self.save_pngs = save_pngs
|
||||
self.subdivide_output = subdivide_output
|
||||
self.png_mode = png_mode
|
||||
self.save_last_frame = save_last_frame
|
||||
self.movie_file_extension = movie_file_extension
|
||||
self.input_file_path = input_file_path
|
||||
self.output_directory = output_directory
|
||||
self.file_name = file_name
|
||||
self.open_file_upon_completion = open_file_upon_completion
|
||||
self.subdirectory_for_videos = subdirectory_for_videos
|
||||
self.show_file_location_upon_completion = show_file_location_upon_completion
|
||||
self.quiet = quiet
|
||||
self.total_frames = total_frames
|
||||
self.progress_description_len = progress_description_len
|
||||
self.ffmpeg_bin = ffmpeg_bin
|
||||
self.video_codec = video_codec
|
||||
self.pixel_format = pixel_format
|
||||
self.saturation = saturation
|
||||
@@ -79,40 +72,39 @@ class SceneFileWriter(object):
|
||||
self.writing_process: sp.Popen | None = None
|
||||
self.progress_display: ProgressDisplay | None = None
|
||||
self.ended_with_interrupt: bool = False
|
||||
|
||||
self.init_output_directories()
|
||||
self.init_audio()
|
||||
|
||||
# Output directories and files
|
||||
def init_output_directories(self) -> None:
|
||||
out_dir = self.output_directory or ""
|
||||
scene_name = self.file_name or self.get_default_scene_name()
|
||||
if self.save_last_frame:
|
||||
image_dir = guarantee_existence(os.path.join(out_dir, "images"))
|
||||
image_file = add_extension_if_not_present(scene_name, ".png")
|
||||
self.image_file_path = os.path.join(image_dir, image_file)
|
||||
self.image_file_path = self.init_image_file_path()
|
||||
if self.write_to_movie:
|
||||
if self.subdirectory_for_videos:
|
||||
movie_dir = guarantee_existence(os.path.join(out_dir, "videos"))
|
||||
else:
|
||||
movie_dir = guarantee_existence(out_dir)
|
||||
movie_file = add_extension_if_not_present(scene_name, self.movie_file_extension)
|
||||
self.movie_file_path = os.path.join(movie_dir, movie_file)
|
||||
if self.break_into_partial_movies:
|
||||
self.partial_movie_directory = guarantee_existence(os.path.join(
|
||||
movie_dir, "partial_movie_files", scene_name,
|
||||
))
|
||||
# A place to save mobjects
|
||||
self.saved_mobject_directory = os.path.join(
|
||||
out_dir, "mobjects", str(self.scene)
|
||||
self.movie_file_path = self.init_movie_file_path()
|
||||
if self.subdivide_output:
|
||||
self.partial_movie_directory = self.init_partial_movie_directory()
|
||||
|
||||
def init_image_file_path(self) -> Path:
|
||||
return self.get_output_file_rootname().with_suffix(".png")
|
||||
|
||||
def init_movie_file_path(self) -> Path:
|
||||
return self.get_output_file_rootname().with_suffix(self.movie_file_extension)
|
||||
|
||||
def init_partial_movie_directory(self):
|
||||
return guarantee_existence(self.get_output_file_rootname())
|
||||
|
||||
def get_output_file_rootname(self) -> Path:
|
||||
return Path(
|
||||
guarantee_existence(self.output_directory),
|
||||
self.get_output_file_name()
|
||||
)
|
||||
|
||||
def get_default_module_directory(self) -> str:
|
||||
path, _ = os.path.splitext(self.input_file_path)
|
||||
if path.startswith("_"):
|
||||
path = path[1:]
|
||||
return path
|
||||
|
||||
def get_default_scene_name(self) -> str:
|
||||
def get_output_file_name(self) -> str:
|
||||
if self.file_name:
|
||||
return self.file_name
|
||||
# Otherwise, use the name of the scene, potentially
|
||||
# appending animation numbers
|
||||
name = str(self.scene)
|
||||
saan = self.scene.start_at_animation_number
|
||||
eaan = self.scene.end_at_animation_number
|
||||
@@ -122,63 +114,17 @@ class SceneFileWriter(object):
|
||||
name += f"_{eaan}"
|
||||
return name
|
||||
|
||||
def get_resolution_directory(self) -> str:
|
||||
pixel_height = self.scene.camera.pixel_height
|
||||
fps = self.scene.camera.fps
|
||||
return "{}p{}".format(
|
||||
pixel_height, fps
|
||||
)
|
||||
|
||||
# Directory getters
|
||||
def get_image_file_path(self) -> str:
|
||||
return self.image_file_path
|
||||
|
||||
def get_next_partial_movie_path(self) -> str:
|
||||
result = os.path.join(
|
||||
self.partial_movie_directory,
|
||||
"{:05}{}".format(
|
||||
self.scene.num_plays,
|
||||
self.movie_file_extension,
|
||||
)
|
||||
)
|
||||
return result
|
||||
result = Path(self.partial_movie_directory, f"{self.scene.num_plays:05}")
|
||||
return result.with_suffix(self.movie_file_extension)
|
||||
|
||||
def get_movie_file_path(self) -> str:
|
||||
return self.movie_file_path
|
||||
|
||||
def get_saved_mobject_directory(self) -> str:
|
||||
return guarantee_existence(self.saved_mobject_directory)
|
||||
|
||||
def get_saved_mobject_path(self, mobject: Mobject) -> str | None:
|
||||
directory = self.get_saved_mobject_directory()
|
||||
files = os.listdir(directory)
|
||||
default_name = str(mobject) + "_0.mob"
|
||||
index = 0
|
||||
while default_name in files:
|
||||
default_name = default_name.replace(str(index), str(index + 1))
|
||||
index += 1
|
||||
if platform.system() == 'Darwin':
|
||||
cmds = [
|
||||
"osascript", "-e",
|
||||
f"""
|
||||
set chosenfile to (choose file name default name "{default_name}" default location "{directory}")
|
||||
POSIX path of chosenfile
|
||||
""",
|
||||
]
|
||||
process = sp.Popen(cmds, stdout=sp.PIPE)
|
||||
file_path = process.stdout.read().decode("utf-8").split("\n")[0]
|
||||
if not file_path:
|
||||
return
|
||||
else:
|
||||
user_name = input(f"Enter mobject file name (default is {default_name}): ")
|
||||
file_path = os.path.join(directory, user_name or default_name)
|
||||
if os.path.exists(file_path) or os.path.exists(file_path + ".mob"):
|
||||
if input(f"{file_path} already exists. Overwrite (y/n)? ") != "y":
|
||||
return
|
||||
if not file_path.endswith(".mob"):
|
||||
file_path = file_path + ".mob"
|
||||
return file_path
|
||||
|
||||
# Sound
|
||||
def init_audio(self) -> None:
|
||||
self.includes_sound: bool = False
|
||||
@@ -230,23 +176,20 @@ class SceneFileWriter(object):
|
||||
|
||||
# Writers
|
||||
def begin(self) -> None:
|
||||
if not self.break_into_partial_movies and self.write_to_movie:
|
||||
if not self.subdivide_output and self.write_to_movie:
|
||||
self.open_movie_pipe(self.get_movie_file_path())
|
||||
|
||||
def begin_animation(self) -> None:
|
||||
if self.break_into_partial_movies and self.write_to_movie:
|
||||
if self.subdivide_output and self.write_to_movie:
|
||||
self.open_movie_pipe(self.get_next_partial_movie_path())
|
||||
|
||||
def end_animation(self) -> None:
|
||||
if self.break_into_partial_movies and self.write_to_movie:
|
||||
if self.subdivide_output and self.write_to_movie:
|
||||
self.close_movie_pipe()
|
||||
|
||||
def finish(self) -> None:
|
||||
if self.write_to_movie:
|
||||
if self.break_into_partial_movies:
|
||||
self.combine_movie_files()
|
||||
else:
|
||||
self.close_movie_pipe()
|
||||
if not self.subdivide_output and self.write_to_movie:
|
||||
self.close_movie_pipe()
|
||||
if self.includes_sound:
|
||||
self.add_sound_to_video()
|
||||
self.print_file_ready_message(self.get_movie_file_path())
|
||||
@@ -265,11 +208,10 @@ class SceneFileWriter(object):
|
||||
width, height = self.scene.camera.get_pixel_shape()
|
||||
|
||||
vf_arg = 'vflip'
|
||||
# if self.pixel_format.startswith("yuv"):
|
||||
vf_arg += f',eq=saturation={self.saturation}:gamma={self.gamma}'
|
||||
|
||||
command = [
|
||||
FFMPEG_BIN,
|
||||
self.ffmpeg_bin,
|
||||
'-y', # overwrite output file if it exists
|
||||
'-f', 'rawvideo',
|
||||
'-s', f'{width}x{height}', # size of one frame
|
||||
@@ -277,7 +219,7 @@ class SceneFileWriter(object):
|
||||
'-r', str(fps), # frames per second
|
||||
'-i', '-', # The input comes from a pipe
|
||||
'-vf', vf_arg,
|
||||
'-an', # Tells FFMPEG not to expect any audio
|
||||
'-an', # Tells ffmpeg not to expect any audio
|
||||
'-loglevel', 'error',
|
||||
]
|
||||
if self.video_codec:
|
||||
@@ -304,8 +246,8 @@ class SceneFileWriter(object):
|
||||
movie_path = Path(self.get_movie_file_path())
|
||||
scene_name = movie_path.stem
|
||||
insert_dir = Path(movie_path.parent, "inserts")
|
||||
guarantee_existence(str(insert_dir))
|
||||
return Path(insert_dir, f"{scene_name}_{index}{movie_path.suffix}")
|
||||
guarantee_existence(insert_dir)
|
||||
return Path(insert_dir, f"{scene_name}_{index}").with_suffix(self.movie_file_extension)
|
||||
|
||||
def begin_insert(self):
|
||||
# Begin writing process
|
||||
@@ -314,7 +256,7 @@ class SceneFileWriter(object):
|
||||
index = 0
|
||||
while (insert_path := self.get_insert_file_path(index)).exists():
|
||||
index += 1
|
||||
self.inserted_file_path = str(insert_path)
|
||||
self.inserted_file_path = insert_path
|
||||
self.open_movie_pipe(self.inserted_file_path)
|
||||
|
||||
def end_insert(self):
|
||||
@@ -358,54 +300,6 @@ class SceneFileWriter(object):
|
||||
else:
|
||||
self.movie_file_path = self.temp_file_path
|
||||
|
||||
def combine_movie_files(self) -> None:
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.movie_file_extension,
|
||||
}
|
||||
if self.scene.start_at_animation_number is not None:
|
||||
kwargs["min_index"] = self.scene.start_at_animation_number
|
||||
if self.scene.end_at_animation_number is not None:
|
||||
kwargs["max_index"] = self.scene.end_at_animation_number
|
||||
else:
|
||||
kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1
|
||||
partial_movie_files = get_sorted_integer_files(
|
||||
self.partial_movie_directory,
|
||||
**kwargs
|
||||
)
|
||||
if len(partial_movie_files) == 0:
|
||||
log.warning("No animations in this scene")
|
||||
return
|
||||
|
||||
# Write a file partial_file_list.txt containing all
|
||||
# partial movie files
|
||||
file_list = os.path.join(
|
||||
self.partial_movie_directory,
|
||||
"partial_movie_file_list.txt"
|
||||
)
|
||||
with open(file_list, 'w') as fp:
|
||||
for pf_path in partial_movie_files:
|
||||
if os.name == 'nt':
|
||||
pf_path = pf_path.replace('\\', '/')
|
||||
fp.write(f"file \'{pf_path}\'\n")
|
||||
|
||||
movie_file_path = self.get_movie_file_path()
|
||||
commands = [
|
||||
FFMPEG_BIN,
|
||||
'-y', # overwrite output file if it exists
|
||||
'-f', 'concat',
|
||||
'-safe', '0',
|
||||
'-i', file_list,
|
||||
'-loglevel', 'error',
|
||||
'-c', 'copy',
|
||||
movie_file_path
|
||||
]
|
||||
if not self.includes_sound:
|
||||
commands.insert(-1, '-an')
|
||||
|
||||
combine_process = sp.Popen(commands)
|
||||
combine_process.wait()
|
||||
|
||||
def add_sound_to_video(self) -> None:
|
||||
movie_file_path = self.get_movie_file_path()
|
||||
stem, ext = os.path.splitext(movie_file_path)
|
||||
@@ -418,7 +312,7 @@ class SceneFileWriter(object):
|
||||
)
|
||||
temp_file_path = stem + "_temp" + ext
|
||||
commands = [
|
||||
FFMPEG_BIN,
|
||||
self.ffmpeg_bin,
|
||||
"-i", movie_file_path,
|
||||
"-i", sound_file_path,
|
||||
'-y', # overwrite output file if it exists
|
||||
|
||||
@@ -10,8 +10,7 @@ import numpy as np
|
||||
from functools import lru_cache
|
||||
|
||||
from manimlib.config import parse_cli
|
||||
from manimlib.config import get_configuration
|
||||
from manimlib.utils.iterables import resize_array
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.utils.shaders import get_shader_code_from_file
|
||||
from manimlib.utils.shaders import get_shader_program
|
||||
from manimlib.utils.shaders import image_path_to_texture
|
||||
@@ -20,8 +19,10 @@ from manimlib.utils.shaders import set_program_uniform
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import List, Optional, Dict
|
||||
from typing import Optional, Tuple, Iterable
|
||||
from manimlib.typing import UniformDict
|
||||
from moderngl.vertex_array import VertexArray
|
||||
from moderngl.framebuffer import Framebuffer
|
||||
|
||||
# Mobjects that should be rendered with
|
||||
# the same shader will be organized and
|
||||
@@ -409,8 +410,7 @@ class VShaderWrapper(ShaderWrapper):
|
||||
which can display that texture as a simple quad onto a screen,
|
||||
along with the rgb value which is meant to be discarded.
|
||||
"""
|
||||
cam_config = get_configuration(parse_cli())['camera_config']
|
||||
size = (cam_config['pixel_width'], cam_config['pixel_height'])
|
||||
size = manim_config.camera.resolution
|
||||
double_size = (2 * size[0], 2 * size[1])
|
||||
|
||||
# Important to make sure dtype is floating point (not fixed point)
|
||||
@@ -446,6 +446,11 @@ class VShaderWrapper(ShaderWrapper):
|
||||
color = texture(Texture, uv);
|
||||
if(color.a == 0) discard;
|
||||
|
||||
if(color.a < 0){
|
||||
color.a = -color.a / (1.0 - color.a);
|
||||
color.rgb *= (color.a - 1);
|
||||
}
|
||||
|
||||
// Counteract scaling in fill frag
|
||||
color *= 1.06;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ uniform vec4 clip_plane;
|
||||
|
||||
void emit_gl_Position(vec3 point){
|
||||
vec4 result = vec4(point, 1.0);
|
||||
// This allow for smooth transitions between objects fixed and unfixed from frame
|
||||
// This allows for smooth transitions between objects fixed and unfixed from frame
|
||||
result = mix(view * result, result, is_fixed_in_frame);
|
||||
// Essentially a projection matrix
|
||||
result.xyz *= frame_rescale_factors;
|
||||
|
||||
@@ -27,7 +27,7 @@ vec4 add_light(vec4 color, vec3 point, vec3 unit_normal){
|
||||
float light_to_normal = dot(to_light, unit_normal);
|
||||
// When unit normal points towards light, brighten
|
||||
float bright_factor = max(light_to_normal, 0) * reflectiveness;
|
||||
// For glossy surface, add extra shine if light beam go towards camera
|
||||
// For glossy surface, add extra shine if light beam goes towards camera
|
||||
vec3 light_reflection = reflect(-to_light, unit_normal);
|
||||
float light_to_cam = dot(light_reflection, to_camera);
|
||||
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));
|
||||
|
||||
@@ -4,7 +4,7 @@ layout (triangles) in;
|
||||
layout (triangle_strip, max_vertices = 64) out; // Related to MAX_STEPS below
|
||||
|
||||
uniform float anti_alias_width;
|
||||
uniform float flat_stroke_float;
|
||||
uniform float flat_stroke;
|
||||
uniform float pixel_size;
|
||||
uniform float joint_type;
|
||||
uniform float frame_scale;
|
||||
@@ -33,7 +33,7 @@ const float COS_THRESHOLD = 0.999;
|
||||
// Used to determine how many lines to break the curve into
|
||||
const float POLYLINE_FACTOR = 100;
|
||||
const int MAX_STEPS = 32;
|
||||
const float MITER_COS_ANGLE_THRESHOLD = -0.9;
|
||||
const float MITER_COS_ANGLE_THRESHOLD = -0.8;
|
||||
|
||||
#INSERT emit_gl_Position.glsl
|
||||
#INSERT finalize_color.glsl
|
||||
@@ -62,13 +62,13 @@ vec3 rotate_vector(vec3 vect, vec3 unit_normal, float angle){
|
||||
}
|
||||
|
||||
|
||||
vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angle, bool inside_curve, bool flat_stroke){
|
||||
vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angle, bool inside_curve, bool draw_flat){
|
||||
/*
|
||||
Step the the left of a curve.
|
||||
First a perpendicular direction is calculated, then it is adjusted
|
||||
so as to make a joint.
|
||||
*/
|
||||
vec3 unit_tan = normalize(flat_stroke ? tangent : project(tangent, unit_normal));
|
||||
vec3 unit_tan = normalize(draw_flat ? tangent : project(tangent, unit_normal));
|
||||
|
||||
// Step to stroke width bound should be perpendicular
|
||||
// both to the tangent and the normal direction
|
||||
@@ -78,11 +78,13 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angl
|
||||
// lines up very closely with the direction to the camera, treated here
|
||||
// as the unit normal. To avoid those, this smoothly transitions to a step
|
||||
// direction perpendicular to the true curve normal.
|
||||
float alignment = abs(dot(normalize(tangent), unit_normal));
|
||||
float alignment_threshold = 0.97; // This could maybe be chosen in a more principled way based on stroke width
|
||||
if (alignment > alignment_threshold) {
|
||||
vec3 perp = normalize(cross(v_unit_normal[1], tangent));
|
||||
step = mix(step, project(step, perp), smoothstep(alignment_threshold, 1.0, alignment));
|
||||
if(joint_angle != 0){
|
||||
float alignment = abs(dot(normalize(tangent), unit_normal));
|
||||
float alignment_threshold = 0.97; // This could maybe be chosen in a more principled way based on stroke width
|
||||
if (alignment > alignment_threshold) {
|
||||
vec3 perp = normalize(cross(v_unit_normal[1], tangent));
|
||||
step = mix(step, project(step, perp), smoothstep(alignment_threshold, 1.0, alignment));
|
||||
}
|
||||
}
|
||||
|
||||
if (inside_curve || int(joint_type) == NO_JOINT) return step;
|
||||
@@ -93,7 +95,7 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angl
|
||||
if (abs(cos_angle) > COS_THRESHOLD) return step;
|
||||
|
||||
// Below here, figure out the adjustment to bevel or miter a joint
|
||||
if (!flat_stroke){
|
||||
if (!draw_flat){
|
||||
// Figure out what joint product would be for everything projected onto
|
||||
// the plane perpendicular to the normal direction (which here would be to_camera)
|
||||
step = normalize(cross(unit_normal, unit_tan)); // Back to original step
|
||||
@@ -128,17 +130,17 @@ void emit_point_with_width(
|
||||
float width,
|
||||
vec4 joint_color,
|
||||
bool inside_curve,
|
||||
bool flat_stroke
|
||||
bool draw_flat
|
||||
){
|
||||
// Find unit normal
|
||||
vec3 unit_normal = flat_stroke ? v_unit_normal[1] : normalize(camera_position - point);
|
||||
vec3 unit_normal = draw_flat ? v_unit_normal[1] : normalize(camera_position - point);
|
||||
|
||||
// Set styling
|
||||
color = finalize_color(joint_color, point, unit_normal);
|
||||
|
||||
// Figure out the step from the point to the corners of the
|
||||
// triangle strip around the polyline
|
||||
vec3 step = step_to_corner(point, tangent, unit_normal, joint_angle, inside_curve, flat_stroke);
|
||||
vec3 step = step_to_corner(point, tangent, unit_normal, joint_angle, inside_curve, draw_flat);
|
||||
float aaw = max(anti_alias_width * pixel_size, 1e-8);
|
||||
|
||||
// Emit two corners
|
||||
@@ -163,7 +165,7 @@ void main() {
|
||||
if (vec3(v_stroke_width[0], v_stroke_width[1], v_stroke_width[2]) == vec3(0.0, 0.0, 0.0)) return;
|
||||
if (vec3(v_color[0].a, v_color[1].a, v_color[2].a) == vec3(0.0, 0.0, 0.0)) return;
|
||||
|
||||
bool flat_stroke = bool(flat_stroke_float) || bool(is_fixed_in_frame);
|
||||
bool draw_flat = bool(flat_stroke) || bool(is_fixed_in_frame);
|
||||
|
||||
// Coefficients such that the quadratic bezier is c0 + c1 * t + c2 * t^2
|
||||
vec3 c0 = verts[0];
|
||||
@@ -207,7 +209,7 @@ void main() {
|
||||
emit_point_with_width(
|
||||
point, tangent, joint_angle,
|
||||
stroke_width, color,
|
||||
inside_curve, flat_stroke
|
||||
inside_curve, draw_flat
|
||||
);
|
||||
}
|
||||
EndPrimitive();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
uniform float frame_scale;
|
||||
uniform float is_fixed_in_frame;
|
||||
uniform float scale_stroke_with_zoom;
|
||||
|
||||
in vec3 point;
|
||||
in vec4 stroke_rgba;
|
||||
@@ -22,7 +23,7 @@ const float STROKE_WIDTH_CONVERSION = 0.01;
|
||||
void main(){
|
||||
verts = point;
|
||||
v_color = stroke_rgba;
|
||||
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, is_fixed_in_frame);
|
||||
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, scale_stroke_with_zoom);
|
||||
v_joint_angle = joint_angle;
|
||||
v_unit_normal = unit_normal;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
#version 330
|
||||
|
||||
in vec3 point;
|
||||
in vec3 du_point;
|
||||
in vec3 dv_point;
|
||||
in vec3 d_normal_point;
|
||||
in vec4 rgba;
|
||||
|
||||
out vec4 v_color;
|
||||
@@ -11,8 +10,10 @@ out vec4 v_color;
|
||||
#INSERT get_unit_normal.glsl
|
||||
#INSERT finalize_color.glsl
|
||||
|
||||
const float EPSILON = 1e-10;
|
||||
|
||||
void main(){
|
||||
emit_gl_Position(point);
|
||||
vec3 normal = cross(normalize(du_point - point), normalize(dv_point - point));
|
||||
v_color = finalize_color(rgba, point, normalize(normal));
|
||||
vec3 unit_normal = normalize(d_normal_point - point);
|
||||
v_color = finalize_color(rgba, point, unit_normal);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ void main() {
|
||||
float alpha = smoothstep(-dark_shift, dark_shift, dp);
|
||||
color = mix(dark_color, color, alpha);
|
||||
}
|
||||
if (color.a == 0) discard;
|
||||
|
||||
frag_color = finalize_color(
|
||||
color,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#version 330
|
||||
|
||||
in vec3 point;
|
||||
in vec3 du_point;
|
||||
in vec3 dv_point;
|
||||
in vec3 d_normal_point;
|
||||
in vec2 im_coords;
|
||||
in float opacity;
|
||||
|
||||
@@ -11,15 +10,17 @@ out vec3 v_unit_normal;
|
||||
out vec2 v_im_coords;
|
||||
out float v_opacity;
|
||||
|
||||
uniform float is_sphere;
|
||||
uniform vec3 center;
|
||||
|
||||
#INSERT emit_gl_Position.glsl
|
||||
#INSERT get_unit_normal.glsl
|
||||
|
||||
const float EPSILON = 1e-10;
|
||||
|
||||
void main(){
|
||||
v_point = point;
|
||||
v_unit_normal = normalize(cross(
|
||||
normalize(du_point - point),
|
||||
normalize(dv_point - point)
|
||||
));
|
||||
v_unit_normal = normalize(d_normal_point - point);;
|
||||
v_im_coords = im_coords;
|
||||
v_opacity = opacity;
|
||||
emit_gl_Position(point);
|
||||
|
||||
@@ -13,7 +13,7 @@ in vec2 uv_coords;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
// This include a delaration of uniform vec3 shading
|
||||
// This includes a declaration of uniform vec3 shading
|
||||
#INSERT finalize_color.glsl
|
||||
|
||||
void main() {
|
||||
|
||||
@@ -24,7 +24,8 @@ default:
|
||||
\usepackage{pifont}
|
||||
\DisableLigatures{encoding = *, family = * }
|
||||
\linespread{1}
|
||||
|
||||
%% Borrowed from https://tex.stackexchange.com/questions/6058/making-a-shorter-minus
|
||||
\DeclareMathSymbol{\minus}{\mathbin}{AMSa}{"39}
|
||||
ctex:
|
||||
description: ""
|
||||
compiler: xelatex
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Union, Tuple, Annotated, Literal, Iterable
|
||||
from typing import Union, Tuple, Annotated, Literal, Iterable, Dict
|
||||
from colour import Color
|
||||
import numpy as np
|
||||
import re
|
||||
|
||||
|
||||
try:
|
||||
from typing import Self
|
||||
except ImportError:
|
||||
from typing_extensions import Self
|
||||
except ImportError:
|
||||
from typing import Self
|
||||
|
||||
# Abbreviations for a common types
|
||||
ManimColor = Union[str, Color, None]
|
||||
@@ -28,7 +28,7 @@ if TYPE_CHECKING:
|
||||
|
||||
# These are various alternate names for np.ndarray meant to specify
|
||||
# certain shapes.
|
||||
#
|
||||
#
|
||||
# In theory, these annotations could be used to check arrays sizes
|
||||
# at runtime, but at the moment nothing actually uses them, and
|
||||
# the names are here primarily to enhance readibility and allow
|
||||
@@ -40,6 +40,7 @@ if TYPE_CHECKING:
|
||||
Vect4 = Annotated[FloatArray, Literal[4]]
|
||||
VectN = Annotated[FloatArray, Literal["N"]]
|
||||
Matrix3x3 = Annotated[FloatArray, Literal[3, 3]]
|
||||
VectArray = Annotated[FloatArray, Literal["N", 1]]
|
||||
Vect2Array = Annotated[FloatArray, Literal["N", 2]]
|
||||
Vect3Array = Annotated[FloatArray, Literal["N", 3]]
|
||||
Vect4Array = Annotated[FloatArray, Literal["N", 4]]
|
||||
|
||||
34
manimlib/utils/cache.py
Normal file
34
manimlib/utils/cache.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from diskcache import Cache
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
|
||||
from manimlib.utils.directories import get_cache_dir
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
CACHE_SIZE = 1e9 # 1 Gig
|
||||
_cache = Cache(get_cache_dir(), size_limit=CACHE_SIZE)
|
||||
|
||||
|
||||
def cache_on_disk(func: Callable[..., T]) -> Callable[..., T]:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
key = hash_string(f"{func.__name__}{args}{kwargs}")
|
||||
value = _cache.get(key)
|
||||
if value is None:
|
||||
value = func(*args, **kwargs)
|
||||
_cache.set(key, value)
|
||||
return value
|
||||
return wrapper
|
||||
|
||||
|
||||
def clear_cache():
|
||||
_cache.clear()
|
||||
@@ -5,6 +5,7 @@ from colour import hex2rgb
|
||||
from colour import rgb2hex
|
||||
import numpy as np
|
||||
import random
|
||||
from matplotlib import pyplot
|
||||
|
||||
from manimlib.constants import COLORMAP_3B1B
|
||||
from manimlib.constants import WHITE
|
||||
@@ -14,8 +15,8 @@ from manimlib.utils.iterables import resize_with_interpolation
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable, Sequence
|
||||
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array
|
||||
from typing import Iterable, Sequence, Callable
|
||||
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array, NDArray
|
||||
|
||||
|
||||
def color_to_rgb(color: ManimColor) -> Vect3:
|
||||
@@ -77,19 +78,25 @@ def int_to_hex(rgb_int: int) -> str:
|
||||
|
||||
def color_gradient(
|
||||
reference_colors: Iterable[ManimColor],
|
||||
length_of_output: int
|
||||
length_of_output: int,
|
||||
interp_by_hsl: bool = False,
|
||||
) -> list[Color]:
|
||||
if length_of_output == 0:
|
||||
return []
|
||||
rgbs = list(map(color_to_rgb, reference_colors))
|
||||
alphas = np.linspace(0, (len(rgbs) - 1), length_of_output)
|
||||
n_ref_colors = len(reference_colors)
|
||||
alphas = np.linspace(0, (n_ref_colors - 1), length_of_output)
|
||||
floors = alphas.astype('int')
|
||||
alphas_mod1 = alphas % 1
|
||||
# End edge case
|
||||
alphas_mod1[-1] = 1
|
||||
floors[-1] = len(rgbs) - 2
|
||||
floors[-1] = n_ref_colors - 2
|
||||
return [
|
||||
rgb_to_color(np.sqrt(interpolate(rgbs[i]**2, rgbs[i + 1]**2, alpha)))
|
||||
interpolate_color(
|
||||
reference_colors[i],
|
||||
reference_colors[i + 1],
|
||||
alpha,
|
||||
interp_by_hsl=interp_by_hsl,
|
||||
)
|
||||
for i, alpha in zip(floors, alphas_mod1)
|
||||
]
|
||||
|
||||
@@ -97,10 +104,16 @@ def color_gradient(
|
||||
def interpolate_color(
|
||||
color1: ManimColor,
|
||||
color2: ManimColor,
|
||||
alpha: float
|
||||
alpha: float,
|
||||
interp_by_hsl: bool = False,
|
||||
) -> Color:
|
||||
rgb = np.sqrt(interpolate(color_to_rgb(color1)**2, color_to_rgb(color2)**2, alpha))
|
||||
return rgb_to_color(rgb)
|
||||
if interp_by_hsl:
|
||||
hsl1 = np.array(Color(color1).get_hsl())
|
||||
hsl2 = np.array(Color(color2).get_hsl())
|
||||
return Color(hsl=interpolate(hsl1, hsl2, alpha))
|
||||
else:
|
||||
rgb = np.sqrt(interpolate(color_to_rgb(color1)**2, color_to_rgb(color2)**2, alpha))
|
||||
return rgb_to_color(rgb)
|
||||
|
||||
|
||||
def interpolate_color_by_hsl(
|
||||
@@ -108,9 +121,7 @@ def interpolate_color_by_hsl(
|
||||
color2: ManimColor,
|
||||
alpha: float
|
||||
) -> Color:
|
||||
hsl1 = np.array(Color(color1).get_hsl())
|
||||
hsl2 = np.array(Color(color2).get_hsl())
|
||||
return Color(hsl=interpolate(hsl1, hsl2, alpha))
|
||||
return interpolate_color(color1, color2, alpha, interp_by_hsl=True)
|
||||
|
||||
|
||||
def average_color(*colors: ManimColor) -> Color:
|
||||
@@ -134,6 +145,33 @@ def random_bright_color(
|
||||
))
|
||||
|
||||
|
||||
def get_colormap_from_colors(colors: Iterable[ManimColor]) -> Callable[[Sequence[float]], Vect4Array]:
|
||||
"""
|
||||
Returns a funciton which takes in values between 0 and 1, and returns
|
||||
a corresponding list of rgba values
|
||||
"""
|
||||
rgbas = np.array([color_to_rgba(color) for color in colors])
|
||||
|
||||
def func(values):
|
||||
alphas = np.clip(values, 0, 1)
|
||||
scaled_alphas = alphas * (len(rgbas) - 1)
|
||||
indices = scaled_alphas.astype(int)
|
||||
next_indices = np.clip(indices + 1, 0, len(rgbas) - 1)
|
||||
inter_alphas = scaled_alphas % 1
|
||||
inter_alphas = inter_alphas.repeat(4).reshape((len(indices), 4))
|
||||
result = interpolate(rgbas[indices], rgbas[next_indices], inter_alphas)
|
||||
return result
|
||||
|
||||
return func
|
||||
|
||||
|
||||
def get_color_map(map_name: str) -> Callable[[Sequence[float]], Vect4Array]:
|
||||
if map_name == "3b1b_colormap":
|
||||
return get_colormap_from_colors(COLORMAP_3B1B)
|
||||
return pyplot.get_cmap(map_name)
|
||||
|
||||
|
||||
# Delete this?
|
||||
def get_colormap_list(
|
||||
map_name: str = "viridis",
|
||||
n_colors: int = 9
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.config import get_manim_dir
|
||||
|
||||
|
||||
CUSTOMIZATION = {}
|
||||
|
||||
|
||||
def get_customization():
|
||||
if not CUSTOMIZATION:
|
||||
CUSTOMIZATION.update(get_custom_config())
|
||||
directories = CUSTOMIZATION["directories"]
|
||||
# Unless user has specified otherwise, use the system default temp
|
||||
# directory for storing tex files, mobject_data, etc.
|
||||
if not directories["temporary_storage"]:
|
||||
directories["temporary_storage"] = tempfile.gettempdir()
|
||||
|
||||
# Assumes all shaders are written into manimlib/shaders
|
||||
directories["shaders"] = os.path.join(
|
||||
get_manim_dir(), "manimlib", "shaders"
|
||||
)
|
||||
return CUSTOMIZATION
|
||||
@@ -20,29 +20,3 @@ def merge_dicts_recursively(*dicts):
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def soft_dict_update(d1, d2):
|
||||
"""
|
||||
Adds key values pairs of d2 to d1 only when d1 doesn't
|
||||
already have that key
|
||||
"""
|
||||
for key, value in list(d2.items()):
|
||||
if key not in d1:
|
||||
d1[key] = value
|
||||
|
||||
|
||||
def dict_eq(d1, d2):
|
||||
if len(d1) != len(d2):
|
||||
return False
|
||||
for key in d1:
|
||||
value1 = d1[key]
|
||||
value2 = d2[key]
|
||||
if type(value1) != type(value2):
|
||||
return False
|
||||
if type(d1[key]) == np.ndarray:
|
||||
if any(d1[key] != d2[key]):
|
||||
return False
|
||||
elif d1[key] != d2[key]:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -1,33 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import appdirs
|
||||
|
||||
from manimlib.utils.customization import get_customization
|
||||
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.config import get_manim_dir
|
||||
from manimlib.utils.file_ops import guarantee_existence
|
||||
|
||||
|
||||
def get_directories() -> dict[str, str]:
|
||||
return get_customization()["directories"]
|
||||
return manim_config.directories
|
||||
|
||||
|
||||
def get_cache_dir() -> str:
|
||||
return get_directories()["cache"] or appdirs.user_cache_dir("manim")
|
||||
|
||||
|
||||
def get_temp_dir() -> str:
|
||||
return get_directories()["temporary_storage"]
|
||||
|
||||
|
||||
def get_tex_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "Tex"))
|
||||
|
||||
|
||||
def get_text_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "Text"))
|
||||
|
||||
|
||||
def get_mobject_data_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data"))
|
||||
return get_directories()["temporary_storage"] or tempfile.gettempdir()
|
||||
|
||||
|
||||
def get_downloads_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads"))
|
||||
return get_directories()["downloads"] or appdirs.user_cache_dir("manim_downloads")
|
||||
|
||||
|
||||
def get_output_dir() -> str:
|
||||
@@ -47,4 +43,4 @@ def get_sound_dir() -> str:
|
||||
|
||||
|
||||
def get_shader_dir() -> str:
|
||||
return get_directories()["shaders"]
|
||||
return os.path.join(get_manim_dir(), "manimlib", "shaders")
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
|
||||
import numpy as np
|
||||
import validators
|
||||
import urllib.request
|
||||
|
||||
import manimlib.utils.directories
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -11,81 +17,41 @@ if TYPE_CHECKING:
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
def add_extension_if_not_present(file_name: str, extension: str) -> str:
|
||||
# This could conceivably be smarter about handling existing differing extensions
|
||||
if(file_name[-len(extension):] != extension):
|
||||
return file_name + extension
|
||||
else:
|
||||
return file_name
|
||||
|
||||
|
||||
def guarantee_existence(path: str) -> str:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
return os.path.abspath(path)
|
||||
def guarantee_existence(path: str | Path) -> Path:
|
||||
path = Path(path)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path.absolute()
|
||||
|
||||
|
||||
def find_file(
|
||||
file_name: str,
|
||||
directories: Iterable[str] | None = None,
|
||||
extensions: Iterable[str] | None = None
|
||||
) -> str:
|
||||
) -> Path:
|
||||
# Check if this is a file online first, and if so, download
|
||||
# it to a temporary directory
|
||||
if validators.url(file_name):
|
||||
import urllib.request
|
||||
from manimlib.utils.directories import get_downloads_dir
|
||||
stem, name = os.path.split(file_name)
|
||||
folder = get_downloads_dir()
|
||||
path = os.path.join(folder, name)
|
||||
suffix = Path(file_name).suffix
|
||||
file_hash = hash_string(file_name)
|
||||
folder = manimlib.utils.directories.get_downloads_dir()
|
||||
|
||||
path = Path(folder, file_hash).with_suffix(suffix)
|
||||
urllib.request.urlretrieve(file_name, path)
|
||||
return path
|
||||
|
||||
# Check if what was passed in is already a valid path to a file
|
||||
if os.path.exists(file_name):
|
||||
return file_name
|
||||
return Path(file_name)
|
||||
|
||||
# Otherwise look in local file system
|
||||
directories = directories or [""]
|
||||
extensions = extensions or [""]
|
||||
possible_paths = (
|
||||
os.path.join(directory, file_name + extension)
|
||||
Path(directory, file_name + extension)
|
||||
for directory in directories
|
||||
for extension in extensions
|
||||
)
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
if path.exists():
|
||||
return path
|
||||
raise IOError(f"{file_name} not Found")
|
||||
|
||||
|
||||
def get_sorted_integer_files(
|
||||
directory: str,
|
||||
min_index: float = 0,
|
||||
max_index: float = np.inf,
|
||||
remove_non_integer_files: bool = False,
|
||||
remove_indices_greater_than: float | None = None,
|
||||
extension: str | None = None,
|
||||
) -> list[str]:
|
||||
indexed_files = []
|
||||
for file in os.listdir(directory):
|
||||
if '.' in file:
|
||||
index_str = file[:file.index('.')]
|
||||
else:
|
||||
index_str = file
|
||||
|
||||
full_path = os.path.join(directory, file)
|
||||
if index_str.isdigit():
|
||||
index = int(index_str)
|
||||
if remove_indices_greater_than is not None:
|
||||
if index > remove_indices_greater_than:
|
||||
os.remove(full_path)
|
||||
continue
|
||||
if extension is not None and not file.endswith(extension):
|
||||
continue
|
||||
if index >= min_index and index < max_index:
|
||||
indexed_files.append((index, file))
|
||||
elif remove_non_integer_files:
|
||||
os.remove(full_path)
|
||||
indexed_files.sort(key=lambda p: p[0])
|
||||
return list(map(lambda p: os.path.join(directory, p[1]), indexed_files))
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.prompt import Confirm
|
||||
from rich.prompt import Prompt
|
||||
from rich.rule import Rule
|
||||
from rich.table import Table
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
def get_manim_dir() -> str:
|
||||
manimlib_module = importlib.import_module("manimlib")
|
||||
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
|
||||
return os.path.abspath(os.path.join(manimlib_dir, ".."))
|
||||
|
||||
|
||||
def remove_empty_value(dictionary: dict[str, Any]) -> None:
|
||||
for key in list(dictionary.keys()):
|
||||
if dictionary[key] == "":
|
||||
dictionary.pop(key)
|
||||
elif isinstance(dictionary[key], dict):
|
||||
remove_empty_value(dictionary[key])
|
||||
|
||||
|
||||
def init_customization() -> None:
|
||||
configuration = {
|
||||
"directories": {
|
||||
"mirror_module_path": False,
|
||||
"output": "",
|
||||
"raster_images": "",
|
||||
"vector_images": "",
|
||||
"sounds": "",
|
||||
"temporary_storage": "",
|
||||
},
|
||||
"universal_import_line": "from manimlib import *",
|
||||
"style": {
|
||||
"tex_template": "",
|
||||
"font": "Consolas",
|
||||
"background_color": "",
|
||||
},
|
||||
"window_position": "UR",
|
||||
"window_monitor": 0,
|
||||
"full_screen": False,
|
||||
"break_into_partial_movies": False,
|
||||
"camera_resolutions": {
|
||||
"low": "854x480",
|
||||
"medium": "1280x720",
|
||||
"high": "1920x1080",
|
||||
"4k": "3840x2160",
|
||||
"default_resolution": "",
|
||||
},
|
||||
"fps": 30,
|
||||
}
|
||||
|
||||
console = Console()
|
||||
console.print(Rule("[bold]Configuration Guide[/bold]"))
|
||||
# print("Initialize configuration")
|
||||
try:
|
||||
scope = Prompt.ask(
|
||||
" Select the scope of the configuration",
|
||||
choices=["global", "local"],
|
||||
default="local"
|
||||
)
|
||||
|
||||
console.print("[bold]Directories:[/bold]")
|
||||
dir_config = configuration["directories"]
|
||||
dir_config["output"] = Prompt.ask(
|
||||
" Where should manim [bold]output[/bold] video and image files place [prompt.default](optional, default is none)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["raster_images"] = Prompt.ask(
|
||||
" Which folder should manim find [bold]raster images[/bold] (.jpg .png .gif) in " + \
|
||||
"[prompt.default](optional, default is none)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["vector_images"] = Prompt.ask(
|
||||
" Which folder should manim find [bold]vector images[/bold] (.svg .xdv) in " + \
|
||||
"[prompt.default](optional, default is none)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["sounds"] = Prompt.ask(
|
||||
" Which folder should manim find [bold]sound files[/bold] (.mp3 .wav) in " + \
|
||||
"[prompt.default](optional, default is none)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["temporary_storage"] = Prompt.ask(
|
||||
" Which folder should manim storage [bold]temporary files[/bold] " + \
|
||||
"[prompt.default](recommended, use system temporary folder by default)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
|
||||
console.print("[bold]Styles:[/bold]")
|
||||
style_config = configuration["style"]
|
||||
tex_template = Prompt.ask(
|
||||
" Select a TeX template to compile a LaTeX source file",
|
||||
default="default"
|
||||
)
|
||||
style_config["tex_template"] = tex_template
|
||||
style_config["background_color"] = Prompt.ask(
|
||||
" Which [bold]background color[/bold] do you want [italic](hex code)",
|
||||
default="#333333"
|
||||
)
|
||||
|
||||
console.print("[bold]Camera qualities:[/bold]")
|
||||
table = Table(
|
||||
"low", "medium", "high", "ultra_high",
|
||||
title="Four defined qualities",
|
||||
box=box.ROUNDED
|
||||
)
|
||||
table.add_row("480p15", "720p30", "1080p60", "2160p60")
|
||||
console.print(table)
|
||||
configuration["camera_resolutions"]["default_resolution"] = Prompt.ask(
|
||||
" Which one to choose as the default rendering quality",
|
||||
choices=["low", "medium", "high", "ultra_high"],
|
||||
default="high"
|
||||
)
|
||||
|
||||
write_to_file = Confirm.ask(
|
||||
"\n[bold]Are you sure to write these configs to file?[/bold]",
|
||||
default=True
|
||||
)
|
||||
if not write_to_file:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
global_file_name = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
if scope == "global":
|
||||
file_name = global_file_name
|
||||
else:
|
||||
if os.path.exists(global_file_name):
|
||||
remove_empty_value(configuration)
|
||||
file_name = os.path.join(os.getcwd(), "custom_config.yml")
|
||||
with open(file_name, "w", encoding="utf-8") as f:
|
||||
yaml.dump(configuration, f)
|
||||
|
||||
console.print(f"\n:rocket: You have successfully set up a {scope} configuration file!")
|
||||
console.print(f"You can manually modify it in: [cyan]`{file_name}`[/cyan]")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[green]Exit configuration guide[/green]")
|
||||
@@ -13,10 +13,7 @@ from manimlib.utils.file_ops import find_file
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence, Optional, Tuple
|
||||
from manimlib.typing import UniformDict
|
||||
from moderngl.vertex_array import VertexArray
|
||||
from moderngl.framebuffer import Framebuffer
|
||||
from typing import Sequence, Optional
|
||||
|
||||
|
||||
# Global maps to reflect uniform status
|
||||
|
||||
@@ -36,12 +36,6 @@ def get_num_args(function: Callable) -> int:
|
||||
def get_parameters(function: Callable) -> Iterable[str]:
|
||||
return inspect.signature(function).parameters.keys()
|
||||
|
||||
# Just to have a less heavyweight name for this extremely common operation
|
||||
#
|
||||
# We may wish to have more fine-grained control over division by zero behavior
|
||||
# in the future (separate specifiable values for 0/0 and x/0 with x != 0),
|
||||
# but for now, we just allow the option to handle indeterminate 0/0.
|
||||
|
||||
|
||||
def clip(a: float, min_a: float, max_a: float) -> float:
|
||||
if a < min_a:
|
||||
@@ -58,6 +52,10 @@ def arr_clip(arr: np.ndarray, min_a: float, max_a: float) -> np.ndarray:
|
||||
|
||||
|
||||
def fdiv(a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None) -> Scalable:
|
||||
"""
|
||||
Less heavyweight name for np.true_divide, enabling
|
||||
default behavior for 0/0
|
||||
"""
|
||||
if zero_over_zero_value is not None:
|
||||
out = np.full_like(a, zero_over_zero_value)
|
||||
where = np.logical_or(a != 0, b != 0)
|
||||
@@ -68,11 +66,13 @@ def fdiv(a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None)
|
||||
return np.true_divide(a, b, out=out, where=where)
|
||||
|
||||
|
||||
def binary_search(function: Callable[[float], float],
|
||||
target: float,
|
||||
lower_bound: float,
|
||||
upper_bound: float,
|
||||
tolerance:float = 1e-4) -> float | None:
|
||||
def binary_search(
|
||||
function: Callable[[float], float],
|
||||
target: float,
|
||||
lower_bound: float,
|
||||
upper_bound: float,
|
||||
tolerance:float = 1e-4
|
||||
) -> float | None:
|
||||
lh = lower_bound
|
||||
rh = upper_bound
|
||||
mh = (lh + rh) / 2
|
||||
@@ -96,7 +96,6 @@ def binary_search(function: Callable[[float], float],
|
||||
return mh
|
||||
|
||||
|
||||
def hash_string(string: str) -> str:
|
||||
# Truncating at 16 bytes for cleanliness
|
||||
def hash_string(string: str, n_bytes=16) -> str:
|
||||
hasher = hashlib.sha256(string.encode())
|
||||
return hasher.hexdigest()[:16]
|
||||
return hasher.hexdigest()[:n_bytes]
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import threading
|
||||
import platform
|
||||
|
||||
from manimlib.utils.directories import get_sound_dir
|
||||
from manimlib.utils.file_ops import find_file
|
||||
|
||||
@@ -10,3 +14,27 @@ def get_full_sound_file_path(sound_file_name: str) -> str:
|
||||
directories=[get_sound_dir()],
|
||||
extensions=[".wav", ".mp3", ""]
|
||||
)
|
||||
|
||||
|
||||
def play_sound(sound_file):
|
||||
"""Play a sound file using the system's audio player"""
|
||||
full_path = get_full_sound_file_path(sound_file)
|
||||
system = platform.system()
|
||||
|
||||
if system == "Windows":
|
||||
# Windows
|
||||
subprocess.Popen(
|
||||
["powershell", "-c", f"(New-Object Media.SoundPlayer '{full_path}').PlaySync()"],
|
||||
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
elif system == "Darwin":
|
||||
# macOS
|
||||
subprocess.Popen(
|
||||
["afplay", full_path],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
else:
|
||||
subprocess.Popen(
|
||||
["aplay", full_path],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
|
||||
@@ -48,6 +48,10 @@ def get_norm(vect: VectN | List[float]) -> float:
|
||||
return sum((x**2 for x in vect))**0.5
|
||||
|
||||
|
||||
def get_dist(vect1: VectN, vect2: VectN):
|
||||
return get_norm(vect2 - vect1)
|
||||
|
||||
|
||||
def normalize(
|
||||
vect: VectN | List[float],
|
||||
fall_back: VectN | List[float] | None = None
|
||||
|
||||
@@ -1,40 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
from manimlib.utils.tex_to_symbol_count import TEX_TO_SYMBOL_COUNT
|
||||
|
||||
|
||||
@lru_cache
|
||||
def num_tex_symbols(tex: str) -> int:
|
||||
tex = remove_tex_environments(tex)
|
||||
commands_pattern = r"""
|
||||
(?P<sqrt>\\sqrt\[[0-9]+\])| # Special sqrt with number
|
||||
(?P<escaped_brace>\\[{}])| # Escaped braces
|
||||
(?P<cmd>\\[a-zA-Z!,-/:;<>]+) # Regular commands
|
||||
"""
|
||||
This function attempts to estimate the number of symbols that
|
||||
a given string of tex would produce.
|
||||
|
||||
Warning, it may not behave perfectly
|
||||
"""
|
||||
# First, remove patterns like \begin{align}, \phantom{thing},
|
||||
# \begin{array}{cc}, etc.
|
||||
pattern = "|".join(
|
||||
rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?"
|
||||
for s in ["begin", "end", "phantom"]
|
||||
)
|
||||
tex = re.sub(pattern, "", tex)
|
||||
|
||||
# Progressively count the symbols associated with certain tex commands,
|
||||
# and remove those commands from the string, adding the number of symbols
|
||||
# that command creates
|
||||
total = 0
|
||||
pos = 0
|
||||
for match in re.finditer(commands_pattern, tex, re.VERBOSE):
|
||||
# Count normal characters up to this command
|
||||
total += sum(1 for c in tex[pos:match.start()] if c not in "^{} \n\t_$\\&")
|
||||
|
||||
# Start with the special case \sqrt[number]
|
||||
for substr in re.findall(r"\\sqrt\[[0-9]+\]", tex):
|
||||
total += len(substr) - 5 # e.g. \sqrt[3] is 3 symbols
|
||||
tex = tex.replace(substr, " ")
|
||||
|
||||
general_command = r"\\[a-zA-Z!,-/:;<>]+"
|
||||
for substr in re.findall(general_command, tex):
|
||||
total += TEX_TO_SYMBOL_COUNT.get(substr, 1)
|
||||
tex = tex.replace(substr, " ")
|
||||
if match.group("sqrt"):
|
||||
total += len(match.group()) - 5
|
||||
elif match.group("escaped_brace"):
|
||||
total += 1 # Count escaped brace as one symbol
|
||||
else:
|
||||
total += TEX_TO_SYMBOL_COUNT.get(match.group(), 1)
|
||||
pos = match.end()
|
||||
|
||||
# Count remaining characters
|
||||
total += sum(map(lambda c: c not in "^{} \n\t_$\\&", tex))
|
||||
total += sum(1 for c in tex[pos:] if c not in "^{} \n\t_$\\&")
|
||||
return total
|
||||
|
||||
|
||||
def remove_tex_environments(tex: str) -> str:
|
||||
# Handle \phantom{...} with any content
|
||||
tex = re.sub(r"\\phantom\{[^}]*\}", "", tex)
|
||||
# Handle other environment commands
|
||||
tex = re.sub(r"\\(begin|end)(\{\w+\})?(\{\w+\})?(\[\w+\])?", "", tex)
|
||||
return tex
|
||||
|
||||
@@ -1,72 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
import subprocess
|
||||
from functools import lru_cache
|
||||
|
||||
from manimlib.config import get_custom_config
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
from manimlib.utils.cache import cache_on_disk
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.config import get_manim_dir
|
||||
from manimlib.logger import log
|
||||
from manimlib.utils.directories import get_tex_dir
|
||||
from manimlib.utils.simple_functions import hash_string
|
||||
|
||||
|
||||
SAVED_TEX_CONFIG = {}
|
||||
|
||||
|
||||
def get_tex_template_config(template_name: str) -> dict[str, str]:
|
||||
name = template_name.replace(" ", "_").lower()
|
||||
with open(os.path.join(
|
||||
get_manim_dir(), "manimlib", "tex_templates.yml"
|
||||
), encoding="utf-8") as tex_templates_file:
|
||||
template_path = os.path.join(get_manim_dir(), "manimlib", "tex_templates.yml")
|
||||
with open(template_path, encoding="utf-8") as tex_templates_file:
|
||||
templates_dict = yaml.safe_load(tex_templates_file)
|
||||
if name not in templates_dict:
|
||||
log.warning(
|
||||
"Cannot recognize template '%s', falling back to 'default'.",
|
||||
name
|
||||
)
|
||||
log.warning(f"Cannot recognize template {name}, falling back to 'default'.")
|
||||
name = "default"
|
||||
return templates_dict[name]
|
||||
|
||||
|
||||
def get_tex_config() -> dict[str, str]:
|
||||
@lru_cache
|
||||
def get_tex_config(template: str = "") -> tuple[str, str]:
|
||||
"""
|
||||
Returns a dict which should look something like this:
|
||||
{
|
||||
"template": "default",
|
||||
"compiler": "latex",
|
||||
"preamble": "..."
|
||||
}
|
||||
Returns a compiler and preamble to use for rendering LaTeX
|
||||
"""
|
||||
# Only load once, then save thereafter
|
||||
if not SAVED_TEX_CONFIG:
|
||||
template_name = get_custom_config()["style"]["tex_template"]
|
||||
template_config = get_tex_template_config(template_name)
|
||||
SAVED_TEX_CONFIG.update({
|
||||
"template": template_name,
|
||||
"compiler": template_config["compiler"],
|
||||
"preamble": template_config["preamble"]
|
||||
})
|
||||
return SAVED_TEX_CONFIG
|
||||
template = template or manim_config.tex.template
|
||||
config = get_tex_template_config(template)
|
||||
return config["compiler"], config["preamble"]
|
||||
|
||||
|
||||
def tex_content_to_svg_file(
|
||||
content: str, template: str, additional_preamble: str,
|
||||
short_tex: str
|
||||
) -> str:
|
||||
tex_config = get_tex_config()
|
||||
if not template or template == tex_config["template"]:
|
||||
compiler = tex_config["compiler"]
|
||||
preamble = tex_config["preamble"]
|
||||
else:
|
||||
config = get_tex_template_config(template)
|
||||
compiler = config["compiler"]
|
||||
preamble = config["preamble"]
|
||||
|
||||
if additional_preamble:
|
||||
preamble += "\n" + additional_preamble
|
||||
full_tex = "\n\n".join((
|
||||
def get_full_tex(content: str, preamble: str = ""):
|
||||
return "\n\n".join((
|
||||
"\\documentclass[preview]{standalone}",
|
||||
preamble,
|
||||
"\\begin{document}",
|
||||
@@ -74,90 +46,105 @@ def tex_content_to_svg_file(
|
||||
"\\end{document}"
|
||||
)) + "\n"
|
||||
|
||||
svg_file = os.path.join(
|
||||
get_tex_dir(), hash_string(full_tex) + ".svg"
|
||||
)
|
||||
if not os.path.exists(svg_file):
|
||||
# If svg doesn't exist, create it
|
||||
with display_during_execution("Writing " + short_tex):
|
||||
create_tex_svg(full_tex, svg_file, compiler)
|
||||
return svg_file
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def latex_to_svg(
|
||||
latex: str,
|
||||
template: str = "",
|
||||
additional_preamble: str = "",
|
||||
short_tex: str = "",
|
||||
show_message_during_execution: bool = True,
|
||||
) -> str:
|
||||
"""Convert LaTeX string to SVG string.
|
||||
|
||||
Args:
|
||||
latex: LaTeX source code
|
||||
template: Path to a template LaTeX file
|
||||
additional_preamble: String including any added "\\usepackage{...}" style imports
|
||||
|
||||
Returns:
|
||||
str: SVG source code
|
||||
|
||||
Raises:
|
||||
LatexError: If LaTeX compilation fails
|
||||
NotImplementedError: If compiler is not supported
|
||||
"""
|
||||
if show_message_during_execution:
|
||||
message = f"Writing {(short_tex or latex)[:70]}..."
|
||||
else:
|
||||
message = ""
|
||||
|
||||
compiler, preamble = get_tex_config(template)
|
||||
|
||||
preamble = "\n".join([preamble, additional_preamble])
|
||||
full_tex = get_full_tex(latex, preamble)
|
||||
return full_tex_to_svg(full_tex, compiler, message)
|
||||
|
||||
|
||||
def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
|
||||
@cache_on_disk
|
||||
def full_tex_to_svg(full_tex: str, compiler: str = "latex", message: str = ""):
|
||||
if message:
|
||||
print(message, end="\r")
|
||||
|
||||
if compiler == "latex":
|
||||
program = "latex"
|
||||
dvi_ext = ".dvi"
|
||||
elif compiler == "xelatex":
|
||||
program = "xelatex -no-pdf"
|
||||
dvi_ext = ".xdv"
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Compiler '{compiler}' is not implemented"
|
||||
raise NotImplementedError(f"Compiler '{compiler}' is not implemented")
|
||||
|
||||
# Write intermediate files to a temporary directory
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tex_path = Path(temp_dir, "working").with_suffix(".tex")
|
||||
dvi_path = tex_path.with_suffix(dvi_ext)
|
||||
|
||||
# Write tex file
|
||||
tex_path.write_text(full_tex)
|
||||
|
||||
# Run latex compiler
|
||||
process = subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
*(['-no-pdf'] if compiler == "xelatex" else []),
|
||||
"-interaction=batchmode",
|
||||
"-halt-on-error",
|
||||
f"-output-directory={temp_dir}",
|
||||
tex_path
|
||||
],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Write tex file
|
||||
root, _ = os.path.splitext(svg_file)
|
||||
with open(root + ".tex", "w", encoding="utf-8") as tex_file:
|
||||
tex_file.write(full_tex)
|
||||
if process.returncode != 0:
|
||||
# Handle error
|
||||
error_str = ""
|
||||
log_path = tex_path.with_suffix(".log")
|
||||
if log_path.exists():
|
||||
content = log_path.read_text()
|
||||
error_match = re.search(r"(?<=\n! ).*\n.*\n", content)
|
||||
if error_match:
|
||||
error_str = error_match.group()
|
||||
raise LatexError(error_str or "LaTeX compilation failed")
|
||||
|
||||
# tex to dvi
|
||||
if os.system(" ".join((
|
||||
program,
|
||||
"-interaction=batchmode",
|
||||
"-halt-on-error",
|
||||
f"-output-directory=\"{os.path.dirname(svg_file)}\"",
|
||||
f"\"{root}.tex\"",
|
||||
">",
|
||||
os.devnull
|
||||
))):
|
||||
log.error(
|
||||
"LaTeX Error! Not a worry, it happens to the best of us."
|
||||
# Run dvisvgm and capture output directly
|
||||
process = subprocess.run(
|
||||
[
|
||||
"dvisvgm",
|
||||
dvi_path,
|
||||
"-n", # no fonts
|
||||
"-v", "0", # quiet
|
||||
"--stdout", # output to stdout instead of file
|
||||
],
|
||||
capture_output=True
|
||||
)
|
||||
error_str = ""
|
||||
with open(root + ".log", "r", encoding="utf-8") as log_file:
|
||||
error_match_obj = re.search(r"(?<=\n! ).*\n.*\n", log_file.read())
|
||||
if error_match_obj:
|
||||
error_str = error_match_obj.group()
|
||||
log.debug(
|
||||
f"The error could be:\n`{error_str}`",
|
||||
)
|
||||
raise LatexError(error_str)
|
||||
|
||||
# dvi to svg
|
||||
os.system(" ".join((
|
||||
"dvisvgm",
|
||||
f"\"{root}{dvi_ext}\"",
|
||||
"-n",
|
||||
"-v",
|
||||
"0",
|
||||
"-o",
|
||||
f"\"{svg_file}\"",
|
||||
">",
|
||||
os.devnull
|
||||
)))
|
||||
# Return SVG string
|
||||
result = process.stdout.decode('utf-8')
|
||||
|
||||
# Cleanup superfluous documents
|
||||
for ext in (".tex", dvi_ext, ".log", ".aux"):
|
||||
try:
|
||||
os.remove(root + ext)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if message:
|
||||
print(" " * len(message), end="\r")
|
||||
|
||||
|
||||
# TODO, perhaps this should live elsewhere
|
||||
@contextmanager
|
||||
def display_during_execution(message: str):
|
||||
# Merge into a single line
|
||||
to_print = message.replace("\n", " ")
|
||||
max_characters = os.get_terminal_size().columns - 1
|
||||
if len(to_print) > max_characters:
|
||||
to_print = to_print[:max_characters - 3] + "..."
|
||||
try:
|
||||
print(to_print, end="\r")
|
||||
yield
|
||||
finally:
|
||||
print(" " * len(to_print), end="\r")
|
||||
return result
|
||||
|
||||
|
||||
class LatexError(Exception):
|
||||
|
||||
@@ -104,6 +104,7 @@ TEX_TO_SYMBOL_COUNT = {
|
||||
R"\mapsto": 2,
|
||||
R"\markright": 0,
|
||||
R"\mathds": 0,
|
||||
R"\mathcal": 0,
|
||||
R"\max": 3,
|
||||
R"\mbox": 0,
|
||||
R"\medskip": 0,
|
||||
|
||||
@@ -5,17 +5,20 @@ import numpy as np
|
||||
import moderngl_window as mglw
|
||||
from moderngl_window.context.pyglet.window import Window as PygletWindow
|
||||
from moderngl_window.timers.clock import Timer
|
||||
from screeninfo import get_monitors
|
||||
from functools import wraps
|
||||
import screeninfo
|
||||
|
||||
from manimlib.constants import ASPECT_RATIO
|
||||
from manimlib.constants import FRAME_SHAPE
|
||||
from manimlib.utils.customization import get_customization
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, TypeVar, Optional
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Window(PygletWindow):
|
||||
fullscreen: bool = False
|
||||
@@ -26,28 +29,84 @@ class Window(PygletWindow):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scene: Scene,
|
||||
size: tuple[int, int] = (1280, 720),
|
||||
scene: Optional[Scene] = None,
|
||||
position_string: str = "UR",
|
||||
monitor_index: int = 1,
|
||||
full_screen: bool = False,
|
||||
size: Optional[tuple[int, int]] = None,
|
||||
position: Optional[tuple[int, int]] = None,
|
||||
samples: int = 0
|
||||
):
|
||||
scene.window = self
|
||||
super().__init__(size=size, samples=samples)
|
||||
|
||||
self.default_size = size
|
||||
self.default_position = self.find_initial_position(size)
|
||||
self.scene = scene
|
||||
self.monitor = self.get_monitor(monitor_index)
|
||||
self.default_size = size or self.get_default_size(full_screen)
|
||||
self.default_position = position or self.position_from_string(position_string)
|
||||
self.pressed_keys = set()
|
||||
self.title = str(scene)
|
||||
self.size = size
|
||||
|
||||
super().__init__(samples=samples)
|
||||
self.to_default_position()
|
||||
|
||||
if self.scene:
|
||||
self.init_for_scene(scene)
|
||||
|
||||
def init_for_scene(self, scene: Scene):
|
||||
"""
|
||||
Resets the state and updates the scene associated to this window.
|
||||
|
||||
This is necessary when we want to reuse an *existing* window after a
|
||||
`scene.reload()` was requested, which will create new scene instances.
|
||||
"""
|
||||
self.pressed_keys.clear()
|
||||
self._has_undrawn_event = True
|
||||
|
||||
mglw.activate_context(window=self)
|
||||
self.scene = scene
|
||||
self.title = str(scene)
|
||||
|
||||
self.init_mgl_context()
|
||||
|
||||
self.timer = Timer()
|
||||
self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer)
|
||||
mglw.activate_context(window=self, ctx=self.ctx)
|
||||
self.timer.start()
|
||||
|
||||
self.to_default_position()
|
||||
# This line seems to resync the viewport
|
||||
self.on_resize(*self.size)
|
||||
|
||||
def get_monitor(self, index):
|
||||
try:
|
||||
monitors = screeninfo.get_monitors()
|
||||
return monitors[min(index, len(monitors) - 1)]
|
||||
except screeninfo.ScreenInfoError:
|
||||
# Default fallback
|
||||
return screeninfo.Monitor(width=1920, height=1080)
|
||||
|
||||
def get_default_size(self, full_screen=False):
|
||||
width = self.monitor.width // (1 if full_screen else 2)
|
||||
height = int(width // ASPECT_RATIO)
|
||||
return (width, height)
|
||||
|
||||
def position_from_string(self, position_string):
|
||||
# Alternatively, it might be specified with a string like
|
||||
# UR, OO, DL, etc. specifying what corner it should go to
|
||||
char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2}
|
||||
size = self.default_size
|
||||
width_diff = self.monitor.width - size[0]
|
||||
height_diff = self.monitor.height - size[1]
|
||||
x_step = char_to_n[position_string[1]] * width_diff // 2
|
||||
y_step = char_to_n[position_string[0]] * height_diff // 2
|
||||
return (self.monitor.x + x_step, -self.monitor.y + y_step)
|
||||
|
||||
def focus(self):
|
||||
"""
|
||||
Puts focus on this window by hiding and showing it again.
|
||||
|
||||
Note that the pyglet `activate()` method didn't work as expected here,
|
||||
so that's why we have to use this workaround. This will produce a small
|
||||
flicker on the window but at least reliably focuses it. It may also
|
||||
offset the window position slightly.
|
||||
"""
|
||||
self._window.set_visible(False)
|
||||
self._window.set_visible(True)
|
||||
|
||||
def to_default_position(self):
|
||||
self.position = self.default_position
|
||||
@@ -57,27 +116,6 @@ class Window(PygletWindow):
|
||||
self.size = (w - 1, h - 1)
|
||||
self.size = (w, h)
|
||||
|
||||
def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]:
|
||||
custom_position = get_customization()["window_position"]
|
||||
monitors = get_monitors()
|
||||
mon_index = get_customization()["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
window_width, window_height = size
|
||||
# Position might be specified with a string of the form
|
||||
# x,y for integers x and y
|
||||
if "," in custom_position:
|
||||
return tuple(map(int, custom_position.split(",")))
|
||||
|
||||
# Alternatively, it might be specified with a string like
|
||||
# UR, OO, DL, etc. specifying what corner it should go to
|
||||
char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2}
|
||||
width_diff = monitor.width - window_width
|
||||
height_diff = monitor.height - window_height
|
||||
return (
|
||||
monitor.x + char_to_n[custom_position[1]] * width_diff // 2,
|
||||
-monitor.y + char_to_n[custom_position[0]] * height_diff // 2,
|
||||
)
|
||||
|
||||
# Delegate event handling to scene
|
||||
def pixel_coords_to_space_coords(
|
||||
self,
|
||||
@@ -85,7 +123,7 @@ class Window(PygletWindow):
|
||||
py: int,
|
||||
relative: bool = False
|
||||
) -> np.ndarray:
|
||||
if not hasattr(self.scene, "frame"):
|
||||
if self.scene is None or not hasattr(self.scene, "frame"):
|
||||
return np.zeros(3)
|
||||
|
||||
pixel_shape = np.array(self.size)
|
||||
@@ -116,6 +154,8 @@ class Window(PygletWindow):
|
||||
@note_undrawn_event
|
||||
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
|
||||
super().on_mouse_motion(x, y, dx, dy)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||
self.scene.on_mouse_motion(point, d_point)
|
||||
@@ -123,6 +163,8 @@ class Window(PygletWindow):
|
||||
@note_undrawn_event
|
||||
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None:
|
||||
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||
self.scene.on_mouse_drag(point, d_point, buttons, modifiers)
|
||||
@@ -130,18 +172,24 @@ class Window(PygletWindow):
|
||||
@note_undrawn_event
|
||||
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
|
||||
super().on_mouse_press(x, y, button, mods)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
self.scene.on_mouse_press(point, button, mods)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
|
||||
super().on_mouse_release(x, y, button, mods)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
self.scene.on_mouse_release(point, button, mods)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None:
|
||||
super().on_mouse_scroll(x, y, x_offset, y_offset)
|
||||
if not self.scene:
|
||||
return
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
|
||||
self.scene.on_mouse_scroll(point, offset, x_offset, y_offset)
|
||||
@@ -150,32 +198,44 @@ class Window(PygletWindow):
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
self.pressed_keys.add(symbol) # Modifiers?
|
||||
super().on_key_press(symbol, modifiers)
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_key_press(symbol, modifiers)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||
self.pressed_keys.difference_update({symbol}) # Modifiers?
|
||||
super().on_key_release(symbol, modifiers)
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_key_release(symbol, modifiers)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
super().on_resize(width, height)
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_resize(width, height)
|
||||
|
||||
@note_undrawn_event
|
||||
def on_show(self) -> None:
|
||||
super().on_show()
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_show()
|
||||
|
||||
@note_undrawn_event
|
||||
def on_hide(self) -> None:
|
||||
super().on_hide()
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_hide()
|
||||
|
||||
@note_undrawn_event
|
||||
def on_close(self) -> None:
|
||||
super().on_close()
|
||||
if not self.scene:
|
||||
return
|
||||
self.scene.on_close()
|
||||
|
||||
def is_key_pressed(self, symbol: int) -> bool:
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
addict
|
||||
appdirs
|
||||
audioop-lts; python_version>='3.13'
|
||||
colour
|
||||
diskcache
|
||||
ipython>=8.18.0
|
||||
isosurfaces
|
||||
fontTools
|
||||
manimpango>=0.4.0.post0,<0.5.0
|
||||
manimpango>=0.6.0
|
||||
mapbox-earcut
|
||||
matplotlib
|
||||
moderngl
|
||||
@@ -13,11 +17,11 @@ pydub
|
||||
pygments
|
||||
PyOpenGL
|
||||
pyperclip
|
||||
pyrr
|
||||
pyyaml
|
||||
rich
|
||||
scipy
|
||||
screeninfo
|
||||
setuptools
|
||||
skia-pathops
|
||||
svgelements>=1.8.1
|
||||
sympy
|
||||
|
||||
13
setup.cfg
13
setup.cfg
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = manimgl
|
||||
version = 1.6.1
|
||||
version = 1.7.2
|
||||
author = Grant Sanderson
|
||||
author_email= grant@3blue1brown.com
|
||||
description = Animation engine for explanatory math videos
|
||||
@@ -29,10 +29,15 @@ classifiers =
|
||||
packages = find:
|
||||
include_package_data = True
|
||||
install_requires =
|
||||
addict
|
||||
appdirs
|
||||
audioop-lts; python_version >= "3.13"
|
||||
colour
|
||||
ipython
|
||||
diskcache
|
||||
ipython>=8.18.0
|
||||
isosurfaces
|
||||
manimpango>=0.4.0.post0,<0.5.0
|
||||
fontTools
|
||||
manimpango>=0.6.0
|
||||
mapbox-earcut
|
||||
matplotlib
|
||||
moderngl
|
||||
@@ -47,10 +52,12 @@ install_requires =
|
||||
rich
|
||||
scipy
|
||||
screeninfo
|
||||
setuptools
|
||||
skia-pathops
|
||||
svgelements>=1.8.1
|
||||
sympy
|
||||
tqdm
|
||||
typing-extensions; python_version < "3.11"
|
||||
validators
|
||||
|
||||
[options.entry_points]
|
||||
|
||||
Reference in New Issue
Block a user