mirror of
https://github.com/3b1b/manim.git
synced 2026-01-13 08:27:54 -05:00
Compare commits
282 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6c23a09e9 | ||
|
|
0e574882b3 | ||
|
|
bee3470856 | ||
|
|
ed3d44120c | ||
|
|
4466cfe727 | ||
|
|
e9aba0b92c | ||
|
|
6cdbe0d67a | ||
|
|
7732d2f0ee | ||
|
|
f77482c864 | ||
|
|
23ebbb2af1 | ||
|
|
14fbed76da | ||
|
|
e10a752c00 | ||
|
|
fde82e09c0 | ||
|
|
cfd362aa56 | ||
|
|
329d2c6eae | ||
|
|
f22a341e84 | ||
|
|
2d115a2c90 | ||
|
|
c726eb7a18 | ||
|
|
33fa76dfac | ||
|
|
0021880fba | ||
|
|
ed99427a3b | ||
|
|
7425057d9f | ||
|
|
ef5253f1bc | ||
|
|
fbccb1ebf3 | ||
|
|
f626a1a1e2 | ||
|
|
c1242d2dd5 | ||
|
|
719c81d72b | ||
|
|
ed1fc4d5f9 | ||
|
|
3822b00bec | ||
|
|
2753beb7bb | ||
|
|
a4afbfd739 | ||
|
|
5f878a2c1a | ||
|
|
9483f26a3b | ||
|
|
b4132e3d5e | ||
|
|
e9b404406d | ||
|
|
b543cc0e32 | ||
|
|
d45ea28dc1 | ||
|
|
788775e419 | ||
|
|
1bca0e63e9 | ||
|
|
54ad3550ef | ||
|
|
d19b386415 | ||
|
|
e359f520bc | ||
|
|
696fc85ff7 | ||
|
|
add1daf500 | ||
|
|
242e4a3471 | ||
|
|
9e563ae3b4 | ||
|
|
da909c0df8 | ||
|
|
0239e12d8a | ||
|
|
0fd8fdc3ca | ||
|
|
762f1abef7 | ||
|
|
17c2772b84 | ||
|
|
0d2d1b5c03 | ||
|
|
4ce123be44 | ||
|
|
d5a88d0fa4 | ||
|
|
3b146636b4 | ||
|
|
b24ba19dec | ||
|
|
0dc096bf57 | ||
|
|
e40a2935b1 | ||
|
|
f84b8a66fe | ||
|
|
952a598e3b | ||
|
|
c635f19f2a | ||
|
|
8645894255 | ||
|
|
e712951f2d | ||
|
|
1b24074369 | ||
|
|
acba13f499 | ||
|
|
61aec6051a | ||
|
|
9a78d13212 | ||
|
|
0787c4f362 | ||
|
|
0b7b3f4f31 | ||
|
|
7356a36fa7 | ||
|
|
f3e3a7c56f | ||
|
|
a35dd5a3cb | ||
|
|
7b4199c674 | ||
|
|
d8378d8157 | ||
|
|
8647a6429d | ||
|
|
3bb8f3f042 | ||
|
|
56df15453f | ||
|
|
d50717a3fc | ||
|
|
ca9b70e218 | ||
|
|
25c5aa2c65 | ||
|
|
c08ea4e645 | ||
|
|
573d630e5b | ||
|
|
6d72893382 | ||
|
|
40290ada83 | ||
|
|
bd356daa99 | ||
|
|
b667db2d31 | ||
|
|
f92211b352 | ||
|
|
1c2b52a128 | ||
|
|
eb315daeda | ||
|
|
e151334675 | ||
|
|
bbeba108bc | ||
|
|
77ce17679c | ||
|
|
7fa2654d8a | ||
|
|
ec620fa849 | ||
|
|
da53a6f808 | ||
|
|
1e621e8278 | ||
|
|
31119b630e | ||
|
|
b0fd520382 | ||
|
|
c1e14ef5b6 | ||
|
|
e9470b6bde | ||
|
|
5c0a1e4b76 | ||
|
|
121e6215f8 | ||
|
|
3d5642f3d7 | ||
|
|
9df58e4ddf | ||
|
|
f09092024f | ||
|
|
4d65c97965 | ||
|
|
76966064ce | ||
|
|
152d03ed27 | ||
|
|
8624168ed9 | ||
|
|
354db4423f | ||
|
|
27344249de | ||
|
|
0b3a1b271c | ||
|
|
fd8904ec83 | ||
|
|
7da6179493 | ||
|
|
17452dcd10 | ||
|
|
2f5acc6a87 | ||
|
|
71f018dfff | ||
|
|
b3ae517a05 | ||
|
|
f7bb5c1b8c | ||
|
|
a3227dda67 | ||
|
|
2ed78c6e0f | ||
|
|
8aa004b0b1 | ||
|
|
45938dd76f | ||
|
|
3fe4d6d2d4 | ||
|
|
a18600e8a4 | ||
|
|
700418a79c | ||
|
|
4940ccac7d | ||
|
|
17d7f0b6f0 | ||
|
|
275cf94b06 | ||
|
|
54fff5523b | ||
|
|
5707585d17 | ||
|
|
0305582e64 | ||
|
|
a4c3bb03d1 | ||
|
|
b00d718431 | ||
|
|
6da5d4c8f6 | ||
|
|
01670cf823 | ||
|
|
5986d0e7d2 | ||
|
|
d384fc1e27 | ||
|
|
8aedb8f33e | ||
|
|
bccc17a3ac | ||
|
|
892df54c9f | ||
|
|
663c57ba74 | ||
|
|
26e9b9cd7c | ||
|
|
a99ccea02c | ||
|
|
892ce2db09 | ||
|
|
d14f22c5ba | ||
|
|
846c10a0ff | ||
|
|
128178b46e | ||
|
|
b4f23e8d8e | ||
|
|
5765ab9055 | ||
|
|
6eb7edc664 | ||
|
|
e836c3bb42 | ||
|
|
5ff8e28ba5 | ||
|
|
6dc1ecb00a | ||
|
|
be78f5257a | ||
|
|
798479536d | ||
|
|
226df63d0b | ||
|
|
b8fb69773e | ||
|
|
fec2306f9a | ||
|
|
17d75bd336 | ||
|
|
23662d093f | ||
|
|
34d4689672 | ||
|
|
607ef334e9 | ||
|
|
b4b4d39ec5 | ||
|
|
1c2942798e | ||
|
|
e9ea5fbea0 | ||
|
|
7ecfc041b3 | ||
|
|
44a9c6337e | ||
|
|
d1a5089acc | ||
|
|
9e5f39a4a9 | ||
|
|
6da93cde7b | ||
|
|
6340db1076 | ||
|
|
f45d81be11 | ||
|
|
baa2adc128 | ||
|
|
f16277f100 | ||
|
|
876f06cc37 | ||
|
|
f682bf97e3 | ||
|
|
025639f026 | ||
|
|
e885ec6ecd | ||
|
|
3b3150c3c5 | ||
|
|
33aa4e979a | ||
|
|
9de7a6477d | ||
|
|
19b8057136 | ||
|
|
b6dd6fe16d | ||
|
|
51b2984ee3 | ||
|
|
957eedc32c | ||
|
|
2614b34d11 | ||
|
|
4ea1d6d64f | ||
|
|
fd67858bb0 | ||
|
|
442e7fc14d | ||
|
|
bb27a78449 | ||
|
|
a06d5613f4 | ||
|
|
6605ab75e8 | ||
|
|
ed9a4bd9eb | ||
|
|
d54b796154 | ||
|
|
bb72718c3b | ||
|
|
d279272762 | ||
|
|
f2f652f85d | ||
|
|
bf9d797d84 | ||
|
|
29e5a8bc90 | ||
|
|
02bad81fc3 | ||
|
|
2bfe7e08ff | ||
|
|
e727faaccb | ||
|
|
6b911f5721 | ||
|
|
565ff09d58 | ||
|
|
4b4a973464 | ||
|
|
8e2799a499 | ||
|
|
a44e230a07 | ||
|
|
f9fb68c011 | ||
|
|
c1ad893030 | ||
|
|
65d0826b91 | ||
|
|
41120b096e | ||
|
|
1f6e911d60 | ||
|
|
c45ff910f0 | ||
|
|
15760cf253 | ||
|
|
f6291d7e82 | ||
|
|
b5e6177afd | ||
|
|
22d9c57f60 | ||
|
|
3a992e136d | ||
|
|
12ef0a26d7 | ||
|
|
7a11e3d20f | ||
|
|
cf63dfddf9 | ||
|
|
42d8888f8e | ||
|
|
322f138490 | ||
|
|
df657c06c2 | ||
|
|
0c61c908b2 | ||
|
|
82658e1db3 | ||
|
|
de9ecbd766 | ||
|
|
ca9f4357fa | ||
|
|
e95aa69c4c | ||
|
|
6997cc9501 | ||
|
|
7f47815230 | ||
|
|
d3e61b962b | ||
|
|
8999ebb556 | ||
|
|
88f0c24c69 | ||
|
|
09579fcd3e | ||
|
|
01d989ba23 | ||
|
|
6c3e4b94ea | ||
|
|
52baf5b7c2 | ||
|
|
fd18e4a21f | ||
|
|
2a1b023442 | ||
|
|
288983e7b9 | ||
|
|
f6ff070a8e | ||
|
|
5126dd1f52 | ||
|
|
8345ca6160 | ||
|
|
611ac7f448 | ||
|
|
933b7fd3da | ||
|
|
15f3b359ae | ||
|
|
0e326c7ac5 | ||
|
|
ed2e3e80d9 | ||
|
|
3c240478b8 | ||
|
|
120d26defa | ||
|
|
5c427ea287 | ||
|
|
503bd116a6 | ||
|
|
f5d5565af1 | ||
|
|
aedf5633aa | ||
|
|
63b497c352 | ||
|
|
531a031b50 | ||
|
|
b48ce3f1de | ||
|
|
5636b41dfd | ||
|
|
402c06c99a | ||
|
|
eec396681c | ||
|
|
d06b3769b8 | ||
|
|
8fcb069808 | ||
|
|
9fb6280f1d | ||
|
|
e35f8466be | ||
|
|
44df81fd70 | ||
|
|
d1fc6c8ed7 | ||
|
|
9d1c8df095 | ||
|
|
1d0b864001 | ||
|
|
5008e20b8e | ||
|
|
c92b6dbd0b | ||
|
|
9c23a5feef | ||
|
|
ba3bb64bce | ||
|
|
448d792473 | ||
|
|
2f202e26b2 | ||
|
|
0c25b56afe | ||
|
|
0f998615ad | ||
|
|
abc018e0d8 | ||
|
|
abe1ea78d0 | ||
|
|
229c809a4b | ||
|
|
1b125df572 |
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Install sphinx and manim env
|
||||
run: |
|
||||
pip3 install --upgrade pip
|
||||
sudo apt install python3-setuptools
|
||||
sudo apt install python3-setuptools libpango1.0-dev
|
||||
pip3 install -r docs/requirements.txt
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
@@ -37,4 +37,4 @@ jobs:
|
||||
with:
|
||||
ACCESS_TOKEN: ${{ secrets.DOC_DEPLOY_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: docs/build/html
|
||||
FOLDER: docs/build/html
|
||||
|
||||
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@@ -19,12 +19,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
pip install setuptools wheel twine build
|
||||
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
python -m build
|
||||
twine upload dist/*
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,6 +15,8 @@ __pycache__/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
manimlib.egg-info/
|
||||
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
@@ -147,4 +149,4 @@ dmypy.json
|
||||
|
||||
# For manim
|
||||
/videos
|
||||
/custom_config.yml
|
||||
/custom_config.yml
|
||||
|
||||
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@@ -0,0 +1,2 @@
|
||||
graft manimlib
|
||||
recursive-exclude manimlib *.pyc *.DS_Store
|
||||
64
README.md
64
README.md
@@ -4,26 +4,39 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](https://pypi.org/project/manimgl/)
|
||||
[](http://choosealicense.com/licenses/mit/)
|
||||
[](https://www.reddit.com/r/manim/)
|
||||
[](https://discord.gg/mMRrZQW)
|
||||
[](https://www.reddit.com/r/manim/)
|
||||
[](https://discord.com/invite/bYCyhM9Kz2)
|
||||
[](https://3b1b.github.io/manim/)
|
||||
|
||||
Manim is an engine for precise programatic animations, designed for creating explanatory math videos.
|
||||
Manim is an engine for precise programmatic animations, designed for creating explanatory math videos.
|
||||
|
||||
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 devlopers 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. You can engage with that community by joining the discord.
|
||||
|
||||
Since the fork, this version has evolved to work on top of OpenGL, and allows real-time rendering to an interactive window before scenes are finalized and written to a file.
|
||||
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/installation/versions.html?highlight=OpenGL#which-version-to-use) 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.
|
||||
>
|
||||
> **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.6 or higher (Python 3.8 is recommended).
|
||||
|
||||
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).
|
||||
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 more options, take a look at the [Using manim](#using-manim) sections further below.
|
||||
|
||||
### Directly
|
||||
|
||||
```sh
|
||||
# Install manimgl
|
||||
pip install manimgl
|
||||
|
||||
# Try it out
|
||||
manimgl
|
||||
```
|
||||
|
||||
For more options, take a look at the [Using manim](#using-manim) sections further below.
|
||||
|
||||
If you want to hack on manimlib itself, clone this repository and in that directory execute:
|
||||
|
||||
```sh
|
||||
@@ -35,7 +48,21 @@ manimgl example_scenes.py OpeningManimExample
|
||||
# or
|
||||
manim-render example_scenes.py OpeningManimExample
|
||||
```
|
||||
|
||||
### Directly (Windows)
|
||||
|
||||
1. [Install FFmpeg](https://www.wikihow.com/Install-FFmpeg-on-Windows).
|
||||
2. Install a LaTeX distribution. [MiKTeX](https://miktex.org/download) is recommended.
|
||||
3. Install the remaining Python packages.
|
||||
```sh
|
||||
git clone https://github.com/3b1b/manim.git
|
||||
cd manim
|
||||
pip install -e .
|
||||
manimgl example_scenes.py OpeningManimExample
|
||||
```
|
||||
|
||||
### Mac OSX
|
||||
|
||||
1. Install FFmpeg, LaTeX in terminal using homebrew.
|
||||
```sh
|
||||
brew install ffmpeg mactex
|
||||
@@ -49,23 +76,12 @@ manim-render example_scenes.py OpeningManimExample
|
||||
manimgl example_scenes.py OpeningManimExample
|
||||
```
|
||||
|
||||
### Directly (Windows)
|
||||
1. [Install FFmpeg](https://www.wikihow.com/Install-FFmpeg-on-Windows).
|
||||
2. Install a LaTeX distribution. [MiKTeX](https://miktex.org/download) is recommended.
|
||||
3. Install the remaining Python packages.
|
||||
```sh
|
||||
git clone https://github.com/3b1b/manim.git
|
||||
cd manim
|
||||
pip install -e .
|
||||
manimgl example_scenes.py OpeningManimExample
|
||||
```
|
||||
|
||||
## Anaconda Install
|
||||
|
||||
* Install LaTeX as above.
|
||||
* Create a conda environment using `conda create -n manim python=3.8`.
|
||||
* Activate the environment using `conda activate manim`.
|
||||
* Install manimgl using `pip install -e .`.
|
||||
1. Install LaTeX as above.
|
||||
2. Create a conda environment using `conda create -n manim python=3.8`.
|
||||
3. Activate the environment using `conda activate manim`.
|
||||
4. Install manimgl using `pip install -e .`.
|
||||
|
||||
|
||||
## Using manim
|
||||
@@ -88,7 +104,7 @@ Take a look at custom_config.yml for further configuration. To add your customi
|
||||
Look through the [example scenes](https://3b1b.github.io/manim/getting_started/example_scenes.html) to get a sense of how it is used, and feel free to look through the code behind [3blue1brown videos](https://github.com/3b1b/videos) for a much larger set of example. Note, however, that developments are often made to the library without considering backwards compatibility with those old videos. To run an old project with a guarantee that it will work, you will have to go back to the commit which completed that project.
|
||||
|
||||
### Documentation
|
||||
Documentation is in progress at [3b1b.github.io/manim](https://3b1b.github.io/manim/). And there is also a Chinese version maintained by **@manim-kindergarten**: [manim.ml](https://manim.ml/) (in Chinese).
|
||||
Documentation is in progress at [3b1b.github.io/manim](https://3b1b.github.io/manim/). And there is also a Chinese version maintained by [**@manim-kindergarten**](https://manim.org.cn): [docs.manim.org.cn](https://docs.manim.org.cn/) (in Chinese).
|
||||
|
||||
[manim-kindergarten](https://github.com/manim-kindergarten/) wrote and collected some useful extra classes and some codes of videos in [manim_sandbox repo](https://github.com/manim-kindergarten/manim_sandbox).
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from manimlib.imports import *
|
||||
from manimlib import *
|
||||
|
||||
class SquareToCircle(Scene):
|
||||
def construct(self):
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
p.color-text {
|
||||
font-size: inherit;
|
||||
font-family: var(--font-stack--monospace);
|
||||
margin-top: 25px;
|
||||
color: WHITE;
|
||||
}
|
||||
|
||||
p.color-text-small {
|
||||
font-size: small;
|
||||
font-family: var(--font-stack--monospace);
|
||||
margin-top: 28px;
|
||||
color: WHITE;
|
||||
}
|
||||
|
||||
.colors {
|
||||
float: left;
|
||||
padding: 10px;
|
||||
border: 10px;
|
||||
margin: 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.BLUE_A {
|
||||
background: #C7E9F1;
|
||||
color:#C7E9F1;
|
||||
}
|
||||
|
||||
.BLUE_B {
|
||||
background: #9CDCEB;
|
||||
color:#9CDCEB;
|
||||
}
|
||||
|
||||
.BLUE_C {
|
||||
background: #58C4DD;
|
||||
color:#58C4DD;
|
||||
}
|
||||
|
||||
.BLUE_D {
|
||||
background: #29ABCA;
|
||||
color:#29ABCA;
|
||||
}
|
||||
|
||||
.BLUE_E {
|
||||
background: #1C758A;
|
||||
color:#1C758A;
|
||||
}
|
||||
|
||||
.TEAL_A {
|
||||
background: #ACEAD7;
|
||||
color:#ACEAD7 ;
|
||||
}
|
||||
|
||||
.TEAL_B {
|
||||
background: #76DDC0;
|
||||
color: #76DDC0;
|
||||
}
|
||||
|
||||
.TEAL_C {
|
||||
background: #5CD0B3;
|
||||
color: #5CD0B3;
|
||||
}
|
||||
|
||||
.TEAL_D {
|
||||
background: #55C1A7;
|
||||
color: #55C1A7;
|
||||
}
|
||||
|
||||
.TEAL_E {
|
||||
background: #49A88F;
|
||||
color: #49A88F;
|
||||
}
|
||||
|
||||
.GREEN_A {
|
||||
background: #C9E2AE;
|
||||
color: #C9E2AE;
|
||||
}
|
||||
|
||||
.GREEN_B {
|
||||
background: #A6CF8C;
|
||||
color: #A6CF8C;
|
||||
}
|
||||
|
||||
.GREEN_C {
|
||||
background: #83C167;
|
||||
color: #83C167;
|
||||
}
|
||||
|
||||
.GREEN_D {
|
||||
background: #77B05D;
|
||||
color: #77B05D;
|
||||
}
|
||||
|
||||
.GREEN_E {
|
||||
background: #699C52;
|
||||
color: #699C52;
|
||||
}
|
||||
|
||||
.YELLOW_A {
|
||||
background: #FFF1B6;
|
||||
color: #FFF1B6;
|
||||
}
|
||||
|
||||
.YELLOW_B {
|
||||
background: #FFEA94;
|
||||
color:#FFEA94 ;
|
||||
}
|
||||
|
||||
.YELLOW_C {
|
||||
background: #FFFF00;
|
||||
color: #FFFF00;
|
||||
}
|
||||
|
||||
.YELLOW_D {
|
||||
background: #F4D345;
|
||||
color: #F4D345;
|
||||
}
|
||||
|
||||
.YELLOW_E {
|
||||
background: #E8C11C;
|
||||
color: #E8C11C;
|
||||
}
|
||||
|
||||
.GOLD_A {
|
||||
background: #F7C797;
|
||||
color:#F7C797;
|
||||
}
|
||||
|
||||
.GOLD_B {
|
||||
background: #F9B775;
|
||||
color:#F9B775;
|
||||
}
|
||||
|
||||
.GOLD_C {
|
||||
background: #F0AC5F;
|
||||
color:#F0AC5F;
|
||||
}
|
||||
|
||||
.GOLD_D {
|
||||
background: #E1A158;
|
||||
color:#E1A158;
|
||||
}
|
||||
|
||||
.GOLD_E {
|
||||
background: #C78D46;
|
||||
color:#C78D46;
|
||||
}
|
||||
|
||||
.RED_A {
|
||||
background: #F7A1A3;
|
||||
color:#F7A1A3;
|
||||
}
|
||||
|
||||
.RED_B {
|
||||
background: #FF8080;
|
||||
color:#FF8080;
|
||||
}
|
||||
|
||||
.RED_C {
|
||||
background: #FC6255;
|
||||
color:#FC6255;
|
||||
}
|
||||
|
||||
.RED_D {
|
||||
background: #E65A4C;
|
||||
color:#E65A4C;
|
||||
}
|
||||
|
||||
.RED_E {
|
||||
background: #CF5044;
|
||||
color:#CF5044;
|
||||
}
|
||||
|
||||
.MAROON_A {
|
||||
background: #ECABC1;
|
||||
color: #ECABC1;
|
||||
}
|
||||
|
||||
.MAROON_B {
|
||||
background: #EC92AB;
|
||||
color: #EC92AB;
|
||||
}
|
||||
|
||||
.MAROON_C {
|
||||
background: #C55F73;
|
||||
color: #C55F73;
|
||||
}
|
||||
|
||||
.MAROON_D {
|
||||
background: #A24D61;
|
||||
color: #A24D61;
|
||||
}
|
||||
|
||||
.MAROON_E {
|
||||
background: #94424F;
|
||||
color: #94424F;
|
||||
}
|
||||
|
||||
.PURPLE_A {
|
||||
background: #CAA3E8;
|
||||
color: #CAA3E8;
|
||||
}
|
||||
|
||||
.PURPLE_B {
|
||||
background: #B189C6;
|
||||
color: #B189C6;
|
||||
}
|
||||
|
||||
.PURPLE_C {
|
||||
background: #9A72AC;
|
||||
color: #9A72AC;
|
||||
}
|
||||
|
||||
.PURPLE_D {
|
||||
background: #715582;
|
||||
color: #715582;
|
||||
}
|
||||
|
||||
.PURPLE_E {
|
||||
background: #644172;
|
||||
color: #644172;
|
||||
}
|
||||
|
||||
.GREY_A {
|
||||
background: #DDDDDD;
|
||||
color: #DDDDDD;
|
||||
}
|
||||
|
||||
.GREY_B {
|
||||
background: #BBBBBB;
|
||||
color: #BBBBBB;
|
||||
}
|
||||
|
||||
.GREY_C {
|
||||
background: #888888;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.GREY_D {
|
||||
background: #444444;
|
||||
color: #444444;
|
||||
}
|
||||
|
||||
.GREY_E {
|
||||
background: #222222;
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
.WHITE {
|
||||
background: #FFFFFF;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.BLACK {
|
||||
background: #000000;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.GREY_BROWN {
|
||||
background: #736357;
|
||||
color: #736357;
|
||||
}
|
||||
|
||||
.DARK_BROWN {
|
||||
background: #8B4513;
|
||||
color: #8B4513;
|
||||
}
|
||||
|
||||
.LIGHT_BROWN {
|
||||
background: #CD853F;
|
||||
color: #CD853F;
|
||||
}
|
||||
|
||||
.PINK {
|
||||
background: #D147BD;
|
||||
color: #D147BD;
|
||||
}
|
||||
|
||||
.LIGHT_PINK {
|
||||
background: #DC75CD;
|
||||
color: #DC75CD;
|
||||
}
|
||||
|
||||
.GREEN_SCREEN {
|
||||
background: #00FF00;
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
.ORANGE {
|
||||
background: #FF862F;
|
||||
color: #FF862F;
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
p {
|
||||
font-size: initial;
|
||||
}
|
||||
|
||||
span.caption-text {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
font-size: initial;
|
||||
}
|
||||
|
||||
.highlight-python.notranslate {
|
||||
margin-top: 0em;
|
||||
}
|
||||
|
||||
.manim-video {
|
||||
width: 99.9%;
|
||||
padding: 8px 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.manim-example {
|
||||
background-color: #333333;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 2px 2px 4px #ddd;
|
||||
}
|
||||
|
||||
.manim-example .manim-video {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.manim-example img {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h5.example-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
padding: 8px 16px;
|
||||
color: white;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
text-transform: none;
|
||||
margin-top: -0.4em;
|
||||
margin-bottom: -0.2em;
|
||||
}
|
||||
|
||||
.manim-example .highlight {
|
||||
background-color: #fafafa;
|
||||
border: 2px solid #333333;
|
||||
padding: 8px 8px 10px 8px;
|
||||
font-size: large;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.manim-example .highlight pre {
|
||||
background-color: inherit;
|
||||
border-left: none;
|
||||
margin: 0;
|
||||
padding: 0 6px 0 6px;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 603 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
Binary file not shown.
@@ -31,7 +31,10 @@ master_doc = 'index'
|
||||
pygments_style = 'default'
|
||||
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = ["custom.css", "colors.css"]
|
||||
html_css_files = [
|
||||
"https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/custom.css",
|
||||
"https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/colors.css"
|
||||
]
|
||||
html_theme = 'furo' # pip install furo==2020.10.5b9
|
||||
html_favicon = '_static/icon.png'
|
||||
html_logo = '../../logo/transparent_graph.png'
|
||||
|
||||
@@ -1,4 +1,113 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
No changes now.
|
||||
v1.2.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- `#1592 <https://github.com/3b1b/manim/pull/1592>`__: Fixed ``put_start_and_end_on`` in 3D
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Fixed ``DecimalNumber``'s scaling issue
|
||||
- `56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__: Fixed bug with common range array used for all coordinate systems
|
||||
- `8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__: Fixed ``CoordinateSystem`` init bug
|
||||
- `0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__: Fixed bug for single-valued ``ValueTracker``
|
||||
- `54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__: Fixed bug with SVG rectangles
|
||||
- `d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__: Fixed ``DotCloud.set_radii``
|
||||
- `b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__: Temporarily fixed bug for ``PMobject`` array resizing
|
||||
- `5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__: Fixed ``match_style``
|
||||
- `719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__: Fixed negative ``path_arc`` case
|
||||
- `c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__: Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis``
|
||||
- `7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__: Fixed ``ComplexPlane`` -i display bug
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `#1598 <https://github.com/3b1b/manim/pull/1598>`__: Supported the elliptical arc command ``A`` for ``SVGMobject``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Added ``FlashyFadeIn``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Save triangulation
|
||||
- `#1625 <https://github.com/3b1b/manim/pull/1625>`__: Added new ``Code`` mobject
|
||||
- `#1637 <https://github.com/3b1b/manim/pull/1637>`__: Add warnings and use rich to display log
|
||||
- `bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__: Added ``VCube``
|
||||
- `6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__: Supported ``ValueTracker`` to track vectors
|
||||
- `3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__: Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject``
|
||||
- `a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__: Added ``TracgTail``
|
||||
- `acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__: Added ``Scene.point_to_mobject``
|
||||
- `f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__: Added poly_fractal shader
|
||||
- `b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__: Added kwargs to ``TipableVMobject.set_length``
|
||||
- `17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__: Added ``Mobject.replicate``
|
||||
- `33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__: Added mandelbrot_fractal shader
|
||||
- `f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__: Saved state before each embed
|
||||
- `e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__: Allowed releasing of Textures
|
||||
- `14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__: Consolidated and renamed newton_fractal shader
|
||||
- `6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__: Hade ``ImageMoject`` remember the filepath to the Image
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Changed back to simpler ``Mobject.scale`` implementation
|
||||
- `b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__: Simplified ``Square``
|
||||
- `40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__: Removed unused parameter ``triangulation_locked``
|
||||
- `8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__: Reimplemented ``Arrow``
|
||||
- `d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__: Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default
|
||||
- `7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__: Refactored to call ``_handle_scale_side_effects`` after scaling takes place
|
||||
- `7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__: Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end``
|
||||
- `0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__: Made sure framerate is 30 for previewed scenes
|
||||
- `c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__: Pushed ``pixel_coords_to_space_coords`` to ``Window``
|
||||
- `d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__: Refactored to pass tuples and not arrays to uniforms
|
||||
- `9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__: Refactored to copy uniform arrays in ``Mobject.copy``
|
||||
- `ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__: Added ``bounding_box`` as exceptional key to point_cloud mobject
|
||||
- `329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__: Made sure stroke width is always a float
|
||||
|
||||
|
||||
v1.1.0
|
||||
-------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- Fixed the bug of :func:`~manimlib.utils.iterables.resize_with_interpolation` in the case of ``length=0``
|
||||
- Fixed the bug of ``__init__`` in :class:`~manimlib.mobject.geometry.Elbow`
|
||||
- If chosen monitor is not available, choose one that does exist
|
||||
- Make sure mobject data gets unlocked after animations
|
||||
- Fixed a bug for off-center vector fields
|
||||
- Had ``Mobject.match_points`` return self
|
||||
- Fixed chaining animation in example scenes
|
||||
- Fixed the default color of tip
|
||||
- Fixed a typo in ``ShowPassingFlashWithThinningStrokeWidth``
|
||||
- Fixed the default size of ``Text``
|
||||
- Fixed a missing import line in ``mobject.py``
|
||||
- Fixed the bug in ControlsExample
|
||||
- Make sure frame is added to the scene when initialization
|
||||
- Fixed zooming directions
|
||||
- Rewrote ``earclip_triangulation`` to fix triangulation
|
||||
- Allowed sound_file_name to be taken in without extensions
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Added :class:`~manimlib.animation.indication.VShowPassingFlash`
|
||||
- Added ``COLORMAP_3B1B``
|
||||
- Added some methods to coordinate system to access all axes ranges
|
||||
|
||||
- :meth:`~manimlib.mobject.coordinate_systems.CoordinateSystem.get_origin`
|
||||
- :meth:`~manimlib.mobject.coordinate_systems.CoordinateSystem.get_all_ranges`
|
||||
- Added :meth:`~manimlib.mobject.mobject.Mobject.set_color_by_rgba_func`
|
||||
- Updated :class:`~manimlib.mobject.vector_field.VectorField` and :class:`~manimlib.mobject.vector_field.StreamLines`
|
||||
- Allow ``3b1b_colormap`` as an option for :func:`~manimlib.utils.color.get_colormap_list`
|
||||
- Return ``stroke_width`` as 1d array
|
||||
- Added :meth:`~manimlib.mobject.svg.text_mobject.Text.get_parts_by_text`
|
||||
- Use Text not TexText for Brace
|
||||
- Update to Cross to make it default to variable stroke width
|
||||
- Added :class:`~manimlib.animation.indication.FlashAround` and :class:`~manimlib.animation.indication.FlashUnder`
|
||||
- Allowed configuration in ``Brace.get_text``
|
||||
- Added :meth:`~manimlib.camera.camera.CameraFrame.reorient` for quicker changes to frame angle
|
||||
- Added ``units`` to :meth:`~manimlib.camera.camera.CameraFrame.set_euler_angles`
|
||||
- Allowed any ``VMobject`` to be passed into ``TransformMatchingTex``
|
||||
- Removed double brace convention in ``Tex`` and ``TexText``
|
||||
- Added support for debugger launch
|
||||
- Added CLI flag ``--config_file`` to load configuration file manually
|
||||
- Added ``tip_style`` to ``tip_config``
|
||||
- Added ``MarkupText``
|
||||
- Take in ``u_range`` and ``v_range`` as arguments to ``ParametricSurface``
|
||||
- Added ``TrueDot``
|
||||
@@ -56,7 +56,7 @@ custom_config
|
||||
|
||||
- ``raster_images``
|
||||
The directory for storing raster images to be used in the code (including
|
||||
``.jpg``, ``.png`` and ``.gif``), which will be read by ``ImageMobject``.
|
||||
``.jpg``, ``.jpeg``, ``.png`` and ``.gif``), which will be read by ``ImageMobject``.
|
||||
|
||||
- ``vector_images``
|
||||
The directory for storing vector images to be used in the code (including
|
||||
@@ -108,6 +108,11 @@ 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)
|
||||
|
||||
``break_into_partial_movies``
|
||||
-----------------------------
|
||||
|
||||
|
||||
@@ -52,12 +52,13 @@ flag abbr function
|
||||
``--finder`` Show the output file in finder
|
||||
``--config`` Guide for automatic configuration
|
||||
``--file_name FILE_NAME`` Name for the movie or image file
|
||||
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passin two comma separated values, e.g. "3,6", it will end the rendering at the second value.
|
||||
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value.
|
||||
``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080"
|
||||
``--frame_rate FRAME_RATE`` Frame rate, as an integer
|
||||
``--color COLOR`` ``-c`` Background color
|
||||
``--leave_progress_bars`` Leave progress bars displayed in terminal
|
||||
``--video_dir VIDEO_DIR`` directory to write video
|
||||
``--config_file CONFIG_FILE`` Path to the custom configuration file
|
||||
========================================================== ====== =================================================================================================================================================================================================
|
||||
|
||||
custom_config
|
||||
@@ -85,5 +86,11 @@ following the directory structure:
|
||||
└── custom_config.yml
|
||||
|
||||
When you enter the ``project/`` folder and run ``manimgl code.py <Scene>``,
|
||||
it will overwrite ``manim/custom_config.yml`` with ``custom_config.yml``
|
||||
in the ``project`` folder.
|
||||
it will overwrite ``manim/default_config.yml`` with ``custom_config.yml``
|
||||
in the ``project`` folder.
|
||||
|
||||
Alternatively, you can use ``--config_file`` flag in CLI to specify configuration file manually.
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
manimgl project/code.py --config_file /path/to/custom_config.yml
|
||||
@@ -8,12 +8,12 @@ the simplest and one by one.
|
||||
InteractiveDevlopment
|
||||
---------------------
|
||||
|
||||
.. manim-example:: InteractiveDevlopment
|
||||
:media: ../_static/example_scenes/InteractiveDevlopment.mp4
|
||||
.. manim-example:: InteractiveDevelopment
|
||||
:media: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/example_scenes/InteractiveDevelopment.mp4
|
||||
|
||||
from manimlib import *
|
||||
|
||||
class InteractiveDevlopment(Scene):
|
||||
class InteractiveDevelopment(Scene):
|
||||
def construct(self):
|
||||
circle = Circle()
|
||||
circle.set_fill(BLUE, opacity=0.5)
|
||||
@@ -66,7 +66,7 @@ AnimatingMethods
|
||||
----------------
|
||||
|
||||
.. manim-example:: AnimatingMethods
|
||||
:media: ../_static/example_scenes/AnimatingMethods.mp4
|
||||
:media: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/example_scenes/AnimatingMethods.mp4
|
||||
|
||||
class AnimatingMethods(Scene):
|
||||
def construct(self):
|
||||
@@ -124,10 +124,12 @@ TextExample
|
||||
-----------
|
||||
|
||||
.. manim-example:: TextExample
|
||||
:media: ../_static/example_scenes/TextExample.mp4
|
||||
:media: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/example_scenes/TextExample.mp4
|
||||
|
||||
class TextExample(Scene):
|
||||
def construct(self):
|
||||
# To run this scene properly, you should have "Consolas" font in your computer
|
||||
# for full usage, you can see https://github.com/3b1b/manim/pull/680
|
||||
text = Text("Here is a text", font="Consolas", font_size=90)
|
||||
difference = Text(
|
||||
"""
|
||||
@@ -135,6 +137,7 @@ TextExample
|
||||
you can change the font more easily, but can't use the LaTeX grammar
|
||||
""",
|
||||
font="Arial", font_size=24,
|
||||
# t2c is a dict that you can choose color for different text
|
||||
t2c={"Text": BLUE, "TexText": BLUE, "LaTeX": ORANGE}
|
||||
)
|
||||
VGroup(text, difference).arrange(DOWN, buff=1)
|
||||
@@ -148,6 +151,7 @@ TextExample
|
||||
t2f={"font": "Consolas", "words": "Consolas"},
|
||||
t2c={"font": BLUE, "words": GREEN}
|
||||
)
|
||||
fonts.set_width(FRAME_WIDTH - 1)
|
||||
slant = Text(
|
||||
"And the same as slant and weight",
|
||||
font="Consolas",
|
||||
@@ -174,26 +178,30 @@ TexTransformExample
|
||||
-------------------
|
||||
|
||||
.. manim-example:: TexTransformExample
|
||||
:media: ../_static/example_scenes/TexTransformExample.mp4
|
||||
:media: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/example_scenes/TexTransformExample.mp4
|
||||
|
||||
class TexTransformExample(Scene):
|
||||
def construct(self):
|
||||
to_isolate = ["B", "C", "=", "(", ")"]
|
||||
lines = VGroup(
|
||||
# Surrounding substrings with double braces
|
||||
# will ensure that those parts are separated
|
||||
# out in the Tex. For example, here the
|
||||
# Tex will have 5 submobjects, corresponding
|
||||
# to the strings [A^2, +, B^2, =, C^2]
|
||||
Tex("{{A^2}} + {{B^2}} = {{C^2}}"),
|
||||
Tex("{{A^2}} = {{C^2}} - {{B^2}}"),
|
||||
# Passing in muliple arguments to Tex will result
|
||||
# in the same expression as if those arguments had
|
||||
# been joined together, except that the submobject
|
||||
# hierarchy of the resulting mobject ensure that the
|
||||
# Tex mobject has a subject corresponding to
|
||||
# each of these strings. For example, the Tex mobject
|
||||
# below will have 5 subjects, corresponding to the
|
||||
# expressions [A^2, +, B^2, =, C^2]
|
||||
Tex("A^2", "+", "B^2", "=", "C^2"),
|
||||
# Likewise here
|
||||
Tex("A^2", "=", "C^2", "-", "B^2"),
|
||||
# Alternatively, you can pass in the keyword argument
|
||||
# "isolate" with a list of strings that should be out as
|
||||
# their own submobject. So both lines below are equivalent
|
||||
# to what you'd get by wrapping every instance of "B", "C"
|
||||
# "=", "(" and ")" with double braces
|
||||
Tex("{{A^2}} = (C + B)(C - B)", isolate=to_isolate),
|
||||
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=to_isolate)
|
||||
# their own submobject. So the line below is equivalent
|
||||
# to the commented out line below it.
|
||||
Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
|
||||
# Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
|
||||
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
|
||||
)
|
||||
lines.arrange(DOWN, buff=LARGE_BUFF)
|
||||
for line in lines:
|
||||
@@ -252,7 +260,7 @@ TexTransformExample
|
||||
# new_line2 and the "\sqrt" from the final line. By passing in,
|
||||
# transform_mismatches=True, it will transform this "^2" part into
|
||||
# the "\sqrt" part.
|
||||
new_line2 = Tex("{{A}}^2 = (C + B)(C - B)", isolate=to_isolate)
|
||||
new_line2 = Tex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
|
||||
new_line2.replace(lines[2])
|
||||
new_line2.match_style(lines[2])
|
||||
|
||||
@@ -295,7 +303,7 @@ UpdatersExample
|
||||
---------------
|
||||
|
||||
.. manim-example:: UpdatersExample
|
||||
:media: ../_static/example_scenes/UpdatersExample.mp4
|
||||
:media: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/example_scenes/UpdatersExample.mp4
|
||||
|
||||
class UpdatersExample(Scene):
|
||||
def construct(self):
|
||||
@@ -327,7 +335,7 @@ UpdatersExample
|
||||
# If the argument itself might change, you can use f_always,
|
||||
# for which the arguments following the initial Mobject method
|
||||
# should be functions returning arguments to that method.
|
||||
# The following line ensures thst decimal.set_value(square.get_y())
|
||||
# The following line ensures that decimal.set_value(square.get_y())
|
||||
# is called every frame
|
||||
f_always(number.set_value, square.get_width)
|
||||
# You could also write the following equivalent line
|
||||
@@ -343,7 +351,7 @@ UpdatersExample
|
||||
)
|
||||
self.wait()
|
||||
self.play(
|
||||
square.set_width(5, stretch=True),
|
||||
square.animate.set_width(5, stretch=True),
|
||||
run_time=3,
|
||||
)
|
||||
self.wait()
|
||||
@@ -380,14 +388,14 @@ CoordinateSystemExample
|
||||
-----------------------
|
||||
|
||||
.. manim-example:: CoordinateSystemExample
|
||||
:media: ../_static/example_scenes/CoordinateSystemExample.mp4
|
||||
:media: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/example_scenes/CoordinateSystemExample.mp4
|
||||
|
||||
class CoordinateSystemExample(Scene):
|
||||
def construct(self):
|
||||
axes = Axes(
|
||||
# x-axis ranges from -1 to 10, with a default step size of 1
|
||||
x_range=(-1, 10),
|
||||
# y-axis ranges from -2 to 10 with a step size of 0.5
|
||||
# y-axis ranges from -2 to 2 with a step size of 0.5
|
||||
y_range=(-2, 2, 0.5),
|
||||
# The axes will be stretched so as to match the specified
|
||||
# height and width
|
||||
@@ -450,8 +458,7 @@ CoordinateSystemExample
|
||||
# system defined by them.
|
||||
f_always(dot.move_to, lambda: axes.c2p(1, 1))
|
||||
self.play(
|
||||
axes.animate.scale(0.75),
|
||||
axes.animate.to_corner(UL),
|
||||
axes.animate.scale(0.75).to_corner(UL),
|
||||
run_time=2,
|
||||
)
|
||||
self.wait()
|
||||
@@ -465,7 +472,7 @@ GraphExample
|
||||
------------
|
||||
|
||||
.. manim-example:: GraphExample
|
||||
:media: ../_static/example_scenes/GraphExample.mp4
|
||||
:media: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/example_scenes/GraphExample.mp4
|
||||
|
||||
class GraphExample(Scene):
|
||||
def construct(self):
|
||||
@@ -551,7 +558,7 @@ SurfaceExample
|
||||
--------------
|
||||
|
||||
.. manim-example:: SurfaceExample
|
||||
:media: ../_static/example_scenes/SurfaceExample.mp4
|
||||
:media: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/example_scenes/SurfaceExample.mp4
|
||||
|
||||
class SurfaceExample(Scene):
|
||||
CONFIG = {
|
||||
@@ -652,7 +659,7 @@ OpeningManimExample
|
||||
-------------------
|
||||
|
||||
.. manim-example:: OpeningManimExample
|
||||
:media: ../_static/example_scenes/OpeningManimExample.mp4
|
||||
:media: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/example_scenes/OpeningManimExample.mp4
|
||||
|
||||
|
||||
class OpeningManimExample(Scene):
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Manim runs on Python 3.8.
|
||||
Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
|
||||
|
||||
System requirements are:
|
||||
|
||||
- `FFmpeg <https://ffmpeg.org/>`__
|
||||
- `OpenGL <https://www.opengl.org//>`__ (included in python package ``PyOpenGL``)
|
||||
- `LaTeX <https://www.latex-project.org>`__ (optional, if you want to use LaTeX)
|
||||
- `Pango <https://pango.org>`__ (only for Linux)
|
||||
|
||||
|
||||
Directly
|
||||
--------
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
# Install manimgl
|
||||
pip install manimgl
|
||||
|
||||
# Try it out
|
||||
manimgl
|
||||
|
||||
If you want to hack on manimlib itself, clone this repository and in
|
||||
that directory execute:
|
||||
|
||||
@@ -56,4 +66,4 @@ For Anaconda
|
||||
cd manim
|
||||
conda create -n manim python=3.8
|
||||
conda activate manim
|
||||
pip install -e .
|
||||
pip install -e .
|
||||
|
||||
@@ -59,7 +59,7 @@ At this time, no window will pop up. When the program is finished, this rendered
|
||||
image will be automatically opened (saved in the subdirectory ``images/`` of the same
|
||||
level directory of ``start.py`` by default):
|
||||
|
||||
.. image:: ../_static/quickstart/SquareToCircle.png
|
||||
.. image:: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/quickstart/SquareToCircle.png
|
||||
:align: center
|
||||
|
||||
Make an image
|
||||
@@ -162,7 +162,7 @@ opened after the operation is over:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<video class="manim-video" controls loop autoplay src="../_static/quickstart/SquareToCircle.mp4"></video>
|
||||
<video class="manim-video" controls loop autoplay src="https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/quickstart/SquareToCircle.mp4"></video>
|
||||
|
||||
Let's take a look at the code this time. The first 7 lines are the same as the previous
|
||||
ones, and the 8th line is similar to the 5th line, which creates an instance of the
|
||||
@@ -221,7 +221,7 @@ For example: input the following lines (without comment lines) into it respectiv
|
||||
.. code-block:: python
|
||||
|
||||
# Stretched 4 times in the vertical direction
|
||||
play(circle.animate.stretch(4, dim=0}))
|
||||
play(circle.animate.stretch(4, dim=0))
|
||||
# Rotate the ellipse 90°
|
||||
play(Rotate(circle, TAU / 4))
|
||||
# Move 2 units to the right and shrink to 1/4 of the original
|
||||
@@ -237,7 +237,7 @@ You will get an animation similar to the following:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<video class="manim-video" controls loop autoplay src="../_static/quickstart/SquareToCircleEmbed.mp4"></video>
|
||||
<video class="manim-video" controls loop autoplay src="https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/quickstart/SquareToCircleEmbed.mp4"></video>
|
||||
|
||||
If you want to enter the interactive mode directly, you don't have to write an
|
||||
empty scene containing only ``self.embed()``, you can directly run the following command
|
||||
|
||||
@@ -99,6 +99,7 @@ Below is the directory structure of manim:
|
||||
├── config_ops.py # Process CONFIG
|
||||
├── customization.py # Read from custom_config.yml
|
||||
├── debug.py # Utilities for debugging in program
|
||||
├── directories.py # Read directories from config file
|
||||
├── family_ops.py # Process family members
|
||||
├── file_ops.py # Process files and directories
|
||||
├── images.py # Read image
|
||||
@@ -119,9 +120,9 @@ Inheritance structure of manim's classes
|
||||
is a pdf showed inheritance structure of manim's classes, large,
|
||||
but basically all classes have included:
|
||||
|
||||
.. image:: ../_static/manim_shaders_structure.png
|
||||
.. image:: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/manim_shaders_structure.png
|
||||
|
||||
Manim execution process
|
||||
-----------------------
|
||||
|
||||
.. image:: ../_static/manim_shaders_process_en.png
|
||||
.. image:: https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/manim_shaders_process_en.png
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Manim's documentation
|
||||
=====================
|
||||
|
||||
.. image:: ../../logo/white_with_name.png
|
||||
.. image:: https://cdn.jsdelivr.net/gh/3b1b/manim@master/logo/white_with_name.png
|
||||
|
||||
Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as seen in the videos
|
||||
at `3Blue1Brown <https://www.3blue1brown.com/>`_.
|
||||
|
||||
And here is a Chinese version of this documentation: https://manim.ml/shaders
|
||||
And here is a Chinese version of this documentation: https://docs.manim.org.cn/shaders
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -2,7 +2,7 @@ from manimlib import *
|
||||
import numpy as np
|
||||
|
||||
# To watch one of these scenes, run the following:
|
||||
# python -m manim example_scenes.py SquareToCircle
|
||||
# manimgl example_scenes.py OpeningManimExample
|
||||
# Use -s to skip to the end and just save the final frame
|
||||
# Use -w to write the animation to a file
|
||||
# Use -o to write it to a file and open it once done
|
||||
@@ -161,20 +161,24 @@ class TexTransformExample(Scene):
|
||||
def construct(self):
|
||||
to_isolate = ["B", "C", "=", "(", ")"]
|
||||
lines = VGroup(
|
||||
# Surrounding substrings with double braces
|
||||
# will ensure that those parts are separated
|
||||
# out in the Tex. For example, here the
|
||||
# Tex will have 5 submobjects, corresponding
|
||||
# to the strings [A^2, +, B^2, =, C^2]
|
||||
Tex("{{A^2}} + {{B^2}} = {{C^2}}"),
|
||||
Tex("{{A^2}} = {{C^2}} - {{B^2}}"),
|
||||
# Passing in muliple arguments to Tex will result
|
||||
# in the same expression as if those arguments had
|
||||
# been joined together, except that the submobject
|
||||
# hierarchy of the resulting mobject ensure that the
|
||||
# Tex mobject has a subject corresponding to
|
||||
# each of these strings. For example, the Tex mobject
|
||||
# below will have 5 subjects, corresponding to the
|
||||
# expressions [A^2, +, B^2, =, C^2]
|
||||
Tex("A^2", "+", "B^2", "=", "C^2"),
|
||||
# Likewise here
|
||||
Tex("A^2", "=", "C^2", "-", "B^2"),
|
||||
# Alternatively, you can pass in the keyword argument
|
||||
# "isolate" with a list of strings that should be out as
|
||||
# their own submobject. So both lines below are equivalent
|
||||
# to what you'd get by wrapping every instance of "B", "C"
|
||||
# "=", "(" and ")" with double braces
|
||||
Tex("{{A^2}} = (C + B)(C - B)", isolate=to_isolate),
|
||||
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=to_isolate)
|
||||
# their own submobject. So the line below is equivalent
|
||||
# to the commented out line below it.
|
||||
Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
|
||||
# Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
|
||||
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
|
||||
)
|
||||
lines.arrange(DOWN, buff=LARGE_BUFF)
|
||||
for line in lines:
|
||||
@@ -233,7 +237,7 @@ class TexTransformExample(Scene):
|
||||
# new_line2 and the "\sqrt" from the final line. By passing in,
|
||||
# transform_mismatches=True, it will transform this "^2" part into
|
||||
# the "\sqrt" part.
|
||||
new_line2 = Tex("{{A}}^2 = (C + B)(C - B)", isolate=to_isolate)
|
||||
new_line2 = Tex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
|
||||
new_line2.replace(lines[2])
|
||||
new_line2.match_style(lines[2])
|
||||
|
||||
@@ -328,7 +332,7 @@ class UpdatersExample(Scene):
|
||||
now = self.time
|
||||
w0 = square.get_width()
|
||||
square.add_updater(
|
||||
lambda m: m.set_width(w0 * math.cos(self.time - now))
|
||||
lambda m: m.set_width(w0 * math.sin(self.time - now) + w0)
|
||||
)
|
||||
self.wait(4 * PI)
|
||||
|
||||
@@ -338,7 +342,7 @@ class CoordinateSystemExample(Scene):
|
||||
axes = Axes(
|
||||
# x-axis ranges from -1 to 10, with a default step size of 1
|
||||
x_range=(-1, 10),
|
||||
# y-axis ranges from -2 to 10 with a step size of 0.5
|
||||
# y-axis ranges from -2 to 2 with a step size of 0.5
|
||||
y_range=(-2, 2, 0.5),
|
||||
# The axes will be stretched so as to match the specified
|
||||
# height and width
|
||||
@@ -401,8 +405,7 @@ class CoordinateSystemExample(Scene):
|
||||
# system defined by them.
|
||||
f_always(dot.move_to, lambda: axes.c2p(1, 1))
|
||||
self.play(
|
||||
axes.animate.scale(0.75),
|
||||
axes.animate.to_corner(UL),
|
||||
axes.animate.scale(0.75).to_corner(UL),
|
||||
run_time=2,
|
||||
)
|
||||
self.wait()
|
||||
@@ -584,7 +587,7 @@ class SurfaceExample(Scene):
|
||||
self.wait()
|
||||
|
||||
|
||||
class InteractiveDevlopment(Scene):
|
||||
class InteractiveDevelopment(Scene):
|
||||
def construct(self):
|
||||
circle = Circle()
|
||||
circle.set_fill(BLUE, opacity=0.5)
|
||||
@@ -636,14 +639,14 @@ class ControlsExample(Scene):
|
||||
self.checkbox = Checkbox()
|
||||
self.color_picker = ColorSliders()
|
||||
self.panel = ControlPanel(
|
||||
Text("Text", size=0.5), self.textbox, Line(),
|
||||
Text("Show/Hide Text", size=0.5), self.checkbox, Line(),
|
||||
Text("Color of Text", size=0.5), self.color_picker
|
||||
Text("Text", font_size=24), self.textbox, Line(),
|
||||
Text("Show/Hide Text", font_size=24), self.checkbox, Line(),
|
||||
Text("Color of Text", font_size=24), self.color_picker
|
||||
)
|
||||
self.add(self.panel)
|
||||
|
||||
def construct(self):
|
||||
text = Text("", size=2)
|
||||
text = Text("text", font_size=96)
|
||||
|
||||
def text_updater(old_text):
|
||||
assert(isinstance(old_text, Text))
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import pkg_resources
|
||||
|
||||
__version__ = pkg_resources.get_distribution("manimgl").version
|
||||
|
||||
from manimlib.constants import *
|
||||
|
||||
from manimlib.animation.animation import *
|
||||
|
||||
@@ -2,16 +2,24 @@
|
||||
import manimlib.config
|
||||
import manimlib.extract_scene
|
||||
import manimlib.utils.init_config
|
||||
from manimlib import __version__
|
||||
|
||||
|
||||
def main():
|
||||
print(f"ManimGL \033[32mv{__version__}\033[0m")
|
||||
|
||||
args = manimlib.config.parse_cli()
|
||||
|
||||
if args.version and args.file == None:
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -26,7 +26,7 @@ class Animation(object):
|
||||
# If 0 < lag_ratio < 1, its applied to each
|
||||
# with lagged start times
|
||||
"lag_ratio": DEFAULT_ANIMATION_LAG_RATIO,
|
||||
"suspend_mobject_updating": False,
|
||||
"suspend_mobject_updating": True,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
|
||||
@@ -25,6 +25,10 @@ class ShowPartial(Animation):
|
||||
if not self.should_match_start:
|
||||
self.mobject.lock_matching_data(self.mobject, self.starting_mobject)
|
||||
|
||||
def finish(self):
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
|
||||
def interpolate_submobject(self, submob, start_submob, alpha):
|
||||
submob.pointwise_become_partial(
|
||||
start_submob, *self.get_bounds(alpha)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import numpy as np
|
||||
import math
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.animation.animation import Animation
|
||||
@@ -8,17 +9,21 @@ from manimlib.animation.composition import Succession
|
||||
from manimlib.animation.creation import ShowCreation
|
||||
from manimlib.animation.creation import ShowPartial
|
||||
from manimlib.animation.fading import FadeOut
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.shape_matchers import SurroundingRectangle
|
||||
from manimlib.mobject.shape_matchers import Underline
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import there_and_back
|
||||
from manimlib.utils.rate_functions import wiggle
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.rate_functions import squish_rate_func
|
||||
|
||||
|
||||
class FocusOn(Transform):
|
||||
@@ -152,6 +157,87 @@ class ShowPassingFlash(ShowPartial):
|
||||
submob.pointwise_become_partial(start, 0, 1)
|
||||
|
||||
|
||||
class VShowPassingFlash(Animation):
|
||||
CONFIG = {
|
||||
"time_width": 0.3,
|
||||
"taper_width": 0.02,
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def begin(self):
|
||||
self.mobject.align_stroke_width_data_to_points()
|
||||
# Compute an array of stroke widths for each submobject
|
||||
# which tapers out at either end
|
||||
self.submob_to_anchor_widths = dict()
|
||||
for sm in self.mobject.get_family():
|
||||
original_widths = sm.get_stroke_widths()
|
||||
anchor_widths = np.array([*original_widths[0::3], original_widths[-1]])
|
||||
|
||||
def taper_kernel(x):
|
||||
if x < self.taper_width:
|
||||
return x
|
||||
elif x > 1 - self.taper_width:
|
||||
return 1.0 - x
|
||||
return 1.0
|
||||
|
||||
taper_array = list(map(taper_kernel, np.linspace(0, 1, len(anchor_widths))))
|
||||
self.submob_to_anchor_widths[hash(sm)] = anchor_widths * taper_array
|
||||
super().begin()
|
||||
|
||||
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
|
||||
anchor_widths = self.submob_to_anchor_widths[hash(submobject)]
|
||||
# Create a gaussian such that 3 sigmas out on either side
|
||||
# will equals time_width
|
||||
tw = self.time_width
|
||||
sigma = tw / 6
|
||||
mu = interpolate(-tw / 2, 1 + tw / 2, alpha)
|
||||
|
||||
def gauss_kernel(x):
|
||||
if abs(x - mu) > 3 * sigma:
|
||||
return 0
|
||||
z = (x - mu) / sigma
|
||||
return math.exp(-0.5 * z * z)
|
||||
|
||||
kernel_array = list(map(gauss_kernel, np.linspace(0, 1, len(anchor_widths))))
|
||||
scaled_widths = anchor_widths * kernel_array
|
||||
new_widths = np.zeros(submobject.get_num_points())
|
||||
new_widths[0::3] = scaled_widths[:-1]
|
||||
new_widths[2::3] = scaled_widths[1:]
|
||||
new_widths[1::3] = (new_widths[0::3] + new_widths[2::3]) / 2
|
||||
submobject.set_stroke(width=new_widths)
|
||||
|
||||
def finish(self):
|
||||
super().finish()
|
||||
for submob, start in self.get_all_families_zipped():
|
||||
submob.match_style(start)
|
||||
|
||||
|
||||
class FlashAround(VShowPassingFlash):
|
||||
CONFIG = {
|
||||
"stroke_width": 4.0,
|
||||
"color": YELLOW,
|
||||
"buff": SMALL_BUFF,
|
||||
"time_width": 1.0,
|
||||
"n_inserted_curves": 20,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
path = self.get_path(mobject)
|
||||
path.insert_n_curves(self.n_inserted_curves)
|
||||
path.set_points(path.get_points_without_null_curves())
|
||||
path.set_stroke(self.color, self.stroke_width)
|
||||
super().__init__(path, **kwargs)
|
||||
|
||||
def get_path(self, mobject):
|
||||
return SurroundingRectangle(mobject, buff=self.buff)
|
||||
|
||||
|
||||
class FlashUnder(FlashAround):
|
||||
def get_path(self, mobject):
|
||||
return Underline(mobject, buff=self.buff)
|
||||
|
||||
|
||||
class ShowCreationThenDestruction(ShowPassingFlash):
|
||||
CONFIG = {
|
||||
"time_width": 2.0,
|
||||
@@ -278,3 +364,22 @@ class TurnInsideOut(Transform):
|
||||
|
||||
def create_target(self):
|
||||
return self.mobject.copy().reverse_points()
|
||||
|
||||
|
||||
class FlashyFadeIn(AnimationGroup):
|
||||
CONFIG = {
|
||||
"fade_lag": 0,
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, stroke_width=2, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
outline = vmobject.copy()
|
||||
outline.set_fill(opacity=0)
|
||||
outline.set_stroke(width=stroke_width, opacity=1)
|
||||
|
||||
rate_func = kwargs.get("rate_func", smooth)
|
||||
super().__init__(
|
||||
FadeIn(vmobject, rate_func=squish_rate_func(rate_func, self.fade_lag, 1)),
|
||||
VShowPassingFlash(outline, time_width=1),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ class Transform(Animation):
|
||||
self.check_target_mobject_validity()
|
||||
# Use a copy of target_mobject for the align_data_and_family
|
||||
# call so that the actual target_mobject stays
|
||||
# preserved, since calling allign_data will potentailly
|
||||
# preserved, since calling allign_data will potentially
|
||||
# change the structure of both arguments
|
||||
self.target_copy = self.target_mobject.copy()
|
||||
self.mobject.align_data_and_family(self.target_copy)
|
||||
@@ -161,7 +161,7 @@ class ApplyMethod(Transform):
|
||||
method is a method of Mobject, *args are arguments for
|
||||
that method. Key word arguments should be passed in
|
||||
as the last arg, as a dict, since **kwargs is for
|
||||
configuration of the transform itslef
|
||||
configuration of the transform itself
|
||||
|
||||
Relies on the fact that mobject methods return the mobject
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,6 @@ from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
|
||||
|
||||
@@ -129,7 +128,7 @@ class TransformMatchingShapes(TransformMatchingParts):
|
||||
|
||||
class TransformMatchingTex(TransformMatchingParts):
|
||||
CONFIG = {
|
||||
"mobject_type": Tex,
|
||||
"mobject_type": VMobject,
|
||||
"group_type": VGroup,
|
||||
}
|
||||
|
||||
|
||||
@@ -77,16 +77,24 @@ class CameraFrame(Mobject):
|
||||
self.set_euler_angles(theta, phi, gamma)
|
||||
return self
|
||||
|
||||
def set_euler_angles(self, theta=None, phi=None, gamma=None):
|
||||
def set_euler_angles(self, theta=None, phi=None, gamma=None, units=RADIANS):
|
||||
if theta is not None:
|
||||
self.data["euler_angles"][0] = theta
|
||||
self.data["euler_angles"][0] = theta * units
|
||||
if phi is not None:
|
||||
self.data["euler_angles"][1] = phi
|
||||
self.data["euler_angles"][1] = phi * units
|
||||
if gamma is not None:
|
||||
self.data["euler_angles"][2] = gamma
|
||||
self.data["euler_angles"][2] = gamma * units
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def reorient(self, theta_degrees=None, phi_degrees=None, gamma_degrees=None):
|
||||
"""
|
||||
Shortcut for set_euler_angles, defaulting to taking
|
||||
in angles in degrees
|
||||
"""
|
||||
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES)
|
||||
return self
|
||||
|
||||
def set_theta(self, theta):
|
||||
return self.set_euler_angles(theta=theta)
|
||||
|
||||
@@ -308,17 +316,6 @@ class Camera(object):
|
||||
self.frame.set_height(frame_height)
|
||||
self.frame.set_width(frame_width)
|
||||
|
||||
def pixel_coords_to_space_coords(self, px, py, relative=False):
|
||||
pw, ph = self.fbo.size
|
||||
fw, fh = self.get_frame_shape()
|
||||
fc = self.get_frame_center()
|
||||
if relative:
|
||||
return 2 * np.array([px / pw, py / ph, 0])
|
||||
else:
|
||||
# Only scale wrt one axis
|
||||
scale = fh / ph
|
||||
return fc + scale * np.array([(px - pw / 2), (py - ph / 2), 0])
|
||||
|
||||
# Rendering
|
||||
def capture(self, *mobjects, **kwargs):
|
||||
self.refresh_perspective_uniforms()
|
||||
@@ -415,6 +412,8 @@ class Camera(object):
|
||||
shader[name].value = tid
|
||||
for name, value in it.chain(shader_wrapper.uniforms.items(), self.perspective_uniforms.items()):
|
||||
try:
|
||||
if isinstance(value, np.ndarray):
|
||||
value = tuple(value)
|
||||
shader[name].value = value
|
||||
except KeyError:
|
||||
pass
|
||||
@@ -441,21 +440,28 @@ class Camera(object):
|
||||
}
|
||||
|
||||
def init_textures(self):
|
||||
self.path_to_texture_id = {}
|
||||
self.n_textures = 0
|
||||
self.path_to_texture = {}
|
||||
|
||||
def get_texture_id(self, path):
|
||||
if path not in self.path_to_texture_id:
|
||||
# A way to increase tid's sequentially
|
||||
tid = len(self.path_to_texture_id)
|
||||
im = Image.open(path)
|
||||
if path not in self.path_to_texture:
|
||||
tid = self.n_textures
|
||||
self.n_textures += 1
|
||||
im = Image.open(path).convert("RGBA")
|
||||
texture = self.ctx.texture(
|
||||
size=im.size,
|
||||
components=len(im.getbands()),
|
||||
data=im.tobytes(),
|
||||
)
|
||||
texture.use(location=tid)
|
||||
self.path_to_texture_id[path] = tid
|
||||
return self.path_to_texture_id[path]
|
||||
self.path_to_texture[path] = (tid, texture)
|
||||
return self.path_to_texture[path][0]
|
||||
|
||||
def release_texture(self, path):
|
||||
tid_and_texture = self.path_to_texture.pop(path, None)
|
||||
if tid_and_texture:
|
||||
tid_and_texture[1].release()
|
||||
return self
|
||||
|
||||
|
||||
# Mostly just defined so old scenes don't break
|
||||
|
||||
@@ -9,6 +9,7 @@ from screeninfo import get_monitors
|
||||
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.init_config import init_customization
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
def parse_cli():
|
||||
@@ -130,12 +131,21 @@ def parse_cli():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--video_dir",
|
||||
help="directory to write video",
|
||||
help="Directory to write video",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config_file",
|
||||
help="Path to the custom configuration file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--version",
|
||||
action="store_true",
|
||||
help="Display the version of manimgl"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
except argparse.ArgumentError as err:
|
||||
print(str(err))
|
||||
log.error(str(err))
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
@@ -155,17 +165,19 @@ def get_module(file_name):
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
__config_file__ = "custom_config.yml"
|
||||
|
||||
def get_custom_config():
|
||||
filename = "custom_config.yml"
|
||||
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:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
if os.path.exists(filename):
|
||||
with open(filename, "r") as file:
|
||||
if os.path.exists(__config_file__):
|
||||
with open(__config_file__, "r") as file:
|
||||
local_defaults = yaml.safe_load(file)
|
||||
if local_defaults:
|
||||
config = merge_dicts_recursively(
|
||||
@@ -173,23 +185,51 @@ def get_custom_config():
|
||||
local_defaults,
|
||||
)
|
||||
else:
|
||||
with open(filename, "r") as file:
|
||||
with open(__config_file__, "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def check_temporary_storage(config):
|
||||
if 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)")
|
||||
|
||||
|
||||
def get_configuration(args):
|
||||
local_config_file = "custom_config.yml"
|
||||
global __config_file__
|
||||
|
||||
# ensure __config_file__ always exists
|
||||
if args.config_file is not None:
|
||||
if not os.path.exists(args.config_file):
|
||||
log.error(f"Can't find {args.config_file}.")
|
||||
if sys.platform == 'win32':
|
||||
log.info(f"Copying default configuration file to {args.config_file}...")
|
||||
os.system(f"copy default_config.yml {args.config_file}")
|
||||
elif sys.platform in ["linux2", "darwin"]:
|
||||
log.info(f"Copying default configuration file to {args.config_file}...")
|
||||
os.system(f"cp default_config.yml {args.config_file}")
|
||||
else:
|
||||
log.info("Please create the configuration file manually.")
|
||||
log.info("Read configuration from default_config.yml.")
|
||||
else:
|
||||
__config_file__ = args.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(local_config_file)):
|
||||
print("There is no configuration file detected. Initial configuration:\n")
|
||||
|
||||
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(local_config_file):
|
||||
print(f"""Warning: Using the default configuration file, which you can modify in {global_defaults_file}
|
||||
If you want to create a local configuration file, you can create a file named {local_config_file}, or run manimgl --config
|
||||
""")
|
||||
|
||||
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`")
|
||||
|
||||
custom_config = get_custom_config()
|
||||
check_temporary_storage(custom_config)
|
||||
|
||||
write_file = any([args.write_file, args.open, args.finder])
|
||||
if args.transparent:
|
||||
@@ -234,7 +274,9 @@ def get_configuration(args):
|
||||
|
||||
# Default to making window half the screen size
|
||||
# but make it full screen if -f is passed in
|
||||
monitor = get_monitors()[custom_config["window_monitor"]]
|
||||
monitors = get_monitors()
|
||||
mon_index = custom_config["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
window_width = monitor.width
|
||||
if not args.full_screen:
|
||||
window_width //= 2
|
||||
@@ -292,9 +334,9 @@ def get_camera_configuration(args, custom_config):
|
||||
try:
|
||||
bg_color = args.color or custom_config["style"]["background_color"]
|
||||
camera_config["background_color"] = colour.Color(bg_color)
|
||||
except AttributeError as err:
|
||||
print("Please use a valid color")
|
||||
print(err)
|
||||
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
|
||||
|
||||
@@ -50,6 +50,9 @@ RIGHT_SIDE = FRAME_X_RADIUS * RIGHT
|
||||
PI = np.pi
|
||||
TAU = 2 * PI
|
||||
DEGREES = TAU / 360
|
||||
# Nice to have a constant for readability
|
||||
# when juxtaposed with expressions like 30 * DEGREES
|
||||
RADIANS = 1
|
||||
|
||||
FFMPEG_BIN = "ffmpeg"
|
||||
|
||||
@@ -136,3 +139,5 @@ RED = RED_C
|
||||
MAROON = MAROON_C
|
||||
PURPLE = PURPLE_C
|
||||
GREY = GREY_C
|
||||
|
||||
COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import inspect
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
class BlankScene(Scene):
|
||||
@@ -41,8 +41,11 @@ def prompt_user_for_choice(scene_classes):
|
||||
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(",")
|
||||
]
|
||||
except IndexError:
|
||||
log.error("Invalid scene number")
|
||||
sys.exit(2)
|
||||
except KeyError:
|
||||
logging.log(logging.ERROR, "Invalid scene")
|
||||
log.error("Invalid scene name")
|
||||
sys.exit(2)
|
||||
except EOFError:
|
||||
sys.exit(1)
|
||||
@@ -78,10 +81,7 @@ def get_scenes_to_render(scene_classes, scene_config, config):
|
||||
found = True
|
||||
break
|
||||
if not found and (scene_name != ""):
|
||||
logging.log(
|
||||
logging.ERROR,
|
||||
f"No scene named {scene_name} found",
|
||||
)
|
||||
log.error(f"No scene named {scene_name} found")
|
||||
if result:
|
||||
return result
|
||||
if len(scene_classes) == 1:
|
||||
|
||||
12
manimlib/logger.py
Normal file
12
manimlib/logger.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import logging
|
||||
from rich.logging import RichHandler
|
||||
|
||||
__all__ = ["log"]
|
||||
|
||||
|
||||
FORMAT = "%(message)s"
|
||||
logging.basicConfig(
|
||||
level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||
)
|
||||
|
||||
log = logging.getLogger("rich")
|
||||
@@ -1,8 +1,13 @@
|
||||
from manimlib.constants import *
|
||||
import numpy as np
|
||||
from manimlib.constants import BLUE_D
|
||||
from manimlib.constants import BLUE_B
|
||||
from manimlib.constants import BLUE_E
|
||||
from manimlib.constants import GREY_BROWN
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
|
||||
class AnimatedBoundary(VGroup):
|
||||
@@ -74,25 +79,63 @@ class TracedPath(VMobject):
|
||||
CONFIG = {
|
||||
"stroke_width": 2,
|
||||
"stroke_color": WHITE,
|
||||
"min_distance_to_new_point": 0.1,
|
||||
"time_traced": np.inf,
|
||||
"fill_opacity": 0,
|
||||
"time_per_anchor": 1 / 15,
|
||||
}
|
||||
|
||||
def __init__(self, traced_point_func, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.add_updater(lambda m: m.update_path())
|
||||
self.time = 0
|
||||
self.traced_points = []
|
||||
self.add_updater(lambda m, dt: m.update_path(dt))
|
||||
|
||||
def update_path(self):
|
||||
new_point = self.traced_point_func()
|
||||
if not self.has_points():
|
||||
self.start_new_path(new_point)
|
||||
self.add_line_to(new_point)
|
||||
def update_path(self, dt):
|
||||
if dt == 0:
|
||||
return self
|
||||
point = self.traced_point_func().copy()
|
||||
self.traced_points.append(point)
|
||||
|
||||
if self.time_traced < np.inf:
|
||||
n_relevant_points = int(self.time_traced / dt + 0.5)
|
||||
# n_anchors = int(self.time_traced / self.time_per_anchor)
|
||||
n_tps = len(self.traced_points)
|
||||
if n_tps < n_relevant_points:
|
||||
points = self.traced_points + [point] * (n_relevant_points - n_tps)
|
||||
else:
|
||||
points = self.traced_points[n_tps - n_relevant_points:]
|
||||
# points = [
|
||||
# self.traced_points[max(n_tps - int(alpha * n_relevant_points) - 1, 0)]
|
||||
# for alpha in np.linspace(1, 0, n_anchors)
|
||||
# ]
|
||||
# Every now and then refresh the list
|
||||
if n_tps > 10 * n_relevant_points:
|
||||
self.traced_points = self.traced_points[-n_relevant_points:]
|
||||
else:
|
||||
# Set the end to be the new point
|
||||
self.get_points()[-1] = new_point
|
||||
# sparseness = max(int(self.time_per_anchor / dt), 1)
|
||||
# points = self.traced_points[::sparseness]
|
||||
# points[-1] = self.traced_points[-1]
|
||||
points = self.traced_points
|
||||
|
||||
# Second to last point
|
||||
nppcc = self.n_points_per_curve
|
||||
dist = get_norm(new_point - self.get_points()[-nppcc])
|
||||
if dist >= self.min_distance_to_new_point:
|
||||
self.add_line_to(new_point)
|
||||
if points:
|
||||
self.set_points_smoothly(points)
|
||||
|
||||
self.time += dt
|
||||
return self
|
||||
|
||||
|
||||
class TracingTail(TracedPath):
|
||||
CONFIG = {
|
||||
"stroke_width": (0, 3),
|
||||
"stroke_opacity": (0, 1),
|
||||
"stroke_color": WHITE,
|
||||
"time_traced": 1.0,
|
||||
}
|
||||
|
||||
def __init__(self, mobject_or_func, **kwargs):
|
||||
if isinstance(mobject_or_func, Mobject):
|
||||
func = mobject_or_func.get_center
|
||||
else:
|
||||
func = mobject_or_func
|
||||
super().__init__(func, **kwargs)
|
||||
|
||||
@@ -10,6 +10,7 @@ from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.number_line import NumberLine
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.simple_functions import binary_search
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
@@ -25,13 +26,18 @@ class CoordinateSystem():
|
||||
"""
|
||||
CONFIG = {
|
||||
"dimension": 2,
|
||||
"x_range": np.array([-8, 8, 1.0]),
|
||||
"y_range": np.array([-4, 4, 1.0]),
|
||||
"width": None,
|
||||
"height": None,
|
||||
"num_sampled_graph_points_per_tick": 5,
|
||||
"default_x_range": [-8.0, 8.0, 1.0],
|
||||
"default_y_range": [-4.0, 4.0, 1.0],
|
||||
"width": FRAME_WIDTH,
|
||||
"height": FRAME_HEIGHT,
|
||||
"num_sampled_graph_points_per_tick": 20,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.x_range = np.array(self.default_x_range)
|
||||
self.y_range = np.array(self.default_y_range)
|
||||
|
||||
def coords_to_point(self, *coords):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
@@ -46,9 +52,15 @@ class CoordinateSystem():
|
||||
"""Abbreviation for point_to_coords"""
|
||||
return self.point_to_coords(point)
|
||||
|
||||
def get_origin(self):
|
||||
return self.c2p(*[0] * self.dimension)
|
||||
|
||||
def get_axes(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def get_all_ranges(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def get_axis(self, index):
|
||||
return self.get_axes()[index]
|
||||
|
||||
@@ -121,6 +133,7 @@ class CoordinateSystem():
|
||||
**kwargs
|
||||
)
|
||||
graph.underlying_function = function
|
||||
graph.x_range = x_range
|
||||
return graph
|
||||
|
||||
def get_parametric_curve(self, function, **kwargs):
|
||||
@@ -276,7 +289,9 @@ class Axes(VGroup, CoordinateSystem):
|
||||
x_range=None,
|
||||
y_range=None,
|
||||
**kwargs):
|
||||
super().__init__(**kwargs)
|
||||
CoordinateSystem.__init__(self, **kwargs)
|
||||
VGroup.__init__(self, **kwargs)
|
||||
|
||||
if x_range is not None:
|
||||
self.x_range[:len(x_range)] = x_range
|
||||
if y_range is not None:
|
||||
@@ -319,6 +334,9 @@ class Axes(VGroup, CoordinateSystem):
|
||||
def get_axes(self):
|
||||
return self.axes
|
||||
|
||||
def get_all_ranges(self):
|
||||
return [self.x_range, self.y_range]
|
||||
|
||||
def add_coordinate_labels(self,
|
||||
x_values=None,
|
||||
y_values=None,
|
||||
@@ -334,11 +352,13 @@ class Axes(VGroup, CoordinateSystem):
|
||||
class ThreeDAxes(Axes):
|
||||
CONFIG = {
|
||||
"dimension": 3,
|
||||
"x_range": np.array([-6, 6, 1]),
|
||||
"y_range": np.array([-5, 5, 1]),
|
||||
"z_range": np.array([-4, 4, 1]),
|
||||
"x_range": np.array([-6.0, 6.0, 1.0]),
|
||||
"y_range": np.array([-5.0, 5.0, 1.0]),
|
||||
"z_range": np.array([-4.0, 4.0, 1.0]),
|
||||
"z_axis_config": {},
|
||||
"z_normal": DOWN,
|
||||
"height": None,
|
||||
"width": None,
|
||||
"depth": None,
|
||||
"num_axis_pieces": 20,
|
||||
"gloss": 0.5,
|
||||
@@ -346,9 +366,11 @@ class ThreeDAxes(Axes):
|
||||
|
||||
def __init__(self, x_range=None, y_range=None, z_range=None, **kwargs):
|
||||
Axes.__init__(self, x_range, y_range, **kwargs)
|
||||
if z_range is not None:
|
||||
self.z_range[:len(z_range)] = z_range
|
||||
|
||||
z_axis = self.create_axis(
|
||||
z_range or self.z_range,
|
||||
self.z_range,
|
||||
self.z_axis_config,
|
||||
self.depth,
|
||||
)
|
||||
@@ -365,6 +387,9 @@ class ThreeDAxes(Axes):
|
||||
for axis in self.axes:
|
||||
axis.insert_n_curves(self.num_axis_pieces - 1)
|
||||
|
||||
def get_all_ranges(self):
|
||||
return [self.x_range, self.y_range, self.z_range]
|
||||
|
||||
|
||||
class NumberPlane(Axes):
|
||||
CONFIG = {
|
||||
@@ -388,7 +413,7 @@ class NumberPlane(Axes):
|
||||
"width": None,
|
||||
# Defaults to a faded version of line_config
|
||||
"faded_line_style": None,
|
||||
"faded_line_ratio": 1,
|
||||
"faded_line_ratio": 4,
|
||||
"make_smooth_after_applying_functions": True,
|
||||
}
|
||||
|
||||
@@ -425,7 +450,7 @@ class NumberPlane(Axes):
|
||||
return lines1, lines2
|
||||
|
||||
def get_lines_parallel_to_axis(self, axis1, axis2):
|
||||
freq = axis1.x_step
|
||||
freq = axis2.x_step
|
||||
ratio = self.faded_line_ratio
|
||||
line = Line(axis1.get_start(), axis1.get_end())
|
||||
dense_freq = (1 + ratio)
|
||||
@@ -485,15 +510,15 @@ class ComplexPlane(NumberPlane):
|
||||
def p2n(self, point):
|
||||
return self.point_to_number(point)
|
||||
|
||||
def get_default_coordinate_values(self):
|
||||
def get_default_coordinate_values(self, skip_first=True):
|
||||
x_numbers = self.get_x_axis().get_tick_range()[1:]
|
||||
y_numbers = self.get_y_axis().get_tick_range()[1:]
|
||||
y_numbers = [complex(0, y) for y in y_numbers if y != 0]
|
||||
return [*x_numbers, *y_numbers]
|
||||
|
||||
def add_coordinate_labels(self, numbers=None, **kwargs):
|
||||
def add_coordinate_labels(self, numbers=None, skip_first=True, **kwargs):
|
||||
if numbers is None:
|
||||
numbers = self.get_default_coordinate_values()
|
||||
numbers = self.get_default_coordinate_values(skip_first)
|
||||
|
||||
self.coordinate_labels = VGroup()
|
||||
for number in numbers:
|
||||
@@ -506,6 +531,15 @@ class ComplexPlane(NumberPlane):
|
||||
axis = self.get_x_axis()
|
||||
value = z.real
|
||||
number_mob = axis.get_number_mobject(value, **kwargs)
|
||||
# For i and -i, remove the "1"
|
||||
if z.imag == 1:
|
||||
number_mob.remove(number_mob[0])
|
||||
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
|
||||
|
||||
@@ -20,6 +20,9 @@ class ScreenRectangle(Rectangle):
|
||||
class FullScreenRectangle(ScreenRectangle):
|
||||
CONFIG = {
|
||||
"height": FRAME_HEIGHT,
|
||||
"fill_color": GREY_E,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import numpy as np
|
||||
import math
|
||||
import numbers
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -27,6 +29,7 @@ DEFAULT_ARROW_TIP_LENGTH = 0.35
|
||||
DEFAULT_ARROW_TIP_WIDTH = 0.35
|
||||
|
||||
|
||||
# Deprecate?
|
||||
class TipableVMobject(VMobject):
|
||||
"""
|
||||
Meant for shared functionality between Arc and Line.
|
||||
@@ -49,6 +52,7 @@ class TipableVMobject(VMobject):
|
||||
"tip_config": {
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
"tip_style": 0, # triangle=0, inner_smooth=1, dot=2
|
||||
},
|
||||
"normal_vector": OUT,
|
||||
}
|
||||
@@ -63,6 +67,7 @@ class TipableVMobject(VMobject):
|
||||
tip = self.create_tip(at_start, **kwargs)
|
||||
self.reset_endpoints_based_on_tip(tip, at_start)
|
||||
self.asign_tip_attr(tip, at_start)
|
||||
tip.set_color(self.get_stroke_color())
|
||||
self.add(tip)
|
||||
return self
|
||||
|
||||
@@ -402,32 +407,38 @@ class Line(TipableVMobject):
|
||||
self.set_points_by_ends(self.start, self.end, self.buff, self.path_arc)
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
if path_arc:
|
||||
self.set_points(Arc.create_quadratic_bezier_points(path_arc))
|
||||
self.put_start_and_end_on(start, end)
|
||||
else:
|
||||
vect = end - start
|
||||
dist = get_norm(vect)
|
||||
if np.isclose(dist, 0):
|
||||
self.set_points_as_corners([start, end])
|
||||
self.account_for_buff(self.buff)
|
||||
return self
|
||||
if path_arc:
|
||||
neg = path_arc < 0
|
||||
if neg:
|
||||
path_arc = -path_arc
|
||||
start, end = end, start
|
||||
radius = (dist / 2) / math.sin(path_arc / 2)
|
||||
alpha = (PI - path_arc) / 2
|
||||
center = start + radius * normalize(rotate_vector(end - start, alpha))
|
||||
|
||||
raw_arc_points = Arc.create_quadratic_bezier_points(
|
||||
angle=path_arc - 2 * buff / radius,
|
||||
start_angle=angle_of_vector(start - center) + buff / radius,
|
||||
)
|
||||
if neg:
|
||||
raw_arc_points = raw_arc_points[::-1]
|
||||
self.set_points(center + radius * raw_arc_points)
|
||||
else:
|
||||
if buff > 0 and dist > 0:
|
||||
start = start + vect * (buff / dist)
|
||||
end = end - vect * (buff / dist)
|
||||
self.set_points_as_corners([start, end])
|
||||
return self
|
||||
|
||||
def set_path_arc(self, new_value):
|
||||
self.path_arc = new_value
|
||||
self.init_points()
|
||||
|
||||
def account_for_buff(self, buff):
|
||||
if buff == 0:
|
||||
return
|
||||
#
|
||||
if self.path_arc == 0:
|
||||
length = self.get_length()
|
||||
else:
|
||||
length = self.get_arc_length()
|
||||
#
|
||||
if length < 2 * buff:
|
||||
return
|
||||
buff_prop = buff / length
|
||||
self.pointwise_become_partial(self, buff_prop, 1 - buff_prop)
|
||||
return self
|
||||
|
||||
def set_start_and_end_attrs(self, start, end):
|
||||
# If either start or end are Mobjects, this
|
||||
# gives their centers
|
||||
@@ -437,8 +448,8 @@ class Line(TipableVMobject):
|
||||
# Now that we know the direction between them,
|
||||
# we can find the appropriate boundary point from
|
||||
# start and end, if they're mobjects
|
||||
self.start = self.pointify(start, vect) + self.buff * vect
|
||||
self.end = self.pointify(end, -vect) - self.buff * vect
|
||||
self.start = self.pointify(start, vect)
|
||||
self.end = self.pointify(end, -vect)
|
||||
|
||||
def pointify(self, mob_or_point, direction=None):
|
||||
"""
|
||||
@@ -459,8 +470,10 @@ class Line(TipableVMobject):
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
curr_start, curr_end = self.get_start_and_end()
|
||||
if (curr_start == curr_end).all():
|
||||
self.set_points_by_ends(start, end, self.path_arc)
|
||||
if np.isclose(curr_start, curr_end).all():
|
||||
# Handle null lines more gracefully
|
||||
self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
|
||||
return self
|
||||
return super().put_start_and_end_on(start, end)
|
||||
|
||||
def get_vector(self):
|
||||
@@ -492,8 +505,8 @@ class Line(TipableVMobject):
|
||||
)
|
||||
return self
|
||||
|
||||
def set_length(self, length):
|
||||
self.scale(length / self.get_length())
|
||||
def set_length(self, length, **kwargs):
|
||||
self.scale(length / self.get_length(), **kwargs)
|
||||
|
||||
|
||||
class DashedLine(Line):
|
||||
@@ -569,13 +582,87 @@ class Elbow(VMobject):
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(self, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.set_points_as_corners([UP, UP + RIGHT, RIGHT])
|
||||
self.set_width(self.width, about_point=ORIGIN)
|
||||
self.rotate(self.angle, about_point=ORIGIN)
|
||||
|
||||
|
||||
class Arrow(Line):
|
||||
CONFIG = {
|
||||
"stroke_color": GREY_A,
|
||||
"stroke_width": 5,
|
||||
"tip_width_ratio": 4,
|
||||
"width_to_tip_len": 0.0075,
|
||||
"max_tip_length_to_length_ratio": 0.3,
|
||||
"max_width_to_length_ratio": 10,
|
||||
"buff": 0.25,
|
||||
}
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
super().set_points_by_ends(start, end, buff, path_arc)
|
||||
self.insert_tip_anchor()
|
||||
return self
|
||||
|
||||
def init_colors(self):
|
||||
super().init_colors()
|
||||
self.create_tip_with_stroke_width()
|
||||
|
||||
def get_arc_length(self):
|
||||
# Push up into Line?
|
||||
arc_len = get_norm(self.get_vector())
|
||||
if self.path_arc > 0:
|
||||
arc_len *= self.path_arc / (2 * math.sin(self.path_arc / 2))
|
||||
return arc_len
|
||||
|
||||
def insert_tip_anchor(self):
|
||||
prev_end = self.get_end()
|
||||
arc_len = self.get_arc_length()
|
||||
tip_len = self.get_stroke_width() * self.width_to_tip_len * self.tip_width_ratio
|
||||
if tip_len >= self.max_tip_length_to_length_ratio * arc_len:
|
||||
alpha = self.max_tip_length_to_length_ratio
|
||||
else:
|
||||
alpha = tip_len / arc_len
|
||||
self.pointwise_become_partial(self, 0, 1 - alpha)
|
||||
self.add_line_to(prev_end)
|
||||
return self
|
||||
|
||||
def create_tip_with_stroke_width(self):
|
||||
width = min(
|
||||
self.max_stroke_width,
|
||||
self.max_width_to_length_ratio * self.get_length(),
|
||||
)
|
||||
widths_array = np.full(self.get_num_points(), width)
|
||||
nppc = self.n_points_per_curve
|
||||
if len(widths_array) > nppc:
|
||||
widths_array[-nppc:] = [
|
||||
a * self.tip_width_ratio * width
|
||||
for a in np.linspace(1, 0, nppc)
|
||||
]
|
||||
self.set_stroke(width=widths_array)
|
||||
return self
|
||||
|
||||
def reset_tip(self):
|
||||
self.set_points_by_ends(
|
||||
self.get_start(),
|
||||
self.get_end(),
|
||||
path_arc=self.path_arc,
|
||||
)
|
||||
self.create_tip_with_stroke_width()
|
||||
return self
|
||||
|
||||
def set_stroke(self, color=None, width=None, *args, **kwargs):
|
||||
super().set_stroke(color=color, width=width, *args, **kwargs)
|
||||
if isinstance(width, numbers.Number):
|
||||
self.max_stroke_width = width
|
||||
self.reset_tip()
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor):
|
||||
return self.reset_tip()
|
||||
|
||||
|
||||
class FillArrow(Line):
|
||||
CONFIG = {
|
||||
"fill_color": GREY_A,
|
||||
"fill_opacity": 1,
|
||||
@@ -786,12 +873,20 @@ class ArrowTip(Triangle):
|
||||
"width": DEFAULT_ARROW_TIP_WIDTH,
|
||||
"length": DEFAULT_ARROW_TIP_LENGTH,
|
||||
"angle": 0,
|
||||
"tip_style": 0, # triangle=0, inner_smooth=1, dot=2
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
Triangle.__init__(self, start_angle=0, **kwargs)
|
||||
self.set_height(self.width)
|
||||
self.set_width(self.length, stretch=True)
|
||||
if self.tip_style == 1:
|
||||
self.set_height(self.length * 0.9, stretch=True)
|
||||
self.data["points"][4] += np.array([0.6 * self.length, 0, 0])
|
||||
elif self.tip_style == 2:
|
||||
h = self.length / 2
|
||||
self.clear_points()
|
||||
self.data["points"] = Dot().set_width(h).get_points()
|
||||
self.rotate(self.angle)
|
||||
|
||||
def get_base(self):
|
||||
@@ -832,16 +927,8 @@ class Rectangle(Polygon):
|
||||
|
||||
|
||||
class Square(Rectangle):
|
||||
CONFIG = {
|
||||
"side_length": 2.0,
|
||||
}
|
||||
|
||||
def __init__(self, side_length=None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
|
||||
if side_length is None:
|
||||
side_length = self.side_length
|
||||
|
||||
def __init__(self, side_length=2.0, **kwargs):
|
||||
self.side_length = side_length
|
||||
super().__init__(side_length, side_length, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -439,7 +439,7 @@ class ControlPanel(Group):
|
||||
},
|
||||
"opener_text_kwargs": {
|
||||
"text": "Control Panel",
|
||||
"size": 0.4
|
||||
"font_size": 20
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,18 +57,18 @@ class Matrix(VMobject):
|
||||
CONFIG = {
|
||||
"v_buff": 0.8,
|
||||
"h_buff": 1.3,
|
||||
"bracket_h_buff": MED_SMALL_BUFF,
|
||||
"bracket_v_buff": MED_SMALL_BUFF,
|
||||
"bracket_h_buff": 0.2,
|
||||
"bracket_v_buff": 0.25,
|
||||
"add_background_rectangles_to_entries": False,
|
||||
"include_background_rectangle": False,
|
||||
"element_to_mobject": Tex,
|
||||
"element_to_mobject_config": {},
|
||||
"element_alignment_corner": DR,
|
||||
"element_alignment_corner": DOWN,
|
||||
}
|
||||
|
||||
def __init__(self, matrix, **kwargs):
|
||||
"""
|
||||
Matrix can either either include numbres, tex_strings,
|
||||
Matrix can either include numbers, tex_strings,
|
||||
or mobjects
|
||||
"""
|
||||
VMobject.__init__(self, **kwargs)
|
||||
@@ -132,6 +132,12 @@ class Matrix(VMobject):
|
||||
for i in range(len(self.mob_matrix[0]))
|
||||
])
|
||||
|
||||
def get_rows(self):
|
||||
return VGroup(*[
|
||||
VGroup(*row)
|
||||
for row in self.mob_matrix
|
||||
])
|
||||
|
||||
def set_column_colors(self, *colors):
|
||||
columns = self.get_columns()
|
||||
for color, column in zip(colors, columns):
|
||||
@@ -163,6 +169,7 @@ class DecimalMatrix(Matrix):
|
||||
class IntegerMatrix(Matrix):
|
||||
CONFIG = {
|
||||
"element_to_mobject": Integer,
|
||||
"element_alignment_corner": UP,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.iterables import make_even
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.paths import straight_path
|
||||
from manimlib.utils.simple_functions import get_parameters
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
@@ -56,9 +57,7 @@ class Mobject(object):
|
||||
# Must match in attributes of vert shader
|
||||
"shader_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
],
|
||||
# Event listener
|
||||
"listen_to_events": False
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -176,6 +175,7 @@ class Mobject(object):
|
||||
|
||||
def match_points(self, mobject):
|
||||
self.set_points(mobject.get_points())
|
||||
return self
|
||||
|
||||
def get_points(self):
|
||||
return self.data["points"]
|
||||
@@ -365,14 +365,17 @@ class Mobject(object):
|
||||
self.center()
|
||||
return self
|
||||
|
||||
def replicate(self, n):
|
||||
return self.get_group_class()(
|
||||
*(self.copy() for x in range(n))
|
||||
)
|
||||
|
||||
def get_grid(self, n_rows, n_cols, height=None, **kwargs):
|
||||
"""
|
||||
Returns a new mobject containing multiple copies of this one
|
||||
arranged in a grid
|
||||
"""
|
||||
grid = self.get_group_class()(
|
||||
*(self.copy() for n in range(n_rows * n_cols))
|
||||
)
|
||||
grid = self.replicate(n_rows * n_cols)
|
||||
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
|
||||
if height is not None:
|
||||
grid.set_height(height)
|
||||
@@ -408,8 +411,10 @@ class Mobject(object):
|
||||
for key in self.data:
|
||||
copy_mobject.data[key] = self.data[key].copy()
|
||||
|
||||
# TODO, are uniforms ever numpy arrays?
|
||||
copy_mobject.uniforms = dict(self.uniforms)
|
||||
for key in self.uniforms:
|
||||
if isinstance(self.uniforms[key], np.ndarray):
|
||||
copy_mobject.uniforms[key] = self.uniforms[key].copy()
|
||||
|
||||
copy_mobject.submobjects = []
|
||||
copy_mobject.add(*[sm.copy() for sm in self.submobjects])
|
||||
@@ -504,7 +509,7 @@ class Mobject(object):
|
||||
|
||||
self.refresh_has_updater_status()
|
||||
if call_updater:
|
||||
self.update()
|
||||
self.update(dt=0)
|
||||
return self
|
||||
|
||||
def remove_updater(self, update_function):
|
||||
@@ -561,7 +566,7 @@ class Mobject(object):
|
||||
)
|
||||
return self
|
||||
|
||||
def scale(self, scale_factor, **kwargs):
|
||||
def scale(self, scale_factor, min_scale_factor=1e-8, about_point=None, about_edge=ORIGIN):
|
||||
"""
|
||||
Default behavior is to scale about the center of the mobject.
|
||||
The argument about_edge can be a vector, indicating which side of
|
||||
@@ -571,13 +576,22 @@ class Mobject(object):
|
||||
Otherwise, if about_point is given a value, scaling is done with
|
||||
respect to that point.
|
||||
"""
|
||||
scale_factor = max(scale_factor, min_scale_factor)
|
||||
self.apply_points_function(
|
||||
lambda points: scale_factor * points,
|
||||
about_point=about_point,
|
||||
about_edge=about_edge,
|
||||
works_on_bounding_box=True,
|
||||
**kwargs
|
||||
)
|
||||
for mob in self.get_family():
|
||||
mob._handle_scale_side_effects(scale_factor)
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor):
|
||||
# In case subclasses, such as DecimalNumber, need to make
|
||||
# any other changes when the size gets altered
|
||||
pass
|
||||
|
||||
def stretch(self, factor, dim, **kwargs):
|
||||
def func(points):
|
||||
points[:, dim] *= factor
|
||||
@@ -764,6 +778,21 @@ class Mobject(object):
|
||||
def set_depth(self, depth, stretch=False, **kwargs):
|
||||
return self.rescale_to_fit(depth, 2, stretch=stretch, **kwargs)
|
||||
|
||||
def set_max_width(self, max_width, **kwargs):
|
||||
if self.get_width() > max_width:
|
||||
self.set_width(max_width, **kwargs)
|
||||
return self
|
||||
|
||||
def set_max_height(self, max_height, **kwargs):
|
||||
if self.get_height() > max_height:
|
||||
self.set_height(max_height, **kwargs)
|
||||
return self
|
||||
|
||||
def set_max_depth(self, max_depth, **kwargs):
|
||||
if self.get_depth() > max_depth:
|
||||
self.set_depth(max_depth, **kwargs)
|
||||
return self
|
||||
|
||||
def set_coord(self, value, dim, direction=ORIGIN):
|
||||
curr = self.get_coord(dim, direction)
|
||||
shift_vect = np.zeros(self.dim)
|
||||
@@ -822,7 +851,6 @@ class Mobject(object):
|
||||
return self
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
# TODO, this doesn't currently work in 3d
|
||||
curr_start, curr_end = self.get_start_and_end()
|
||||
curr_vect = curr_end - curr_start
|
||||
if np.all(curr_vect == 0):
|
||||
@@ -834,14 +862,40 @@ class Mobject(object):
|
||||
)
|
||||
self.rotate(
|
||||
angle_of_vector(target_vect) - angle_of_vector(curr_vect),
|
||||
about_point=curr_start
|
||||
)
|
||||
self.shift(start - curr_start)
|
||||
self.rotate(
|
||||
np.arctan2(curr_vect[2], get_norm(curr_vect[:2])) - np.arctan2(target_vect[2], get_norm(target_vect[:2])),
|
||||
axis=np.array([-target_vect[1], target_vect[0], 0]),
|
||||
)
|
||||
self.shift(start - self.get_start())
|
||||
return self
|
||||
|
||||
# Color functions
|
||||
|
||||
def set_rgba_array(self, color=None, opacity=None, name="rgbas", recurse=True):
|
||||
def set_rgba_array(self, rgba_array, name="rgbas", recurse=False):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data[name] = np.array(rgba_array)
|
||||
return self
|
||||
|
||||
def set_color_by_rgba_func(self, func, recurse=True):
|
||||
"""
|
||||
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)
|
||||
return self
|
||||
|
||||
def set_color_by_rgb_func(self, func, opacity=1, recurse=True):
|
||||
"""
|
||||
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)
|
||||
return self
|
||||
|
||||
def set_rgba_array_by_color(self, color=None, opacity=None, name="rgbas", recurse=True):
|
||||
if color is not None:
|
||||
rgbs = np.array([color_to_rgb(c) for c in listify(color)])
|
||||
if opacity is not None:
|
||||
@@ -870,8 +924,8 @@ class Mobject(object):
|
||||
return self
|
||||
|
||||
def set_color(self, color, opacity=None, recurse=True):
|
||||
self.set_rgba_array(color, opacity, recurse=False)
|
||||
# Recurse to submobjects differently from how set_rgba_array
|
||||
self.set_rgba_array_by_color(color, opacity, recurse=False)
|
||||
# Recurse to submobjects differently from how set_rgba_array_by_color
|
||||
# in case they implement set_color differently
|
||||
if recurse:
|
||||
for submob in self.submobjects:
|
||||
@@ -879,7 +933,7 @@ class Mobject(object):
|
||||
return self
|
||||
|
||||
def set_opacity(self, opacity, recurse=True):
|
||||
self.set_rgba_array(color=None, opacity=opacity, recurse=False)
|
||||
self.set_rgba_array_by_color(color=None, opacity=opacity, recurse=False)
|
||||
if recurse:
|
||||
for submob in self.submobjects:
|
||||
submob.set_opacity(opacity, recurse=True)
|
||||
@@ -1039,14 +1093,16 @@ class Mobject(object):
|
||||
|
||||
def get_start(self):
|
||||
self.throw_error_if_no_points()
|
||||
return np.array(self.get_points()[0])
|
||||
return self.get_points()[0].copy()
|
||||
|
||||
def get_end(self):
|
||||
self.throw_error_if_no_points()
|
||||
return np.array(self.get_points()[-1])
|
||||
return self.get_points()[-1].copy()
|
||||
|
||||
def get_start_and_end(self):
|
||||
return self.get_start(), self.get_end()
|
||||
self.throw_error_if_no_points()
|
||||
points = self.get_points()
|
||||
return (points[0].copy(), points[-1].copy())
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
points = self.get_points()
|
||||
@@ -1412,7 +1468,7 @@ class Mobject(object):
|
||||
return result
|
||||
|
||||
def check_data_alignment(self, array, data_key):
|
||||
# Makes sure that self.data[key] can be brodcast into
|
||||
# Makes sure that self.data[key] can be broadcast into
|
||||
# the given array, meaning its length has to be either 1
|
||||
# or the length of the array
|
||||
d_len = len(self.data[data_key])
|
||||
|
||||
@@ -23,7 +23,7 @@ def always(method, *args, **kwargs):
|
||||
def f_always(method, *arg_generators, **kwargs):
|
||||
"""
|
||||
More functional version of always, where instead
|
||||
of taking in args, it takes in functions which ouput
|
||||
of taking in args, it takes in functions which output
|
||||
the relevant arguments.
|
||||
"""
|
||||
assert_is_mobject_method(method)
|
||||
|
||||
@@ -5,9 +5,7 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.iterables import list_difference_update
|
||||
from manimlib.utils.simple_functions import fdiv
|
||||
from manimlib.utils.space_ops import normalize
|
||||
|
||||
|
||||
class NumberLine(Line):
|
||||
@@ -84,7 +82,7 @@ class NumberLine(Line):
|
||||
ticks = VGroup()
|
||||
for x in self.get_tick_range():
|
||||
size = self.tick_size
|
||||
if x in self.numbers_with_elongated_ticks:
|
||||
if np.isclose(self.numbers_with_elongated_ticks, x).any():
|
||||
size *= self.longer_tick_multiple
|
||||
ticks.add(self.get_tick(x, size))
|
||||
self.add(ticks)
|
||||
@@ -107,11 +105,13 @@ class NumberLine(Line):
|
||||
return interpolate(self.get_start(), self.get_end(), alpha)
|
||||
|
||||
def point_to_number(self, point):
|
||||
start, end = self.get_start_and_end()
|
||||
unit_vect = normalize(end - start)
|
||||
points = self.get_points()
|
||||
start = points[0]
|
||||
end = points[-1]
|
||||
vect = end - start
|
||||
proportion = fdiv(
|
||||
np.dot(point - start, unit_vect),
|
||||
np.dot(end - start, unit_vect),
|
||||
np.dot(point - start, vect),
|
||||
np.dot(end - start, vect),
|
||||
)
|
||||
return interpolate(self.x_min, self.x_max, proportion)
|
||||
|
||||
@@ -144,7 +144,7 @@ class NumberLine(Line):
|
||||
direction=direction,
|
||||
buff=buff
|
||||
)
|
||||
if x < 0 and self.line_to_number_direction[0] == 0:
|
||||
if x < 0 and direction[0] == 0:
|
||||
# Align without the minus sign
|
||||
num_mob.shift(num_mob[0].get_width() * LEFT / 2)
|
||||
return num_mob
|
||||
@@ -155,10 +155,11 @@ class NumberLine(Line):
|
||||
|
||||
kwargs["font_size"] = font_size
|
||||
|
||||
if excluding is None:
|
||||
excluding = self.numbers_to_exclude
|
||||
|
||||
numbers = VGroup()
|
||||
for x in x_values:
|
||||
if x in self.numbers_to_exclude:
|
||||
continue
|
||||
if excluding is not None and x in excluding:
|
||||
continue
|
||||
numbers.add(self.get_number_mobject(x, **kwargs))
|
||||
|
||||
@@ -36,7 +36,9 @@ class DecimalNumber(VMobject):
|
||||
|
||||
# Add non-numerical bits
|
||||
if self.show_ellipsis:
|
||||
self.add(self.string_to_mob("..."))
|
||||
dots = self.string_to_mob("...")
|
||||
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
|
||||
self.add(dots)
|
||||
if self.unit is not None:
|
||||
self.unit_sign = self.string_to_mob(self.unit, SingleStringTex)
|
||||
self.add(self.unit_sign)
|
||||
@@ -128,16 +130,15 @@ class DecimalNumber(VMobject):
|
||||
|
||||
def set_value(self, number):
|
||||
move_to_point = self.get_edge_center(self.edge_to_fix)
|
||||
style = self.get_style()
|
||||
old_submobjects = list(self.submobjects)
|
||||
self.set_submobjects_from_number(number)
|
||||
self.move_to(move_to_point, self.edge_to_fix)
|
||||
self.set_style(**style)
|
||||
for sm1, sm2 in zip(self.submobjects, old_submobjects):
|
||||
sm1.match_style(sm2)
|
||||
return self
|
||||
|
||||
def scale(self, scale_factor, **kwargs):
|
||||
super().scale(scale_factor, **kwargs)
|
||||
def _handle_scale_side_effects(self, scale_factor):
|
||||
self.data["font_size"] *= scale_factor
|
||||
return self
|
||||
|
||||
def get_value(self):
|
||||
return self.number
|
||||
|
||||
@@ -47,7 +47,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||
fill_opacity=None,
|
||||
family=True
|
||||
):
|
||||
# Unchangable style, except for fill_opacity
|
||||
# Unchangeable style, except for fill_opacity
|
||||
VMobject.set_style_data(
|
||||
self,
|
||||
stroke_color=BLACK,
|
||||
@@ -64,16 +64,17 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||
class Cross(VGroup):
|
||||
CONFIG = {
|
||||
"stroke_color": RED,
|
||||
"stroke_width": 6,
|
||||
"stroke_width": [0, 6, 0],
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
VGroup.__init__(self,
|
||||
Line(UP + LEFT, DOWN + RIGHT),
|
||||
Line(UP + RIGHT, DOWN + LEFT),
|
||||
)
|
||||
super().__init__(
|
||||
Line(UL, DR),
|
||||
Line(UR, DL),
|
||||
)
|
||||
self.insert_n_curves(2)
|
||||
self.replace(mobject, stretch=True)
|
||||
self.set_stroke(self.stroke_color, self.stroke_width)
|
||||
self.set_stroke(self.stroke_color, width=self.stroke_width)
|
||||
|
||||
|
||||
class Underline(Line):
|
||||
|
||||
@@ -8,6 +8,7 @@ from manimlib.animation.growing import GrowFromCenter
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import SingleStringTex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
@@ -61,9 +62,10 @@ class Brace(SingleStringTex):
|
||||
mob.shift(self.get_direction() * shift_distance)
|
||||
return self
|
||||
|
||||
def get_text(self, *text, **kwargs):
|
||||
text_mob = TexText(*text)
|
||||
self.put_at_tip(text_mob, **kwargs)
|
||||
def get_text(self, text, **kwargs):
|
||||
buff = kwargs.pop("buff", SMALL_BUFF)
|
||||
text_mob = Text(text, **kwargs)
|
||||
self.put_at_tip(text_mob, buff=buff)
|
||||
return text_mob
|
||||
|
||||
def get_tex(self, *tex, **kwargs):
|
||||
|
||||
@@ -41,7 +41,6 @@ class Exmark(TexText):
|
||||
|
||||
class Lightbulb(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name": "lightbulb",
|
||||
"height": 1,
|
||||
"stroke_color": YELLOW,
|
||||
"stroke_width": 3,
|
||||
@@ -49,6 +48,9 @@ class Lightbulb(SVGMobject):
|
||||
"fill_opacity": 0,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("lightbulb", **kwargs)
|
||||
|
||||
|
||||
class Speedometer(VMobject):
|
||||
CONFIG = {
|
||||
|
||||
@@ -11,6 +11,7 @@ from manimlib.constants import DEFAULT_STROKE_WIDTH
|
||||
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import DEGREES, PI
|
||||
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
@@ -21,6 +22,7 @@ from manimlib.utils.color import *
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.directories import get_mobject_data_dir
|
||||
from manimlib.utils.images import get_full_vector_image_path
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
|
||||
def string_to_numbers(num_string):
|
||||
@@ -86,10 +88,10 @@ class SVGMobject(VMobject):
|
||||
elif element.tagName == 'style':
|
||||
pass # TODO, handle style
|
||||
elif element.tagName in ['g', 'svg', 'symbol']:
|
||||
result += it.chain(*[
|
||||
result += it.chain(*(
|
||||
self.get_mobjects_from(child)
|
||||
for child in element.childNodes
|
||||
])
|
||||
))
|
||||
elif element.tagName == 'path':
|
||||
result.append(self.path_string_to_mobject(
|
||||
element.getAttribute('d')
|
||||
@@ -169,7 +171,11 @@ class SVGMobject(VMobject):
|
||||
else 0.0
|
||||
for key in ("cx", "cy", "rx", "ry")
|
||||
]
|
||||
return Circle().scale(rx * RIGHT + ry * UP).shift(x * RIGHT + y * DOWN)
|
||||
result = Circle()
|
||||
result.stretch(rx, 0)
|
||||
result.stretch(ry, 1)
|
||||
result.shift(x * RIGHT + y * DOWN)
|
||||
return result
|
||||
|
||||
def rect_to_mobject(self, rect_element):
|
||||
fill_color = rect_element.getAttribute("fill")
|
||||
@@ -178,8 +184,9 @@ class SVGMobject(VMobject):
|
||||
corner_radius = rect_element.getAttribute("rx")
|
||||
|
||||
# input preprocessing
|
||||
fill_opacity = 1
|
||||
if fill_color in ["", "none", "#FFF", "#FFFFFF"] or Color(fill_color) == Color(WHITE):
|
||||
opacity = 0
|
||||
fill_opacity = 0
|
||||
fill_color = BLACK # shdn't be necessary but avoids error msgs
|
||||
if fill_color in ["#000", "#000000"]:
|
||||
fill_color = WHITE
|
||||
@@ -207,7 +214,7 @@ class SVGMobject(VMobject):
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=opacity
|
||||
fill_opacity=fill_opacity
|
||||
)
|
||||
else:
|
||||
mob = RoundedRectangle(
|
||||
@@ -315,7 +322,7 @@ class SVGMobject(VMobject):
|
||||
|
||||
class VMobjectFromSVGPathstring(VMobject):
|
||||
CONFIG = {
|
||||
"long_lines": True,
|
||||
"long_lines": False,
|
||||
"should_subdivide_sharp_curves": False,
|
||||
"should_remove_null_curves": False,
|
||||
}
|
||||
@@ -335,6 +342,8 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
|
||||
if os.path.exists(points_filepath) and os.path.exists(tris_filepath):
|
||||
self.set_points(np.load(points_filepath))
|
||||
self.triangulation = np.load(tris_filepath)
|
||||
self.needs_new_triangulation = False
|
||||
else:
|
||||
self.relative_point = np.array(ORIGIN)
|
||||
for command, coord_string in self.get_commands_and_coord_strings():
|
||||
@@ -350,6 +359,7 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
self.stretch(-1, 1, about_point=ORIGIN)
|
||||
# Save to a file for future use
|
||||
np.save(points_filepath, self.get_points())
|
||||
np.save(tris_filepath, self.get_triangulation())
|
||||
|
||||
def get_commands_and_coord_strings(self):
|
||||
all_commands = list(self.get_command_to_function_map().keys())
|
||||
@@ -363,10 +373,18 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
def handle_command(self, command, new_points):
|
||||
if command.islower():
|
||||
# Treat it as a relative command
|
||||
new_points += self.relative_point
|
||||
if command == "a":
|
||||
# Only the last `self.dim` columns refer to points
|
||||
new_points[:, -self.dim:] += self.relative_point
|
||||
else:
|
||||
new_points += self.relative_point
|
||||
|
||||
func, n_points = self.command_to_function(command)
|
||||
func(*new_points[:n_points])
|
||||
command_points = new_points[:n_points]
|
||||
if command.upper() == "A":
|
||||
func(*command_points[0][:-self.dim], np.array(command_points[0][-self.dim:]))
|
||||
else:
|
||||
func(*command_points)
|
||||
leftover_points = new_points[n_points:]
|
||||
|
||||
# Recursively handle the rest of the points
|
||||
@@ -375,7 +393,10 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
# Treat following points as relative line coordinates
|
||||
command = "l"
|
||||
if command.islower():
|
||||
leftover_points -= self.relative_point
|
||||
if command == "a":
|
||||
leftover_points[:, -self.dim:] -= self.relative_point
|
||||
else:
|
||||
leftover_points -= self.relative_point
|
||||
self.relative_point = self.get_last_point()
|
||||
self.handle_command(command, leftover_points)
|
||||
else:
|
||||
@@ -384,20 +405,131 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
|
||||
def string_to_points(self, command, coord_string):
|
||||
numbers = string_to_numbers(coord_string)
|
||||
if command.upper() == "A":
|
||||
# Only the last `self.dim` columns refer to points
|
||||
# Each "point" returned here has a size of `(5 + self.dim)`
|
||||
params = np.array(numbers).reshape((-1, 7))
|
||||
result = np.zeros((params.shape[0], 5 + self.dim))
|
||||
result[:, :7] = params
|
||||
return result
|
||||
if command.upper() in ["H", "V"]:
|
||||
i = {"H": 0, "V": 1}[command.upper()]
|
||||
xy = np.zeros((len(numbers), 2))
|
||||
xy[:, i] = numbers
|
||||
if command.isupper():
|
||||
xy[:, 1 - i] = self.relative_point[1 - i]
|
||||
elif command.upper() == "A":
|
||||
raise Exception("Not implemented")
|
||||
else:
|
||||
xy = np.array(numbers).reshape((len(numbers) // 2, 2))
|
||||
xy = np.array(numbers).reshape((-1, 2))
|
||||
result = np.zeros((xy.shape[0], self.dim))
|
||||
result[:, :2] = xy
|
||||
return result
|
||||
|
||||
def add_elliptical_arc_to(self, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, point):
|
||||
"""
|
||||
In fact, this method only suits 2d VMobjects.
|
||||
"""
|
||||
def close_to_zero(a, threshold=1e-5):
|
||||
return abs(a) < threshold
|
||||
|
||||
def solve_2d_linear_equation(a, b, c):
|
||||
"""
|
||||
Using Crammer's rule to solve the linear equation `[a b]x = c`
|
||||
where `a`, `b` and `c` are all 2d vectors.
|
||||
"""
|
||||
def det(a, b):
|
||||
return a[0] * b[1] - a[1] * b[0]
|
||||
d = det(a, b)
|
||||
if close_to_zero(d):
|
||||
raise Exception("Cannot handle 0 determinant.")
|
||||
return [det(c, b) / d, det(a, c) / d]
|
||||
|
||||
def get_arc_center_and_angles(x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, x1, y1):
|
||||
"""
|
||||
The parameter functions of an ellipse rotated `phi` radians counterclockwise is (on `alpha`):
|
||||
x = cx + rx * cos(alpha) * cos(phi) + ry * sin(alpha) * sin(phi),
|
||||
y = cy + rx * cos(alpha) * sin(phi) - ry * sin(alpha) * cos(phi).
|
||||
Now we have two points sitting on the ellipse: `(x0, y0)`, `(x1, y1)`, corresponding to 4 equations,
|
||||
and we want to hunt for 4 variables: `cx`, `cy`, `alpha0` and `alpha_1`.
|
||||
Let `d_alpha = alpha1 - alpha0`, then:
|
||||
if `sweep_flag = 0` and `large_arc_flag = 1`, then `PI <= d_alpha < 2 * PI`;
|
||||
if `sweep_flag = 0` and `large_arc_flag = 0`, then `0 < d_alpha <= PI`;
|
||||
if `sweep_flag = 1` and `large_arc_flag = 0`, then `-PI <= d_alpha < 0`;
|
||||
if `sweep_flag = 1` and `large_arc_flag = 1`, then `-2 * PI < d_alpha <= -PI`.
|
||||
"""
|
||||
xd = x1 - x0
|
||||
yd = y1 - y0
|
||||
if close_to_zero(xd) and close_to_zero(yd):
|
||||
raise Exception("Cannot find arc center since the start point and the end point meet.")
|
||||
# Find `p = cos(alpha1) - cos(alpha0)`, `q = sin(alpha1) - sin(alpha0)`
|
||||
eq0 = [rx * np.cos(phi), ry * np.sin(phi), xd]
|
||||
eq1 = [rx * np.sin(phi), -ry * np.cos(phi), yd]
|
||||
p, q = solve_2d_linear_equation(*zip(eq0, eq1))
|
||||
# Find `s = (alpha1 - alpha0) / 2`, `t = (alpha1 + alpha0) / 2`
|
||||
# If `sin(s) = 0`, this requires `p = q = 0`,
|
||||
# implying `xd = yd = 0`, which is impossible.
|
||||
sin_s = (p ** 2 + q ** 2) ** 0.5 / 2
|
||||
if sweep_flag:
|
||||
sin_s = -sin_s
|
||||
sin_s = clip(sin_s, -1, 1)
|
||||
s = np.arcsin(sin_s)
|
||||
if large_arc_flag:
|
||||
if not sweep_flag:
|
||||
s = PI - s
|
||||
else:
|
||||
s = -PI - s
|
||||
sin_t = -p / (2 * sin_s)
|
||||
cos_t = q / (2 * sin_s)
|
||||
cos_t = clip(cos_t, -1, 1)
|
||||
t = np.arccos(cos_t)
|
||||
if sin_t <= 0:
|
||||
t = -t
|
||||
# We can make sure `0 < abs(s) < PI`, `-PI <= t < PI`.
|
||||
alpha0 = t - s
|
||||
alpha_1 = t + s
|
||||
cx = x0 - rx * np.cos(alpha0) * np.cos(phi) - ry * np.sin(alpha0) * np.sin(phi)
|
||||
cy = y0 - rx * np.cos(alpha0) * np.sin(phi) + ry * np.sin(alpha0) * np.cos(phi)
|
||||
return cx, cy, alpha0, alpha_1
|
||||
|
||||
def get_point_on_ellipse(cx, cy, rx, ry, phi, angle):
|
||||
return np.array([
|
||||
cx + rx * np.cos(angle) * np.cos(phi) + ry * np.sin(angle) * np.sin(phi),
|
||||
cy + rx * np.cos(angle) * np.sin(phi) - ry * np.sin(angle) * np.cos(phi),
|
||||
0
|
||||
])
|
||||
|
||||
def convert_elliptical_arc_to_quadratic_bezier_curve(
|
||||
cx, cy, rx, ry, phi, start_angle, end_angle, n_components=8
|
||||
):
|
||||
theta = (end_angle - start_angle) / n_components / 2
|
||||
handles = np.array([
|
||||
get_point_on_ellipse(cx, cy, rx / np.cos(theta), ry / np.cos(theta), phi, a)
|
||||
for a in np.linspace(
|
||||
start_angle + theta,
|
||||
end_angle - theta,
|
||||
n_components,
|
||||
)
|
||||
])
|
||||
anchors = np.array([
|
||||
get_point_on_ellipse(cx, cy, rx, ry, phi, a)
|
||||
for a in np.linspace(
|
||||
start_angle + theta * 2,
|
||||
end_angle,
|
||||
n_components,
|
||||
)
|
||||
])
|
||||
return handles, anchors
|
||||
|
||||
phi = x_axis_rotation * DEGREES
|
||||
x0, y0 = self.get_last_point()[:2]
|
||||
cx, cy, start_angle, end_angle = get_arc_center_and_angles(
|
||||
x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, point[0], point[1]
|
||||
)
|
||||
handles, anchors = convert_elliptical_arc_to_quadratic_bezier_curve(
|
||||
cx, cy, rx, ry, phi, start_angle, end_angle
|
||||
)
|
||||
for handle, anchor in zip(handles, anchors):
|
||||
self.add_quadratic_bezier_curve_to(handle, anchor)
|
||||
|
||||
def command_to_function(self, command):
|
||||
return self.get_command_to_function_map()[command.upper()]
|
||||
|
||||
@@ -415,7 +547,7 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
"S": (self.add_smooth_cubic_curve_to, 2),
|
||||
"Q": (self.add_quadratic_bezier_curve_to, 2),
|
||||
"T": (self.add_smooth_curve_to, 1),
|
||||
"A": (self.add_quadratic_bezier_curve_to, 2), # TODO
|
||||
"A": (self.add_elliptical_arc_to, 1),
|
||||
"Z": (self.close_path, 0),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from functools import reduce
|
||||
import operator as op
|
||||
import re
|
||||
import itertools as it
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
@@ -65,6 +64,8 @@ class SingleStringTex(VMobject):
|
||||
if self.math_mode:
|
||||
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
|
||||
|
||||
new_tex = self.alignment + "\n" + new_tex
|
||||
|
||||
tex_config = get_tex_config()
|
||||
return tex_config["tex_body"].replace(
|
||||
tex_config["text_to_replace"],
|
||||
@@ -72,10 +73,7 @@ class SingleStringTex(VMobject):
|
||||
)
|
||||
|
||||
def get_modified_expression(self, tex_string):
|
||||
result = self.alignment + " " + tex_string
|
||||
result = result.strip()
|
||||
result = self.modify_special_strings(result)
|
||||
return result
|
||||
return self.modify_special_strings(tex_string.strip())
|
||||
|
||||
def modify_special_strings(self, tex):
|
||||
tex = tex.strip()
|
||||
@@ -152,10 +150,7 @@ class SingleStringTex(VMobject):
|
||||
|
||||
class Tex(SingleStringTex):
|
||||
CONFIG = {
|
||||
"arg_separator": " ",
|
||||
# Note, use of isolate is largely rendered
|
||||
# moot by the fact that you can surround such strings in
|
||||
# {{ and }} as needed.
|
||||
"arg_separator": "",
|
||||
"isolate": [],
|
||||
"tex_to_color_map": {},
|
||||
}
|
||||
@@ -172,18 +167,22 @@ class Tex(SingleStringTex):
|
||||
self.organize_submobjects_left_to_right()
|
||||
|
||||
def break_up_tex_strings(self, tex_strings):
|
||||
# Separate out anything surrounded in double braces
|
||||
patterns = ["{{", "}}"]
|
||||
# Separate out any strings specified in the isolate
|
||||
# or tex_to_color_map lists.
|
||||
patterns.extend([
|
||||
substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()]
|
||||
if len(substrings_to_isolate) == 0:
|
||||
return tex_strings
|
||||
patterns = (
|
||||
"({})".format(re.escape(ss))
|
||||
for ss in it.chain(self.isolate, self.tex_to_color_map.keys())
|
||||
])
|
||||
for ss in substrings_to_isolate
|
||||
)
|
||||
pattern = "|".join(patterns)
|
||||
pieces = []
|
||||
for s in tex_strings:
|
||||
pieces.extend(re.split(pattern, s))
|
||||
if pattern:
|
||||
pieces.extend(re.split(pattern, s))
|
||||
else:
|
||||
pieces.append(s)
|
||||
return list(filter(lambda s: s, pieces))
|
||||
|
||||
def break_up_by_substrings(self):
|
||||
|
||||
@@ -2,7 +2,15 @@ import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import io
|
||||
import typing
|
||||
import warnings
|
||||
import xml.etree.ElementTree as ET
|
||||
import functools
|
||||
import pygments
|
||||
import pygments.lexers
|
||||
import pygments.styles
|
||||
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
@@ -10,13 +18,14 @@ import manimpango
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.customization import get_customization
|
||||
from manimlib.utils.directories import get_downloads_dir, get_text_dir
|
||||
from manimpango import PangoUtils
|
||||
from manimpango import TextSetting
|
||||
from manimpango import PangoUtils, TextSetting, MarkupUtils
|
||||
|
||||
TEXT_MOB_SCALE_FACTOR = 0.001048
|
||||
TEXT_MOB_SCALE_FACTOR = 0.0076
|
||||
DEFAULT_LINE_SPACING_SCALE = 0.6
|
||||
|
||||
|
||||
class Text(SVGMobject):
|
||||
@@ -29,7 +38,7 @@ class Text(SVGMobject):
|
||||
"font": '',
|
||||
"gradient": None,
|
||||
"lsh": -1,
|
||||
"size": 1,
|
||||
"size": None,
|
||||
"font_size": 48,
|
||||
"tab_width": 4,
|
||||
"slant": NORMAL,
|
||||
@@ -42,10 +51,20 @@ class Text(SVGMobject):
|
||||
"disable_ligatures": True,
|
||||
}
|
||||
|
||||
def __init__(self, text, **config):
|
||||
self.full2short(config)
|
||||
digest_config(self, config)
|
||||
self.lsh = self.size if self.lsh == -1 else self.lsh
|
||||
def __init__(self, text, **kwargs):
|
||||
self.full2short(kwargs)
|
||||
digest_config(self, kwargs)
|
||||
if self.size:
|
||||
warnings.warn(
|
||||
"self.size has been deprecated and will "
|
||||
"be removed in future.",
|
||||
DeprecationWarning
|
||||
)
|
||||
self.font_size = self.size
|
||||
if self.lsh == -1:
|
||||
self.lsh = self.font_size + self.font_size * DEFAULT_LINE_SPACING_SCALE
|
||||
else:
|
||||
self.lsh = self.font_size + self.font_size * self.lsh
|
||||
text_without_tabs = text
|
||||
if text.find('\t') != -1:
|
||||
text_without_tabs = text.replace('\t', ' ' * self.tab_width)
|
||||
@@ -53,7 +72,7 @@ class Text(SVGMobject):
|
||||
file_name = self.text2svg()
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
self.remove_empty_path(file_name)
|
||||
SVGMobject.__init__(self, file_name, **config)
|
||||
SVGMobject.__init__(self, file_name, **kwargs)
|
||||
self.text = text
|
||||
if self.disable_ligatures:
|
||||
self.apply_space_chars()
|
||||
@@ -66,7 +85,7 @@ class Text(SVGMobject):
|
||||
|
||||
# anti-aliasing
|
||||
if self.height is None:
|
||||
self.scale(TEXT_MOB_SCALE_FACTOR * self.font_size)
|
||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||
|
||||
def remove_empty_path(self, file_name):
|
||||
with open(file_name, 'r') as fpr:
|
||||
@@ -100,6 +119,19 @@ class Text(SVGMobject):
|
||||
index = self.text.find(word, index + len(word))
|
||||
return indexes
|
||||
|
||||
def get_parts_by_text(self, word):
|
||||
return VGroup(*(
|
||||
self[i:j]
|
||||
for i, j in self.find_indexes(word)
|
||||
))
|
||||
|
||||
def get_part_by_text(self, word):
|
||||
parts = self.get_parts_by_text(word)
|
||||
if len(parts) > 0:
|
||||
return parts[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def full2short(self, config):
|
||||
for kwargs in [config, self.CONFIG]:
|
||||
if kwargs.__contains__('line_spacing_height'):
|
||||
@@ -117,75 +149,88 @@ class Text(SVGMobject):
|
||||
|
||||
def set_color_by_t2c(self, t2c=None):
|
||||
t2c = t2c if t2c else self.t2c
|
||||
for word, color in list(t2c.items()):
|
||||
for word, color in t2c.items():
|
||||
for start, end in self.find_indexes(word):
|
||||
self[start:end].set_color(color)
|
||||
|
||||
def set_color_by_t2g(self, t2g=None):
|
||||
t2g = t2g if t2g else self.t2g
|
||||
for word, gradient in list(t2g.items()):
|
||||
for word, gradient in t2g.items():
|
||||
for start, end in self.find_indexes(word):
|
||||
self[start:end].set_color_by_gradient(*gradient)
|
||||
|
||||
def text2hash(self):
|
||||
settings = self.font + self.slant + self.weight
|
||||
settings += str(self.t2f) + str(self.t2s) + str(self.t2w)
|
||||
settings += str(self.lsh) + str(self.size)
|
||||
settings += str(self.lsh) + str(self.font_size)
|
||||
id_str = self.text + settings
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update(id_str.encode())
|
||||
return hasher.hexdigest()[:16]
|
||||
|
||||
def text2settings(self):
|
||||
"""
|
||||
Substrings specified in t2f, t2s, t2w can occupy each other.
|
||||
For each category of style, a stack following first-in-last-out is constructed,
|
||||
and the last value in each stack takes effect.
|
||||
"""
|
||||
settings = []
|
||||
t2x = [self.t2f, self.t2s, self.t2w]
|
||||
for i in range(len(t2x)):
|
||||
fsw = [self.font, self.slant, self.weight]
|
||||
if t2x[i]:
|
||||
for word, x in list(t2x[i].items()):
|
||||
for start, end in self.find_indexes(word):
|
||||
fsw[i] = x
|
||||
settings.append(TextSetting(start, end, *fsw))
|
||||
self.line_num = 0
|
||||
def add_text_settings(start, end, style_stacks):
|
||||
if start == end:
|
||||
return
|
||||
breakdown_indices = [start, *[
|
||||
i + start + 1 for i, char in enumerate(self.text[start:end]) if char == "\n"
|
||||
], end]
|
||||
style = [stack[-1] for stack in style_stacks]
|
||||
for atom_start, atom_end in zip(breakdown_indices[:-1], breakdown_indices[1:]):
|
||||
if atom_start < atom_end:
|
||||
settings.append(TextSetting(atom_start, atom_end, *style, self.line_num))
|
||||
self.line_num += 1
|
||||
self.line_num -= 1
|
||||
|
||||
# Set All text settings(default font slant weight)
|
||||
fsw = [self.font, self.slant, self.weight]
|
||||
settings.sort(key=lambda setting: setting.start)
|
||||
temp_settings = settings.copy()
|
||||
start = 0
|
||||
for setting in settings:
|
||||
if setting.start != start:
|
||||
temp_settings.append(TextSetting(start, setting.start, *fsw))
|
||||
start = setting.end
|
||||
if start != len(self.text):
|
||||
temp_settings.append(TextSetting(start, len(self.text), *fsw))
|
||||
settings = sorted(temp_settings, key=lambda setting: setting.start)
|
||||
# Set all the default and specified values.
|
||||
len_text = len(self.text)
|
||||
t2x_items = sorted([
|
||||
*[
|
||||
(0, len_text, t2x_index, value)
|
||||
for t2x_index, value in enumerate([self.font, self.slant, self.weight])
|
||||
],
|
||||
*[
|
||||
(start, end, t2x_index, value)
|
||||
for t2x_index, t2x in enumerate([self.t2f, self.t2s, self.t2w])
|
||||
for word, value in t2x.items()
|
||||
for start, end in self.find_indexes(word)
|
||||
]
|
||||
], key=lambda item: item[0])
|
||||
|
||||
if re.search(r'\n', self.text):
|
||||
line_num = 0
|
||||
for start, end in self.find_indexes('\n'):
|
||||
for setting in settings:
|
||||
if setting.line_num == -1:
|
||||
setting.line_num = line_num
|
||||
if start < setting.end:
|
||||
line_num += 1
|
||||
new_setting = copy.copy(setting)
|
||||
setting.end = end
|
||||
new_setting.start = end
|
||||
new_setting.line_num = line_num
|
||||
settings.append(new_setting)
|
||||
settings.sort(key=lambda setting: setting.start)
|
||||
break
|
||||
|
||||
for setting in settings:
|
||||
if setting.line_num == -1:
|
||||
setting.line_num = 0
|
||||
# Break down ranges and construct settings separately.
|
||||
active_items = []
|
||||
style_stacks = [[] for _ in range(3)]
|
||||
for item, next_start in zip(t2x_items, [*[item[0] for item in t2x_items[1:]], len_text]):
|
||||
active_items.append(item)
|
||||
start, end, t2x_index, value = item
|
||||
style_stacks[t2x_index].append(value)
|
||||
halting_items = sorted(filter(
|
||||
lambda item: item[1] <= next_start,
|
||||
active_items
|
||||
), key=lambda item: item[1])
|
||||
atom_start = start
|
||||
for halting_item in halting_items:
|
||||
active_items.remove(halting_item)
|
||||
_, atom_end, t2x_index, _ = halting_item
|
||||
add_text_settings(atom_start, atom_end, style_stacks)
|
||||
style_stacks[t2x_index].pop()
|
||||
atom_start = atom_end
|
||||
add_text_settings(atom_start, next_start, style_stacks)
|
||||
|
||||
del self.line_num
|
||||
return settings
|
||||
|
||||
def text2svg(self):
|
||||
# anti-aliasing
|
||||
size = self.size * 10
|
||||
lsh = self.lsh * 10
|
||||
size = self.font_size
|
||||
lsh = self.lsh
|
||||
|
||||
if self.font == '':
|
||||
self.font = get_customization()['style']['font']
|
||||
@@ -196,8 +241,8 @@ class Text(SVGMobject):
|
||||
if os.path.exists(file_name):
|
||||
return file_name
|
||||
settings = self.text2settings()
|
||||
width = 600
|
||||
height = 400
|
||||
width = DEFAULT_PIXEL_WIDTH
|
||||
height = DEFAULT_PIXEL_HEIGHT
|
||||
disable_liga = self.disable_ligatures
|
||||
return manimpango.text2svg(
|
||||
settings,
|
||||
@@ -212,6 +257,296 @@ class Text(SVGMobject):
|
||||
self.text,
|
||||
)
|
||||
|
||||
|
||||
class MarkupText(SVGMobject):
|
||||
CONFIG = {
|
||||
# Mobject
|
||||
"color": WHITE,
|
||||
"height": None,
|
||||
# Text
|
||||
"font": '',
|
||||
"font_size": 48,
|
||||
"lsh": None,
|
||||
"justify": False,
|
||||
"slant": NORMAL,
|
||||
"weight": NORMAL,
|
||||
"tab_width": 4,
|
||||
"gradient": None,
|
||||
"disable_ligatures": True,
|
||||
}
|
||||
|
||||
def __init__(self, text, **config):
|
||||
digest_config(self, config)
|
||||
self.text = f'<span>{text}</span>'
|
||||
self.original_text = self.text
|
||||
self.text_for_parsing = self.text
|
||||
text_without_tabs = text
|
||||
if "\t" in text:
|
||||
text_without_tabs = text.replace("\t", " " * self.tab_width)
|
||||
try:
|
||||
colormap = self.extract_color_tags()
|
||||
gradientmap = self.extract_gradient_tags()
|
||||
except ET.ParseError:
|
||||
# let pango handle that error
|
||||
pass
|
||||
validate_error = MarkupUtils.validate(self.text)
|
||||
if validate_error:
|
||||
raise ValueError(validate_error)
|
||||
file_name = self.text2svg()
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
super().__init__(
|
||||
file_name,
|
||||
**config,
|
||||
)
|
||||
self.chars = self.get_group_class()(*self.submobjects)
|
||||
self.text = text_without_tabs.replace(" ", "").replace("\n", "")
|
||||
if self.gradient:
|
||||
self.set_color_by_gradient(*self.gradient)
|
||||
for col in colormap:
|
||||
self.chars[
|
||||
col["start"]
|
||||
- col["start_offset"] : col["end"]
|
||||
- col["start_offset"]
|
||||
- col["end_offset"]
|
||||
].set_color(self._parse_color(col["color"]))
|
||||
for grad in gradientmap:
|
||||
self.chars[
|
||||
grad["start"]
|
||||
- grad["start_offset"] : grad["end"]
|
||||
- grad["start_offset"]
|
||||
- grad["end_offset"]
|
||||
].set_color_by_gradient(
|
||||
*(self._parse_color(grad["from"]), self._parse_color(grad["to"]))
|
||||
)
|
||||
# anti-aliasing
|
||||
if self.height is None:
|
||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||
|
||||
def text2hash(self):
|
||||
"""Generates ``sha256`` hash for file name."""
|
||||
settings = (
|
||||
"MARKUPPANGO" + self.font + self.slant + self.weight + self.color
|
||||
) # to differentiate from classical Pango Text
|
||||
settings += str(self.lsh) + str(self.font_size)
|
||||
settings += str(self.disable_ligatures)
|
||||
settings += str(self.justify)
|
||||
id_str = self.text + settings
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update(id_str.encode())
|
||||
return hasher.hexdigest()[:16]
|
||||
|
||||
def text2svg(self):
|
||||
"""Convert the text to SVG using Pango."""
|
||||
size = self.font_size
|
||||
dir_name = get_text_dir()
|
||||
disable_liga = self.disable_ligatures
|
||||
if not os.path.exists(dir_name):
|
||||
os.makedirs(dir_name)
|
||||
hash_name = self.text2hash()
|
||||
file_name = os.path.join(dir_name, hash_name) + ".svg"
|
||||
if os.path.exists(file_name):
|
||||
return file_name
|
||||
|
||||
extra_kwargs = {}
|
||||
extra_kwargs['justify'] = self.justify
|
||||
extra_kwargs['pango_width'] = DEFAULT_PIXEL_WIDTH - 100
|
||||
if self.lsh:
|
||||
extra_kwargs['line_spacing']=self.lsh
|
||||
return MarkupUtils.text2svg(
|
||||
f'<span foreground="{self.color}">{self.text}</span>',
|
||||
self.font,
|
||||
self.slant,
|
||||
self.weight,
|
||||
size,
|
||||
0, # empty parameter
|
||||
disable_liga,
|
||||
file_name,
|
||||
START_X,
|
||||
START_Y,
|
||||
DEFAULT_PIXEL_WIDTH, # width
|
||||
DEFAULT_PIXEL_HEIGHT, # height
|
||||
**extra_kwargs
|
||||
)
|
||||
|
||||
def _parse_color(self, col):
|
||||
"""Parse color given in ``<color>`` or ``<gradient>`` tags."""
|
||||
if re.match("#[0-9a-f]{6}", col):
|
||||
return col
|
||||
else:
|
||||
return globals()[col.upper()] # this is hacky
|
||||
|
||||
@functools.lru_cache(10)
|
||||
def get_text_from_markup(self, element=None):
|
||||
if not element:
|
||||
element = ET.fromstring(self.text_for_parsing)
|
||||
final_text = ''
|
||||
for i in element.itertext():
|
||||
final_text += i
|
||||
return final_text
|
||||
|
||||
def extract_color_tags(self, text=None, colormap = None):
|
||||
"""Used to determine which parts (if any) of the string should be formatted
|
||||
with a custom color.
|
||||
Removes the ``<color>`` tag, as it is not part of Pango's markup and would cause an error.
|
||||
Note: Using the ``<color>`` tags is deprecated. As soon as the legacy syntax is gone, this function
|
||||
will be removed.
|
||||
"""
|
||||
if not text:
|
||||
text = self.text_for_parsing
|
||||
if not colormap:
|
||||
colormap = list()
|
||||
elements = ET.fromstring(text)
|
||||
text_from_markup = self.get_text_from_markup()
|
||||
final_xml = ET.fromstring(f'<span>{elements.text if elements.text else ""}</span>')
|
||||
def get_color_map(elements):
|
||||
for element in elements:
|
||||
if element.tag == 'color':
|
||||
element_text = self.get_text_from_markup(element)
|
||||
start = text_from_markup.find(element_text)
|
||||
end = start + len(element_text)
|
||||
offsets = element.get('offset').split(",") if element.get('offset') else [0]
|
||||
start_offset = int(offsets[0]) if offsets[0] else 0
|
||||
end_offset = int(offsets[1]) if len(offsets) == 2 and offsets[1] else 0
|
||||
colormap.append(
|
||||
{
|
||||
"start": start,
|
||||
"end": end,
|
||||
"color": element.get('col'),
|
||||
"start_offset": start_offset,
|
||||
"end_offset": end_offset,
|
||||
}
|
||||
)
|
||||
|
||||
_elements_list = list(element.iter())
|
||||
if len(_elements_list) <= 1:
|
||||
final_xml.append(ET.fromstring(f'<span>{element.text if element.text else ""}</span>'))
|
||||
else:
|
||||
final_xml.append(_elements_list[-1])
|
||||
else:
|
||||
if len(list(element.iter())) == 1:
|
||||
final_xml.append(element)
|
||||
else:
|
||||
get_color_map(element)
|
||||
get_color_map(elements)
|
||||
with io.BytesIO() as f:
|
||||
tree = ET.ElementTree()
|
||||
tree._setroot(final_xml)
|
||||
tree.write(f)
|
||||
self.text = f.getvalue().decode()
|
||||
self.text_for_parsing = self.text # gradients will use it
|
||||
return colormap
|
||||
|
||||
def extract_gradient_tags(self, text=None,gradientmap=None):
|
||||
"""Used to determine which parts (if any) of the string should be formatted
|
||||
with a gradient.
|
||||
Removes the ``<gradient>`` tag, as it is not part of Pango's markup and would cause an error.
|
||||
"""
|
||||
if not text:
|
||||
text = self.text_for_parsing
|
||||
if not gradientmap:
|
||||
gradientmap = list()
|
||||
|
||||
elements = ET.fromstring(text)
|
||||
text_from_markup = self.get_text_from_markup()
|
||||
final_xml = ET.fromstring(f'<span>{elements.text if elements.text else ""}</span>')
|
||||
def get_gradient_map(elements):
|
||||
for element in elements:
|
||||
if element.tag == 'gradient':
|
||||
element_text = self.get_text_from_markup(element)
|
||||
start = text_from_markup.find(element_text)
|
||||
end = start + len(element_text)
|
||||
offsets = element.get('offset').split(",") if element.get('offset') else [0]
|
||||
start_offset = int(offsets[0]) if offsets[0] else 0
|
||||
end_offset = int(offsets[1]) if len(offsets) == 2 and offsets[1] else 0
|
||||
gradientmap.append(
|
||||
{
|
||||
"start": start,
|
||||
"end": end,
|
||||
"from": element.get('from'),
|
||||
"to": element.get('to'),
|
||||
"start_offset": start_offset,
|
||||
"end_offset": end_offset,
|
||||
}
|
||||
)
|
||||
_elements_list = list(element.iter())
|
||||
if len(_elements_list) == 1:
|
||||
final_xml.append(ET.fromstring(f'<span>{element.text if element.text else ""}</span>'))
|
||||
else:
|
||||
final_xml.append(_elements_list[-1])
|
||||
else:
|
||||
if len(list(element.iter())) == 1:
|
||||
final_xml.append(element)
|
||||
else:
|
||||
get_gradient_map(element)
|
||||
get_gradient_map(elements)
|
||||
with io.BytesIO() as f:
|
||||
tree = ET.ElementTree()
|
||||
tree._setroot(final_xml)
|
||||
tree.write(f)
|
||||
self.text = f.getvalue().decode()
|
||||
|
||||
return gradientmap
|
||||
|
||||
def __repr__(self):
|
||||
return f"MarkupText({repr(self.original_text)})"
|
||||
|
||||
|
||||
class Code(Text):
|
||||
CONFIG = {
|
||||
"font": "Consolas",
|
||||
"font_size": 24,
|
||||
"lsh": 1.0,
|
||||
"language": "python",
|
||||
# Visit https://pygments.org/demo/ to have a preview of more styles.
|
||||
"code_style": "monokai",
|
||||
# If not None, then each character will cover a space of equal width.
|
||||
"char_width": None
|
||||
}
|
||||
|
||||
def __init__(self, code, **kwargs):
|
||||
self.full2short(kwargs)
|
||||
digest_config(self, kwargs)
|
||||
code = code.lstrip("\n") # avoid mismatches of character indices
|
||||
lexer = pygments.lexers.get_lexer_by_name(self.language)
|
||||
tokens_generator = pygments.lex(code, lexer)
|
||||
styles_dict = dict(pygments.styles.get_style_by_name(self.code_style))
|
||||
default_color_hex = styles_dict[pygments.token.Text]["color"]
|
||||
if not default_color_hex:
|
||||
default_color_hex = self.color[1:]
|
||||
start_index = 0
|
||||
t2c = {}
|
||||
t2s = {}
|
||||
t2w = {}
|
||||
for pair in tokens_generator:
|
||||
ttype, token = pair
|
||||
end_index = start_index + len(token)
|
||||
range_str = f"[{start_index}:{end_index}]"
|
||||
style_dict = styles_dict[ttype]
|
||||
t2c[range_str] = "#" + (style_dict["color"] or default_color_hex)
|
||||
t2s[range_str] = ITALIC if style_dict["italic"] else NORMAL
|
||||
t2w[range_str] = BOLD if style_dict["bold"] else NORMAL
|
||||
start_index = end_index
|
||||
t2c.update(self.t2c)
|
||||
t2s.update(self.t2s)
|
||||
t2w.update(self.t2w)
|
||||
kwargs["t2c"] = t2c
|
||||
kwargs["t2s"] = t2s
|
||||
kwargs["t2w"] = t2w
|
||||
Text.__init__(self, code, **kwargs)
|
||||
if self.char_width is not None:
|
||||
self.set_monospace(self.char_width)
|
||||
|
||||
def set_monospace(self, char_width):
|
||||
current_char_index = 0
|
||||
for i, char in enumerate(self.text):
|
||||
if char == "\n":
|
||||
current_char_index = 0
|
||||
continue
|
||||
self[i].set_x(current_char_index * char_width)
|
||||
current_char_index += 1
|
||||
self.center()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def register_font(font_file: typing.Union[str, Path]):
|
||||
"""Temporarily add a font file to Pango's search path.
|
||||
@@ -240,8 +575,8 @@ def register_font(font_file: typing.Union[str, Path]):
|
||||
-----
|
||||
This method of adding font files also works with :class:`CairoText`.
|
||||
.. important ::
|
||||
This method isn't available for macOS. Using this
|
||||
method on macOS will raise an :class:`AttributeError`.
|
||||
This method is available for macOS for ``ManimPango>=v0.2.3``. Using this
|
||||
method with previous releases will raise an :class:`AttributeError` on macOS.
|
||||
"""
|
||||
|
||||
input_folder = Path(get_downloads_dir()).parent.resolve()
|
||||
|
||||
@@ -5,16 +5,18 @@ from manimlib.mobject.types.surface import Surface
|
||||
from manimlib.mobject.types.surface import SGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.geometry import Square
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.space_ops import z_to_vector
|
||||
from manimlib.utils.space_ops import compass_directions
|
||||
|
||||
|
||||
class SurfaceMesh(VGroup):
|
||||
CONFIG = {
|
||||
"resolution": (21, 21),
|
||||
"stroke_width": 1,
|
||||
"normal_nudge": 1e-2,
|
||||
"normal_nudge": 1e-3,
|
||||
"depth_test": True,
|
||||
"flat_stroke": False,
|
||||
}
|
||||
@@ -35,7 +37,7 @@ class SurfaceMesh(VGroup):
|
||||
|
||||
points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points()
|
||||
normals = uv_surface.get_unit_normals()
|
||||
nudge = 1e-2
|
||||
nudge = self.normal_nudge
|
||||
nudged_points = points + nudge * normals
|
||||
|
||||
for ui in u_indices:
|
||||
@@ -161,15 +163,49 @@ class Cube(SGroup):
|
||||
"gloss": 0.5,
|
||||
"square_resolution": (2, 2),
|
||||
"side_length": 2,
|
||||
"square_class": Square3D,
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
for vect in [OUT, RIGHT, UP, LEFT, DOWN, IN]:
|
||||
face = Square3D(resolution=self.square_resolution)
|
||||
face.shift(OUT)
|
||||
face.apply_matrix(z_to_vector(vect))
|
||||
self.add(face)
|
||||
self.set_height(self.side_length)
|
||||
face = Square3D(
|
||||
resolution=self.square_resolution,
|
||||
side_length=self.side_length,
|
||||
)
|
||||
self.add(*self.square_to_cube_faces(face))
|
||||
|
||||
@staticmethod
|
||||
def square_to_cube_faces(square):
|
||||
radius = square.get_height() / 2
|
||||
square.move_to(radius * OUT)
|
||||
result = [square]
|
||||
result.extend([
|
||||
square.copy().rotate(PI / 2, axis=vect, about_point=ORIGIN)
|
||||
for vect in compass_directions(4)
|
||||
])
|
||||
result.append(square.copy().rotate(PI, RIGHT, about_point=ORIGIN))
|
||||
return result
|
||||
|
||||
def _get_face(self):
|
||||
return Square3D(resolution=self.square_resolution)
|
||||
|
||||
|
||||
class VCube(VGroup):
|
||||
CONFIG = {
|
||||
"fill_color": BLUE_D,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
"gloss": 0.5,
|
||||
"shadow": 0.5,
|
||||
}
|
||||
|
||||
def __init__(self, side_length=2, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
face = Square(side_length=side_length)
|
||||
face.get_triangulation()
|
||||
self.add(*Cube.square_to_cube_faces(face))
|
||||
self.init_colors()
|
||||
self.apply_depth_test()
|
||||
self.refresh_unit_normal()
|
||||
|
||||
|
||||
class Prism(Cube):
|
||||
|
||||
@@ -2,11 +2,12 @@ import numpy as np
|
||||
import moderngl
|
||||
|
||||
from manimlib.constants import GREY_C
|
||||
from manimlib.constants import ORIGIN
|
||||
from manimlib.mobject.types.point_cloud_mobject import PMobject
|
||||
from manimlib.utils.iterables import resize_preserving_order
|
||||
|
||||
|
||||
DEFAULT_DOT_CLOUD_RADIUS = 0.05
|
||||
DEFAULT_DOT_RADIUS = 0.05
|
||||
DEFAULT_GRID_HEIGHT = 6
|
||||
DEFAULT_BUFF_RATIO = 0.5
|
||||
|
||||
@@ -15,7 +16,7 @@ class DotCloud(PMobject):
|
||||
CONFIG = {
|
||||
"color": GREY_C,
|
||||
"opacity": 1,
|
||||
"radius": DEFAULT_DOT_CLOUD_RADIUS,
|
||||
"radius": DEFAULT_DOT_RADIUS,
|
||||
"shader_folder": "true_dot",
|
||||
"render_primitive": moderngl.POINTS,
|
||||
"shader_dtype": [
|
||||
@@ -67,7 +68,9 @@ class DotCloud(PMobject):
|
||||
return self
|
||||
|
||||
def set_radii(self, radii):
|
||||
self.data["radii"][:] = resize_preserving_order(radii, len(self.data["radii"]))
|
||||
n_points = len(self.get_points())
|
||||
radii = np.array(radii).reshape((len(radii), 1))
|
||||
self.data["radii"] = resize_preserving_order(radii, n_points)
|
||||
self.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
@@ -106,3 +109,8 @@ class DotCloud(PMobject):
|
||||
self.read_data_to_shader(shader_data, "radius", "radii")
|
||||
self.read_data_to_shader(shader_data, "color", "rgbas")
|
||||
return shader_data
|
||||
|
||||
|
||||
class TrueDot(DotCloud):
|
||||
def __init__(self, center=ORIGIN, radius=DEFAULT_DOT_RADIUS, **kwargs):
|
||||
super().__init__(points=[center], radius=radius, **kwargs)
|
||||
|
||||
@@ -22,10 +22,13 @@ class ImageMobject(Mobject):
|
||||
}
|
||||
|
||||
def __init__(self, filename, **kwargs):
|
||||
path = get_full_raster_image_path(filename)
|
||||
self.set_image_path(get_full_raster_image_path(filename))
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def set_image_path(self, path):
|
||||
self.path = path
|
||||
self.image = Image.open(path)
|
||||
self.texture_paths = {"Texture": path}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_data(self):
|
||||
self.data = {
|
||||
|
||||
@@ -14,10 +14,17 @@ class PMobject(Mobject):
|
||||
def resize_points(self, size, resize_func=resize_array):
|
||||
# TODO
|
||||
for key in self.data:
|
||||
if key == "bounding_box":
|
||||
continue
|
||||
if len(self.data[key]) != size:
|
||||
self.data[key] = resize_array(self.data[key], size)
|
||||
return self
|
||||
|
||||
def set_points(self, points):
|
||||
super().set_points(points)
|
||||
self.resize_points(len(points))
|
||||
return self
|
||||
|
||||
def add_points(self, points, rgbas=None, color=None, opacity=None):
|
||||
"""
|
||||
points must be a Nx3 numpy array, as must rgbas if it is not None
|
||||
@@ -54,6 +61,8 @@ class PMobject(Mobject):
|
||||
for mob in self.family_members_with_points():
|
||||
to_keep = ~np.apply_along_axis(condition, 1, mob.get_points())
|
||||
for key in mob.data:
|
||||
if key == "bounding_box":
|
||||
continue
|
||||
mob.data[key] = mob.data[key][to_keep]
|
||||
return self
|
||||
|
||||
@@ -85,7 +94,9 @@ class PMobject(Mobject):
|
||||
lower_index = int(a * pmobject.get_num_points())
|
||||
upper_index = int(b * pmobject.get_num_points())
|
||||
for key in self.data:
|
||||
self.data[key] = pmobject.data[key][lower_index:upper_index]
|
||||
if key == "bounding_box":
|
||||
continue
|
||||
self.data[key] = pmobject.data[key][lower_index:upper_index].copy()
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@@ -181,9 +181,9 @@ class Surface(Mobject):
|
||||
|
||||
|
||||
class ParametricSurface(Surface):
|
||||
def __init__(self, uv_func, **kwargs):
|
||||
def __init__(self, uv_func, u_range=(0, 1), v_range=(0, 1), **kwargs):
|
||||
self.passed_uv_func = uv_func
|
||||
super().__init__(**kwargs)
|
||||
super().__init__(u_range=u_range, v_range=v_range, **kwargs)
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return self.passed_uv_func(u, v)
|
||||
|
||||
@@ -52,9 +52,8 @@ class VMobject(Mobject):
|
||||
"fill_shader_folder": "quadratic_bezier_fill",
|
||||
# Could also be "bevel", "miter", "round"
|
||||
"joint_type": "auto",
|
||||
"flat_stroke": True,
|
||||
"flat_stroke": False,
|
||||
"render_primitive": moderngl.TRIANGLES,
|
||||
"triangulation_locked": False,
|
||||
"fill_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
('unit_normal', np.float32, (3,)),
|
||||
@@ -106,24 +105,42 @@ class VMobject(Mobject):
|
||||
self.set_flat_stroke(self.flat_stroke)
|
||||
return self
|
||||
|
||||
def set_rgba_array(self, rgba_array, name=None, recurse=False):
|
||||
if name is None:
|
||||
names = ["fill_rgba", "stroke_rgba"]
|
||||
else:
|
||||
names = [name]
|
||||
|
||||
for name in names:
|
||||
super().set_rgba_array(rgba_array, name, recurse)
|
||||
return self
|
||||
|
||||
def set_fill(self, color=None, opacity=None, recurse=True):
|
||||
self.set_rgba_array(color, opacity, 'fill_rgba', recurse)
|
||||
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
|
||||
return self
|
||||
|
||||
def set_stroke(self, color=None, width=None, opacity=None, background=None, recurse=True):
|
||||
self.set_rgba_array(color, opacity, 'stroke_rgba', recurse)
|
||||
self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse)
|
||||
|
||||
if width is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data['stroke_width'] = np.array([
|
||||
[width] for width in listify(width)
|
||||
])
|
||||
if isinstance(width, np.ndarray):
|
||||
arr = width.reshape((len(width), 1))
|
||||
else:
|
||||
arr = np.array([[w] for w in listify(width)], dtype=float)
|
||||
mob.data['stroke_width'] = arr
|
||||
|
||||
if background is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.draw_stroke_behind_fill = background
|
||||
return self
|
||||
|
||||
def align_stroke_width_data_to_points(self, recurse=True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["stroke_width"] = resize_with_interpolation(
|
||||
mob.data["stroke_width"], len(mob.get_points())
|
||||
)
|
||||
|
||||
def set_style(self,
|
||||
fill_color=None,
|
||||
fill_opacity=None,
|
||||
@@ -132,6 +149,7 @@ class VMobject(Mobject):
|
||||
stroke_opacity=None,
|
||||
stroke_rgba=None,
|
||||
stroke_width=None,
|
||||
stroke_background=True,
|
||||
gloss=None,
|
||||
shadow=None,
|
||||
recurse=True):
|
||||
@@ -146,13 +164,17 @@ class VMobject(Mobject):
|
||||
|
||||
if stroke_rgba is not None:
|
||||
self.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba))
|
||||
self.set_stroke(width=stroke_width)
|
||||
self.set_stroke(
|
||||
width=stroke_width,
|
||||
background=stroke_background,
|
||||
)
|
||||
else:
|
||||
self.set_stroke(
|
||||
color=stroke_color,
|
||||
width=stroke_width,
|
||||
opacity=stroke_opacity,
|
||||
recurse=recurse,
|
||||
background=stroke_background,
|
||||
)
|
||||
|
||||
if gloss is not None:
|
||||
@@ -166,6 +188,7 @@ class VMobject(Mobject):
|
||||
"fill_rgba": self.data['fill_rgba'],
|
||||
"stroke_rgba": self.data['stroke_rgba'],
|
||||
"stroke_width": self.data['stroke_width'],
|
||||
"stroke_background": self.draw_stroke_behind_fill,
|
||||
"gloss": self.get_gloss(),
|
||||
"shadow": self.get_shadow(),
|
||||
}
|
||||
@@ -226,7 +249,7 @@ class VMobject(Mobject):
|
||||
return self.data['stroke_rgba'][:, 3]
|
||||
|
||||
def get_stroke_widths(self):
|
||||
return self.data['stroke_width']
|
||||
return self.data['stroke_width'][:, 0]
|
||||
|
||||
# TODO, it's weird for these to return the first of various lists
|
||||
# rather than the full information
|
||||
@@ -259,7 +282,7 @@ class VMobject(Mobject):
|
||||
return self.get_fill_color()
|
||||
|
||||
def has_stroke(self):
|
||||
return any(self.get_stroke_widths()) and any(self.get_stroke_opacities())
|
||||
return self.get_stroke_widths().any() and self.get_stroke_opacities().any()
|
||||
|
||||
def has_fill(self):
|
||||
return any(self.get_fill_opacities())
|
||||
@@ -406,7 +429,10 @@ class VMobject(Mobject):
|
||||
|
||||
def set_points_smoothly(self, points, true_smooth=False):
|
||||
self.set_points_as_corners(points)
|
||||
self.make_smooth()
|
||||
if true_smooth:
|
||||
self.make_smooth()
|
||||
else:
|
||||
self.make_approximately_smooth()
|
||||
return self
|
||||
|
||||
def change_anchor_mode(self, mode):
|
||||
@@ -830,8 +856,8 @@ class VMobject(Mobject):
|
||||
old_points = self.get_points()
|
||||
func(self, *args, **kwargs)
|
||||
if not np.all(self.get_points() == old_points):
|
||||
self.refresh_triangulation()
|
||||
self.refresh_unit_normal()
|
||||
self.refresh_triangulation()
|
||||
return wrapper
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
@@ -852,9 +878,10 @@ class VMobject(Mobject):
|
||||
self.make_approximately_smooth()
|
||||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def flip(self, *args, **kwargs):
|
||||
super().flip(*args, **kwargs)
|
||||
self.refresh_unit_normal()
|
||||
self.refresh_triangulation()
|
||||
return self
|
||||
|
||||
# For shaders
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import numpy as np
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import listify
|
||||
|
||||
|
||||
class ValueTracker(Mobject):
|
||||
@@ -15,18 +16,25 @@ class ValueTracker(Mobject):
|
||||
}
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
self.value = value
|
||||
super().__init__(**kwargs)
|
||||
self.set_value(value)
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
self.data["value"] = np.zeros((1, 1), dtype=self.value_type)
|
||||
self.data["value"] = np.array(
|
||||
listify(self.value),
|
||||
ndmin=2,
|
||||
dtype=self.value_type,
|
||||
)
|
||||
|
||||
def get_value(self):
|
||||
return self.data["value"][0, 0]
|
||||
result = self.data["value"][0, :]
|
||||
if len(result) == 1:
|
||||
return result[0]
|
||||
return result
|
||||
|
||||
def set_value(self, value):
|
||||
self.data["value"][0, 0] = value
|
||||
self.data["value"][0, :] = value
|
||||
return self
|
||||
|
||||
def increment_value(self, d_value):
|
||||
|
||||
@@ -1,66 +1,32 @@
|
||||
import numpy as np
|
||||
import os
|
||||
import itertools as it
|
||||
from PIL import Image
|
||||
import random
|
||||
|
||||
from manimlib.constants import *
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.animation.indication import ShowPassingFlash
|
||||
from manimlib.mobject.geometry import Vector
|
||||
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 inverse_interpolate
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.color import color_to_rgb
|
||||
from manimlib.utils.color import rgb_to_color
|
||||
from manimlib.utils.color import get_colormap_list
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.simple_functions import sigmoid
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
# from manimlib.utils.space_ops import normalize
|
||||
|
||||
|
||||
DEFAULT_SCALAR_FIELD_COLORS = [BLUE_E, GREEN, YELLOW, RED]
|
||||
|
||||
|
||||
def get_colored_background_image(scalar_field_func,
|
||||
number_to_rgb_func,
|
||||
pixel_height=DEFAULT_PIXEL_HEIGHT,
|
||||
pixel_width=DEFAULT_PIXEL_WIDTH):
|
||||
ph = pixel_height
|
||||
pw = pixel_width
|
||||
fw = FRAME_WIDTH
|
||||
fh = FRAME_HEIGHT
|
||||
points_array = np.zeros((ph, pw, 3))
|
||||
x_array = np.linspace(-fw / 2, fw / 2, pw)
|
||||
x_array = x_array.reshape((1, len(x_array)))
|
||||
x_array = x_array.repeat(ph, axis=0)
|
||||
|
||||
y_array = np.linspace(fh / 2, -fh / 2, ph)
|
||||
y_array = y_array.reshape((len(y_array), 1))
|
||||
y_array.repeat(pw, axis=1)
|
||||
points_array[:, :, 0] = x_array
|
||||
points_array[:, :, 1] = y_array
|
||||
scalars = np.apply_along_axis(scalar_field_func, 2, points_array)
|
||||
rgb_array = number_to_rgb_func(scalars.flatten()).reshape((ph, pw, 3))
|
||||
return Image.fromarray((rgb_array * 255).astype('uint8'))
|
||||
|
||||
|
||||
def get_rgb_gradient_function(min_value=0, max_value=1,
|
||||
colors=[BLUE, RED],
|
||||
flip_alphas=True, # Why?
|
||||
):
|
||||
rgbs = np.array(list(map(color_to_rgb, colors)))
|
||||
def get_vectorized_rgb_gradient_function(min_value, max_value, color_map):
|
||||
rgbs = np.array(get_colormap_list(color_map))
|
||||
|
||||
def func(values):
|
||||
alphas = inverse_interpolate(
|
||||
min_value, max_value, np.array(values)
|
||||
)
|
||||
alphas = np.clip(alphas, 0, 1)
|
||||
# if flip_alphas:
|
||||
# alphas = 1 - alphas
|
||||
scaled_alphas = alphas * (len(rgbs) - 1)
|
||||
indices = scaled_alphas.astype(int)
|
||||
next_indices = np.clip(indices + 1, 0, len(rgbs) - 1)
|
||||
@@ -71,29 +37,9 @@ def get_rgb_gradient_function(min_value=0, max_value=1,
|
||||
return func
|
||||
|
||||
|
||||
def get_color_field_image_file(scalar_func,
|
||||
min_value=0, max_value=2,
|
||||
colors=DEFAULT_SCALAR_FIELD_COLORS
|
||||
):
|
||||
# try_hash
|
||||
np.random.seed(0)
|
||||
sample_inputs = 5 * np.random.random(size=(10, 3)) - 10
|
||||
sample_outputs = np.apply_along_axis(scalar_func, 1, sample_inputs)
|
||||
func_hash = hash(
|
||||
str(min_value) + str(max_value) + str(colors) + str(sample_outputs)
|
||||
)
|
||||
file_name = "%d.png" % func_hash
|
||||
full_path = os.path.join(RASTER_IMAGE_DIR, file_name)
|
||||
if not os.path.exists(full_path):
|
||||
print("Rendering color field image " + str(func_hash))
|
||||
rgb_gradient_func = get_rgb_gradient_function(
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
colors=colors
|
||||
)
|
||||
image = get_colored_background_image(scalar_func, rgb_gradient_func)
|
||||
image.save(full_path)
|
||||
return full_path
|
||||
def get_rgb_gradient_function(min_value, max_value, color_map):
|
||||
vectorized_func = get_vectorized_rgb_gradient_function(min_value, max_value, color_map)
|
||||
return lambda value: vectorized_func([value])[0]
|
||||
|
||||
|
||||
def move_along_vector_field(mobject, func):
|
||||
@@ -116,217 +62,200 @@ def move_submobjects_along_vector_field(mobject, func):
|
||||
return mobject
|
||||
|
||||
|
||||
def move_points_along_vector_field(mobject, func):
|
||||
def move_points_along_vector_field(mobject, func, coordinate_system):
|
||||
cs = coordinate_system
|
||||
origin = cs.get_origin()
|
||||
|
||||
def apply_nudge(self, dt):
|
||||
self.mobject.apply_function(
|
||||
lambda p: p + func(p) * dt
|
||||
mobject.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(coordinate_system, step_multiple):
|
||||
ranges = []
|
||||
for range_args in coordinate_system.get_all_ranges():
|
||||
_min, _max, step = range_args
|
||||
step *= step_multiple
|
||||
ranges.append(np.arange(_min, _max + step, step))
|
||||
return it.product(*ranges)
|
||||
|
||||
|
||||
# Mobjects
|
||||
|
||||
class VectorField(VGroup):
|
||||
CONFIG = {
|
||||
"delta_x": 0.5,
|
||||
"delta_y": 0.5,
|
||||
"x_min": int(np.floor(-FRAME_WIDTH / 2)),
|
||||
"x_max": int(np.ceil(FRAME_WIDTH / 2)),
|
||||
"y_min": int(np.floor(-FRAME_HEIGHT / 2)),
|
||||
"y_max": int(np.ceil(FRAME_HEIGHT / 2)),
|
||||
"min_magnitude": 0,
|
||||
"max_magnitude": 2,
|
||||
"colors": DEFAULT_SCALAR_FIELD_COLORS,
|
||||
"step_multiple": 0.5,
|
||||
"magnitude_range": (0, 2),
|
||||
"color_map": "3b1b_colormap",
|
||||
# Takes in actual norm, spits out displayed norm
|
||||
"length_func": lambda norm: 0.45 * sigmoid(norm),
|
||||
"opacity": 1.0,
|
||||
"vector_config": {},
|
||||
}
|
||||
|
||||
def __init__(self, func, **kwargs):
|
||||
def __init__(self, func, coordinate_system, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.func = func
|
||||
self.rgb_gradient_function = get_rgb_gradient_function(
|
||||
self.min_magnitude,
|
||||
self.max_magnitude,
|
||||
self.colors,
|
||||
flip_alphas=False
|
||||
self.coordinate_system = coordinate_system
|
||||
self.value_to_rgb = get_rgb_gradient_function(
|
||||
*self.magnitude_range, self.color_map,
|
||||
)
|
||||
x_range = np.arange(
|
||||
self.x_min,
|
||||
self.x_max + self.delta_x,
|
||||
self.delta_x
|
||||
)
|
||||
y_range = np.arange(
|
||||
self.y_min,
|
||||
self.y_max + self.delta_y,
|
||||
self.delta_y
|
||||
)
|
||||
for x, y in it.product(x_range, y_range):
|
||||
point = x * RIGHT + y * UP
|
||||
self.add(self.get_vector(point))
|
||||
self.set_opacity(self.opacity)
|
||||
|
||||
def get_vector(self, point, **kwargs):
|
||||
output = np.array(self.func(point))
|
||||
norm = get_norm(output)
|
||||
if norm == 0:
|
||||
output *= 0
|
||||
else:
|
||||
output *= self.length_func(norm) / norm
|
||||
vector_config = dict(self.vector_config)
|
||||
vector_config.update(kwargs)
|
||||
vect = Vector(output, **vector_config)
|
||||
vect.shift(point)
|
||||
fill_color = rgb_to_color(
|
||||
self.rgb_gradient_function(np.array([norm]))[0]
|
||||
samples = get_sample_points_from_coordinate_system(
|
||||
coordinate_system, self.step_multiple
|
||||
)
|
||||
vect.set_color(fill_color)
|
||||
self.add(*(
|
||||
self.get_vector(coords)
|
||||
for coords in samples
|
||||
))
|
||||
|
||||
def get_vector(self, coords, **kwargs):
|
||||
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_rgba_array([[*self.value_to_rgb(norm), self.opacity]])
|
||||
return vect
|
||||
|
||||
|
||||
class StreamLines(VGroup):
|
||||
CONFIG = {
|
||||
# TODO, this is an awkward way to inherit
|
||||
# defaults to a method.
|
||||
"start_points_generator_config": {},
|
||||
# Config for choosing start points
|
||||
"x_min": -8,
|
||||
"x_max": 8,
|
||||
"y_min": -5,
|
||||
"y_max": 5,
|
||||
"delta_x": 0.5,
|
||||
"delta_y": 0.5,
|
||||
"step_multiple": 0.5,
|
||||
"n_repeats": 1,
|
||||
"noise_factor": None,
|
||||
# Config for drawing lines
|
||||
"dt": 0.05,
|
||||
"virtual_time": 3,
|
||||
"n_anchors_per_line": 100,
|
||||
"arc_len": 3,
|
||||
"max_time_steps": 200,
|
||||
"n_samples_per_line": 10,
|
||||
"cutoff_norm": 15,
|
||||
# Style info
|
||||
"stroke_width": 1,
|
||||
"stroke_color": WHITE,
|
||||
"color_by_arc_length": True,
|
||||
# Min and max arc lengths meant to define
|
||||
# the color range, should color_by_arc_length be True
|
||||
"min_arc_length": 0,
|
||||
"max_arc_length": 12,
|
||||
"color_by_magnitude": False,
|
||||
# Min and max magnitudes meant to define
|
||||
# the color range, should color_by_magnitude be True
|
||||
"min_magnitude": 0.5,
|
||||
"max_magnitude": 1.5,
|
||||
"colors": DEFAULT_SCALAR_FIELD_COLORS,
|
||||
"cutoff_norm": 15,
|
||||
"stroke_opacity": 1,
|
||||
"color_by_magnitude": True,
|
||||
"magnitude_range": (0, 2.0),
|
||||
"taper_stroke_width": False,
|
||||
"color_map": "3b1b_colormap",
|
||||
}
|
||||
|
||||
def __init__(self, func, **kwargs):
|
||||
VGroup.__init__(self, **kwargs)
|
||||
def __init__(self, func, coordinate_system, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.func = func
|
||||
dt = self.dt
|
||||
self.coordinate_system = coordinate_system
|
||||
self.draw_lines()
|
||||
self.init_style()
|
||||
|
||||
start_points = self.get_start_points(
|
||||
**self.start_points_generator_config
|
||||
)
|
||||
for point in start_points:
|
||||
def point_func(self, point):
|
||||
in_coords = self.coordinate_system.p2c(point)
|
||||
out_coords = self.func(*in_coords)
|
||||
return self.coordinate_system.c2p(*out_coords)
|
||||
|
||||
def draw_lines(self):
|
||||
lines = []
|
||||
origin = self.coordinate_system.get_origin()
|
||||
for point in self.get_start_points():
|
||||
points = [point]
|
||||
for t in np.arange(0, self.virtual_time, dt):
|
||||
total_arc_len = 0
|
||||
time = 0
|
||||
for x in range(self.max_time_steps):
|
||||
time += self.dt
|
||||
last_point = points[-1]
|
||||
points.append(last_point + dt * func(last_point))
|
||||
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
|
||||
line = VMobject()
|
||||
step = max(1, int(len(points) / self.n_anchors_per_line))
|
||||
line.set_points_smoothly(points[::step])
|
||||
self.add(line)
|
||||
|
||||
self.set_stroke(self.stroke_color, self.stroke_width)
|
||||
|
||||
if self.color_by_arc_length:
|
||||
len_to_rgb = get_rgb_gradient_function(
|
||||
self.min_arc_length,
|
||||
self.max_arc_length,
|
||||
colors=self.colors,
|
||||
)
|
||||
for line in self:
|
||||
arc_length = line.get_arc_length()
|
||||
rgb = len_to_rgb([arc_length])[0]
|
||||
color = rgb_to_color(rgb)
|
||||
line.set_color(color)
|
||||
elif self.color_by_magnitude:
|
||||
image_file = get_color_field_image_file(
|
||||
lambda p: get_norm(func(p)),
|
||||
min_value=self.min_magnitude,
|
||||
max_value=self.max_magnitude,
|
||||
colors=self.colors,
|
||||
)
|
||||
self.color_using_background_image(image_file)
|
||||
line.virtual_time = time
|
||||
step = max(1, int(len(points) / self.n_samples_per_line))
|
||||
line.set_points_as_corners(points[::step])
|
||||
line.make_approximately_smooth()
|
||||
lines.append(line)
|
||||
self.set_submobjects(lines)
|
||||
|
||||
def get_start_points(self):
|
||||
x_min = self.x_min
|
||||
x_max = self.x_max
|
||||
y_min = self.y_min
|
||||
y_max = self.y_max
|
||||
delta_x = self.delta_x
|
||||
delta_y = self.delta_y
|
||||
n_repeats = self.n_repeats
|
||||
cs = self.coordinate_system
|
||||
sample_coords = get_sample_points_from_coordinate_system(
|
||||
cs, self.step_multiple,
|
||||
)
|
||||
|
||||
noise_factor = self.noise_factor
|
||||
|
||||
if noise_factor is None:
|
||||
noise_factor = delta_y / 2
|
||||
noise_factor = cs.x_range[2] * self.step_multiple * 0.5
|
||||
|
||||
return np.array([
|
||||
x * RIGHT + y * UP + noise_factor * np.random.random(3)
|
||||
for n in range(n_repeats)
|
||||
for x in np.arange(x_min, x_max + delta_x, delta_x)
|
||||
for y in np.arange(y_min, y_max + delta_y, delta_y)
|
||||
cs.c2p(*coords) + noise_factor * np.random.random(3)
|
||||
for n in range(self.n_repeats)
|
||||
for coords in sample_coords
|
||||
])
|
||||
|
||||
|
||||
# TODO: Make it so that you can have a group of stream_lines
|
||||
# varying in response to a changing vector field, and still
|
||||
# animate the resulting flow
|
||||
class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
||||
CONFIG = {
|
||||
"n_segments": 10,
|
||||
"time_width": 0.1,
|
||||
"remover": True
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
max_stroke_width = vmobject.get_stroke_width()
|
||||
max_time_width = kwargs.pop("time_width", self.time_width)
|
||||
AnimationGroup.__init__(self, *[
|
||||
ShowPassingFlash(
|
||||
vmobject.deepcopy().set_stroke(width=stroke_width),
|
||||
time_width=time_width,
|
||||
**kwargs
|
||||
def init_style(self):
|
||||
if self.color_by_magnitude:
|
||||
values_to_rgbs = get_vectorized_rgb_gradient_function(
|
||||
*self.magnitude_range, self.color_map,
|
||||
)
|
||||
for stroke_width, time_width in zip(
|
||||
np.linspace(0, max_stroke_width, self.n_segments),
|
||||
np.linspace(max_time_width, 0, self.n_segments)
|
||||
)
|
||||
])
|
||||
cs = self.coordinate_system
|
||||
for line in self.submobjects:
|
||||
norms = [
|
||||
get_norm(self.func(*cs.p2c(point)))
|
||||
for point in line.get_points()
|
||||
]
|
||||
rgbs = values_to_rgbs(norms)
|
||||
rgbas = np.zeros((len(rgbs), 4))
|
||||
rgbas[:, :3] = rgbs
|
||||
rgbas[:, 3] = self.stroke_opacity
|
||||
line.set_rgba_array(rgbas, "stroke_rgba")
|
||||
else:
|
||||
self.set_stroke(self.stroke_color, opacity=self.stroke_opacity)
|
||||
|
||||
if self.taper_stroke_width:
|
||||
width = [0, self.stroke_width, 0]
|
||||
else:
|
||||
width = self.stroke_width
|
||||
self.set_stroke(width=width)
|
||||
|
||||
|
||||
# TODO, this is untested after turning it from a
|
||||
# ContinualAnimation into a VGroup
|
||||
class AnimatedStreamLines(VGroup):
|
||||
CONFIG = {
|
||||
"lag_range": 4,
|
||||
"line_anim_class": ShowPassingFlash,
|
||||
"line_anim_class": VShowPassingFlash,
|
||||
"line_anim_config": {
|
||||
"run_time": 4,
|
||||
# "run_time": 4,
|
||||
"rate_func": linear,
|
||||
"time_width": 0.3,
|
||||
"time_width": 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, stream_lines, **kwargs):
|
||||
VGroup.__init__(self, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.stream_lines = stream_lines
|
||||
for line in stream_lines:
|
||||
line.anim = self.line_anim_class(line, **self.line_anim_config)
|
||||
line.anim = self.line_anim_class(
|
||||
line,
|
||||
run_time=line.virtual_time,
|
||||
**self.line_anim_config,
|
||||
)
|
||||
line.anim.begin()
|
||||
line.time = -self.lag_range * random.random()
|
||||
self.add(line.anim.mobject)
|
||||
@@ -339,3 +268,28 @@ class AnimatedStreamLines(VGroup):
|
||||
line.time += dt
|
||||
adjusted_time = max(line.time, 0) % line.anim.run_time
|
||||
line.anim.update(adjusted_time / line.anim.run_time)
|
||||
|
||||
|
||||
# TODO: This class should be deleted
|
||||
class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
||||
CONFIG = {
|
||||
"n_segments": 10,
|
||||
"time_width": 0.1,
|
||||
"remover": True
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
max_stroke_width = vmobject.get_stroke_width()
|
||||
max_time_width = kwargs.pop("time_width", self.time_width)
|
||||
AnimationGroup.__init__(self, *[
|
||||
VShowPassingFlash(
|
||||
vmobject.deepcopy().set_stroke(width=stroke_width),
|
||||
time_width=time_width,
|
||||
**kwargs
|
||||
)
|
||||
for stroke_width, time_width in zip(
|
||||
np.linspace(0, max_stroke_width, self.n_segments),
|
||||
np.linspace(max_time_width, 0, self.n_segments)
|
||||
)
|
||||
])
|
||||
|
||||
@@ -10,6 +10,7 @@ from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
import itertools as it
|
||||
|
||||
class CountingScene(Scene):
|
||||
CONFIG = {
|
||||
@@ -219,7 +220,7 @@ class CountInTernary(PowerCounter):
|
||||
def construct(self):
|
||||
self.count(27)
|
||||
|
||||
# def get_template_configuration(self):
|
||||
# def get_template_configuration(self, place):
|
||||
# return [ORIGIN, UP]
|
||||
|
||||
|
||||
@@ -233,7 +234,7 @@ class CountInBinaryTo256(PowerCounter):
|
||||
def construct(self):
|
||||
self.count(128, 0.3)
|
||||
|
||||
def get_template_configuration(self):
|
||||
def get_template_configuration(self, place):
|
||||
return [ORIGIN, UP]
|
||||
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@ class DiscreteGraphScene(Scene):
|
||||
x_coord_of = {root: 0}
|
||||
y_coord_of = {root: bottom}
|
||||
# width to allocate to a given node, computed as
|
||||
# the maxium number of decendents in a single generation,
|
||||
# the maximum number of decendents in a single generation,
|
||||
# minus 1, multiplied by x_sep
|
||||
width_of = {}
|
||||
for index in indices:
|
||||
|
||||
@@ -74,12 +74,14 @@ class SwitchOff(LaggedStartMap):
|
||||
|
||||
class Lighthouse(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name": "lighthouse",
|
||||
"height": LIGHTHOUSE_HEIGHT,
|
||||
"fill_color": WHITE,
|
||||
"fill_opacity": 1.0,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("lighthouse", **kwargs)
|
||||
|
||||
def move_to(self, point):
|
||||
self.next_to(point, DOWN, buff=0)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import adjacent_pairs
|
||||
|
||||
# Warning: This is all now pretty depricated, and should not be expected to work
|
||||
# Warning: This is all now pretty deprecated, and should not be expected to work
|
||||
|
||||
|
||||
class Region(Mobject):
|
||||
|
||||
@@ -2,7 +2,6 @@ import inspect
|
||||
import random
|
||||
import platform
|
||||
import itertools as it
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
@@ -11,16 +10,17 @@ import time
|
||||
|
||||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.animation.transform import MoveToTarget
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
from manimlib.utils.family_ops import restructure_list_to_exclude_certain_family_members
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
from manimlib.event_handler import EVENT_DISPATCHER
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
class Scene(object):
|
||||
@@ -45,12 +45,13 @@ class Scene(object):
|
||||
from manimlib.window import Window
|
||||
self.window = Window(scene=self, **self.window_config)
|
||||
self.camera_config["ctx"] = self.window.ctx
|
||||
self.camera_config["frame_rate"] = 30 # Where's that 30 from?
|
||||
else:
|
||||
self.window = None
|
||||
|
||||
self.camera = self.camera_class(**self.camera_config)
|
||||
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
|
||||
self.mobjects = []
|
||||
self.mobjects = [self.camera.frame]
|
||||
self.num_plays = 0
|
||||
self.time = 0
|
||||
self.skip_time = 0
|
||||
@@ -100,10 +101,12 @@ class Scene(object):
|
||||
# If there is a window, enter a loop
|
||||
# which updates the frame while under
|
||||
# the hood calling the pyglet event loop
|
||||
log.info("Tips: You are now in the interactive mode. Now you can use the keyboard"
|
||||
" and the mouse to interact with the scene. Just press `q` if you want to quit.")
|
||||
self.quit_interaction = False
|
||||
self.lock_static_mobject_data()
|
||||
while not (self.window.is_closing or self.quit_interaction):
|
||||
self.update_frame()
|
||||
self.update_frame(1 / self.camera.frame_rate)
|
||||
if self.window.is_closing:
|
||||
self.window.destroy()
|
||||
if self.quit_interaction:
|
||||
@@ -117,17 +120,22 @@ class Scene(object):
|
||||
self.stop_skipping()
|
||||
self.linger_after_completion = False
|
||||
self.update_frame()
|
||||
|
||||
|
||||
# Save scene state at the point of embedding
|
||||
self.save_state()
|
||||
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
shell = InteractiveShellEmbed()
|
||||
# Have the frame update after each command
|
||||
shell.events.register('post_run_cell', lambda *a, **kw: self.update_frame())
|
||||
# Use the locals of the caller as the local namespace
|
||||
# once embeded, and add a few custom shortcuts
|
||||
# once embedded, and add a few custom shortcuts
|
||||
local_ns = inspect.currentframe().f_back.f_locals
|
||||
local_ns["touch"] = self.interact
|
||||
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
|
||||
local_ns[term] = getattr(self, term)
|
||||
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
||||
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
||||
shell(local_ns=local_ns, stack_depth=2)
|
||||
# End scene when exiting an embed.
|
||||
raise EndSceneEarlyException()
|
||||
@@ -247,6 +255,18 @@ class Scene(object):
|
||||
def get_mobject_copies(self):
|
||||
return [m.copy() for m in self.mobjects]
|
||||
|
||||
def point_to_mobject(self, point, search_set=None, buff=0):
|
||||
"""
|
||||
E.g. if clicking on the scene, this returns the top layer mobject
|
||||
under a given point
|
||||
"""
|
||||
if search_set is None:
|
||||
search_set = self.mobjects
|
||||
for mobject in reversed(search_set):
|
||||
if mobject.is_point_touching(point, buff=buff):
|
||||
return mobject
|
||||
return None
|
||||
|
||||
# Related to skipping
|
||||
def update_skipping_status(self):
|
||||
if self.start_at_animation_number is not None:
|
||||
@@ -272,7 +292,7 @@ class Scene(object):
|
||||
times,
|
||||
total=n_iterations,
|
||||
leave=self.leave_progress_bars,
|
||||
ascii=False if platform.system() != 'Windows' else True
|
||||
ascii=True if platform.system() == 'Windows' else None
|
||||
)
|
||||
return time_progression
|
||||
|
||||
@@ -443,10 +463,7 @@ class Scene(object):
|
||||
@handle_play_like_call
|
||||
def play(self, *args, **kwargs):
|
||||
if len(args) == 0:
|
||||
logging.log(
|
||||
logging.WARNING,
|
||||
"Called Scene.play with no animations"
|
||||
)
|
||||
log.warning("Called Scene.play with no animations")
|
||||
return
|
||||
animations = self.anims_from_play_args(*args, **kwargs)
|
||||
self.lock_static_mobject_data(*animations)
|
||||
@@ -570,7 +587,7 @@ class Scene(object):
|
||||
frame = self.camera.frame
|
||||
if self.window.is_key_pressed(ord("z")):
|
||||
factor = 1 + np.arctan(10 * offset[1])
|
||||
frame.scale(factor, about_point=point)
|
||||
frame.scale(1/factor, about_point=point)
|
||||
else:
|
||||
transform = frame.get_inverse_camera_rotation_matrix()
|
||||
shift = np.dot(np.transpose(transform), offset)
|
||||
@@ -586,7 +603,7 @@ class Scene(object):
|
||||
try:
|
||||
char = chr(symbol)
|
||||
except OverflowError:
|
||||
print(" Warning: The value of the pressed key is too large.")
|
||||
log.warning("The value of the pressed key is too large.")
|
||||
return
|
||||
|
||||
event_data = {"symbol": symbol, "modifiers": modifiers}
|
||||
|
||||
@@ -12,6 +12,7 @@ from manimlib.utils.file_ops import guarantee_existence
|
||||
from manimlib.utils.file_ops import add_extension_if_not_present
|
||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
from manimlib.utils.sounds import get_full_sound_file_path
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
class SceneFileWriter(object):
|
||||
@@ -183,7 +184,7 @@ class SceneFileWriter(object):
|
||||
'-s', f'{width}x{height}', # size of one frame
|
||||
'-pix_fmt', 'rgba',
|
||||
'-r', str(fps), # frames per second
|
||||
'-i', '-', # The imput comes from a pipe
|
||||
'-i', '-', # The input comes from a pipe
|
||||
'-vf', 'vflip',
|
||||
'-an', # Tells FFMPEG not to expect any audio
|
||||
'-loglevel', 'error',
|
||||
@@ -231,7 +232,7 @@ class SceneFileWriter(object):
|
||||
**kwargs
|
||||
)
|
||||
if len(partial_movie_files) == 0:
|
||||
print("No animations in this scene")
|
||||
log.warning("No animations in this scene")
|
||||
return
|
||||
|
||||
# Write a file partial_file_list.txt containing all
|
||||
@@ -300,7 +301,7 @@ class SceneFileWriter(object):
|
||||
self.print_file_ready_message(file_path)
|
||||
|
||||
def print_file_ready_message(self, file_path):
|
||||
print(f"\nFile ready at {file_path}\n")
|
||||
log.info(f"File ready at {file_path}")
|
||||
|
||||
def should_open_file(self):
|
||||
return any([
|
||||
|
||||
@@ -5,6 +5,7 @@ class ThreeDScene(Scene):
|
||||
CONFIG = {
|
||||
"camera_config": {
|
||||
"samples": 4,
|
||||
"anti_alias_width": 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@ out vec4 frag_color;
|
||||
|
||||
void main() {
|
||||
frag_color = texture(Texture, v_im_coords);
|
||||
frag_color.a = v_opacity;
|
||||
frag_color.a *= v_opacity;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
///// INSERT COLOR_MAP FUNCTION HERE /////
|
||||
|
||||
vec4 add_light(vec4 color,
|
||||
vec3 point,
|
||||
vec3 unit_normal,
|
||||
vec3 light_coords,
|
||||
float gloss,
|
||||
float shadow){
|
||||
///// INSERT COLOR FUNCTION HERE /////
|
||||
// The line above may be replaced by arbitrary code snippets, as per
|
||||
// the method Mobject.set_color_by_code
|
||||
if(gloss == 0.0 && shadow == 0.0) return color;
|
||||
|
||||
// TODO, do we actually want this? It effectively treats surfaces as two-sided
|
||||
if(unit_normal.z < 0){
|
||||
unit_normal *= -1;
|
||||
}
|
||||
|
||||
// TODO, read this in as a uniform?
|
||||
float camera_distance = 6;
|
||||
// Assume everything has already been rotated such that camera is in the z-direction
|
||||
vec3 to_camera = vec3(0, 0, camera_distance) - point;
|
||||
vec3 to_light = light_coords - point;
|
||||
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
|
||||
float dot_prod = dot(normalize(light_reflection), normalize(to_camera));
|
||||
float shine = gloss * exp(-3 * pow(1 - dot_prod, 2));
|
||||
float dp2 = dot(normalize(to_light), unit_normal);
|
||||
float darkening = mix(1, max(dp2, 0), shadow);
|
||||
return vec4(
|
||||
darkening * mix(color.rgb, vec3(1.0), shine),
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
vec4 finalize_color(vec4 color,
|
||||
vec3 point,
|
||||
vec3 unit_normal,
|
||||
vec3 light_coords,
|
||||
float gloss,
|
||||
float shadow){
|
||||
// Put insertion here instead
|
||||
return add_light(color, point, unit_normal, light_coords, gloss, shadow);
|
||||
}
|
||||
15
manimlib/shaders/inserts/complex_functions.glsl
Normal file
15
manimlib/shaders/inserts/complex_functions.glsl
Normal file
@@ -0,0 +1,15 @@
|
||||
vec2 complex_mult(vec2 z, vec2 w){
|
||||
return vec2(z.x * w.x - z.y * w.y, z.x * w.y + z.y * w.x);
|
||||
}
|
||||
|
||||
vec2 complex_div(vec2 z, vec2 w){
|
||||
return complex_mult(z, vec2(w.x, -w.y)) / (w.x * w.x + w.y * w.y);
|
||||
}
|
||||
|
||||
vec2 complex_pow(vec2 z, int n){
|
||||
vec2 result = vec2(1.0, 0.0);
|
||||
for(int i = 0; i < n; i++){
|
||||
result = complex_mult(result, z);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -17,16 +17,16 @@ vec4 add_light(vec4 color,
|
||||
float shadow){
|
||||
if(gloss == 0.0 && shadow == 0.0) return color;
|
||||
|
||||
// TODO, do we actually want this? It effectively treats surfaces as two-sided
|
||||
if(unit_normal.z < 0){
|
||||
unit_normal *= -1;
|
||||
}
|
||||
|
||||
// TODO, read this in as a uniform?
|
||||
float camera_distance = 6;
|
||||
float camera_distance = focal_distance;
|
||||
// Assume everything has already been rotated such that camera is in the z-direction
|
||||
vec3 to_camera = vec3(0, 0, camera_distance) - point;
|
||||
vec3 to_light = light_coords - point;
|
||||
|
||||
// TODO, do we actually want this? It effectively treats surfaces as two-sided
|
||||
if(dot(to_camera,unit_normal) < 0){
|
||||
unit_normal *= -1;
|
||||
}
|
||||
|
||||
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
|
||||
float dot_prod = dot(normalize(light_reflection), normalize(to_camera));
|
||||
float shine = gloss * exp(-3 * pow(1 - dot_prod, 2));
|
||||
|
||||
77
manimlib/shaders/mandelbrot_fractal/frag.glsl
Normal file
77
manimlib/shaders/mandelbrot_fractal/frag.glsl
Normal file
@@ -0,0 +1,77 @@
|
||||
#version 330
|
||||
|
||||
uniform vec3 light_source_position;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
|
||||
uniform vec2 parameter;
|
||||
uniform float opacity;
|
||||
uniform float n_steps;
|
||||
uniform float mandelbrot;
|
||||
|
||||
uniform vec3 color0;
|
||||
uniform vec3 color1;
|
||||
uniform vec3 color2;
|
||||
uniform vec3 color3;
|
||||
uniform vec3 color4;
|
||||
uniform vec3 color5;
|
||||
uniform vec3 color6;
|
||||
uniform vec3 color7;
|
||||
uniform vec3 color8;
|
||||
|
||||
uniform vec2 frame_shape;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
#INSERT finalize_color.glsl
|
||||
#INSERT complex_functions.glsl
|
||||
|
||||
const int MAX_DEGREE = 5;
|
||||
|
||||
void main() {
|
||||
vec3 color_map[9] = vec3[9](
|
||||
color0, color1, color2, color3,
|
||||
color4, color5, color6, color7, color8
|
||||
);
|
||||
vec3 color;
|
||||
|
||||
vec2 z;
|
||||
vec2 c;
|
||||
|
||||
if(bool(mandelbrot)){
|
||||
c = xyz_coords.xy;
|
||||
z = vec2(0.0, 0.0);
|
||||
}else{
|
||||
c = parameter;
|
||||
z = xyz_coords.xy;
|
||||
}
|
||||
|
||||
float outer_bound = 2.0;
|
||||
bool stable = true;
|
||||
for(int n = 0; n < int(n_steps); n++){
|
||||
z = complex_mult(z, z) + c;
|
||||
if(length(z) > outer_bound){
|
||||
float float_n = float(n);
|
||||
float_n += log(outer_bound) / log(length(z));
|
||||
float_n += 0.5 * length(c);
|
||||
color = float_to_color(sqrt(float_n), 1.5, 8.0, color_map);
|
||||
stable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(stable){
|
||||
color = vec3(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
frag_color = finalize_color(
|
||||
vec4(color, opacity),
|
||||
xyz_coords,
|
||||
vec3(0.0, 0.0, 1.0),
|
||||
light_source_position,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
}
|
||||
17
manimlib/shaders/mandelbrot_fractal/vert.glsl
Normal file
17
manimlib/shaders/mandelbrot_fractal/vert.glsl
Normal file
@@ -0,0 +1,17 @@
|
||||
#version 330
|
||||
|
||||
#INSERT camera_uniform_declarations.glsl
|
||||
|
||||
in vec3 point;
|
||||
out vec3 xyz_coords;
|
||||
|
||||
uniform float scale_factor;
|
||||
uniform vec3 offset;
|
||||
|
||||
#INSERT position_point_into_frame.glsl
|
||||
#INSERT get_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
xyz_coords = (point - offset) / scale_factor;
|
||||
gl_Position = get_gl_Position(position_point_into_frame(point));
|
||||
}
|
||||
157
manimlib/shaders/newton_fractal/frag.glsl
Normal file
157
manimlib/shaders/newton_fractal/frag.glsl
Normal file
@@ -0,0 +1,157 @@
|
||||
#version 330
|
||||
|
||||
uniform vec3 light_source_position;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
|
||||
uniform vec4 color0;
|
||||
uniform vec4 color1;
|
||||
uniform vec4 color2;
|
||||
uniform vec4 color3;
|
||||
uniform vec4 color4;
|
||||
|
||||
uniform vec2 coef0;
|
||||
uniform vec2 coef1;
|
||||
uniform vec2 coef2;
|
||||
uniform vec2 coef3;
|
||||
uniform vec2 coef4;
|
||||
uniform vec2 coef5;
|
||||
|
||||
uniform vec2 root0;
|
||||
uniform vec2 root1;
|
||||
uniform vec2 root2;
|
||||
uniform vec2 root3;
|
||||
uniform vec2 root4;
|
||||
|
||||
uniform float n_roots;
|
||||
uniform float n_steps;
|
||||
uniform float julia_highlight;
|
||||
uniform float saturation_factor;
|
||||
uniform float black_for_cycles;
|
||||
uniform float is_parameter_space;
|
||||
|
||||
uniform vec2 frame_shape;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
#INSERT finalize_color.glsl
|
||||
#INSERT complex_functions.glsl
|
||||
|
||||
const int MAX_DEGREE = 5;
|
||||
const float CLOSE_ENOUGH = 1e-3;
|
||||
|
||||
|
||||
vec2 poly(vec2 z, vec2[MAX_DEGREE + 1] coefs){
|
||||
vec2 result = vec2(0.0);
|
||||
for(int n = 0; n < int(n_roots) + 1; n++){
|
||||
result += complex_mult(coefs[n], complex_pow(z, n));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
vec2 dpoly(vec2 z, vec2[MAX_DEGREE + 1] coefs){
|
||||
vec2 result = vec2(0.0);
|
||||
for(int n = 1; n < int(n_roots) + 1; n++){
|
||||
result += n * complex_mult(coefs[n], complex_pow(z, n - 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
vec2 seek_root(vec2 z, vec2[MAX_DEGREE + 1] coefs, int max_steps, out float n_iters){
|
||||
float last_len;
|
||||
float curr_len;
|
||||
float threshold = CLOSE_ENOUGH;
|
||||
|
||||
for(int i = 0; i < max_steps; i++){
|
||||
last_len = curr_len;
|
||||
n_iters = float(i);
|
||||
vec2 step = complex_div(poly(z, coefs), dpoly(z, coefs));
|
||||
curr_len = length(step);
|
||||
if(curr_len < threshold){
|
||||
break;
|
||||
}
|
||||
z = z - step;
|
||||
}
|
||||
n_iters -= clamp((threshold - curr_len) / (last_len - curr_len), 0.0, 1.0);
|
||||
|
||||
return z;
|
||||
}
|
||||
|
||||
|
||||
void main() {
|
||||
vec2[MAX_DEGREE + 1] coefs = vec2[MAX_DEGREE + 1](coef0, coef1, coef2, coef3, coef4, coef5);
|
||||
vec2[MAX_DEGREE] roots = vec2[MAX_DEGREE](root0, root1, root2, root3, root4);
|
||||
vec4[MAX_DEGREE] colors = vec4[MAX_DEGREE](color0, color1, color2, color3, color4);
|
||||
|
||||
vec2 z = xyz_coords.xy;
|
||||
|
||||
if(is_parameter_space > 0){
|
||||
// In this case, pixel should correspond to one of the roots
|
||||
roots[2] = xyz_coords.xy;
|
||||
vec2 r0 = roots[0];
|
||||
vec2 r1 = roots[1];
|
||||
vec2 r2 = roots[2];
|
||||
|
||||
// It is assumed that the polynomial is cubid...
|
||||
coefs[0] = -complex_mult(complex_mult(r0, r1), r2);
|
||||
coefs[1] = complex_mult(r0, r1) + complex_mult(r0, r2) + complex_mult(r1, r2);
|
||||
coefs[2] = -(r0 + r1 + r2);
|
||||
coefs[3] = vec2(1.0, 0.0);
|
||||
|
||||
// Seed value is always center of the roots
|
||||
z = -coefs[2] / 3.0;
|
||||
}
|
||||
|
||||
float n_iters;
|
||||
vec2 found_root = seek_root(z, coefs, int(n_steps), n_iters);
|
||||
|
||||
vec4 color = vec4(0.0);
|
||||
float min_dist = 1e10;
|
||||
float dist;
|
||||
for(int i = 0; i < int(n_roots); i++){
|
||||
dist = distance(roots[i], found_root);
|
||||
if(dist < min_dist){
|
||||
min_dist = dist;
|
||||
color = colors[i];
|
||||
}
|
||||
}
|
||||
color *= 1.0 + (0.01 * saturation_factor) * (n_iters - 5 * saturation_factor);
|
||||
|
||||
if(black_for_cycles > 0 && min_dist > CLOSE_ENOUGH){
|
||||
color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
if(julia_highlight > 0.0){
|
||||
float radius = julia_highlight;
|
||||
vec2[4] samples = vec2[4](
|
||||
z + vec2(radius, 0.0),
|
||||
z + vec2(-radius, 0.0),
|
||||
z + vec2(0.0, radius),
|
||||
z + vec2(0.0, -radius)
|
||||
);
|
||||
for(int i = 0; i < 4; i++){
|
||||
for(int j = 0; j < n_steps; j++){
|
||||
vec2 z = samples[i];
|
||||
z = z - complex_div(poly(z, coefs), dpoly(z, coefs));
|
||||
samples[i] = z;
|
||||
}
|
||||
}
|
||||
float max_dist = 0.0;
|
||||
for(int i = 0; i < 4; i++){
|
||||
max_dist = max(max_dist, distance(samples[i], samples[(i + 1) % 4]));
|
||||
}
|
||||
color *= 1.0 * smoothstep(0, 0.1, max_dist);
|
||||
}
|
||||
|
||||
frag_color = finalize_color(
|
||||
color,
|
||||
xyz_coords,
|
||||
vec3(0.0, 0.0, 1.0),
|
||||
light_source_position,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
}
|
||||
17
manimlib/shaders/newton_fractal/vert.glsl
Normal file
17
manimlib/shaders/newton_fractal/vert.glsl
Normal file
@@ -0,0 +1,17 @@
|
||||
#version 330
|
||||
|
||||
#INSERT camera_uniform_declarations.glsl
|
||||
|
||||
in vec3 point;
|
||||
out vec3 xyz_coords;
|
||||
|
||||
uniform float scale_factor;
|
||||
uniform vec3 offset;
|
||||
|
||||
#INSERT position_point_into_frame.glsl
|
||||
#INSERT get_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
xyz_coords = (point - offset) / scale_factor;
|
||||
gl_Position = get_gl_Position(position_point_into_frame(point));
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
uniform vec3 light_source_position;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
in vec3 v_normal;
|
||||
|
||||
@@ -6,6 +6,7 @@ uniform float num_textures;
|
||||
uniform vec3 light_source_position;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
in vec3 v_normal;
|
||||
|
||||
@@ -4,6 +4,7 @@ uniform vec3 light_source_position;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float anti_alias_width;
|
||||
uniform float focal_distance;
|
||||
|
||||
in vec4 color;
|
||||
in float radius;
|
||||
|
||||
@@ -4,6 +4,8 @@ import numpy as np
|
||||
from manimlib.utils.simple_functions import choose
|
||||
from manimlib.utils.space_ops import find_intersection
|
||||
from manimlib.utils.space_ops import cross2d
|
||||
from manimlib.utils.space_ops import midpoint
|
||||
from manimlib.logger import log
|
||||
|
||||
CLOSED_THRESHOLD = 0.001
|
||||
|
||||
@@ -67,9 +69,9 @@ def interpolate(start, end, alpha):
|
||||
try:
|
||||
return (1 - alpha) * start + alpha * end
|
||||
except TypeError:
|
||||
print(type(start), start.dtype)
|
||||
print(type(end), start.dtype)
|
||||
print(alpha)
|
||||
log.debug(f"`start` parameter with type `{type(start)}` and dtype `{start.dtype}`")
|
||||
log.debug(f"`end` parameter with type `{type(end)}` and dtype `{end.dtype}`")
|
||||
log.debug(f"`alpha` parameter with value `{alpha}`")
|
||||
import sys
|
||||
sys.exit(2)
|
||||
|
||||
@@ -130,6 +132,8 @@ def get_smooth_quadratic_bezier_handle_points(points):
|
||||
another that would produce a parabola passing through P0, call it smooth_to_left,
|
||||
and use the midpoint between the two.
|
||||
"""
|
||||
if len(points) == 2:
|
||||
return midpoint(*points)
|
||||
smooth_to_right, smooth_to_left = [
|
||||
0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:]
|
||||
for ps in (points, points[::-1])
|
||||
@@ -157,7 +161,7 @@ def get_smooth_cubic_bezier_handle_points(points):
|
||||
l, u = 2, 1
|
||||
# diag is a representation of the matrix in diagonal form
|
||||
# See https://www.particleincell.com/2012/bezier-splines/
|
||||
# for how to arive at these equations
|
||||
# for how to arrive at these equations
|
||||
diag = np.zeros((l + u + 1, 2 * num_handles))
|
||||
diag[0, 1::2] = -1
|
||||
diag[0, 2::2] = 1
|
||||
|
||||
@@ -4,7 +4,9 @@ from colour import Color
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import COLORMAP_3B1B
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.simple_functions import clip_in_place
|
||||
from manimlib.utils.space_ops import normalize
|
||||
|
||||
@@ -114,10 +116,22 @@ def get_shaded_rgb(rgb, point, unit_normal_vect, light_source):
|
||||
|
||||
|
||||
def get_colormap_list(map_name="viridis", n_colors=9):
|
||||
"""
|
||||
Options for map_name:
|
||||
3b1b_colormap
|
||||
magma
|
||||
inferno
|
||||
plasma
|
||||
viridis
|
||||
cividis
|
||||
twilight
|
||||
twilight_shifted
|
||||
turbo
|
||||
"""
|
||||
from matplotlib.cm import get_cmap
|
||||
|
||||
rgbs = get_cmap(map_name).colors # Make more general?
|
||||
return [
|
||||
rgbs[int(n)]
|
||||
for n in np.linspace(0, len(rgbs) - 1, n_colors)
|
||||
]
|
||||
if map_name == "3b1b_colormap":
|
||||
rgbs = [color_to_rgb(color) for color in COLORMAP_3B1B]
|
||||
else:
|
||||
rgbs = get_cmap(map_name).colors # Make more general?
|
||||
return resize_with_interpolation(np.array(rgbs), n_colors)
|
||||
|
||||
@@ -3,11 +3,12 @@ import time
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.mobject.numbers import Integer
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
def print_family(mobject, n_tabs=0):
|
||||
"""For debugging purposes"""
|
||||
print("\t" * n_tabs, mobject, id(mobject))
|
||||
log.debug("\t" * n_tabs + str(mobject) + " " + str(id(mobject)))
|
||||
for submob in mobject.submobjects:
|
||||
print_family(submob, n_tabs + 1)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ def get_text_dir():
|
||||
def get_mobject_data_dir():
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data"))
|
||||
|
||||
|
||||
def get_downloads_dir():
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads"))
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ def get_full_raster_image_path(image_file_name):
|
||||
return find_file(
|
||||
image_file_name,
|
||||
directories=[get_raster_image_dir()],
|
||||
extensions=[".jpg", ".png", ".gif", ""]
|
||||
extensions=[".jpg", ".jpeg", ".png", ".gif", ""]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ def init_customization():
|
||||
"background_color": "",
|
||||
},
|
||||
"window_position": "UR",
|
||||
"window_monitor": 0,
|
||||
"break_into_partial_movies": False,
|
||||
"camera_qualities": {
|
||||
"low": {
|
||||
@@ -45,6 +46,7 @@ def init_customization():
|
||||
}
|
||||
}
|
||||
|
||||
print("Initialize configuration")
|
||||
scope = input(" Please select the scope of the configuration [global/local]: ")
|
||||
if scope == "global":
|
||||
from manimlib.config import get_manim_dir
|
||||
|
||||
@@ -5,7 +5,7 @@ import numpy as np
|
||||
def remove_list_redundancies(l):
|
||||
"""
|
||||
Used instead of list(set(l)) to maintain order
|
||||
Keeps the last occurance of each element
|
||||
Keeps the last occurrence of each element
|
||||
"""
|
||||
reversed_result = []
|
||||
used = set()
|
||||
@@ -98,6 +98,8 @@ def resize_preserving_order(nparray, length):
|
||||
def resize_with_interpolation(nparray, length):
|
||||
if len(nparray) == length:
|
||||
return nparray
|
||||
if length == 0:
|
||||
return np.zeros((0, *nparray.shape[1:]))
|
||||
cont_indices = np.linspace(0, len(nparray) - 1, length)
|
||||
return np.array([
|
||||
(1 - a) * nparray[lh] + a * nparray[rh]
|
||||
|
||||
@@ -6,5 +6,5 @@ def get_full_sound_file_path(sound_file_name):
|
||||
return find_file(
|
||||
sound_file_name,
|
||||
directories=[get_sound_dir()],
|
||||
extensions=[".wav", ".mp3"]
|
||||
extensions=[".wav", ".mp3", ""]
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
import operator as op
|
||||
from functools import reduce
|
||||
import math
|
||||
from mapbox_earcut import triangulate_float32 as earcut
|
||||
|
||||
@@ -12,7 +13,7 @@ from manimlib.utils.iterables import adjacent_pairs
|
||||
|
||||
|
||||
def get_norm(vect):
|
||||
return sum([x**2 for x in vect])**0.5
|
||||
return sum((x**2 for x in vect))**0.5
|
||||
|
||||
|
||||
# Quaternions
|
||||
@@ -359,77 +360,66 @@ def earclip_triangulation(verts, ring_ends):
|
||||
the ends of new paths are
|
||||
"""
|
||||
|
||||
# First, connect all the rings so that the polygon
|
||||
# with holes is instead treated as a (very convex)
|
||||
# polygon with one edge. Do this by drawing connections
|
||||
# between rings close to each other
|
||||
rings = [
|
||||
list(range(e0, e1))
|
||||
for e0, e1 in zip([0, *ring_ends], ring_ends)
|
||||
]
|
||||
attached_rings = rings[:1]
|
||||
detached_rings = rings[1:]
|
||||
loop_connections = dict()
|
||||
|
||||
while detached_rings:
|
||||
i_range, j_range = [
|
||||
list(filter(
|
||||
# Ignore indices that are already being
|
||||
# used to draw some connection
|
||||
lambda i: i not in loop_connections,
|
||||
it.chain(*ring_group)
|
||||
))
|
||||
for ring_group in (attached_rings, detached_rings)
|
||||
]
|
||||
def is_in(point, ring_id):
|
||||
return abs(abs(get_winding_number([i - point for i in verts[rings[ring_id]]])) - 1) < 1e-5
|
||||
|
||||
# Closet point on the atttached rings to an estimated midpoint
|
||||
# of the detached rings
|
||||
tmp_j_vert = midpoint(
|
||||
verts[j_range[0]],
|
||||
verts[j_range[len(j_range) // 2]]
|
||||
)
|
||||
i = min(i_range, key=lambda i: norm_squared(verts[i] - tmp_j_vert))
|
||||
# Closet point of the detached rings to the aforementioned
|
||||
# point of the attached rings
|
||||
j = min(j_range, key=lambda j: norm_squared(verts[i] - verts[j]))
|
||||
# Recalculate i based on new j
|
||||
i = min(i_range, key=lambda i: norm_squared(verts[i] - verts[j]))
|
||||
def ring_area(ring_id):
|
||||
ring = rings[ring_id]
|
||||
s = 0
|
||||
for i, j in zip(ring[1:], ring):
|
||||
s += cross2d(verts[i], verts[j])
|
||||
return abs(s) / 2
|
||||
|
||||
# Remember to connect the polygon at these points
|
||||
loop_connections[i] = j
|
||||
loop_connections[j] = i
|
||||
# Points at the same position may cause problems
|
||||
for i in rings:
|
||||
verts[i[0]] += (verts[i[1]] - verts[i[0]]) * 1e-6
|
||||
verts[i[-1]] += (verts[i[-2]] - verts[i[-1]]) * 1e-6
|
||||
|
||||
# Move the ring which j belongs to from the
|
||||
# attached list to the detached list
|
||||
new_ring = next(filter(
|
||||
lambda ring: ring[0] <= j < ring[-1],
|
||||
detached_rings
|
||||
# First, we should know which rings are directly contained in it for each ring
|
||||
|
||||
right = [max(verts[rings[i], 0]) for i in range(len(rings))]
|
||||
left = [min(verts[rings[i], 0]) for i in range(len(rings))]
|
||||
top = [max(verts[rings[i], 1]) for i in range(len(rings))]
|
||||
bottom = [min(verts[rings[i], 1]) for i in range(len(rings))]
|
||||
area = [ring_area(i) for i in range(len(rings))]
|
||||
|
||||
# The larger ring must be outside
|
||||
rings_sorted = list(range(len(rings)))
|
||||
rings_sorted.sort(key=lambda x: area[x], reverse=True)
|
||||
|
||||
def is_in_fast(ring_a, ring_b):
|
||||
# Whether a is in b
|
||||
return reduce(op.and_, (
|
||||
left[ring_b] <= left[ring_a] <= right[ring_a] <= right[ring_b],
|
||||
bottom[ring_b] <= bottom[ring_a] <= top[ring_a] <= top[ring_b],
|
||||
is_in(verts[rings[ring_a][0]], ring_b)
|
||||
))
|
||||
detached_rings.remove(new_ring)
|
||||
attached_rings.append(new_ring)
|
||||
|
||||
# Setup linked list
|
||||
after = []
|
||||
end0 = 0
|
||||
for end1 in ring_ends:
|
||||
after.extend(range(end0 + 1, end1))
|
||||
after.append(end0)
|
||||
end0 = end1
|
||||
chilren = [[] for i in rings]
|
||||
for idx, i in enumerate(rings_sorted):
|
||||
for j in rings_sorted[:idx][::-1]:
|
||||
if is_in_fast(i, j):
|
||||
chilren[j].append(i)
|
||||
break
|
||||
|
||||
# Find an ordering of indices walking around the polygon
|
||||
indices = []
|
||||
i = 0
|
||||
for x in range(len(verts) + len(ring_ends) - 1):
|
||||
# starting = False
|
||||
if i in loop_connections:
|
||||
j = loop_connections[i]
|
||||
indices.extend([i, j])
|
||||
i = after[j]
|
||||
else:
|
||||
indices.append(i)
|
||||
i = after[i]
|
||||
if i == 0:
|
||||
break
|
||||
res = []
|
||||
|
||||
meta_indices = earcut(verts[indices, :2], [len(indices)])
|
||||
return [indices[mi] for mi in meta_indices]
|
||||
# Then, we can use earcut for each part
|
||||
used = [False] * len(rings)
|
||||
for i in rings_sorted:
|
||||
if used[i]:
|
||||
continue
|
||||
v = rings[i]
|
||||
ring_ends = [len(v)]
|
||||
for j in chilren[i]:
|
||||
used[j] = True
|
||||
v += rings[j]
|
||||
ring_ends.append(len(v))
|
||||
res += [v[i] for i in earcut(verts[v, :2], ring_ends)]
|
||||
|
||||
return res
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
@@ -7,6 +6,7 @@ from contextlib import contextmanager
|
||||
from manimlib.utils.directories import get_tex_dir
|
||||
from manimlib.config import get_manim_dir
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
SAVED_TEX_CONFIG = {}
|
||||
@@ -87,15 +87,11 @@ def tex_to_dvi(tex_file):
|
||||
exit_code = os.system(" ".join(commands))
|
||||
if exit_code != 0:
|
||||
log_file = tex_file.replace(".tex", ".log")
|
||||
logging.log(
|
||||
logging.ERROR,
|
||||
"\n\n LaTeX Error! Not a worry, it happens to the best of us.\n"
|
||||
)
|
||||
log.error("LaTeX Error! Not a worry, it happens to the best of us.")
|
||||
with open(log_file, "r") as file:
|
||||
for line in file.readlines():
|
||||
if line.startswith("!"):
|
||||
print(line[1:])
|
||||
logging.log(logging.INFO, line)
|
||||
log.debug(f"The error could be: `{line[2:-1]}`")
|
||||
sys.exit(2)
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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
|
||||
@@ -15,7 +16,7 @@ class Window(PygletWindow):
|
||||
cursor = True
|
||||
|
||||
def __init__(self, scene, size=(1280, 720), **kwargs):
|
||||
super().__init__()
|
||||
super().__init__(size=size)
|
||||
digest_config(self, kwargs)
|
||||
|
||||
self.scene = scene
|
||||
@@ -38,7 +39,9 @@ class Window(PygletWindow):
|
||||
|
||||
def find_initial_position(self, size):
|
||||
custom_position = get_customization()["window_position"]
|
||||
monitor = get_monitors()[get_customization()["window_monitor"]]
|
||||
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
|
||||
@@ -46,7 +49,7 @@ class Window(PygletWindow):
|
||||
return tuple(map(int, custom_position.split(",")))
|
||||
|
||||
# Alternatively, it might be specified with a string like
|
||||
# UR, OO, DL, etc. specifiying what corner it should go to
|
||||
# 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
|
||||
@@ -57,7 +60,17 @@ class Window(PygletWindow):
|
||||
|
||||
# Delegate event handling to scene
|
||||
def pixel_coords_to_space_coords(self, px, py, relative=False):
|
||||
return self.scene.camera.pixel_coords_to_space_coords(px, py, relative)
|
||||
pw, ph = self.size
|
||||
fw, fh = self.scene.camera.get_frame_shape()
|
||||
fc = self.scene.camera.get_frame_center()
|
||||
if relative:
|
||||
return np.array([px / pw, py / ph, 0])
|
||||
else:
|
||||
return np.array([
|
||||
fc[0] + px * fw / pw - fw / 2,
|
||||
fc[1] + py * fh / ph - fh / 2,
|
||||
0
|
||||
])
|
||||
|
||||
def on_mouse_motion(self, x, y, dx, dy):
|
||||
super().on_mouse_motion(x, y, dx, dy)
|
||||
|
||||
2
pyproject.toml
Normal file
2
pyproject.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
@@ -2,7 +2,6 @@ argparse
|
||||
colour
|
||||
numpy
|
||||
Pillow
|
||||
progressbar
|
||||
scipy
|
||||
sympy
|
||||
tqdm
|
||||
@@ -11,10 +10,12 @@ matplotlib
|
||||
moderngl
|
||||
moderngl_window
|
||||
pydub
|
||||
pygments
|
||||
pyyaml
|
||||
rich
|
||||
screeninfo
|
||||
pyreadline; sys_platform == 'win32'
|
||||
validators
|
||||
ipython
|
||||
PyOpenGL
|
||||
manimpango>=0.2.0,<0.3.0'
|
||||
manimpango>=0.2.0,<0.4.0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user