mirror of
https://github.com/3b1b/manim.git
synced 2026-01-12 16:08:16 -05:00
Compare commits
672 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bda7f98d2e | ||
|
|
9d74e8bce3 | ||
|
|
845ee83f71 | ||
|
|
55684af27d | ||
|
|
859680d5ab | ||
|
|
dc4b9bc93c | ||
|
|
705f1a528b | ||
|
|
773520bcd9 | ||
|
|
d26b8a826c | ||
|
|
12bfe88f40 | ||
|
|
36d62ae1a3 | ||
|
|
e23f667c3d | ||
|
|
2277679111 | ||
|
|
9d7db7aacd | ||
|
|
e8430b38b2 | ||
|
|
1f32a9e674 | ||
|
|
d31f3df5af | ||
|
|
e9bf13882e | ||
|
|
3550108ff7 | ||
|
|
557707ea75 | ||
|
|
13c731e166 | ||
|
|
d349c9283d | ||
|
|
18963fb9fe | ||
|
|
a69c9887f9 | ||
|
|
93f8d3f1ca | ||
|
|
e4ccbdfba9 | ||
|
|
fc97bfb647 | ||
|
|
f9d8a76767 | ||
|
|
55a91a2354 | ||
|
|
50ffcbc5c7 | ||
|
|
b764791258 | ||
|
|
7f616987a3 | ||
|
|
648855dae0 | ||
|
|
974d9d5ab0 | ||
|
|
3c3264d7d6 | ||
|
|
39673a80d7 | ||
|
|
84c56b3624 | ||
|
|
dc816c9f8d | ||
|
|
d5ab9a91c4 | ||
|
|
106f2a3837 | ||
|
|
724a500cc6 | ||
|
|
461500637e | ||
|
|
fc4f649570 | ||
|
|
852da9ac2a | ||
|
|
637d779190 | ||
|
|
9bbbed3a83 | ||
|
|
1cde28838f | ||
|
|
a8039d803e | ||
|
|
0add9b6e3a | ||
|
|
c5ec47b0e9 | ||
|
|
769a4bbaf9 | ||
|
|
0f8d7ed597 | ||
|
|
2a7a7ac518 | ||
|
|
0610f331a4 | ||
|
|
a0ba9c8b30 | ||
|
|
7e8b3a4c6b | ||
|
|
393f77cb03 | ||
|
|
82c972b946 | ||
|
|
89e139009b | ||
|
|
45faa9063b | ||
|
|
0e31ff12e2 | ||
|
|
473aaea399 | ||
|
|
c4ea794107 | ||
|
|
cfba6c431f | ||
|
|
e11c5def63 | ||
|
|
a3e4246938 | ||
|
|
305c6e6ee9 | ||
|
|
3b01ec48e6 | ||
|
|
969aa82f04 | ||
|
|
e44a2fc8c6 | ||
|
|
9ac1805e7e | ||
|
|
6ad8636fab | ||
|
|
4a03d196a6 | ||
|
|
519e2f4f1e | ||
|
|
aefde2969f | ||
|
|
b7a3201fb3 | ||
|
|
a9349057ad | ||
|
|
e812b99594 | ||
|
|
0c8b333a42 | ||
|
|
f690164087 | ||
|
|
9d0cc810c5 | ||
|
|
8b1f0a8749 | ||
|
|
c0b7b55e49 | ||
|
|
41b52c6117 | ||
|
|
e5ce0ca286 | ||
|
|
a8c2a9fa3f | ||
|
|
cabc1322d6 | ||
|
|
c51811d2f1 | ||
|
|
7bf3615bb1 | ||
|
|
1872b0516b | ||
|
|
625460467f | ||
|
|
e19f35585d | ||
|
|
66819f5dbc | ||
|
|
f249da95fb | ||
|
|
bb3bd41605 | ||
|
|
67f8007764 | ||
|
|
2a0709664d | ||
|
|
de46df78dc | ||
|
|
bd6c731e67 | ||
|
|
c3e13fff05 | ||
|
|
bf2d9edfe6 | ||
|
|
fa38b56fd8 | ||
|
|
dfbbb34035 | ||
|
|
0cef9a1e61 | ||
|
|
2d764e12f4 | ||
|
|
d744311f15 | ||
|
|
11af9508f2 | ||
|
|
a227ffde05 | ||
|
|
e0b0ae280e | ||
|
|
fce38fd8a5 | ||
|
|
52a99a0c49 | ||
|
|
956e3a69c7 | ||
|
|
95a3ac6876 | ||
|
|
b06a5d3f23 | ||
|
|
fa8962e024 | ||
|
|
0a4c4d5849 | ||
|
|
e879da32d5 | ||
|
|
6b12bc2f5e | ||
|
|
4aeccd7769 | ||
|
|
4fbe948b63 | ||
|
|
05bee011d2 | ||
|
|
37b548395c | ||
|
|
4356c42e00 | ||
|
|
aea79be6cc | ||
|
|
a08e9b01de | ||
|
|
9f3b404df6 | ||
|
|
8ef42fae24 | ||
|
|
6be6bd3075 | ||
|
|
a33eac7aa8 | ||
|
|
9d6a28bc29 | ||
|
|
06405d5758 | ||
|
|
46e356e791 | ||
|
|
97ca42d454 | ||
|
|
a4eee6f44c | ||
|
|
8cac16b452 | ||
|
|
719cd8cde3 | ||
|
|
0bb9216c14 | ||
|
|
6f9df8db26 | ||
|
|
3756605a45 | ||
|
|
0e4d4155a3 | ||
|
|
0cab23b2ba | ||
|
|
854f7cd2bf | ||
|
|
41c4023986 | ||
|
|
d19e0cb9ab | ||
|
|
f085e6c2dd | ||
|
|
91ffdeb2d4 | ||
|
|
db71ed1ae9 | ||
|
|
4c16bfc2c0 | ||
|
|
aef02bfcf9 | ||
|
|
3744844efa | ||
|
|
9d04e287d7 | ||
|
|
97c0f4857b | ||
|
|
7f9b0a7eac | ||
|
|
133724d29a | ||
|
|
559b96e7ce | ||
|
|
773e013af9 | ||
|
|
61c70b426c | ||
|
|
9bdcc8b635 | ||
|
|
66caf0c1ad | ||
|
|
62cab9feaf | ||
|
|
be5de32d70 | ||
|
|
09ce4717aa | ||
|
|
7fb6f352c4 | ||
|
|
f29ef87bba | ||
|
|
e39f81ccff | ||
|
|
a0ed9edb42 | ||
|
|
fc1e916f42 | ||
|
|
b3b7d214ad | ||
|
|
602809758e | ||
|
|
960463d143 | ||
|
|
9a8aee481d | ||
|
|
1064e2bb30 | ||
|
|
992e61ddf2 | ||
|
|
19187ead06 | ||
|
|
7f8216bb09 | ||
|
|
e78113373a | ||
|
|
35025631eb | ||
|
|
f9351536e4 | ||
|
|
6e292daf58 | ||
|
|
67f5b10626 | ||
|
|
baba6929df | ||
|
|
d6b20a7306 | ||
|
|
4c3ba7f674 | ||
|
|
3883f57bf8 | ||
|
|
d2e0811285 | ||
|
|
1e2a6ffb8a | ||
|
|
56e5696163 | ||
|
|
1ec00629a5 | ||
|
|
aa6335cd90 | ||
|
|
7093f7d02d | ||
|
|
fad9ed2df7 | ||
|
|
725155409b | ||
|
|
a6675eb043 | ||
|
|
5d2dcec307 | ||
|
|
f60dc7cd07 | ||
|
|
6c39cac62b | ||
|
|
2bd25a55fa | ||
|
|
0e4edfdd79 | ||
|
|
277256a407 | ||
|
|
831b7d455c | ||
|
|
1d14a23af9 | ||
|
|
dffa70ea15 | ||
|
|
31976063df | ||
|
|
aa135280ac | ||
|
|
f0160822ba | ||
|
|
48e07d1817 | ||
|
|
3ef5899a24 | ||
|
|
f895455264 | ||
|
|
3baa14103e | ||
|
|
c315300ff1 | ||
|
|
3b17d6d0eb | ||
|
|
8a29de5ef0 | ||
|
|
ecb729850a | ||
|
|
a770291053 | ||
|
|
27c666fab5 | ||
|
|
942a7e71b8 | ||
|
|
ebb75d1235 | ||
|
|
9af23415a2 | ||
|
|
19778e405a | ||
|
|
833e40c2d4 | ||
|
|
9df53b8a18 | ||
|
|
ff86b0e378 | ||
|
|
92adcd75d4 | ||
|
|
240f5020b4 | ||
|
|
e8205a5049 | ||
|
|
6c8dd14adc | ||
|
|
07f84e2676 | ||
|
|
8db1164ece | ||
|
|
790bf0a104 | ||
|
|
8205edcc4c | ||
|
|
05b3c9852e | ||
|
|
925f2e123f | ||
|
|
565763a2ff | ||
|
|
6a74c241b8 | ||
|
|
416cc8e6d5 | ||
|
|
d694aed452 | ||
|
|
11379283aa | ||
|
|
dd13559b11 | ||
|
|
1658438fef | ||
|
|
f4eb2724c5 | ||
|
|
33f720c73a | ||
|
|
bbb4fa155c | ||
|
|
2318c9e716 | ||
|
|
e80dd243f1 | ||
|
|
3ffe300f96 | ||
|
|
24e3caa072 | ||
|
|
9efd02c500 | ||
|
|
0a318486c5 | ||
|
|
919133c6bf | ||
|
|
066a2ed5dc | ||
|
|
09ced7ce9a | ||
|
|
505b229117 | ||
|
|
5aa8d15d85 | ||
|
|
7aa05572ab | ||
|
|
f1996f8479 | ||
|
|
37b63ca956 | ||
|
|
84fd657d9b | ||
|
|
b489490f41 | ||
|
|
bbf45f95c6 | ||
|
|
b61f1473a5 | ||
|
|
e3d5b49a55 | ||
|
|
4d6a0db1e1 | ||
|
|
0af46e149d | ||
|
|
896b011d76 | ||
|
|
3adaf8e325 | ||
|
|
8762177df5 | ||
|
|
a1d51474ea | ||
|
|
83841ae415 | ||
|
|
b81f244c3c | ||
|
|
7023548ec6 | ||
|
|
758f329a06 | ||
|
|
8f1dfabff0 | ||
|
|
7fa01d5de8 | ||
|
|
0de303d5e0 | ||
|
|
155839bde9 | ||
|
|
3a1e5e1bcf | ||
|
|
264f7b1172 | ||
|
|
85e90a1488 | ||
|
|
f8e6e7df3c | ||
|
|
5dd7cce67f | ||
|
|
f21a4a4696 | ||
|
|
98b0d266d2 | ||
|
|
6821a7c20e | ||
|
|
00f72da493 | ||
|
|
744916507c | ||
|
|
88d863c1d7 | ||
|
|
d7dcc9d76f | ||
|
|
4631508b7d | ||
|
|
8803088121 | ||
|
|
1d466cb299 | ||
|
|
5a1f00b1cb | ||
|
|
17d31045b2 | ||
|
|
950466c1da | ||
|
|
62151e52f1 | ||
|
|
b4ce0b910c | ||
|
|
9dd1f47dab | ||
|
|
49743daf32 | ||
|
|
ba23fbe71e | ||
|
|
ee1594a3cb | ||
|
|
e9afb0ee33 | ||
|
|
8b1715379d | ||
|
|
2501fac32f | ||
|
|
1aec0462ec | ||
|
|
83c70a59d8 | ||
|
|
9b8a6e7ff8 | ||
|
|
758f2ec236 | ||
|
|
d9cac38618 | ||
|
|
e8ebfa312b | ||
|
|
dae24891fa | ||
|
|
a4f9de1ca1 | ||
|
|
697028cd4c | ||
|
|
c84acc0023 | ||
|
|
b1d869cd11 | ||
|
|
13a5f6d6ff | ||
|
|
e3f87d835b | ||
|
|
7ffab788b7 | ||
|
|
bcd09906be | ||
|
|
407c53f97c | ||
|
|
eea3c6b294 | ||
|
|
d2182b9112 | ||
|
|
fbc329d7ce | ||
|
|
25045143a1 | ||
|
|
e899604a2d | ||
|
|
0b898a5594 | ||
|
|
ee2f68cd49 | ||
|
|
2cce4ccdd7 | ||
|
|
f3ecebee43 | ||
|
|
e764da3c3a | ||
|
|
fbbea47d11 | ||
|
|
781a9934fd | ||
|
|
a7173142bf | ||
|
|
0e78027186 | ||
|
|
82bd02d21f | ||
|
|
d065e1973d | ||
|
|
7070777408 | ||
|
|
5c2a9f2129 | ||
|
|
1b695e1c19 | ||
|
|
da1cc44d90 | ||
|
|
3bbb759112 | ||
|
|
41c6cbcb59 | ||
|
|
5930e6a176 | ||
|
|
8f3ff91165 | ||
|
|
b12677bc1a | ||
|
|
cdec64e3f1 | ||
|
|
2dc8bc9b9c | ||
|
|
94f0bf557a | ||
|
|
e20690b7c1 | ||
|
|
2c7689ed9e | ||
|
|
c73d507c76 | ||
|
|
317a5d6226 | ||
|
|
4339f97c56 | ||
|
|
81c3ae3037 | ||
|
|
61b04079f5 | ||
|
|
5a0e5a16ea | ||
|
|
f0b5181694 | ||
|
|
185782a2e7 | ||
|
|
8ab95ebe9d | ||
|
|
77159eea2e | ||
|
|
6766e459f2 | ||
|
|
01f4ef3e5d | ||
|
|
b531c82bc4 | ||
|
|
5d942d5ac0 | ||
|
|
b285ca7c22 | ||
|
|
82540edae9 | ||
|
|
f9a6fa7036 | ||
|
|
4eabaecfc8 | ||
|
|
b881e55fca | ||
|
|
f1c50640a3 | ||
|
|
deb1311e48 | ||
|
|
82fa6ab125 | ||
|
|
4d91ff3f2f | ||
|
|
b6f9da87d0 | ||
|
|
c60e97ebf9 | ||
|
|
b1ed16e81a | ||
|
|
c94ebaa260 | ||
|
|
030fb52018 | ||
|
|
487f582302 | ||
|
|
6d0c55d2ba | ||
|
|
c82f60e29e | ||
|
|
c03279d626 | ||
|
|
7b72fa8ca1 | ||
|
|
8b454fbe93 | ||
|
|
f77e25ff86 | ||
|
|
872ef67cf7 | ||
|
|
305ca72ebe | ||
|
|
4d81d3678b | ||
|
|
55e968e174 | ||
|
|
97d1609849 | ||
|
|
e10f850d0d | ||
|
|
b8584fe5ab | ||
|
|
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
|
||||
|
||||
15
.github/workflows/publish.yml
vendored
15
.github/workflows/publish.yml
vendored
@@ -8,6 +8,11 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ["py37", "py38", "py39", "py310"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -19,12 +24,14 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
|
||||
- name: Build and publish
|
||||
pip install setuptools wheel twine build
|
||||
|
||||
- name: Build wheels
|
||||
run: python setup.py bdist_wheel --python-tag ${{ matrix.python }}
|
||||
|
||||
- name: Upload wheels
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
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
|
||||
66
README.md
66
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
|
||||
Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
|
||||
> **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.
|
||||
|
||||
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).
|
||||
Manim runs on Python 3.7 or higher.
|
||||
|
||||
System requirements are [FFmpeg](https://ffmpeg.org/), [OpenGL](https://www.opengl.org/) and [LaTeX](https://www.latex-project.org) (optional, if you want to use LaTeX).
|
||||
For Linux, [Pango](https://pango.gnome.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
|
||||
|
||||
For 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,321 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
No changes now.
|
||||
v1.6.1
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Fixed the bug of ``MTex`` with multi-line tex string (`#1785 <https://github.com/3b1b/manim/pull/1785>`__)
|
||||
- Fixed ``interpolate`` (`#1788 <https://github.com/3b1b/manim/pull/1788>`__)
|
||||
- Fixed ``ImageMobject`` (`#1791 <https://github.com/3b1b/manim/pull/1791>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Added ``\overset`` as a special string in ``Tex`` (`#1783 <https://github.com/3b1b/manim/pull/1783>`__)
|
||||
- Added ``outer_interpolate`` to perform interpolation using ``np.outer`` on arrays (`#1788 <https://github.com/3b1b/manim/pull/1788>`__)
|
||||
|
||||
v1.6.0
|
||||
------
|
||||
|
||||
Breaking changes
|
||||
^^^^^^^^^^^^^^^^
|
||||
- **Python 3.6 is no longer supported** (`#1736 <https://github.com/3b1b/manim/pull/1736>`__)
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Fixed the width of riemann rectangles (`#1762 <https://github.com/3b1b/manim/pull/1762>`__)
|
||||
- Bug fixed in cases where empty array is passed to shader (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/fa38b56fd87f713657c7f778f39dca7faf15baa8>`__)
|
||||
- Fixed ``AddTextWordByWord`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
|
||||
- Fixed ``ControlsExample`` (`#1781 <https://github.com/3b1b/manim/pull/1781>`__)
|
||||
|
||||
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
- Added more functions to ``Text`` (details: `#1751 <https://github.com/3b1b/manim/pull/1751>`__)
|
||||
- Allowed ``interpolate`` to work on an array of alpha values (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/bf2d9edfe67c7e63ac0107d1d713df7ae7c3fb8f>`__)
|
||||
- Allowed ``Numberline.number_to_point`` and ``CoordinateSystem.coords_to_point`` to work on an array of inputs (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/c3e13fff0587d3bb007e71923af7eaf9e4926560>`__)
|
||||
- Added a basic ``Prismify`` to turn a flat ``VMobject`` into something with depth (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/f249da95fb65ed5495cd1db1f12ece7e90061af6>`__)
|
||||
- Added ``GlowDots``, analogous to ``GlowDot`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/e19f35585d817e74b40bc30b1ab7cee84b24da05>`__)
|
||||
- Added ``TransformMatchingStrings`` which is compatible with ``Text`` and ``MTex`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
|
||||
- Added support for ``substring`` and ``case_sensitive`` parameters for ``LabelledString.get_parts_by_string`` (`#1780 <https://github.com/3b1b/manim/pull/1780>`__)
|
||||
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Added type hints (`#1736 <https://github.com/3b1b/manim/pull/1736>`__)
|
||||
- Specifid UTF-8 encoding for tex files (`#1748 <https://github.com/3b1b/manim/pull/1748>`__)
|
||||
- Refactored ``Text`` with the latest manimpango (`#1751 <https://github.com/3b1b/manim/pull/1751>`__)
|
||||
- Reorganized getters for ``ParametricCurve`` (`#1757 <https://github.com/3b1b/manim/pull/1757>`__)
|
||||
- Refactored ``CameraFrame`` to use ``scipy.spatial.transform.Rotation`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/625460467fdc01fc1b6621cbb3d2612195daedb9>`__)
|
||||
- Refactored rotation methods to use ``scipy.spatial.transform.Rotation`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/7bf3615bb15cc6d15506d48ac800a23313054c8e>`__)
|
||||
- Used ``stroke_color`` to init ``Arrow`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/c0b7b55e49f06b75ae133b5a810bebc28c212cd6>`__)
|
||||
- Refactored ``Mobject.set_rgba_array_by_color`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/8b1f0a8749d91eeda4b674ed156cbc7f8e1e48a8>`__)
|
||||
- Made panning more sensitive to mouse movements (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/9d0cc810c5fcb4252990e706c6bf880d571cb1a2>`__)
|
||||
- Added loading progress for large SVGs (`#1766 <https://github.com/3b1b/manim/pull/1766>`__)
|
||||
- Added getter/setter of ``field_of_view`` for ``CameraFrame`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0610f331a4f7a126a3aae34f8a2a86eabcb692f4>`__)
|
||||
- Renamed ``focal_distance`` to ``focal_dist_to_height`` and added getter/setter (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0610f331a4f7a126a3aae34f8a2a86eabcb692f4>`__)
|
||||
- Added getter and setter for ``VMobject.joint_type`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/2a7a7ac5189a14170f883533137e8a2ae09aac41>`__)
|
||||
- Refactored ``VCube`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0f8d7ed59751d42d5011813ba5694ecb506082f7>`__)
|
||||
- Refactored ``Prism`` to receive ``width height depth`` instead of ``dimensions`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0f8d7ed59751d42d5011813ba5694ecb506082f7>`__)
|
||||
- Refactored ``Text``, ``MarkupText`` and ``MTex`` based on ``LabelledString`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
|
||||
- Refactored ``LabelledString`` and relevant classes (`#1779 <https://github.com/3b1b/manim/pull/1779>`__)
|
||||
|
||||
|
||||
v1.5.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Bug fix for the case of calling ``Write`` on a null object (`#1740 <https://github.com/3b1b/manim/pull/1740>`__)
|
||||
|
||||
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
- Added ``TransformMatchingMTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
|
||||
- Added ``ImplicitFunction`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
|
||||
- Added ``Polyline`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
- Allowed ``Mobject.set_points`` to take in an empty list, and added ``Mobject.add_point`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/a64259158538eae6043566aaf3d3329ff4ac394b>`__)
|
||||
- Added ``Scene.refresh_locked_data`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/33d2894c167c577a15fdadbaf26488ff1f5bff87>`__)
|
||||
- Added presenter mode to scenes with ``-p`` option (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9a9cc8bdacb7541b7cd4a52ad705abc21f3e27fe>`__ and `#1742 <https://github.com/3b1b/manim/pull/1742>`__)
|
||||
- Allowed for an embed by hitting ``ctrl+shift+e`` during interaction (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9df12fcb7d8360e51cd7021d6877ca1a5c31835e>`__ and `#1746 <https://github.com/3b1b/manim/pull/1746>`__)
|
||||
- Added ``Mobject.set_min_width/height/depth`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2798d15591a0375ae6bb9135473e6f5328267323>`__)
|
||||
- Allowed ``Mobject.match_coord/x/y/z`` to take in a point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/29a4d3e82ba94c007c996b2d1d0f923941452698>`__)
|
||||
- Added ``text_config`` to ``DecimalNumber`` (`#1744 <https://github.com/3b1b/manim/pull/1744>`__)
|
||||
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Refactored ``MTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
|
||||
- Refactored ``SVGMobject`` with svgelements (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
- Made sure ``ParametricCurve`` has at least one point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2488b9e866fb1ecb842a27dd9f4956ec167e3dee>`__)
|
||||
- Set default to no tips on ``Axes`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/6c6d387a210756c38feca7d34838aa9ac99bb58a>`__)
|
||||
- Stopped displaying when writing tex string is happening (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/58e06e8f6b7c5059ff315d51fd0018fec5cfbb05>`__)
|
||||
- Reorganize inheriting order and refactor SVGMobject (`#1745 <https://github.com/3b1b/manim/pull/1745>`__)
|
||||
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
- Added dependency on ``isosurfaces`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
|
||||
- Removed dependency on ``argparse`` since it's a built-in module (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
|
||||
- Removed dependency on ``pyreadline`` (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
|
||||
- Removed dependency on ``cssselect2`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
- Added dependency on ``svgelements`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
|
||||
|
||||
v1.4.1
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Temporarily fixed boolean operations' bug (`#1724 <https://github.com/3b1b/manim/pull/1724>`__)
|
||||
- Import ``Iterable`` from ``collections.abc`` instead of ``collections`` which is deprecated since python 3.9 (`d2e0811 <https://github.com/3b1b/manim/commit/d2e0811285f7908e71a65e664fec88b1af1c6144>`__)
|
||||
|
||||
v1.4.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Temporarily fixed ``Lightbulb`` (`f1996f8 <https://github.com/3b1b/manim/pull/1697/commits/f1996f8479f9e33d626b3b66e9eb6995ce231d86>`__)
|
||||
- Fixed some bugs of ``SVGMobject`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
|
||||
- Fixed some bugs of SVG path string parser (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
|
||||
- Fixed some bugs of ``MTex`` (`#1720 <https://github.com/3b1b/manim/pull/1720>`__)
|
||||
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
- Added option to add ticks on x-axis in ``BarChart`` (`#1694 <https://github.com/3b1b/manim/pull/1694>`__)
|
||||
- Added ``lable_buff`` config parameter for ``Brace`` (`#1704 <https://github.com/3b1b/manim/pull/1704>`__)
|
||||
- Added support for ``rotate skewX skewY`` transform in SVG (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
|
||||
- Added style support to ``SVGMobject`` (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
|
||||
- Added parser to <style> element of SVG (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
|
||||
- Added support for <line> element in ``SVGMobject`` (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Used ``FFMPEG_BIN`` instead of ``"ffmpeg"`` for sound incorporation (`5aa8d15 <https://github.com/3b1b/manim/pull/1697/commits/5aa8d15d85797f68a8f169ca69fd90d441a3abbe>`__)
|
||||
- Decorated ``CoordinateSystem.get_axes`` and ``.get_all_ranges`` as abstract method (`#1709 <https://github.com/3b1b/manim/pull/1709>`__)
|
||||
- Refactored SVG path string parser (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
|
||||
- Allowed ``Mobject.scale`` to receive iterable ``scale_factor`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
|
||||
- Refactored ``MTex`` (`#1716 <https://github.com/3b1b/manim/pull/1716>`__)
|
||||
- Improved config helper (``manimgl --config``) (`#1721 <https://github.com/3b1b/manim/pull/1721>`__)
|
||||
- Refactored ``MTex`` (`#1723 <https://github.com/3b1b/manim/pull/1723>`__)
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
- Added dependency on python package `cssselect2 <https://github.com/Kozea/cssselect2>`__ (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
|
||||
|
||||
|
||||
v1.3.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- Fixed ``Mobject.stretch_to_fit_depth`` (`#1653 <https://github.com/3b1b/manim/pull/1653>`__)
|
||||
- Fixed the bug of rotating camera (`#1655 <https://github.com/3b1b/manim/pull/1655>`__)
|
||||
- Fixed ``SurfaceMesh`` to be evenly spaced (`c73d507 <https://github.com/3b1b/manim/pull/1688/commits/c73d507c76af5c8602d4118bc7538ba04c03ebae>`__)
|
||||
- Fixed ``angle_between_vectors`` add ``rotation_between_vectors`` (`82bd02d <https://github.com/3b1b/manim/pull/1688/commits/82bd02d21fbd89b71baa21e077e143f440df9014>`__)
|
||||
- Fixed ``VMobject.fade`` (`a717314 <https://github.com/3b1b/manim/pull/1688/commits/a7173142bf93fd309def0cc10f3c56f5e6972332>`__)
|
||||
- Fixed ``angle_between_vectors`` (`fbc329d <https://github.com/3b1b/manim/pull/1688/commits/fbc329d7ce3b11821d47adf6052d932f7eff724a>`__)
|
||||
- Fixed bug in ``ShowSubmobjectsOneByOne`` (`bcd0990 <https://github.com/3b1b/manim/pull/1688/commits/bcd09906bea5eaaa5352e7bee8f3153f434cf606>`__)
|
||||
- Fixed bug in ``TransformMatchingParts`` (`7023548 <https://github.com/3b1b/manim/pull/1691/commits/7023548ec62c4adb2f371aab6a8c7f62deb7c33c>`__)
|
||||
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Added CLI flag ``--log-level`` to specify log level (`e10f850 <https://github.com/3b1b/manim/commit/e10f850d0d9f971931cc85d44befe67dc842af6d>`__)
|
||||
- Added operations (``+`` and ``*``) for ``Mobject`` (`#1667 <https://github.com/3b1b/manim/pull/1667>`__)
|
||||
- Added 4 boolean operations for ``VMobject`` in ``manimlib/mobject/boolean_ops.py`` (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
|
||||
|
||||
- ``Union(*vmobjects, **kwargs)``
|
||||
- ``Difference(subject, clip, **kwargs)``
|
||||
- ``Intersection(*vmobjects, **kwargs)``
|
||||
- ``Exclusion(*vmobjects, **kwargs)``
|
||||
- Added reflectiveness (`81c3ae3 <https://github.com/3b1b/manim/pull/1688/commits/81c3ae30372e288dc772633dbd17def6e603753e>`__)
|
||||
- Enabled ``glow_factor`` on ``DotCloud`` (`2c7689e <https://github.com/3b1b/manim/pull/1688/commits/2c7689ed9e81229ce87c648f97f26267956c0bc9>`__)
|
||||
- Added option ``-e`` to insert embed line from the command line (`d065e19 <https://github.com/3b1b/manim/pull/1688/commits/d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772>`__)
|
||||
- Improved ``point_from_proportion`` to account for arc length (`0e78027 <https://github.com/3b1b/manim/pull/1688/commits/0e78027186a976f7e5fa8d586f586bf6e6baab8d>`__)
|
||||
- Added shortcut ``set_backstroke`` for setting black background stroke (`781a993 <https://github.com/3b1b/manim/pull/1688/commits/781a9934fda6ba11f22ba32e8ccddcb3ba78592e>`__)
|
||||
- Added ``Suface.always_sort_to_camera`` (`0b898a5 <https://github.com/3b1b/manim/pull/1688/commits/0b898a5594203668ed9cad38b490ab49ba233bd4>`__)
|
||||
- Added getter methods for specific euler angles (`e899604 <https://github.com/3b1b/manim/pull/1688/commits/e899604a2d05f78202fcb3b9824ec34647237eae>`__)
|
||||
- Hade ``rotation_between_vectors`` handle identical/similar vectors (`407c53f <https://github.com/3b1b/manim/pull/1688/commits/407c53f97c061bfd8a53beacd88af4c786f9e9ee>`__)
|
||||
- Added ``Mobject.insert_submobject`` method (`49743da <https://github.com/3b1b/manim/pull/1688/commits/49743daf3244bfa11a427040bdde8e2bb79589e8>`__)
|
||||
- Created single progress display for full scene render (`9dd1f47 <https://github.com/3b1b/manim/pull/1688/commits/9dd1f47dabca1580d6102e34e44574b0cba556e7>`__)
|
||||
- Added ``Circle.get_radius`` (`264f7b1 <https://github.com/3b1b/manim/pull/1691/commits/264f7b11726e9e736f0fe472f66e38539f74e848>`__)
|
||||
- Added ``Dodecahedron`` (`83841ae <https://github.com/3b1b/manim/pull/1691/commits/83841ae41568a9c9dff44cd163106c19a74ac281>`__)
|
||||
- Added ``GlowDot`` (`a1d5147 <https://github.com/3b1b/manim/pull/1691/commits/a1d51474ea1ce3b7aa3efbe4c5e221be70ee2f5b>`__)
|
||||
- Added ``MTex`` , see `#1678 <https://github.com/3b1b/manim/pull/1678>`__ for details (`#1678 <https://github.com/3b1b/manim/pull/1678>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
|
||||
- Refactored support for command ``A`` in path of SVG (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
|
||||
- Refactored ``SingleStringTex.balance_braces`` (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
|
||||
- Slight tweaks to how saturation_factor works on newton-fractal (`8b454fb <https://github.com/3b1b/manim/pull/1688/commits/8b454fbe9335a7011e947093230b07a74ba9c653>`__)
|
||||
- Made it possible to set full screen preview as a default (`317a5d6 <https://github.com/3b1b/manim/pull/1688/commits/317a5d6226475b6b54a78db7116c373ef84ea923>`__)
|
||||
- Used ``quick_point_from_proportion`` for graph points (`e764da3 <https://github.com/3b1b/manim/pull/1688/commits/e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc>`__)
|
||||
- Made sure ``Line.set_length`` returns self (`d2182b9 <https://github.com/3b1b/manim/pull/1688/commits/d2182b9112300558b6c074cefd685f97c10b3898>`__)
|
||||
- Better align ``SurfaceMesh`` to the corresponding surface polygons (`eea3c6b <https://github.com/3b1b/manim/pull/1688/commits/eea3c6b29438f9e9325329c4355e76b9f635e97a>`__)
|
||||
- Match ``fix_in_frame`` status for ``FlashAround`` mobject (`ee1594a <https://github.com/3b1b/manim/pull/1688/commits/ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3>`__)
|
||||
- Made sure ``Mobject.is_fixed_in_frame`` stays updated with uniforms (`ba23fbe <https://github.com/3b1b/manim/pull/1688/commits/ba23fbe71e4a038201cd7df1d200514ed1c13bc2>`__)
|
||||
- Made sure ``skip_animations`` and ``start_at_animation_number`` play well together (`98b0d26 <https://github.com/3b1b/manim/pull/1691/commits/98b0d266d2475926a606331923cca3dc1dea97ad>`__)
|
||||
- Updated progress display for full scene render (`f8e6e7d <https://github.com/3b1b/manim/pull/1691/commits/f8e6e7df3ceb6f3d845ced4b690a85b35e0b8d00>`__)
|
||||
- ``VectorizedPoint`` should call ``__init__`` for both super classes (`8f1dfab <https://github.com/3b1b/manim/pull/1691/commits/8f1dfabff04a8456f5c4df75b0f97d50b2755003>`__)
|
||||
- Used array copy when checking need for refreshing triangulation (`758f329 <https://github.com/3b1b/manim/pull/1691/commits/758f329a06a0c198b27a48c577575d94554305bf>`__)
|
||||
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__ (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
|
||||
|
||||
v1.2.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- Fixed ``put_start_and_end_on`` in 3D (`#1592 <https://github.com/3b1b/manim/pull/1592>`__)
|
||||
- Fixed ``DecimalNumber``'s scaling issue (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
|
||||
- Fixed bug with common range array used for all coordinate systems (`56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__)
|
||||
- Fixed ``CoordinateSystem`` init bug (`8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__)
|
||||
- Fixed bug for single-valued ``ValueTracker`` (`0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__)
|
||||
- Fixed bug with SVG rectangles (`54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__)
|
||||
- Fixed ``DotCloud.set_radii`` (`d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__)
|
||||
- Temporarily fixed bug for ``PMobject`` array resizing (`b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__)
|
||||
- Fixed ``match_style`` (`5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__)
|
||||
- Fixed negative ``path_arc`` case (`719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__)
|
||||
- Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis`` (`c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__)
|
||||
- Fixed ``ComplexPlane`` -i display bug (`7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__)
|
||||
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Supported the elliptical arc command ``A`` for ``SVGMobject`` (`#1598 <https://github.com/3b1b/manim/pull/1598>`__)
|
||||
- Added ``FlashyFadeIn`` (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
|
||||
- Save triangulation (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
|
||||
- Added new ``Code`` mobject (`#1625 <https://github.com/3b1b/manim/pull/1625>`__)
|
||||
- Add warnings and use rich to display log (`#1637 <https://github.com/3b1b/manim/pull/1637>`__)
|
||||
- Added ``VCube`` (`bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__)
|
||||
- Supported ``ValueTracker`` to track vectors (`6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__)
|
||||
- Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject`` (`3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__)
|
||||
- Added ``TracgTail`` (`a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__)
|
||||
- Added ``Scene.point_to_mobject`` (`acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__)
|
||||
- Added poly_fractal shader (`f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__)
|
||||
- Added kwargs to ``TipableVMobject.set_length`` (`b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__)
|
||||
- Added ``Mobject.replicate`` (`17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__)
|
||||
- Added mandelbrot_fractal shader (`33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__)
|
||||
- Saved state before each embed (`f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__)
|
||||
- Allowed releasing of Textures (`e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__)
|
||||
- Consolidated and renamed newton_fractal shader (`14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__)
|
||||
- Hade ``ImageMoject`` remember the filepath to the Image (`6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
|
||||
- Changed back to simpler ``Mobject.scale`` implementation (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
|
||||
- Simplified ``Square`` (`b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__)
|
||||
- Removed unused parameter ``triangulation_locked`` (`40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__)
|
||||
- Reimplemented ``Arrow`` (`8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__)
|
||||
- Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default (`d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__)
|
||||
- Refactored to call ``_handle_scale_side_effects`` after scaling takes place (`7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__)
|
||||
- Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end`` (`7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__)
|
||||
- Made sure framerate is 30 for previewed scenes (`0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__)
|
||||
- Pushed ``pixel_coords_to_space_coords`` to ``Window`` (`c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__)
|
||||
- Refactored to pass tuples and not arrays to uniforms (`d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__)
|
||||
- Refactored to copy uniform arrays in ``Mobject.copy`` (`9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__)
|
||||
- Added ``bounding_box`` as exceptional key to point_cloud mobject (`ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__)
|
||||
- Made sure stroke width is always a float (`329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__)
|
||||
|
||||
|
||||
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``
|
||||
@@ -84,8 +84,6 @@ Text
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
START_X = 30
|
||||
START_Y = 20
|
||||
NORMAL = "NORMAL"
|
||||
ITALIC = "ITALIC"
|
||||
OBLIQUE = "OBLIQUE"
|
||||
|
||||
@@ -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``
|
||||
-----------------------------
|
||||
|
||||
|
||||
@@ -83,22 +83,3 @@ Its value is a dictionary, passed in as ``kwargs`` when initializing the ``Camer
|
||||
to modify the value of the properties of the ``Camera`` class.
|
||||
|
||||
So the nesting of the ``CONFIG`` dictionary **essentially** passes in the value as ``kwargs``.
|
||||
|
||||
Common usage
|
||||
------------
|
||||
|
||||
When writing a class by yourself, you can add attributes or modify the attributes
|
||||
of the parent class through ``CONFIG``.
|
||||
|
||||
The most commonly used is to modify the properties of the camera when writing a ``Scene``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class YourScene(Scene):
|
||||
CONFIG = {
|
||||
"camera_config": {
|
||||
"background_color": WHITE,
|
||||
},
|
||||
}
|
||||
|
||||
For example, the above dictionary will change the background color to white, etc.
|
||||
@@ -43,6 +43,7 @@ flag abbr function
|
||||
``--hd`` Render at a 1080p quality
|
||||
``--uhd`` Render at a 4k quality
|
||||
``--full_screen`` ``-f`` Show window in full screen
|
||||
``--presenter_mode`` ``-p`` Scene will stay paused during wait calls until space bar or right arrow is hit, like a slide show
|
||||
``--save_pngs`` ``-g`` Save each frame as a png
|
||||
``--save_as_gif`` ``-i`` Save the video as gif
|
||||
``--transparent`` ``-t`` Render to a movie file with an alpha channel
|
||||
@@ -52,12 +53,14 @@ 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.
|
||||
``--embed LINENO`` ``-e`` Takes a line number as an argument, and results in the scene being called as if the line ``self.embed()`` was inserted into the scene code at that line number
|
||||
``--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
|
||||
``--video_dir VIDEO_DIR`` Directory to write video
|
||||
``--config_file CONFIG_FILE`` Path to the custom configuration file
|
||||
========================================================== ====== =================================================================================================================================================================================================
|
||||
|
||||
custom_config
|
||||
@@ -85,5 +88,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.7 or higher.
|
||||
|
||||
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/
|
||||
|
||||
.. 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])
|
||||
|
||||
@@ -270,7 +274,7 @@ class UpdatersExample(Scene):
|
||||
square = Square()
|
||||
square.set_fill(BLUE_E, 1)
|
||||
|
||||
# On all all frames, the constructor Brace(square, UP) will
|
||||
# On all frames, the constructor Brace(square, UP) will
|
||||
# be called, and the mobject brace will set its data to match
|
||||
# that of the newly constructed object
|
||||
brace = always_redraw(Brace, square, UP)
|
||||
@@ -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,18 +639,18 @@ 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))
|
||||
new_text = Text(self.textbox.get_value(), size=old_text.size)
|
||||
new_text = Text(self.textbox.get_value(), font_size=old_text.font_size)
|
||||
# new_text.align_data_and_family(old_text)
|
||||
new_text.move_to(old_text)
|
||||
if self.checkbox.get_value():
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import pkg_resources
|
||||
|
||||
__version__ = pkg_resources.get_distribution("manimgl").version
|
||||
|
||||
from manimlib.constants import *
|
||||
|
||||
from manimlib.animation.animation import *
|
||||
@@ -18,6 +22,7 @@ from manimlib.camera.camera import *
|
||||
|
||||
from manimlib.window import *
|
||||
|
||||
from manimlib.mobject.boolean_ops import *
|
||||
from manimlib.mobject.coordinate_systems import *
|
||||
from manimlib.mobject.changing import *
|
||||
from manimlib.mobject.frame import *
|
||||
@@ -32,6 +37,8 @@ from manimlib.mobject.probability import *
|
||||
from manimlib.mobject.shape_matchers import *
|
||||
from manimlib.mobject.svg.brace import *
|
||||
from manimlib.mobject.svg.drawings import *
|
||||
from manimlib.mobject.svg.labelled_string import *
|
||||
from manimlib.mobject.svg.mtex_mobject import *
|
||||
from manimlib.mobject.svg.svg_mobject import *
|
||||
from manimlib.mobject.svg.tex_mobject import *
|
||||
from manimlib.mobject.svg.text_mobject import *
|
||||
@@ -62,4 +69,3 @@ from manimlib.utils.rate_functions import *
|
||||
from manimlib.utils.simple_functions import *
|
||||
from manimlib.utils.sounds import *
|
||||
from manimlib.utils.space_ops import *
|
||||
from manimlib.utils.strings import *
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
#!/usr/bin/env python
|
||||
import manimlib.config
|
||||
import manimlib.logger
|
||||
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 is None:
|
||||
return
|
||||
if args.log_level:
|
||||
manimlib.logger.log.setLevel(args.log_level)
|
||||
|
||||
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()
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -6,6 +9,11 @@ from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
DEFAULT_ANIMATION_RUN_TIME = 1.0
|
||||
DEFAULT_ANIMATION_LAG_RATIO = 0
|
||||
@@ -26,20 +34,20 @@ 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):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
assert(isinstance(mobject, Mobject))
|
||||
digest_config(self, kwargs)
|
||||
self.mobject = mobject
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
if self.name:
|
||||
return self.name
|
||||
return self.__class__.__name__ + str(self.mobject)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
# This is called right as an animation is being
|
||||
# played. As much initialization as possible,
|
||||
# especially any mobject copying, should live in
|
||||
@@ -56,32 +64,32 @@ class Animation(object):
|
||||
self.families = list(self.get_all_families_zipped())
|
||||
self.interpolate(0)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
self.interpolate(self.final_alpha_value)
|
||||
if self.suspend_mobject_updating:
|
||||
self.mobject.resume_updating()
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
if self.is_remover():
|
||||
scene.remove(self.mobject)
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
# Keep track of where the mobject starts
|
||||
return self.mobject.copy()
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> tuple[Mobject, Mobject]:
|
||||
"""
|
||||
Ordering must match the ording of arguments to interpolate_submobject
|
||||
"""
|
||||
return self.mobject, self.starting_mobject
|
||||
|
||||
def get_all_families_zipped(self):
|
||||
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
|
||||
return zip(*[
|
||||
mob.get_family()
|
||||
for mob in self.get_all_mobjects()
|
||||
])
|
||||
|
||||
def update_mobjects(self, dt):
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
"""
|
||||
Updates things like starting_mobject, and (for
|
||||
Transforms) target_mobject. Note, since typically
|
||||
@@ -92,7 +100,7 @@ class Animation(object):
|
||||
for mob in self.get_all_mobjects_to_update():
|
||||
mob.update(dt)
|
||||
|
||||
def get_all_mobjects_to_update(self):
|
||||
def get_all_mobjects_to_update(self) -> list[Mobject]:
|
||||
# The surrounding scene typically handles
|
||||
# updating of self.mobject. Besides, in
|
||||
# most cases its updating is suspended anyway
|
||||
@@ -109,27 +117,37 @@ class Animation(object):
|
||||
return self
|
||||
|
||||
# Methods for interpolation, the mean of an Animation
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
alpha = clip(alpha, 0, 1)
|
||||
self.interpolate_mobject(self.rate_func(alpha))
|
||||
|
||||
def update(self, alpha):
|
||||
def update(self, alpha: float) -> None:
|
||||
"""
|
||||
This method shouldn't exist, but it's here to
|
||||
keep many old scenes from breaking
|
||||
"""
|
||||
self.interpolate(alpha)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
for i, mobs in enumerate(self.families):
|
||||
sub_alpha = self.get_sub_alpha(alpha, i, len(self.families))
|
||||
self.interpolate_submobject(*mobs, sub_alpha)
|
||||
|
||||
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
alpha: float
|
||||
):
|
||||
# Typically ipmlemented by subclass
|
||||
pass
|
||||
|
||||
def get_sub_alpha(self, alpha, index, num_submobjects):
|
||||
def get_sub_alpha(
|
||||
self,
|
||||
alpha: float,
|
||||
index: int,
|
||||
num_submobjects: int
|
||||
) -> float:
|
||||
# TODO, make this more understanable, and/or combine
|
||||
# its functionality with AnimationGroup's method
|
||||
# build_animations_with_timings
|
||||
@@ -140,29 +158,29 @@ class Animation(object):
|
||||
return clip((value - lower), 0, 1)
|
||||
|
||||
# Getters and setters
|
||||
def set_run_time(self, run_time):
|
||||
def set_run_time(self, run_time: float):
|
||||
self.run_time = run_time
|
||||
return self
|
||||
|
||||
def get_run_time(self):
|
||||
def get_run_time(self) -> float:
|
||||
return self.run_time
|
||||
|
||||
def set_rate_func(self, rate_func):
|
||||
def set_rate_func(self, rate_func: Callable[[float], float]):
|
||||
self.rate_func = rate_func
|
||||
return self
|
||||
|
||||
def get_rate_func(self):
|
||||
def get_rate_func(self) -> Callable[[float], float]:
|
||||
return self.rate_func
|
||||
|
||||
def set_name(self, name):
|
||||
def set_name(self, name: str):
|
||||
self.name = name
|
||||
return self
|
||||
|
||||
def is_remover(self):
|
||||
def is_remover(self) -> bool:
|
||||
return self.remover
|
||||
|
||||
|
||||
def prepare_animation(anim):
|
||||
def prepare_animation(anim: Animation | _AnimationBuilder):
|
||||
if isinstance(anim, _AnimationBuilder):
|
||||
return anim.build()
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.animation.animation import Animation, prepare_animation
|
||||
from manimlib.mobject.mobject import Group
|
||||
@@ -9,6 +12,12 @@ from manimlib.utils.iterables import remove_list_redundancies
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
DEFAULT_LAGGED_START_LAG_RATIO = 0.05
|
||||
|
||||
@@ -27,7 +36,7 @@ class AnimationGroup(Animation):
|
||||
"group": None,
|
||||
}
|
||||
|
||||
def __init__(self, *animations, **kwargs):
|
||||
def __init__(self, *animations: Animation, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.animations = [prepare_animation(anim) for anim in animations]
|
||||
if self.group is None:
|
||||
@@ -37,27 +46,27 @@ class AnimationGroup(Animation):
|
||||
self.init_run_time()
|
||||
Animation.__init__(self, self.group, **kwargs)
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> Group:
|
||||
return self.group
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
for anim in self.animations:
|
||||
anim.begin()
|
||||
# self.init_run_time()
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
for anim in self.animations:
|
||||
anim.finish()
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
for anim in self.animations:
|
||||
anim.clean_up_from_scene(scene)
|
||||
|
||||
def update_mobjects(self, dt):
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
for anim in self.animations:
|
||||
anim.update_mobjects(dt)
|
||||
|
||||
def init_run_time(self):
|
||||
def init_run_time(self) -> None:
|
||||
self.build_animations_with_timings()
|
||||
if self.anims_with_timings:
|
||||
self.max_end_time = np.max([
|
||||
@@ -68,7 +77,7 @@ class AnimationGroup(Animation):
|
||||
if self.run_time is None:
|
||||
self.run_time = self.max_end_time
|
||||
|
||||
def build_animations_with_timings(self):
|
||||
def build_animations_with_timings(self) -> None:
|
||||
"""
|
||||
Creates a list of triplets of the form
|
||||
(anim, start_time, end_time)
|
||||
@@ -87,7 +96,7 @@ class AnimationGroup(Animation):
|
||||
start_time, end_time, self.lag_ratio
|
||||
)
|
||||
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
# Note, if the run_time of AnimationGroup has been
|
||||
# set to something other than its default, these
|
||||
# times might not correspond to actual times,
|
||||
@@ -111,19 +120,19 @@ class Succession(AnimationGroup):
|
||||
"lag_ratio": 1,
|
||||
}
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
assert(len(self.animations) > 0)
|
||||
self.init_run_time()
|
||||
self.active_animation = self.animations[0]
|
||||
self.active_animation.begin()
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
self.active_animation.finish()
|
||||
|
||||
def update_mobjects(self, dt):
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
self.active_animation.update_mobjects(dt)
|
||||
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
index, subalpha = integer_interpolate(
|
||||
0, len(self.animations), alpha
|
||||
)
|
||||
@@ -146,7 +155,13 @@ class LaggedStartMap(LaggedStart):
|
||||
"run_time": 2,
|
||||
}
|
||||
|
||||
def __init__(self, AnimationClass, mobject, arg_creator=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
AnimationClass: type,
|
||||
mobject: Mobject,
|
||||
arg_creator: Callable[[Mobject], tuple] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
args_list = []
|
||||
for submob in mobject:
|
||||
if arg_creator:
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
from abc import abstractmethod
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.composition import Succession
|
||||
from manimlib.mobject.svg.labelled_string import LabelledString
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.rate_functions import double_smooth
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Group
|
||||
|
||||
|
||||
class ShowPartial(Animation):
|
||||
@@ -20,17 +29,27 @@ class ShowPartial(Animation):
|
||||
"should_match_start": False,
|
||||
}
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
super().begin()
|
||||
if not self.should_match_start:
|
||||
self.mobject.lock_matching_data(self.mobject, self.starting_mobject)
|
||||
|
||||
def interpolate_submobject(self, submob, start_submob, alpha):
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: VMobject,
|
||||
start_submob: VMobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
submob.pointwise_become_partial(
|
||||
start_submob, *self.get_bounds(alpha)
|
||||
)
|
||||
|
||||
def get_bounds(self, alpha):
|
||||
@abstractmethod
|
||||
def get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
raise Exception("Not Implemented")
|
||||
|
||||
|
||||
@@ -39,7 +58,7 @@ class ShowCreation(ShowPartial):
|
||||
"lag_ratio": 1,
|
||||
}
|
||||
|
||||
def get_bounds(self, alpha):
|
||||
def get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
return (0, alpha)
|
||||
|
||||
|
||||
@@ -61,7 +80,7 @@ class DrawBorderThenFill(Animation):
|
||||
"fill_animation_config": {},
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
assert(isinstance(vmobject, VMobject))
|
||||
self.sm_to_index = dict([
|
||||
(hash(sm), 0)
|
||||
@@ -69,7 +88,7 @@ class DrawBorderThenFill(Animation):
|
||||
])
|
||||
super().__init__(vmobject, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
# Trigger triangulation calculation
|
||||
for submob in self.mobject.get_family():
|
||||
submob.get_triangulation()
|
||||
@@ -79,11 +98,11 @@ class DrawBorderThenFill(Animation):
|
||||
self.mobject.match_style(self.outline)
|
||||
self.mobject.lock_matching_data(self.mobject, self.outline)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
|
||||
def get_outline(self):
|
||||
def get_outline(self) -> VMobject:
|
||||
outline = self.mobject.copy()
|
||||
outline.set_fill(opacity=0)
|
||||
for sm in outline.get_family():
|
||||
@@ -93,17 +112,23 @@ class DrawBorderThenFill(Animation):
|
||||
)
|
||||
return outline
|
||||
|
||||
def get_stroke_color(self, vmobject):
|
||||
def get_stroke_color(self, vmobject: VMobject) -> str:
|
||||
if self.stroke_color:
|
||||
return self.stroke_color
|
||||
elif vmobject.get_stroke_width() > 0:
|
||||
return vmobject.get_stroke_color()
|
||||
return vmobject.get_color()
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> list[VMobject]:
|
||||
return [*super().get_all_mobjects(), self.outline]
|
||||
|
||||
def interpolate_submobject(self, submob, start, outline, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: VMobject,
|
||||
start: VMobject,
|
||||
outline: VMobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
index, subalpha = integer_interpolate(0, 2, alpha)
|
||||
|
||||
if index == 1 and self.sm_to_index[hash(submob)] == 0:
|
||||
@@ -130,20 +155,20 @@ class Write(DrawBorderThenFill):
|
||||
"rate_func": linear,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.set_default_config_from_length(mobject)
|
||||
super().__init__(mobject, **kwargs)
|
||||
self.set_default_config_from_length(vmobject)
|
||||
super().__init__(vmobject, **kwargs)
|
||||
|
||||
def set_default_config_from_length(self, mobject):
|
||||
length = len(mobject.family_members_with_points())
|
||||
def set_default_config_from_length(self, vmobject: VMobject) -> None:
|
||||
length = len(vmobject.family_members_with_points())
|
||||
if self.run_time is None:
|
||||
if length < 15:
|
||||
self.run_time = 1
|
||||
else:
|
||||
self.run_time = 2
|
||||
if self.lag_ratio is None:
|
||||
self.lag_ratio = min(4.0 / length, 0.2)
|
||||
self.lag_ratio = min(4.0 / (length + 1.0), 0.2)
|
||||
|
||||
|
||||
class ShowIncreasingSubsets(Animation):
|
||||
@@ -152,16 +177,16 @@ class ShowIncreasingSubsets(Animation):
|
||||
"int_func": np.round,
|
||||
}
|
||||
|
||||
def __init__(self, group, **kwargs):
|
||||
def __init__(self, group: Group, **kwargs):
|
||||
self.all_submobs = list(group.submobjects)
|
||||
super().__init__(group, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
n_submobs = len(self.all_submobs)
|
||||
index = int(self.int_func(alpha * n_submobs))
|
||||
self.update_submobject_list(index)
|
||||
|
||||
def update_submobject_list(self, index):
|
||||
def update_submobject_list(self, index: int) -> None:
|
||||
self.mobject.set_submobjects(self.all_submobs[:index])
|
||||
|
||||
|
||||
@@ -170,35 +195,27 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
|
||||
"int_func": np.ceil,
|
||||
}
|
||||
|
||||
def __init__(self, group, **kwargs):
|
||||
new_group = Group(*group)
|
||||
super().__init__(new_group, **kwargs)
|
||||
|
||||
def update_submobject_list(self, index):
|
||||
def update_submobject_list(self, index: int) -> None:
|
||||
# N = len(self.all_submobs)
|
||||
if index == 0:
|
||||
self.mobject.set_submobjects([])
|
||||
else:
|
||||
self.mobject.set_submobjects(self.all_submobs[index - 1])
|
||||
self.mobject.set_submobjects([self.all_submobs[index - 1]])
|
||||
|
||||
|
||||
# TODO, this is broken...
|
||||
class AddTextWordByWord(Succession):
|
||||
class AddTextWordByWord(ShowIncreasingSubsets):
|
||||
CONFIG = {
|
||||
# If given a value for run_time, it will
|
||||
# override the time_per_char
|
||||
# override the time_per_word
|
||||
"run_time": None,
|
||||
"time_per_char": 0.06,
|
||||
"time_per_word": 0.2,
|
||||
"rate_func": linear,
|
||||
}
|
||||
|
||||
def __init__(self, text_mobject, **kwargs):
|
||||
def __init__(self, string_mobject, **kwargs):
|
||||
assert isinstance(string_mobject, LabelledString)
|
||||
grouped_mobject = string_mobject.submob_groups
|
||||
digest_config(self, kwargs)
|
||||
tpc = self.time_per_char
|
||||
anims = it.chain(*[
|
||||
[
|
||||
ShowIncreasingSubsets(word, run_time=tpc * len(word)),
|
||||
Animation(word, run_time=0.005 * len(word)**1.5),
|
||||
]
|
||||
for word in text_mobject
|
||||
])
|
||||
super().__init__(*anims, **kwargs)
|
||||
if self.run_time is None:
|
||||
self.run_time = self.time_per_word * len(grouped_mobject)
|
||||
super().__init__(grouped_mobject, **kwargs)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
@@ -7,6 +9,13 @@ from manimlib.constants import ORIGIN
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.rate_functions import there_and_back
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
DEFAULT_FADE_LAG_RATIO = 0
|
||||
|
||||
@@ -16,7 +25,13 @@ class Fade(Transform):
|
||||
"lag_ratio": DEFAULT_FADE_LAG_RATIO,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, shift=ORIGIN, scale=1, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
shift: np.ndarray = ORIGIN,
|
||||
scale: float = 1,
|
||||
**kwargs
|
||||
):
|
||||
self.shift_vect = shift
|
||||
self.scale_factor = scale
|
||||
super().__init__(mobject, **kwargs)
|
||||
@@ -27,10 +42,10 @@ class FadeIn(Fade):
|
||||
"lag_ratio": DEFAULT_FADE_LAG_RATIO,
|
||||
}
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
start = super().create_starting_mobject()
|
||||
start.set_opacity(0)
|
||||
start.scale(1.0 / self.scale_factor)
|
||||
@@ -45,7 +60,7 @@ class FadeOut(Fade):
|
||||
"final_alpha_value": 0,
|
||||
}
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
result = self.mobject.copy()
|
||||
result.set_opacity(0)
|
||||
result.shift(self.shift_vect)
|
||||
@@ -54,7 +69,7 @@ class FadeOut(Fade):
|
||||
|
||||
|
||||
class FadeInFromPoint(FadeIn):
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
def __init__(self, mobject: Mobject, point: np.ndarray, **kwargs):
|
||||
super().__init__(
|
||||
mobject,
|
||||
shift=mobject.get_center() - point,
|
||||
@@ -64,7 +79,7 @@ class FadeInFromPoint(FadeIn):
|
||||
|
||||
|
||||
class FadeOutToPoint(FadeOut):
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
def __init__(self, mobject: Mobject, point: np.ndarray, **kwargs):
|
||||
super().__init__(
|
||||
mobject,
|
||||
shift=point - mobject.get_center(),
|
||||
@@ -79,7 +94,7 @@ class FadeTransform(Transform):
|
||||
"dim_to_match": 1,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, target_mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs):
|
||||
self.to_add_on_completion = target_mobject
|
||||
mobject.save_state()
|
||||
super().__init__(
|
||||
@@ -87,7 +102,7 @@ class FadeTransform(Transform):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.ending_mobject = self.mobject.copy()
|
||||
Animation.begin(self)
|
||||
# Both 'start' and 'end' consists of the source and target mobjects.
|
||||
@@ -97,21 +112,21 @@ class FadeTransform(Transform):
|
||||
for m0, m1 in ((start[1], start[0]), (end[0], end[1])):
|
||||
self.ghost_to(m0, m1)
|
||||
|
||||
def ghost_to(self, source, target):
|
||||
def ghost_to(self, source: Mobject, target: Mobject) -> None:
|
||||
source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match)
|
||||
source.set_opacity(0)
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> list[Mobject]:
|
||||
return [
|
||||
self.mobject,
|
||||
self.starting_mobject,
|
||||
self.ending_mobject,
|
||||
]
|
||||
|
||||
def get_all_families_zipped(self):
|
||||
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
|
||||
return Animation.get_all_families_zipped(self)
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
Animation.clean_up_from_scene(self, scene)
|
||||
scene.remove(self.mobject)
|
||||
self.mobject[0].restore()
|
||||
@@ -119,11 +134,11 @@ class FadeTransform(Transform):
|
||||
|
||||
|
||||
class FadeTransformPieces(FadeTransform):
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.mobject[0].align_family(self.mobject[1])
|
||||
super().begin()
|
||||
|
||||
def ghost_to(self, source, target):
|
||||
def ghost_to(self, source: Mobject, target: Mobject) -> None:
|
||||
for sm0, sm1 in zip(source.get_family(), target.get_family()):
|
||||
super().ghost_to(sm0, sm1)
|
||||
|
||||
@@ -136,7 +151,12 @@ class VFadeIn(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def interpolate_submobject(self, submob, start, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: VMobject,
|
||||
start: VMobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
submob.set_stroke(
|
||||
opacity=interpolate(0, start.get_stroke_opacity(), alpha)
|
||||
)
|
||||
@@ -152,7 +172,12 @@ class VFadeOut(VFadeIn):
|
||||
"final_alpha_value": 0,
|
||||
}
|
||||
|
||||
def interpolate_submobject(self, submob, start, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: VMobject,
|
||||
start: VMobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
super().interpolate_submobject(submob, start, 1 - alpha)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
from manimlib.animation.transform import Transform
|
||||
# from manimlib.utils.paths import counterclockwise_path
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import PI
|
||||
from manimlib.animation.transform import Transform
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
|
||||
|
||||
class GrowFromPoint(Transform):
|
||||
@@ -8,14 +16,14 @@ class GrowFromPoint(Transform):
|
||||
"point_color": None,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
def __init__(self, mobject: Mobject, point: np.ndarray, **kwargs):
|
||||
self.point = point
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
start = super().create_starting_mobject()
|
||||
start.scale(0)
|
||||
start.move_to(self.point)
|
||||
@@ -25,19 +33,19 @@ class GrowFromPoint(Transform):
|
||||
|
||||
|
||||
class GrowFromCenter(GrowFromPoint):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
point = mobject.get_center()
|
||||
super().__init__(mobject, point, **kwargs)
|
||||
|
||||
|
||||
class GrowFromEdge(GrowFromPoint):
|
||||
def __init__(self, mobject, edge, **kwargs):
|
||||
def __init__(self, mobject: Mobject, edge: np.ndarray, **kwargs):
|
||||
point = mobject.get_bounding_box_point(edge)
|
||||
super().__init__(mobject, point, **kwargs)
|
||||
|
||||
|
||||
class GrowArrow(GrowFromPoint):
|
||||
def __init__(self, arrow, **kwargs):
|
||||
def __init__(self, arrow: Arrow, **kwargs):
|
||||
point = arrow.get_start()
|
||||
super().__init__(arrow, point, **kwargs)
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Union, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import *
|
||||
@@ -8,17 +13,28 @@ 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
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import colour
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
class FocusOn(Transform):
|
||||
@@ -29,13 +45,13 @@ class FocusOn(Transform):
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def __init__(self, focus_point, **kwargs):
|
||||
def __init__(self, focus_point: np.ndarray, **kwargs):
|
||||
self.focus_point = focus_point
|
||||
# Initialize with blank mobject, while create_target
|
||||
# and create_starting_mobject handle the meat
|
||||
super().__init__(VMobject(), **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Dot:
|
||||
little_dot = Dot(radius=0)
|
||||
little_dot.set_fill(self.color, opacity=self.opacity)
|
||||
little_dot.add_updater(
|
||||
@@ -43,7 +59,7 @@ class FocusOn(Transform):
|
||||
)
|
||||
return little_dot
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Dot:
|
||||
return Dot(
|
||||
radius=FRAME_X_RADIUS + FRAME_Y_RADIUS,
|
||||
stroke_width=0,
|
||||
@@ -59,7 +75,7 @@ class Indicate(Transform):
|
||||
"color": YELLOW,
|
||||
}
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
target = self.mobject.copy()
|
||||
target.scale(self.scale_factor)
|
||||
target.set_color(self.color)
|
||||
@@ -75,7 +91,12 @@ class Flash(AnimationGroup):
|
||||
"run_time": 1,
|
||||
}
|
||||
|
||||
def __init__(self, point, color=YELLOW, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
color: ManimColor = YELLOW,
|
||||
**kwargs
|
||||
):
|
||||
self.point = point
|
||||
self.color = color
|
||||
digest_config(self, kwargs)
|
||||
@@ -87,7 +108,7 @@ class Flash(AnimationGroup):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def create_lines(self):
|
||||
def create_lines(self) -> VGroup:
|
||||
lines = VGroup()
|
||||
for angle in np.arange(0, TAU, TAU / self.num_lines):
|
||||
line = Line(ORIGIN, self.line_length * RIGHT)
|
||||
@@ -101,7 +122,7 @@ class Flash(AnimationGroup):
|
||||
lines.add_updater(lambda l: l.move_to(self.point))
|
||||
return lines
|
||||
|
||||
def create_line_anims(self):
|
||||
def create_line_anims(self) -> list[Animation]:
|
||||
return [
|
||||
ShowCreationThenDestruction(line)
|
||||
for line in self.lines
|
||||
@@ -117,17 +138,17 @@ class CircleIndicate(Indicate):
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
circle = self.get_circle(mobject)
|
||||
super().__init__(circle, **kwargs)
|
||||
|
||||
def get_circle(self, mobject):
|
||||
def get_circle(self, mobject: Mobject) -> Circle:
|
||||
circle = Circle(**self.circle_config)
|
||||
circle.add_updater(lambda c: c.surround(mobject))
|
||||
return circle
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
super().interpolate_mobject(alpha)
|
||||
self.mobject.set_stroke(opacity=alpha)
|
||||
|
||||
@@ -138,7 +159,7 @@ class ShowPassingFlash(ShowPartial):
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def get_bounds(self, alpha):
|
||||
def get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
tw = self.time_width
|
||||
upper = interpolate(0, 1 + tw, alpha)
|
||||
lower = upper - tw
|
||||
@@ -146,12 +167,100 @@ class ShowPassingFlash(ShowPartial):
|
||||
lower = max(lower, 0)
|
||||
return (lower, upper)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
for submob, start in self.get_all_families_zipped():
|
||||
submob.pointwise_become_partial(start, 0, 1)
|
||||
|
||||
|
||||
class VShowPassingFlash(Animation):
|
||||
CONFIG = {
|
||||
"time_width": 0.3,
|
||||
"taper_width": 0.02,
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def begin(self) -> None:
|
||||
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: VMobject,
|
||||
starting_sumobject: None,
|
||||
alpha: float
|
||||
) -> None:
|
||||
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) -> None:
|
||||
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: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
path = self.get_path(mobject)
|
||||
if mobject.is_fixed_in_frame:
|
||||
path.fix_in_frame()
|
||||
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: Mobject) -> SurroundingRectangle:
|
||||
return SurroundingRectangle(mobject, buff=self.buff)
|
||||
|
||||
|
||||
class FlashUnder(FlashAround):
|
||||
def get_path(self, mobject: Mobject) -> Underline:
|
||||
return Underline(mobject, buff=self.buff)
|
||||
|
||||
|
||||
class ShowCreationThenDestruction(ShowPassingFlash):
|
||||
CONFIG = {
|
||||
"time_width": 2.0,
|
||||
@@ -164,7 +273,7 @@ class ShowCreationThenFadeOut(Succession):
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(
|
||||
ShowCreation(mobject),
|
||||
FadeOut(mobject),
|
||||
@@ -181,7 +290,7 @@ class AnimationOnSurroundingRectangle(AnimationGroup):
|
||||
"rect_animation": Animation
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
if "surrounding_rectangle_config" in kwargs:
|
||||
kwargs.pop("surrounding_rectangle_config")
|
||||
@@ -194,7 +303,7 @@ class AnimationOnSurroundingRectangle(AnimationGroup):
|
||||
self.rect_animation(rect, **kwargs),
|
||||
)
|
||||
|
||||
def get_rect(self):
|
||||
def get_rect(self) -> SurroundingRectangle:
|
||||
return SurroundingRectangle(
|
||||
self.mobject_to_surround,
|
||||
**self.surrounding_rectangle_config
|
||||
@@ -226,7 +335,7 @@ class ApplyWave(Homotopy):
|
||||
"run_time": 1,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
left_x = mobject.get_left()[0]
|
||||
right_x = mobject.get_right()[0]
|
||||
@@ -251,15 +360,20 @@ class WiggleOutThenIn(Animation):
|
||||
"rotate_about_point": None,
|
||||
}
|
||||
|
||||
def get_scale_about_point(self):
|
||||
def get_scale_about_point(self) -> np.ndarray:
|
||||
if self.scale_about_point is None:
|
||||
return self.mobject.get_center()
|
||||
|
||||
def get_rotate_about_point(self):
|
||||
def get_rotate_about_point(self) -> np.ndarray:
|
||||
if self.rotate_about_point is None:
|
||||
return self.mobject.get_center()
|
||||
|
||||
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submobject: Mobject,
|
||||
starting_sumobject: Mobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
submobject.match_points(starting_sumobject)
|
||||
submobject.scale(
|
||||
interpolate(1, self.scale_value, there_and_back(alpha)),
|
||||
@@ -276,5 +390,24 @@ class TurnInsideOut(Transform):
|
||||
"path_arc": TAU / 4,
|
||||
}
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject.copy().reverse_points()
|
||||
|
||||
|
||||
class FlashyFadeIn(AnimationGroup):
|
||||
CONFIG = {
|
||||
"fade_lag": 0,
|
||||
}
|
||||
|
||||
def __init__(self, vmobject: VMobject, stroke_width: float = 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
|
||||
)
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Sequence
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.utils.rate_functions import linear
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class Homotopy(Animation):
|
||||
CONFIG = {
|
||||
@@ -8,7 +18,12 @@ class Homotopy(Animation):
|
||||
"apply_function_kwargs": {},
|
||||
}
|
||||
|
||||
def __init__(self, homotopy, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
homotopy: Callable[[float, float, float, float], Sequence[float]],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Homotopy is a function from
|
||||
(x, y, z, t) to (x', y', z')
|
||||
@@ -16,10 +31,18 @@ class Homotopy(Animation):
|
||||
self.homotopy = homotopy
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def function_at_time_t(self, t):
|
||||
def function_at_time_t(
|
||||
self,
|
||||
t: float
|
||||
) -> Callable[[np.ndarray], Sequence[float]]:
|
||||
return lambda p: self.homotopy(*p, t)
|
||||
|
||||
def interpolate_submobject(self, submob, start, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: Mobject,
|
||||
start: Mobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
submob.match_points(start)
|
||||
submob.apply_function(
|
||||
self.function_at_time_t(alpha),
|
||||
@@ -34,7 +57,12 @@ class SmoothedVectorizedHomotopy(Homotopy):
|
||||
|
||||
|
||||
class ComplexHomotopy(Homotopy):
|
||||
def __init__(self, complex_homotopy, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
complex_homotopy: Callable[[complex, float], Sequence[float]],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Given a function form (z, t) -> w, where z and w
|
||||
are complex numbers and t is time, this animates
|
||||
@@ -53,11 +81,16 @@ class PhaseFlow(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
if hasattr(self, "last_alpha"):
|
||||
dt = self.virtual_time * (alpha - self.last_alpha)
|
||||
self.mobject.apply_function(
|
||||
@@ -71,10 +104,10 @@ class MoveAlongPath(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, path, **kwargs):
|
||||
def __init__(self, mobject: Mobject, path: Mobject, **kwargs):
|
||||
self.path = path
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
point = self.path.point_from_proportion(alpha)
|
||||
self.mobject.move_to(point)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.utils.bezier import interpolate
|
||||
@@ -8,19 +12,29 @@ class ChangingDecimal(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, decimal_mob, number_update_func, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
number_update_func: Callable[[float], float],
|
||||
**kwargs
|
||||
):
|
||||
assert(isinstance(decimal_mob, DecimalNumber))
|
||||
self.number_update_func = number_update_func
|
||||
super().__init__(decimal_mob, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.set_value(
|
||||
self.number_update_func(alpha)
|
||||
)
|
||||
|
||||
|
||||
class ChangeDecimalToValue(ChangingDecimal):
|
||||
def __init__(self, decimal_mob, target_number, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
target_number: float | complex,
|
||||
**kwargs
|
||||
):
|
||||
start_number = decimal_mob.number
|
||||
super().__init__(
|
||||
decimal_mob,
|
||||
@@ -30,7 +44,12 @@ class ChangeDecimalToValue(ChangingDecimal):
|
||||
|
||||
|
||||
class CountInFrom(ChangingDecimal):
|
||||
def __init__(self, decimal_mob, source_number=0, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
source_number: float | complex = 0,
|
||||
**kwargs
|
||||
):
|
||||
start_number = decimal_mob.number
|
||||
super().__init__(
|
||||
decimal_mob,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.constants import OUT
|
||||
from manimlib.constants import PI
|
||||
@@ -6,6 +8,12 @@ from manimlib.constants import ORIGIN
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class Rotating(Animation):
|
||||
CONFIG = {
|
||||
@@ -18,12 +26,18 @@ class Rotating(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, angle=TAU, axis=OUT, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
angle: float = TAU,
|
||||
axis: np.ndarray = OUT,
|
||||
**kwargs
|
||||
):
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
for sm1, sm2 in self.get_all_families_zipped():
|
||||
sm1.set_points(sm2.get_points())
|
||||
self.mobject.rotate(
|
||||
@@ -41,5 +55,11 @@ class Rotate(Rotating):
|
||||
"about_edge": ORIGIN,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, angle=PI, axis=OUT, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
angle: float = PI,
|
||||
axis: np.ndarray = OUT,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject, angle, axis, **kwargs)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.composition import LaggedStart
|
||||
from manimlib.animation.transform import Restore
|
||||
from manimlib.constants import WHITE
|
||||
@@ -17,10 +21,9 @@ class Broadcast(LaggedStart):
|
||||
"remover": True,
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": 3,
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def __init__(self, focal_point, **kwargs):
|
||||
def __init__(self, focal_point: np.ndarray, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
circles = VGroup()
|
||||
for x in range(self.n_circles):
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from typing import Callable, Union, Sequence
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.constants import DEFAULT_POINTWISE_FUNCTION_RUN_TIME
|
||||
@@ -14,6 +18,13 @@ from manimlib.utils.paths import straight_path
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.rate_functions import squish_rate_func
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import colour
|
||||
from manimlib.scene.scene import Scene
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
class Transform(Animation):
|
||||
CONFIG = {
|
||||
@@ -23,12 +34,17 @@ class Transform(Animation):
|
||||
"replace_mobject_with_target_in_scene": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, target_mobject=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
target_mobject: Mobject | None = None,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject, **kwargs)
|
||||
self.target_mobject = target_mobject
|
||||
self.init_path_func()
|
||||
|
||||
def init_path_func(self):
|
||||
def init_path_func(self) -> None:
|
||||
if self.path_func is not None:
|
||||
return
|
||||
elif self.path_arc == 0:
|
||||
@@ -39,12 +55,12 @@ class Transform(Animation):
|
||||
self.path_arc_axis,
|
||||
)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.target_mobject = self.create_target()
|
||||
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)
|
||||
@@ -54,28 +70,28 @@ class Transform(Animation):
|
||||
self.target_copy,
|
||||
)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
# Has no meaningful effect here, but may be useful
|
||||
# in subclasses
|
||||
return self.target_mobject
|
||||
|
||||
def check_target_mobject_validity(self):
|
||||
def check_target_mobject_validity(self) -> None:
|
||||
if self.target_mobject is None:
|
||||
raise Exception(
|
||||
f"{self.__class__.__name__}.create_target not properly implemented"
|
||||
)
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
if self.replace_mobject_with_target_in_scene:
|
||||
scene.remove(self.mobject)
|
||||
scene.add(self.target_mobject)
|
||||
|
||||
def update_config(self, **kwargs):
|
||||
def update_config(self, **kwargs) -> None:
|
||||
Animation.update_config(self, **kwargs)
|
||||
if "path_arc" in kwargs:
|
||||
self.path_func = path_along_arc(
|
||||
@@ -83,7 +99,7 @@ class Transform(Animation):
|
||||
kwargs.get("path_arc_axis", OUT)
|
||||
)
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> list[Mobject]:
|
||||
return [
|
||||
self.mobject,
|
||||
self.starting_mobject,
|
||||
@@ -91,7 +107,7 @@ class Transform(Animation):
|
||||
self.target_copy,
|
||||
]
|
||||
|
||||
def get_all_families_zipped(self):
|
||||
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
|
||||
return zip(*[
|
||||
mob.get_family()
|
||||
for mob in [
|
||||
@@ -101,7 +117,13 @@ class Transform(Animation):
|
||||
]
|
||||
])
|
||||
|
||||
def interpolate_submobject(self, submob, start, target_copy, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: Mobject,
|
||||
start: Mobject,
|
||||
target_copy: Mobject,
|
||||
alpha: float
|
||||
):
|
||||
submob.interpolate(start, target_copy, alpha, self.path_func)
|
||||
return self
|
||||
|
||||
@@ -117,10 +139,10 @@ class TransformFromCopy(Transform):
|
||||
Performs a reversed Transform
|
||||
"""
|
||||
|
||||
def __init__(self, mobject, target_mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs):
|
||||
super().__init__(target_mobject, mobject, **kwargs)
|
||||
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
super().interpolate(1 - alpha)
|
||||
|
||||
|
||||
@@ -137,11 +159,11 @@ class CounterclockwiseTransform(Transform):
|
||||
|
||||
|
||||
class MoveToTarget(Transform):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
self.check_validity_of_input(mobject)
|
||||
super().__init__(mobject, mobject.target, **kwargs)
|
||||
|
||||
def check_validity_of_input(self, mobject):
|
||||
def check_validity_of_input(self, mobject: Mobject) -> None:
|
||||
if not hasattr(mobject, "target"):
|
||||
raise Exception(
|
||||
"MoveToTarget called on mobject"
|
||||
@@ -150,18 +172,18 @@ class MoveToTarget(Transform):
|
||||
|
||||
|
||||
class _MethodAnimation(MoveToTarget):
|
||||
def __init__(self, mobject, methods):
|
||||
def __init__(self, mobject: Mobject, methods: Callable):
|
||||
self.methods = methods
|
||||
super().__init__(mobject)
|
||||
|
||||
|
||||
class ApplyMethod(Transform):
|
||||
def __init__(self, method, *args, **kwargs):
|
||||
def __init__(self, method: Callable, *args, **kwargs):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
@@ -170,7 +192,7 @@ class ApplyMethod(Transform):
|
||||
self.method_args = args
|
||||
super().__init__(method.__self__, **kwargs)
|
||||
|
||||
def check_validity_of_input(self, method):
|
||||
def check_validity_of_input(self, method: Callable) -> None:
|
||||
if not inspect.ismethod(method):
|
||||
raise Exception(
|
||||
"Whoops, looks like you accidentally invoked "
|
||||
@@ -178,7 +200,7 @@ class ApplyMethod(Transform):
|
||||
)
|
||||
assert(isinstance(method.__self__, Mobject))
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
method = self.method
|
||||
# Make sure it's a list so that args.pop() works
|
||||
args = list(self.method_args)
|
||||
@@ -197,16 +219,26 @@ class ApplyPointwiseFunction(ApplyMethod):
|
||||
"run_time": DEFAULT_POINTWISE_FUNCTION_RUN_TIME
|
||||
}
|
||||
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject.apply_function, function, **kwargs)
|
||||
|
||||
|
||||
class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction):
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
super().__init__(mobject.move_to, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.method_args = [
|
||||
self.function(self.mobject.get_center())
|
||||
]
|
||||
@@ -214,31 +246,46 @@ class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction):
|
||||
|
||||
|
||||
class FadeToColor(ApplyMethod):
|
||||
def __init__(self, mobject, color, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
color: ManimColor,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject.set_color, color, **kwargs)
|
||||
|
||||
|
||||
class ScaleInPlace(ApplyMethod):
|
||||
def __init__(self, mobject, scale_factor, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
scale_factor: npt.ArrayLike,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject.scale, scale_factor, **kwargs)
|
||||
|
||||
|
||||
class ShrinkToCenter(ScaleInPlace):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(mobject, 0, **kwargs)
|
||||
|
||||
|
||||
class Restore(ApplyMethod):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(mobject.restore, **kwargs)
|
||||
|
||||
|
||||
class ApplyFunction(Transform):
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[Mobject], Mobject],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
target = self.function(self.mobject.copy())
|
||||
if not isinstance(target, Mobject):
|
||||
raise Exception("Functions passed to ApplyFunction must return object of type Mobject")
|
||||
@@ -246,7 +293,12 @@ class ApplyFunction(Transform):
|
||||
|
||||
|
||||
class ApplyMatrix(ApplyPointwiseFunction):
|
||||
def __init__(self, matrix, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
matrix: npt.ArrayLike,
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
matrix = self.initialize_matrix(matrix)
|
||||
|
||||
def func(p):
|
||||
@@ -254,7 +306,7 @@ class ApplyMatrix(ApplyPointwiseFunction):
|
||||
|
||||
super().__init__(func, mobject, **kwargs)
|
||||
|
||||
def initialize_matrix(self, matrix):
|
||||
def initialize_matrix(self, matrix: npt.ArrayLike) -> np.ndarray:
|
||||
matrix = np.array(matrix)
|
||||
if matrix.shape == (2, 2):
|
||||
new_matrix = np.identity(3)
|
||||
@@ -266,12 +318,17 @@ class ApplyMatrix(ApplyPointwiseFunction):
|
||||
|
||||
|
||||
class ApplyComplexFunction(ApplyMethod):
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[complex], complex],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
method = mobject.apply_complex_function
|
||||
super().__init__(method, function, **kwargs)
|
||||
|
||||
def init_path_func(self):
|
||||
def init_path_func(self) -> None:
|
||||
func1 = self.function(complex(1))
|
||||
self.path_arc = np.log(func1).imag
|
||||
super().init_path_func()
|
||||
@@ -284,11 +341,11 @@ class CyclicReplace(Transform):
|
||||
"path_arc": 90 * DEGREES,
|
||||
}
|
||||
|
||||
def __init__(self, *mobjects, **kwargs):
|
||||
def __init__(self, *mobjects: Mobject, **kwargs):
|
||||
self.group = Group(*mobjects)
|
||||
super().__init__(self.group, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
target = self.group.copy()
|
||||
cycled_targets = [target[-1], *target[:-1]]
|
||||
for m1, m2 in zip(cycled_targets, self.group):
|
||||
@@ -306,7 +363,7 @@ class TransformAnimations(Transform):
|
||||
"rate_func": squish_rate_func(smooth)
|
||||
}
|
||||
|
||||
def __init__(self, start_anim, end_anim, **kwargs):
|
||||
def __init__(self, start_anim: Animation, end_anim: Animation, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
if "run_time" in kwargs:
|
||||
self.run_time = kwargs.pop("run_time")
|
||||
@@ -327,7 +384,7 @@ class TransformAnimations(Transform):
|
||||
start_anim.mobject = self.starting_mobject
|
||||
end_anim.mobject = self.target_mobject
|
||||
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
self.start_anim.interpolate(alpha)
|
||||
self.end_anim.interpolate(alpha)
|
||||
Transform.interpolate(self, alpha)
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.animation.fading import FadeTransformPieces
|
||||
from manimlib.animation.fading import FadeInFromPoint
|
||||
from manimlib.animation.fading import FadeOutToPoint
|
||||
from manimlib.animation.transform import ReplacementTransform
|
||||
from manimlib.animation.transform import Transform
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.svg.labelled_string import LabelledString
|
||||
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
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.mobject.svg.tex_mobject import Tex, SingleStringTex
|
||||
|
||||
|
||||
class TransformMatchingParts(AnimationGroup):
|
||||
@@ -23,7 +34,7 @@ class TransformMatchingParts(AnimationGroup):
|
||||
"key_map": dict(),
|
||||
}
|
||||
|
||||
def __init__(self, mobject, target_mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
assert(isinstance(mobject, self.mobject_type))
|
||||
assert(isinstance(target_mobject, self.mobject_type))
|
||||
@@ -69,10 +80,10 @@ class TransformMatchingParts(AnimationGroup):
|
||||
anims.append(FadeTransformPieces(fade_source, fade_target, **kwargs))
|
||||
else:
|
||||
anims.append(FadeOutToPoint(
|
||||
fade_source, fade_target.get_center(), **kwargs
|
||||
fade_source, target_mobject.get_center(), **kwargs
|
||||
))
|
||||
anims.append(FadeInFromPoint(
|
||||
fade_target.copy(), fade_source.get_center(), **kwargs
|
||||
fade_target.copy(), mobject.get_center(), **kwargs
|
||||
))
|
||||
|
||||
super().__init__(*anims)
|
||||
@@ -80,8 +91,8 @@ class TransformMatchingParts(AnimationGroup):
|
||||
self.to_remove = mobject
|
||||
self.to_add = target_mobject
|
||||
|
||||
def get_shape_map(self, mobject):
|
||||
shape_map = {}
|
||||
def get_shape_map(self, mobject: Mobject) -> dict[int, VGroup]:
|
||||
shape_map: dict[int, VGroup] = {}
|
||||
for sm in self.get_mobject_parts(mobject):
|
||||
key = self.get_mobject_key(sm)
|
||||
if key not in shape_map:
|
||||
@@ -89,7 +100,7 @@ class TransformMatchingParts(AnimationGroup):
|
||||
shape_map[key].add(sm)
|
||||
return shape_map
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
for anim in self.animations:
|
||||
anim.update(0)
|
||||
scene.remove(self.mobject)
|
||||
@@ -97,12 +108,12 @@ class TransformMatchingParts(AnimationGroup):
|
||||
scene.add(self.to_add)
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject):
|
||||
def get_mobject_parts(mobject: Mobject) -> Mobject:
|
||||
# To be implemented in subclass
|
||||
return mobject
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
def get_mobject_key(mobject: Mobject) -> int:
|
||||
# To be implemented in subclass
|
||||
return hash(mobject)
|
||||
|
||||
@@ -114,11 +125,11 @@ class TransformMatchingShapes(TransformMatchingParts):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject):
|
||||
def get_mobject_parts(mobject: VMobject) -> list[VMobject]:
|
||||
return mobject.family_members_with_points()
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
def get_mobject_key(mobject: VMobject) -> int:
|
||||
mobject.save_state()
|
||||
mobject.center()
|
||||
mobject.set_height(1)
|
||||
@@ -129,14 +140,121 @@ class TransformMatchingShapes(TransformMatchingParts):
|
||||
|
||||
class TransformMatchingTex(TransformMatchingParts):
|
||||
CONFIG = {
|
||||
"mobject_type": Tex,
|
||||
"mobject_type": VMobject,
|
||||
"group_type": VGroup,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject):
|
||||
def get_mobject_parts(mobject: Tex) -> list[SingleStringTex]:
|
||||
return mobject.submobjects
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
def get_mobject_key(mobject: Tex) -> str:
|
||||
return mobject.get_tex()
|
||||
|
||||
|
||||
class TransformMatchingStrings(AnimationGroup):
|
||||
CONFIG = {
|
||||
"key_map": dict(),
|
||||
"transform_mismatches": False,
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
source: LabelledString,
|
||||
target: LabelledString,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
assert isinstance(source, LabelledString)
|
||||
assert isinstance(target, LabelledString)
|
||||
anims = []
|
||||
source_indices = list(range(len(source.labelled_submobjects)))
|
||||
target_indices = list(range(len(target.labelled_submobjects)))
|
||||
|
||||
def get_indices_lists(mobject, parts):
|
||||
return [
|
||||
[
|
||||
mobject.labelled_submobjects.index(submob)
|
||||
for submob in part
|
||||
]
|
||||
for part in parts
|
||||
]
|
||||
|
||||
def add_anims_from(anim_class, func, source_args, target_args=None):
|
||||
if target_args is None:
|
||||
target_args = source_args.copy()
|
||||
for source_arg, target_arg in zip(source_args, target_args):
|
||||
source_parts = func(source, source_arg)
|
||||
target_parts = func(target, target_arg)
|
||||
source_indices_lists = list(filter(
|
||||
lambda indices_list: all([
|
||||
index in source_indices
|
||||
for index in indices_list
|
||||
]), get_indices_lists(source, source_parts)
|
||||
))
|
||||
target_indices_lists = list(filter(
|
||||
lambda indices_list: all([
|
||||
index in target_indices
|
||||
for index in indices_list
|
||||
]), get_indices_lists(target, target_parts)
|
||||
))
|
||||
if not source_indices_lists or not target_indices_lists:
|
||||
continue
|
||||
anims.append(anim_class(source_parts, target_parts, **kwargs))
|
||||
for index in it.chain(*source_indices_lists):
|
||||
source_indices.remove(index)
|
||||
for index in it.chain(*target_indices_lists):
|
||||
target_indices.remove(index)
|
||||
|
||||
def get_common_substrs(substrs_from_source, substrs_from_target):
|
||||
return sorted([
|
||||
substr for substr in substrs_from_source
|
||||
if substr and substr in substrs_from_target
|
||||
], key=len, reverse=True)
|
||||
|
||||
def get_parts_from_keys(mobject, keys):
|
||||
if isinstance(keys, str):
|
||||
keys = [keys]
|
||||
result = VGroup()
|
||||
for key in keys:
|
||||
if not isinstance(key, str):
|
||||
raise TypeError(key)
|
||||
result.add(*mobject.get_parts_by_string(key))
|
||||
return result
|
||||
|
||||
add_anims_from(
|
||||
ReplacementTransform, get_parts_from_keys,
|
||||
self.key_map.keys(), self.key_map.values()
|
||||
)
|
||||
add_anims_from(
|
||||
FadeTransformPieces,
|
||||
LabelledString.get_parts_by_string,
|
||||
get_common_substrs(
|
||||
source.specified_substrs,
|
||||
target.specified_substrs
|
||||
)
|
||||
)
|
||||
add_anims_from(
|
||||
FadeTransformPieces,
|
||||
LabelledString.get_parts_by_group_substr,
|
||||
get_common_substrs(
|
||||
source.group_substrs,
|
||||
target.group_substrs
|
||||
)
|
||||
)
|
||||
|
||||
rest_source = VGroup(*[source[index] for index in source_indices])
|
||||
rest_target = VGroup(*[target[index] for index in target_indices])
|
||||
if self.transform_mismatches:
|
||||
anims.append(
|
||||
ReplacementTransform(rest_source, rest_target, **kwargs)
|
||||
)
|
||||
else:
|
||||
anims.append(
|
||||
FadeOutToPoint(rest_source, target.get_center(), **kwargs)
|
||||
)
|
||||
anims.append(
|
||||
FadeInFromPoint(rest_target, source.get_center(), **kwargs)
|
||||
)
|
||||
|
||||
super().__init__(*anims)
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import operator as op
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class UpdateFromFunc(Animation):
|
||||
"""
|
||||
@@ -13,21 +21,31 @@ class UpdateFromFunc(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, update_function, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
update_function: Callable[[Mobject]],
|
||||
**kwargs
|
||||
):
|
||||
self.update_function = update_function
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject)
|
||||
|
||||
|
||||
class UpdateFromAlphaFunc(UpdateFromFunc):
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject, alpha)
|
||||
|
||||
|
||||
class MaintainPositionRelativeTo(Animation):
|
||||
def __init__(self, mobject, tracked_mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
tracked_mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.tracked_mobject = tracked_mobject
|
||||
self.diff = op.sub(
|
||||
mobject.get_center(),
|
||||
@@ -35,7 +53,7 @@ class MaintainPositionRelativeTo(Animation):
|
||||
)
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
target = self.tracked_mobject.get_center()
|
||||
location = self.mobject.get_center()
|
||||
self.mobject.shift(target - location + self.diff)
|
||||
|
||||
@@ -1,139 +1,154 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import moderngl
|
||||
from colour import Color
|
||||
import OpenGL.GL as gl
|
||||
import math
|
||||
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial.transform import Rotation
|
||||
from PIL import Image
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.simple_functions import fdiv
|
||||
from manimlib.utils.simple_functions import clip
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
from manimlib.utils.space_ops import rotation_matrix_transpose_from_quaternion
|
||||
from manimlib.utils.space_ops import rotation_matrix_transpose
|
||||
from manimlib.utils.space_ops import quaternion_from_angle_axis
|
||||
from manimlib.utils.space_ops import quaternion_mult
|
||||
from manimlib.utils.space_ops import normalize
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.shader_wrapper import ShaderWrapper
|
||||
|
||||
|
||||
class CameraFrame(Mobject):
|
||||
CONFIG = {
|
||||
"frame_shape": (FRAME_WIDTH, FRAME_HEIGHT),
|
||||
"center_point": ORIGIN,
|
||||
# Theta, phi, gamma
|
||||
"euler_angles": [0, 0, 0],
|
||||
"focal_distance": 2,
|
||||
"focal_dist_to_height": 2,
|
||||
}
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
self.data["euler_angles"] = np.array(self.euler_angles, dtype=float)
|
||||
self.refresh_rotation_matrix()
|
||||
def init_uniforms(self) -> None:
|
||||
super().init_uniforms()
|
||||
# As a quaternion
|
||||
self.uniforms["orientation"] = Rotation.identity().as_quat()
|
||||
self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP])
|
||||
self.set_width(self.frame_shape[0], stretch=True)
|
||||
self.set_height(self.frame_shape[1], stretch=True)
|
||||
self.move_to(self.center_point)
|
||||
|
||||
def set_orientation(self, rotation: Rotation):
|
||||
self.uniforms["orientation"][:] = rotation.as_quat()
|
||||
return self
|
||||
|
||||
def get_orientation(self):
|
||||
return Rotation.from_quat(self.uniforms["orientation"])
|
||||
|
||||
def to_default_state(self):
|
||||
self.center()
|
||||
self.set_height(FRAME_HEIGHT)
|
||||
self.set_width(FRAME_WIDTH)
|
||||
self.set_euler_angles(0, 0, 0)
|
||||
self.set_orientation(Rotation.identity())
|
||||
return self
|
||||
|
||||
def get_euler_angles(self):
|
||||
return self.data["euler_angles"]
|
||||
return self.get_orientation().as_euler("zxz")[::-1]
|
||||
|
||||
def get_inverse_camera_rotation_matrix(self):
|
||||
return self.inverse_camera_rotation_matrix
|
||||
return self.get_orientation().as_matrix().T
|
||||
|
||||
def refresh_rotation_matrix(self):
|
||||
# Rotate based on camera orientation
|
||||
theta, phi, gamma = self.get_euler_angles()
|
||||
quat = quaternion_mult(
|
||||
quaternion_from_angle_axis(theta, OUT, axis_normalized=True),
|
||||
quaternion_from_angle_axis(phi, RIGHT, axis_normalized=True),
|
||||
quaternion_from_angle_axis(gamma, OUT, axis_normalized=True),
|
||||
)
|
||||
self.inverse_camera_rotation_matrix = rotation_matrix_transpose_from_quaternion(quat)
|
||||
|
||||
def rotate(self, angle, axis=OUT, **kwargs):
|
||||
curr_rot_T = self.get_inverse_camera_rotation_matrix()
|
||||
added_rot_T = rotation_matrix_transpose(angle, axis)
|
||||
new_rot_T = np.dot(curr_rot_T, added_rot_T)
|
||||
Fz = new_rot_T[2]
|
||||
phi = np.arccos(Fz[2])
|
||||
theta = angle_of_vector(Fz[:2]) + PI / 2
|
||||
partial_rot_T = np.dot(
|
||||
rotation_matrix_transpose(phi, RIGHT),
|
||||
rotation_matrix_transpose(theta, OUT),
|
||||
)
|
||||
gamma = angle_of_vector(np.dot(partial_rot_T, new_rot_T.T)[:, 0])
|
||||
self.set_euler_angles(theta, phi, gamma)
|
||||
def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
|
||||
rot = Rotation.from_rotvec(angle * normalize(axis))
|
||||
self.set_orientation(rot * self.get_orientation())
|
||||
return self
|
||||
|
||||
def set_euler_angles(self, theta=None, phi=None, gamma=None):
|
||||
if theta is not None:
|
||||
self.data["euler_angles"][0] = theta
|
||||
if phi is not None:
|
||||
self.data["euler_angles"][1] = phi
|
||||
if gamma is not None:
|
||||
self.data["euler_angles"][2] = gamma
|
||||
self.refresh_rotation_matrix()
|
||||
def set_euler_angles(
|
||||
self,
|
||||
theta: float | None = None,
|
||||
phi: float | None = None,
|
||||
gamma: float | None = None,
|
||||
units: float = RADIANS
|
||||
):
|
||||
eulers = self.get_euler_angles() # theta, phi, gamma
|
||||
for i, var in enumerate([theta, phi, gamma]):
|
||||
if var is not None:
|
||||
eulers[i] = var * units
|
||||
self.set_orientation(Rotation.from_euler("zxz", eulers[::-1]))
|
||||
return self
|
||||
|
||||
def set_theta(self, theta):
|
||||
def reorient(
|
||||
self,
|
||||
theta_degrees: float | None = None,
|
||||
phi_degrees: float | None = None,
|
||||
gamma_degrees: float | None = 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: float):
|
||||
return self.set_euler_angles(theta=theta)
|
||||
|
||||
def set_phi(self, phi):
|
||||
def set_phi(self, phi: float):
|
||||
return self.set_euler_angles(phi=phi)
|
||||
|
||||
def set_gamma(self, gamma):
|
||||
def set_gamma(self, gamma: float):
|
||||
return self.set_euler_angles(gamma=gamma)
|
||||
|
||||
def increment_theta(self, dtheta):
|
||||
self.data["euler_angles"][0] += dtheta
|
||||
self.refresh_rotation_matrix()
|
||||
def increment_theta(self, dtheta: float):
|
||||
self.rotate(dtheta, OUT)
|
||||
return self
|
||||
|
||||
def increment_phi(self, dphi):
|
||||
phi = self.data["euler_angles"][1]
|
||||
new_phi = clip(phi + dphi, 0, PI)
|
||||
self.data["euler_angles"][1] = new_phi
|
||||
self.refresh_rotation_matrix()
|
||||
def increment_phi(self, dphi: float):
|
||||
self.rotate(dphi, self.get_inverse_camera_rotation_matrix()[0])
|
||||
return self
|
||||
|
||||
def increment_gamma(self, dgamma):
|
||||
self.data["euler_angles"][2] += dgamma
|
||||
self.refresh_rotation_matrix()
|
||||
def increment_gamma(self, dgamma: float):
|
||||
self.rotate(dgamma, self.get_inverse_camera_rotation_matrix()[2])
|
||||
return self
|
||||
|
||||
def set_focal_distance(self, focal_distance: float):
|
||||
self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height()
|
||||
return self
|
||||
|
||||
def set_field_of_view(self, field_of_view: float):
|
||||
self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2)
|
||||
return self
|
||||
|
||||
def get_shape(self):
|
||||
return (self.get_width(), self.get_height())
|
||||
|
||||
def get_center(self):
|
||||
def get_center(self) -> np.ndarray:
|
||||
# Assumes first point is at the center
|
||||
return self.get_points()[0]
|
||||
|
||||
def get_width(self):
|
||||
def get_width(self) -> float:
|
||||
points = self.get_points()
|
||||
return points[2, 0] - points[1, 0]
|
||||
|
||||
def get_height(self):
|
||||
def get_height(self) -> float:
|
||||
points = self.get_points()
|
||||
return points[4, 1] - points[3, 1]
|
||||
|
||||
def get_focal_distance(self):
|
||||
return self.focal_distance * self.get_height()
|
||||
def get_focal_distance(self) -> float:
|
||||
return self.uniforms["focal_dist_to_height"] * self.get_height()
|
||||
|
||||
def interpolate(self, *args, **kwargs):
|
||||
super().interpolate(*args, **kwargs)
|
||||
self.refresh_rotation_matrix()
|
||||
def get_field_of_view(self) -> float:
|
||||
return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2)
|
||||
|
||||
def get_implied_camera_location(self) -> np.ndarray:
|
||||
to_camera = self.get_inverse_camera_rotation_matrix()[2]
|
||||
dist = self.get_focal_distance()
|
||||
return self.get_center() + dist * to_camera
|
||||
|
||||
|
||||
class Camera(object):
|
||||
@@ -162,10 +177,10 @@ class Camera(object):
|
||||
"samples": 0,
|
||||
}
|
||||
|
||||
def __init__(self, ctx=None, **kwargs):
|
||||
def __init__(self, ctx: moderngl.Context | None = None, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
|
||||
self.background_rgba = [
|
||||
self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max
|
||||
self.background_rgba: list[float] = [
|
||||
*Color(self.background_color).get_rgb(),
|
||||
self.background_opacity
|
||||
]
|
||||
@@ -177,35 +192,49 @@ class Camera(object):
|
||||
self.refresh_perspective_uniforms()
|
||||
self.static_mobject_to_render_group_list = {}
|
||||
|
||||
def init_frame(self):
|
||||
def init_frame(self) -> None:
|
||||
self.frame = CameraFrame(**self.frame_config)
|
||||
|
||||
def init_context(self, ctx=None):
|
||||
def init_context(self, ctx: moderngl.Context | None = None) -> None:
|
||||
if ctx is None:
|
||||
ctx = moderngl.create_standalone_context()
|
||||
fbo = self.get_fbo(ctx, 0)
|
||||
else:
|
||||
fbo = ctx.detect_framebuffer()
|
||||
self.ctx = ctx
|
||||
self.fbo = fbo
|
||||
self.set_ctx_blending()
|
||||
|
||||
# For multisample antialiasing
|
||||
fbo_msaa = self.get_fbo(ctx, self.samples)
|
||||
fbo_msaa.use()
|
||||
|
||||
ctx.enable(moderngl.BLEND)
|
||||
ctx.blend_func = (
|
||||
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
|
||||
moderngl.ONE, moderngl.ONE
|
||||
)
|
||||
|
||||
self.ctx = ctx
|
||||
self.fbo = fbo
|
||||
self.fbo_msaa = fbo_msaa
|
||||
|
||||
def init_light_source(self):
|
||||
def set_ctx_blending(self, enable: bool = True) -> None:
|
||||
if enable:
|
||||
self.ctx.enable(moderngl.BLEND)
|
||||
else:
|
||||
self.ctx.disable(moderngl.BLEND)
|
||||
self.ctx.blend_func = (
|
||||
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
|
||||
# moderngl.ONE, moderngl.ONE
|
||||
)
|
||||
|
||||
def set_ctx_depth_test(self, enable: bool = True) -> None:
|
||||
if enable:
|
||||
self.ctx.enable(moderngl.DEPTH_TEST)
|
||||
else:
|
||||
self.ctx.disable(moderngl.DEPTH_TEST)
|
||||
|
||||
def init_light_source(self) -> None:
|
||||
self.light_source = Point(self.light_source_position)
|
||||
|
||||
# Methods associated with the frame buffer
|
||||
def get_fbo(self, ctx, samples=0):
|
||||
def get_fbo(
|
||||
self,
|
||||
ctx: moderngl.Context,
|
||||
samples: int = 0
|
||||
) -> moderngl.Framebuffer:
|
||||
pw = self.pixel_width
|
||||
ph = self.pixel_height
|
||||
return ctx.framebuffer(
|
||||
@@ -220,16 +249,16 @@ class Camera(object):
|
||||
)
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
def clear(self) -> None:
|
||||
self.fbo.clear(*self.background_rgba)
|
||||
self.fbo_msaa.clear(*self.background_rgba)
|
||||
|
||||
def reset_pixel_shape(self, new_width, new_height):
|
||||
def reset_pixel_shape(self, new_width: int, new_height: int) -> None:
|
||||
self.pixel_width = new_width
|
||||
self.pixel_height = new_height
|
||||
self.refresh_perspective_uniforms()
|
||||
|
||||
def get_raw_fbo_data(self, dtype='f1'):
|
||||
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
|
||||
# Copy blocks from the fbo_msaa to the drawn fbo using Blit
|
||||
pw, ph = (self.pixel_width, self.pixel_height)
|
||||
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo_msaa.glo)
|
||||
@@ -241,7 +270,7 @@ class Camera(object):
|
||||
dtype=dtype,
|
||||
)
|
||||
|
||||
def get_image(self, pixel_array=None):
|
||||
def get_image(self) -> Image.Image:
|
||||
return Image.frombytes(
|
||||
'RGBA',
|
||||
self.get_pixel_shape(),
|
||||
@@ -249,7 +278,7 @@ class Camera(object):
|
||||
'raw', 'RGBA', 0, -1
|
||||
)
|
||||
|
||||
def get_pixel_array(self):
|
||||
def get_pixel_array(self) -> np.ndarray:
|
||||
raw = self.get_raw_fbo_data(dtype='f4')
|
||||
flat_arr = np.frombuffer(raw, dtype='f4')
|
||||
arr = flat_arr.reshape([*self.fbo.size, self.n_channels])
|
||||
@@ -257,7 +286,7 @@ class Camera(object):
|
||||
return (self.rgb_max_val * arr).astype(self.pixel_array_dtype)
|
||||
|
||||
# Needed?
|
||||
def get_texture(self):
|
||||
def get_texture(self) -> moderngl.Texture:
|
||||
texture = self.ctx.texture(
|
||||
size=self.fbo.size,
|
||||
components=4,
|
||||
@@ -267,29 +296,32 @@ class Camera(object):
|
||||
return texture
|
||||
|
||||
# Getting camera attributes
|
||||
def get_pixel_shape(self):
|
||||
def get_pixel_shape(self) -> tuple[int, int]:
|
||||
return self.fbo.viewport[2:4]
|
||||
# return (self.pixel_width, self.pixel_height)
|
||||
|
||||
def get_pixel_width(self):
|
||||
def get_pixel_width(self) -> int:
|
||||
return self.get_pixel_shape()[0]
|
||||
|
||||
def get_pixel_height(self):
|
||||
def get_pixel_height(self) -> int:
|
||||
return self.get_pixel_shape()[1]
|
||||
|
||||
def get_frame_height(self):
|
||||
def get_frame_height(self) -> float:
|
||||
return self.frame.get_height()
|
||||
|
||||
def get_frame_width(self):
|
||||
def get_frame_width(self) -> float:
|
||||
return self.frame.get_width()
|
||||
|
||||
def get_frame_shape(self):
|
||||
def get_frame_shape(self) -> tuple[float, float]:
|
||||
return (self.get_frame_width(), self.get_frame_height())
|
||||
|
||||
def get_frame_center(self):
|
||||
def get_frame_center(self) -> np.ndarray:
|
||||
return self.frame.get_center()
|
||||
|
||||
def resize_frame_shape(self, fixed_dimension=0):
|
||||
def get_location(self) -> tuple[float, float, float]:
|
||||
return self.frame.get_implied_camera_location()
|
||||
|
||||
def resize_frame_shape(self, fixed_dimension: bool = False) -> None:
|
||||
"""
|
||||
Changes frame_shape to match the aspect ratio
|
||||
of the pixels, where fixed_dimension determines
|
||||
@@ -301,53 +333,40 @@ class Camera(object):
|
||||
frame_height = self.get_frame_height()
|
||||
frame_width = self.get_frame_width()
|
||||
aspect_ratio = fdiv(pixel_width, pixel_height)
|
||||
if fixed_dimension == 0:
|
||||
if not fixed_dimension:
|
||||
frame_height = frame_width / aspect_ratio
|
||||
else:
|
||||
frame_width = aspect_ratio * frame_height
|
||||
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):
|
||||
def capture(self, *mobjects: Mobject, **kwargs) -> None:
|
||||
self.refresh_perspective_uniforms()
|
||||
for mobject in mobjects:
|
||||
for render_group in self.get_render_group_list(mobject):
|
||||
self.render(render_group)
|
||||
|
||||
def render(self, render_group):
|
||||
def render(self, render_group: dict[str]) -> None:
|
||||
shader_wrapper = render_group["shader_wrapper"]
|
||||
shader_program = render_group["prog"]
|
||||
self.set_shader_uniforms(shader_program, shader_wrapper)
|
||||
self.update_depth_test(shader_wrapper)
|
||||
self.set_ctx_depth_test(shader_wrapper.depth_test)
|
||||
render_group["vao"].render(int(shader_wrapper.render_primitive))
|
||||
if render_group["single_use"]:
|
||||
self.release_render_group(render_group)
|
||||
|
||||
def update_depth_test(self, shader_wrapper):
|
||||
if shader_wrapper.depth_test:
|
||||
self.ctx.enable(moderngl.DEPTH_TEST)
|
||||
else:
|
||||
self.ctx.disable(moderngl.DEPTH_TEST)
|
||||
|
||||
def get_render_group_list(self, mobject):
|
||||
def get_render_group_list(self, mobject: Mobject) -> list[dict[str]] | map[dict[str]]:
|
||||
try:
|
||||
return self.static_mobject_to_render_group_list[id(mobject)]
|
||||
except KeyError:
|
||||
return map(self.get_render_group, mobject.get_shader_wrapper_list())
|
||||
|
||||
def get_render_group(self, shader_wrapper, single_use=True):
|
||||
def get_render_group(
|
||||
self,
|
||||
shader_wrapper: ShaderWrapper,
|
||||
single_use: bool = True
|
||||
) -> dict[str]:
|
||||
# Data buffers
|
||||
vbo = self.ctx.buffer(shader_wrapper.vert_data.tobytes())
|
||||
if shader_wrapper.vert_indices is None:
|
||||
@@ -375,12 +394,12 @@ class Camera(object):
|
||||
"single_use": single_use,
|
||||
}
|
||||
|
||||
def release_render_group(self, render_group):
|
||||
def release_render_group(self, render_group: dict[str]) -> None:
|
||||
for key in ["vbo", "ibo", "vao"]:
|
||||
if render_group[key] is not None:
|
||||
render_group[key].release()
|
||||
|
||||
def set_mobjects_as_static(self, *mobjects):
|
||||
def set_mobjects_as_static(self, *mobjects: Mobject) -> None:
|
||||
# Creates buffer and array objects holding each mobjects shader data
|
||||
for mob in mobjects:
|
||||
self.static_mobject_to_render_group_list[id(mob)] = [
|
||||
@@ -388,18 +407,23 @@ class Camera(object):
|
||||
for sw in mob.get_shader_wrapper_list()
|
||||
]
|
||||
|
||||
def release_static_mobjects(self):
|
||||
def release_static_mobjects(self) -> None:
|
||||
for rg_list in self.static_mobject_to_render_group_list.values():
|
||||
for render_group in rg_list:
|
||||
self.release_render_group(render_group)
|
||||
self.static_mobject_to_render_group_list = {}
|
||||
|
||||
# Shaders
|
||||
def init_shaders(self):
|
||||
def init_shaders(self) -> None:
|
||||
# Initialize with the null id going to None
|
||||
self.id_to_shader_program = {"": None}
|
||||
self.id_to_shader_program: dict[
|
||||
int | str, tuple[moderngl.Program, str] | None
|
||||
] = {"": None}
|
||||
|
||||
def get_shader_program(self, shader_wrapper):
|
||||
def get_shader_program(
|
||||
self,
|
||||
shader_wrapper: ShaderWrapper
|
||||
) -> tuple[moderngl.Program, str]:
|
||||
sid = shader_wrapper.get_program_id()
|
||||
if sid not in self.id_to_shader_program:
|
||||
# Create shader program for the first time, then cache
|
||||
@@ -409,17 +433,23 @@ class Camera(object):
|
||||
self.id_to_shader_program[sid] = (program, vert_format)
|
||||
return self.id_to_shader_program[sid]
|
||||
|
||||
def set_shader_uniforms(self, shader, shader_wrapper):
|
||||
def set_shader_uniforms(
|
||||
self,
|
||||
shader: moderngl.Program,
|
||||
shader_wrapper: ShaderWrapper
|
||||
) -> None:
|
||||
for name, path in shader_wrapper.texture_paths.items():
|
||||
tid = self.get_texture_id(path)
|
||||
shader[name].value = tid
|
||||
for name, value in it.chain(shader_wrapper.uniforms.items(), self.perspective_uniforms.items()):
|
||||
for name, value in it.chain(self.perspective_uniforms.items(), shader_wrapper.uniforms.items()):
|
||||
try:
|
||||
if isinstance(value, np.ndarray) and value.ndim > 0:
|
||||
value = tuple(value)
|
||||
shader[name].value = value
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def refresh_perspective_uniforms(self):
|
||||
def refresh_perspective_uniforms(self) -> None:
|
||||
frame = self.frame
|
||||
pw, ph = self.get_pixel_shape()
|
||||
fw, fh = frame.get_shape()
|
||||
@@ -428,38 +458,54 @@ class Camera(object):
|
||||
anti_alias_width = self.anti_alias_width / (ph / fh)
|
||||
# Orient light
|
||||
rotation = frame.get_inverse_camera_rotation_matrix()
|
||||
light_pos = self.light_source.get_location()
|
||||
light_pos = np.dot(rotation, light_pos)
|
||||
offset = frame.get_center()
|
||||
light_pos = np.dot(
|
||||
rotation, self.light_source.get_location() + offset
|
||||
)
|
||||
cam_pos = self.frame.get_implied_camera_location() # TODO
|
||||
|
||||
self.perspective_uniforms = {
|
||||
"frame_shape": frame.get_shape(),
|
||||
"anti_alias_width": anti_alias_width,
|
||||
"camera_center": tuple(frame.get_center()),
|
||||
"camera_offset": tuple(offset),
|
||||
"camera_rotation": tuple(np.array(rotation).T.flatten()),
|
||||
"camera_position": tuple(cam_pos),
|
||||
"light_source_position": tuple(light_pos),
|
||||
"focal_distance": frame.get_focal_distance(),
|
||||
}
|
||||
|
||||
def init_textures(self):
|
||||
self.path_to_texture_id = {}
|
||||
def init_textures(self) -> None:
|
||||
self.n_textures: int = 0
|
||||
self.path_to_texture: dict[
|
||||
str, tuple[int, moderngl.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)
|
||||
def get_texture_id(self, path: str) -> int:
|
||||
if path not in self.path_to_texture:
|
||||
if self.n_textures == 15: # I have no clue why this is needed
|
||||
self.n_textures += 1
|
||||
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: str):
|
||||
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
|
||||
class ThreeDCamera(Camera):
|
||||
CONFIG = {
|
||||
"samples": 4,
|
||||
"anti_alias_width": 0,
|
||||
}
|
||||
|
||||
@@ -5,10 +5,15 @@ import importlib
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from contextlib import contextmanager
|
||||
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
|
||||
|
||||
|
||||
__config_file__ = "custom_config.yml"
|
||||
|
||||
|
||||
def parse_cli():
|
||||
@@ -18,7 +23,7 @@ def parse_cli():
|
||||
module_location.add_argument(
|
||||
"file",
|
||||
nargs="?",
|
||||
help="path to file holding the python code for the scene",
|
||||
help="Path to file holding the python code for the scene",
|
||||
)
|
||||
parser.add_argument(
|
||||
"scene_names",
|
||||
@@ -60,6 +65,12 @@ def parse_cli():
|
||||
action="store_true",
|
||||
help="Show window in full screen",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--presenter_mode",
|
||||
action="store_true",
|
||||
help="Scene will stay paused during wait calls until "
|
||||
"space bar or right arrow is hit, like a slide show"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-g", "--save_pngs",
|
||||
action="store_true",
|
||||
@@ -111,6 +122,12 @@ def parse_cli():
|
||||
"in two comma separated values, e.g. \"3,6\", it will end"
|
||||
"the rendering at the second value",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e", "--embed", metavar="LINENO",
|
||||
help="Takes a line number as an argument, and results"
|
||||
"in the scene being called as if the line `self.embed()`"
|
||||
"was inserted into the scene code at that line number."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--resolution",
|
||||
help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"",
|
||||
@@ -130,12 +147,25 @@ 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"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
help="Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
except argparse.ArgumentError as err:
|
||||
print(str(err))
|
||||
log.error(str(err))
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
@@ -148,24 +178,42 @@ def get_manim_dir():
|
||||
def get_module(file_name):
|
||||
if file_name is None:
|
||||
return None
|
||||
else:
|
||||
module_name = file_name.replace(os.sep, ".").replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
module_name = file_name.replace(os.sep, ".").replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@contextmanager
|
||||
def insert_embed_line(file_name, lineno):
|
||||
with open(file_name, 'r') as fp:
|
||||
lines = fp.readlines()
|
||||
line = lines[lineno - 1]
|
||||
n_spaces = len(line) - len(line.lstrip())
|
||||
lines.insert(lineno, " " * n_spaces + "self.embed()\n")
|
||||
|
||||
alt_file = file_name.replace(".py", "_inserted_embed.py")
|
||||
with open(alt_file, 'w') as fp:
|
||||
fp.writelines(lines)
|
||||
|
||||
try:
|
||||
yield alt_file
|
||||
finally:
|
||||
os.remove(alt_file)
|
||||
|
||||
|
||||
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 +221,55 @@ 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:
|
||||
@@ -216,16 +296,23 @@ def get_configuration(args):
|
||||
"quiet": args.quiet,
|
||||
}
|
||||
|
||||
module = get_module(args.file)
|
||||
if args.embed is None:
|
||||
module = get_module(args.file)
|
||||
else:
|
||||
with insert_embed_line(args.file, int(args.embed)) as alt_file:
|
||||
module = get_module(alt_file)
|
||||
|
||||
config = {
|
||||
"module": module,
|
||||
"scene_names": args.scene_names,
|
||||
"file_writer_config": file_writer_config,
|
||||
"quiet": args.quiet or args.write_all,
|
||||
"write_all": args.write_all,
|
||||
"skip_animations": args.skip_animations,
|
||||
"start_at_animation_number": args.start_at_animation_number,
|
||||
"preview": not write_file,
|
||||
"end_at_animation_number": None,
|
||||
"preview": not write_file,
|
||||
"presenter_mode": args.presenter_mode,
|
||||
"leave_progress_bars": args.leave_progress_bars,
|
||||
}
|
||||
|
||||
@@ -234,9 +321,11 @@ 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:
|
||||
if not (args.full_screen or custom_config["full_screen"]):
|
||||
window_width //= 2
|
||||
window_height = window_width * 9 // 16
|
||||
config["window_config"] = {
|
||||
@@ -253,10 +342,6 @@ def get_configuration(args):
|
||||
else:
|
||||
config["start_at_animation_number"] = int(stan)
|
||||
|
||||
config["skip_animations"] = any([
|
||||
args.skip_animations,
|
||||
args.start_at_animation_number,
|
||||
])
|
||||
return config
|
||||
|
||||
|
||||
@@ -292,9 +377,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"
|
||||
|
||||
@@ -61,8 +64,6 @@ JOINT_TYPE_MAP = {
|
||||
}
|
||||
|
||||
# Related to Text
|
||||
START_X = 30
|
||||
START_Y = 20
|
||||
NORMAL = "NORMAL"
|
||||
ITALIC = "ITALIC"
|
||||
OBLIQUE = "OBLIQUE"
|
||||
@@ -136,3 +137,5 @@ RED = RED_C
|
||||
MAROON = MAROON_C
|
||||
PURPLE = PURPLE_C
|
||||
GREY = GREY_C
|
||||
|
||||
COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED]
|
||||
|
||||
@@ -34,6 +34,7 @@ style:
|
||||
# the window on the monitor, e.g. "960,540"
|
||||
window_position: UR
|
||||
window_monitor: 0
|
||||
full_screen: False
|
||||
# If break_into_partial_movies is set to True, then many small
|
||||
# files will be written corresponding to each Scene.play and
|
||||
# Scene.wait call, and these files will then be combined
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
@@ -6,21 +8,23 @@ from manimlib.event_handler.event_listner import EventListner
|
||||
|
||||
class EventDispatcher(object):
|
||||
def __init__(self):
|
||||
self.event_listners = {
|
||||
self.event_listners: dict[
|
||||
EventType, list[EventListner]
|
||||
] = {
|
||||
event_type: []
|
||||
for event_type in EventType
|
||||
}
|
||||
self.mouse_point = np.array((0., 0., 0.))
|
||||
self.mouse_drag_point = np.array((0., 0., 0.))
|
||||
self.pressed_keys = set()
|
||||
self.draggable_object_listners = []
|
||||
self.pressed_keys: set[int] = set()
|
||||
self.draggable_object_listners: list[EventListner] = []
|
||||
|
||||
def add_listner(self, event_listner):
|
||||
def add_listner(self, event_listner: EventListner):
|
||||
assert(isinstance(event_listner, EventListner))
|
||||
self.event_listners[event_listner.event_type].append(event_listner)
|
||||
return self
|
||||
|
||||
def remove_listner(self, event_listner):
|
||||
def remove_listner(self, event_listner: EventListner):
|
||||
assert(isinstance(event_listner, EventListner))
|
||||
try:
|
||||
while event_listner in self.event_listners[event_listner.event_type]:
|
||||
@@ -30,8 +34,7 @@ class EventDispatcher(object):
|
||||
pass
|
||||
return self
|
||||
|
||||
def dispatch(self, event_type, **event_data):
|
||||
|
||||
def dispatch(self, event_type: EventType, **event_data):
|
||||
if event_type == EventType.MouseMotionEvent:
|
||||
self.mouse_point = event_data["point"]
|
||||
elif event_type == EventType.MouseDragEvent:
|
||||
@@ -74,16 +77,16 @@ class EventDispatcher(object):
|
||||
|
||||
return propagate_event
|
||||
|
||||
def get_listners_count(self):
|
||||
def get_listners_count(self) -> int:
|
||||
return sum([len(value) for key, value in self.event_listners.items()])
|
||||
|
||||
def get_mouse_point(self):
|
||||
def get_mouse_point(self) -> np.ndarray:
|
||||
return self.mouse_point
|
||||
|
||||
def get_mouse_drag_point(self):
|
||||
def get_mouse_drag_point(self) -> np.ndarray:
|
||||
return self.mouse_drag_point
|
||||
|
||||
def is_key_pressed(self, symbol):
|
||||
def is_key_pressed(self, symbol: int) -> bool:
|
||||
return (symbol in self.pressed_keys)
|
||||
|
||||
__iadd__ = add_listner
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
|
||||
class EventListner(object):
|
||||
def __init__(self, mobject, event_type, event_callback):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
event_type: EventType,
|
||||
event_callback: Callable[[Mobject, dict[str]]]
|
||||
):
|
||||
self.mobject = mobject
|
||||
self.event_type = event_type
|
||||
self.callback = event_callback
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import inspect
|
||||
import sys
|
||||
import logging
|
||||
import copy
|
||||
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
class BlankScene(Scene):
|
||||
@@ -38,11 +39,14 @@ def prompt_user_for_choice(scene_classes):
|
||||
"\nScene Name or Number: "
|
||||
)
|
||||
return [
|
||||
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str)-1]
|
||||
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)
|
||||
@@ -60,10 +64,31 @@ def get_scene_config(config):
|
||||
"end_at_animation_number",
|
||||
"leave_progress_bars",
|
||||
"preview",
|
||||
"presenter_mode",
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
def compute_total_frames(scene_class, scene_config):
|
||||
"""
|
||||
When a scene is being written to file, a copy of the scene is run with
|
||||
skip_animations set to true so as to count how many frames it will require.
|
||||
This allows for a total progress bar on rendering, and also allows runtime
|
||||
errors to be exposed preemptively for long running scenes. The final frame
|
||||
is saved by default, so that one can more quickly check that the last frame
|
||||
looks as expected.
|
||||
"""
|
||||
pre_config = copy.deepcopy(scene_config)
|
||||
pre_config["file_writer_config"]["write_to_movie"] = False
|
||||
pre_config["file_writer_config"]["save_last_frame"] = True
|
||||
pre_config["file_writer_config"]["quiet"] = True
|
||||
pre_config["skip_animations"] = True
|
||||
pre_scene = scene_class(**pre_config)
|
||||
pre_scene.run()
|
||||
total_time = pre_scene.time - pre_scene.skip_time
|
||||
return int(total_time * scene_config["camera_config"]["frame_rate"])
|
||||
|
||||
|
||||
def get_scenes_to_render(scene_classes, scene_config, config):
|
||||
if config["write_all"]:
|
||||
return [sc(**scene_config) for sc in scene_classes]
|
||||
@@ -73,15 +98,15 @@ def get_scenes_to_render(scene_classes, scene_config, config):
|
||||
found = False
|
||||
for scene_class in scene_classes:
|
||||
if scene_class.__name__ == scene_name:
|
||||
fw_config = scene_config["file_writer_config"]
|
||||
if fw_config["write_to_movie"]:
|
||||
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
|
||||
scene = scene_class(**scene_config)
|
||||
result.append(scene)
|
||||
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:
|
||||
|
||||
13
manimlib/logger.py
Normal file
13
manimlib/logger.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import logging
|
||||
from rich.logging import RichHandler
|
||||
|
||||
__all__ = ["log"]
|
||||
|
||||
|
||||
FORMAT = "%(message)s"
|
||||
logging.basicConfig(
|
||||
level=logging.WARNING, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||
)
|
||||
|
||||
log = logging.getLogger("manimgl")
|
||||
log.setLevel("DEBUG")
|
||||
121
manimlib/mobject/boolean_ops.py
Normal file
121
manimlib/mobject/boolean_ops.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pathops
|
||||
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
# Boolean operations between 2D mobjects
|
||||
# Borrowed from from https://github.com/ManimCommunity/manim/
|
||||
|
||||
def _convert_vmobject_to_skia_path(vmobject: VMobject) -> pathops.Path:
|
||||
path = pathops.Path()
|
||||
subpaths = vmobject.get_subpaths_from_points(vmobject.get_all_points())
|
||||
for subpath in subpaths:
|
||||
quads = vmobject.get_bezier_tuples_from_points(subpath)
|
||||
start = subpath[0]
|
||||
path.moveTo(*start[:2])
|
||||
for p0, p1, p2 in quads:
|
||||
path.quadTo(*p1[:2], *p2[:2])
|
||||
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
return path
|
||||
|
||||
|
||||
def _convert_skia_path_to_vmobject(
|
||||
path: pathops.Path,
|
||||
vmobject: VMobject
|
||||
) -> VMobject:
|
||||
PathVerb = pathops.PathVerb
|
||||
current_path_start = np.array([0.0, 0.0, 0.0])
|
||||
for path_verb, points in path:
|
||||
if path_verb == PathVerb.CLOSE:
|
||||
vmobject.add_line_to(current_path_start)
|
||||
else:
|
||||
points = np.hstack((np.array(points), np.zeros((len(points), 1))))
|
||||
if path_verb == PathVerb.MOVE:
|
||||
for point in points:
|
||||
current_path_start = point
|
||||
vmobject.start_new_path(point)
|
||||
elif path_verb == PathVerb.CUBIC:
|
||||
vmobject.add_cubic_bezier_curve_to(*points)
|
||||
elif path_verb == PathVerb.LINE:
|
||||
vmobject.add_line_to(points[0])
|
||||
elif path_verb == PathVerb.QUAD:
|
||||
vmobject.add_quadratic_bezier_curve_to(*points)
|
||||
else:
|
||||
raise Exception(f"Unsupported: {path_verb}")
|
||||
return vmobject.reverse_points()
|
||||
|
||||
|
||||
class Union(VMobject):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Union.")
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
paths = [
|
||||
_convert_vmobject_to_skia_path(vmobject)
|
||||
for vmobject in vmobjects
|
||||
]
|
||||
pathops.union(paths, outpen.getPen())
|
||||
_convert_skia_path_to_vmobject(outpen, self)
|
||||
|
||||
|
||||
class Difference(VMobject):
|
||||
def __init__(self, subject: VMobject, clip: VMobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
pathops.difference(
|
||||
[_convert_vmobject_to_skia_path(subject)],
|
||||
[_convert_vmobject_to_skia_path(clip)],
|
||||
outpen.getPen(),
|
||||
)
|
||||
_convert_skia_path_to_vmobject(outpen, self)
|
||||
|
||||
|
||||
class Intersection(VMobject):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Intersection.")
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
pathops.intersection(
|
||||
[_convert_vmobject_to_skia_path(vmobjects[0])],
|
||||
[_convert_vmobject_to_skia_path(vmobjects[1])],
|
||||
outpen.getPen(),
|
||||
)
|
||||
new_outpen = outpen
|
||||
for _i in range(2, len(vmobjects)):
|
||||
new_outpen = pathops.Path()
|
||||
pathops.intersection(
|
||||
[outpen],
|
||||
[_convert_vmobject_to_skia_path(vmobjects[_i])],
|
||||
new_outpen.getPen(),
|
||||
)
|
||||
outpen = new_outpen
|
||||
_convert_skia_path_to_vmobject(outpen, self)
|
||||
|
||||
|
||||
class Exclusion(VMobject):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Exclusion.")
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
pathops.xor(
|
||||
[_convert_vmobject_to_skia_path(vmobjects[0])],
|
||||
[_convert_vmobject_to_skia_path(vmobjects[1])],
|
||||
outpen.getPen(),
|
||||
)
|
||||
new_outpen = outpen
|
||||
for _i in range(2, len(vmobjects)):
|
||||
new_outpen = pathops.Path()
|
||||
pathops.xor(
|
||||
[outpen],
|
||||
[_convert_vmobject_to_skia_path(vmobjects[_i])],
|
||||
new_outpen.getPen(),
|
||||
)
|
||||
outpen = new_outpen
|
||||
_convert_skia_path_to_vmobject(outpen, self)
|
||||
@@ -1,8 +1,18 @@
|
||||
from manimlib.constants import *
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
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):
|
||||
@@ -15,10 +25,10 @@ class AnimatedBoundary(VGroup):
|
||||
"fade_rate_func": smooth,
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.vmobject = vmobject
|
||||
self.boundary_copies = [
|
||||
self.vmobject: VMobject = vmobject
|
||||
self.boundary_copies: list[VMobject] = [
|
||||
vmobject.copy().set_style(
|
||||
stroke_width=0,
|
||||
fill_opacity=0
|
||||
@@ -26,12 +36,12 @@ class AnimatedBoundary(VGroup):
|
||||
for x in range(2)
|
||||
]
|
||||
self.add(*self.boundary_copies)
|
||||
self.total_time = 0
|
||||
self.total_time: float = 0
|
||||
self.add_updater(
|
||||
lambda m, dt: self.update_boundary_copies(dt)
|
||||
)
|
||||
|
||||
def update_boundary_copies(self, dt):
|
||||
def update_boundary_copies(self, dt: float) -> None:
|
||||
# Not actual time, but something which passes at
|
||||
# an altered rate to make the implementation below
|
||||
# cleaner
|
||||
@@ -62,7 +72,13 @@ class AnimatedBoundary(VGroup):
|
||||
|
||||
self.total_time += dt
|
||||
|
||||
def full_family_become_partial(self, mob1, mob2, a, b):
|
||||
def full_family_become_partial(
|
||||
self,
|
||||
mob1: VMobject,
|
||||
mob2: VMobject,
|
||||
a: float,
|
||||
b: float
|
||||
):
|
||||
family1 = mob1.family_members_with_points()
|
||||
family2 = mob2.family_members_with_points()
|
||||
for sm1, sm2 in zip(family1, family2):
|
||||
@@ -74,25 +90,67 @@ 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):
|
||||
def __init__(self, traced_point_func: Callable[[], np.ndarray], **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.add_updater(lambda m: m.update_path())
|
||||
self.time: float = 0
|
||||
self.traced_points: list[np.ndarray] = []
|
||||
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: float):
|
||||
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: Mobject | Callable[[], np.ndarray],
|
||||
**kwargs
|
||||
):
|
||||
if isinstance(mobject_or_func, Mobject):
|
||||
func = mobject_or_func.get_center
|
||||
else:
|
||||
func = mobject_or_func
|
||||
super().__init__(func, **kwargs)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import numbers
|
||||
from abc import abstractmethod
|
||||
from typing import Type, TypeVar, Union, Callable, Iterable, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.functions import ParametricCurve
|
||||
@@ -10,12 +15,22 @@ 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
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import colour
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
T = TypeVar("T", bound=Mobject)
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
EPSILON = 1e-8
|
||||
|
||||
|
||||
@@ -25,55 +40,89 @@ 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 coords_to_point(self, *coords):
|
||||
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)
|
||||
|
||||
@abstractmethod
|
||||
def coords_to_point(self, *coords: float) -> np.ndarray:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def point_to_coords(self, point):
|
||||
@abstractmethod
|
||||
def point_to_coords(self, point: np.ndarray) -> tuple[float, ...]:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def c2p(self, *coords):
|
||||
def c2p(self, *coords: float):
|
||||
"""Abbreviation for coords_to_point"""
|
||||
return self.coords_to_point(*coords)
|
||||
|
||||
def p2c(self, point):
|
||||
def p2c(self, point: np.ndarray):
|
||||
"""Abbreviation for point_to_coords"""
|
||||
return self.point_to_coords(point)
|
||||
|
||||
def get_axes(self):
|
||||
def get_origin(self) -> np.ndarray:
|
||||
return self.c2p(*[0] * self.dimension)
|
||||
|
||||
@abstractmethod
|
||||
def get_axes(self) -> VGroup:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def get_axis(self, index):
|
||||
@abstractmethod
|
||||
def get_all_ranges(self) -> list[np.ndarray]:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def get_axis(self, index: int) -> NumberLine:
|
||||
return self.get_axes()[index]
|
||||
|
||||
def get_x_axis(self):
|
||||
def get_x_axis(self) -> NumberLine:
|
||||
return self.get_axis(0)
|
||||
|
||||
def get_y_axis(self):
|
||||
def get_y_axis(self) -> NumberLine:
|
||||
return self.get_axis(1)
|
||||
|
||||
def get_z_axis(self):
|
||||
def get_z_axis(self) -> NumberLine:
|
||||
return self.get_axis(2)
|
||||
|
||||
def get_x_axis_label(self, label_tex, edge=RIGHT, direction=DL, **kwargs):
|
||||
def get_x_axis_label(
|
||||
self,
|
||||
label_tex: str,
|
||||
edge: np.ndarray = RIGHT,
|
||||
direction: np.ndarray = DL,
|
||||
**kwargs
|
||||
) -> Tex:
|
||||
return self.get_axis_label(
|
||||
label_tex, self.get_x_axis(),
|
||||
edge, direction, **kwargs
|
||||
)
|
||||
|
||||
def get_y_axis_label(self, label_tex, edge=UP, direction=DR, **kwargs):
|
||||
def get_y_axis_label(
|
||||
self,
|
||||
label_tex: str,
|
||||
edge: np.ndarray = UP,
|
||||
direction: np.ndarray = DR,
|
||||
**kwargs
|
||||
) -> Tex:
|
||||
return self.get_axis_label(
|
||||
label_tex, self.get_y_axis(),
|
||||
edge, direction, **kwargs
|
||||
)
|
||||
|
||||
def get_axis_label(self, label_tex, axis, edge, direction, buff=MED_SMALL_BUFF):
|
||||
def get_axis_label(
|
||||
self,
|
||||
label_tex: str,
|
||||
axis: np.ndarray,
|
||||
edge: np.ndarray,
|
||||
direction: np.ndarray,
|
||||
buff: float = MED_SMALL_BUFF
|
||||
) -> Tex:
|
||||
label = Tex(label_tex)
|
||||
label.next_to(
|
||||
axis.get_edge_center(edge), direction,
|
||||
@@ -82,30 +131,43 @@ class CoordinateSystem():
|
||||
label.shift_onto_screen(buff=MED_SMALL_BUFF)
|
||||
return label
|
||||
|
||||
def get_axis_labels(self, x_label_tex="x", y_label_tex="y"):
|
||||
def get_axis_labels(
|
||||
self,
|
||||
x_label_tex: str = "x",
|
||||
y_label_tex: str = "y"
|
||||
) -> VGroup:
|
||||
self.axis_labels = VGroup(
|
||||
self.get_x_axis_label(x_label_tex),
|
||||
self.get_y_axis_label(y_label_tex),
|
||||
)
|
||||
return self.axis_labels
|
||||
|
||||
def get_line_from_axis_to_point(self, index, point,
|
||||
line_func=DashedLine,
|
||||
color=GREY_A,
|
||||
stroke_width=2):
|
||||
def get_line_from_axis_to_point(
|
||||
self,
|
||||
index: int,
|
||||
point: np.ndarray,
|
||||
line_func: Type[T] = DashedLine,
|
||||
color: ManimColor = GREY_A,
|
||||
stroke_width: float = 2
|
||||
) -> T:
|
||||
axis = self.get_axis(index)
|
||||
line = line_func(axis.get_projection(point), point)
|
||||
line.set_stroke(color, stroke_width)
|
||||
return line
|
||||
|
||||
def get_v_line(self, point, **kwargs):
|
||||
def get_v_line(self, point: np.ndarray, **kwargs):
|
||||
return self.get_line_from_axis_to_point(0, point, **kwargs)
|
||||
|
||||
def get_h_line(self, point, **kwargs):
|
||||
def get_h_line(self, point: np.ndarray, **kwargs):
|
||||
return self.get_line_from_axis_to_point(1, point, **kwargs)
|
||||
|
||||
# Useful for graphing
|
||||
def get_graph(self, function, x_range=None, **kwargs):
|
||||
def get_graph(
|
||||
self,
|
||||
function: Callable[[float], float],
|
||||
x_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
) -> ParametricCurve:
|
||||
t_range = np.array(self.x_range, dtype=float)
|
||||
if x_range is not None:
|
||||
t_range[:len(x_range)] = x_range
|
||||
@@ -121,9 +183,14 @@ class CoordinateSystem():
|
||||
**kwargs
|
||||
)
|
||||
graph.underlying_function = function
|
||||
graph.x_range = x_range
|
||||
return graph
|
||||
|
||||
def get_parametric_curve(self, function, **kwargs):
|
||||
def get_parametric_curve(
|
||||
self,
|
||||
function: Callable[[float], np.ndarray],
|
||||
**kwargs
|
||||
) -> ParametricCurve:
|
||||
dim = self.dimension
|
||||
graph = ParametricCurve(
|
||||
lambda t: self.coords_to_point(*function(t)[:dim]),
|
||||
@@ -132,36 +199,42 @@ class CoordinateSystem():
|
||||
graph.underlying_function = function
|
||||
return graph
|
||||
|
||||
def input_to_graph_point(self, x, graph):
|
||||
def input_to_graph_point(
|
||||
self,
|
||||
x: float,
|
||||
graph: ParametricCurve
|
||||
) -> np.ndarray | None:
|
||||
if hasattr(graph, "underlying_function"):
|
||||
return self.coords_to_point(x, graph.underlying_function(x))
|
||||
else:
|
||||
alpha = binary_search(
|
||||
function=lambda a: self.point_to_coords(
|
||||
graph.point_from_proportion(a)
|
||||
graph.quick_point_from_proportion(a)
|
||||
)[0],
|
||||
target=x,
|
||||
lower_bound=self.x_range[0],
|
||||
upper_bound=self.x_range[1],
|
||||
)
|
||||
if alpha is not None:
|
||||
return graph.point_from_proportion(alpha)
|
||||
return graph.quick_point_from_proportion(alpha)
|
||||
else:
|
||||
return None
|
||||
|
||||
def i2gp(self, x, graph):
|
||||
def i2gp(self, x: float, graph: ParametricCurve) -> np.ndarray | None:
|
||||
"""
|
||||
Alias for input_to_graph_point
|
||||
"""
|
||||
return self.input_to_graph_point(x, graph)
|
||||
|
||||
def get_graph_label(self,
|
||||
graph,
|
||||
label="f(x)",
|
||||
x=None,
|
||||
direction=RIGHT,
|
||||
buff=MED_SMALL_BUFF,
|
||||
color=None):
|
||||
def get_graph_label(
|
||||
self,
|
||||
graph: ParametricCurve,
|
||||
label: str | Mobject = "f(x)",
|
||||
x: float | None = None,
|
||||
direction: np.ndarray = RIGHT,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
color: ManimColor | None = None
|
||||
) -> Tex | Mobject:
|
||||
if isinstance(label, str):
|
||||
label = Tex(label)
|
||||
if color is None:
|
||||
@@ -188,38 +261,57 @@ class CoordinateSystem():
|
||||
label.shift_onto_screen()
|
||||
return label
|
||||
|
||||
def get_v_line_to_graph(self, x, graph, **kwargs):
|
||||
def get_v_line_to_graph(self, x: float, graph: ParametricCurve, **kwargs):
|
||||
return self.get_v_line(self.i2gp(x, graph), **kwargs)
|
||||
|
||||
def get_h_line_to_graph(self, x, graph, **kwargs):
|
||||
def get_h_line_to_graph(self, x: float, graph: ParametricCurve, **kwargs):
|
||||
return self.get_h_line(self.i2gp(x, graph), **kwargs)
|
||||
|
||||
# For calculus
|
||||
def angle_of_tangent(self, x, graph, dx=EPSILON):
|
||||
def angle_of_tangent(
|
||||
self,
|
||||
x: float,
|
||||
graph: ParametricCurve,
|
||||
dx: float = EPSILON
|
||||
) -> float:
|
||||
p0 = self.input_to_graph_point(x, graph)
|
||||
p1 = self.input_to_graph_point(x + dx, graph)
|
||||
return angle_of_vector(p1 - p0)
|
||||
|
||||
def slope_of_tangent(self, x, graph, **kwargs):
|
||||
def slope_of_tangent(
|
||||
self,
|
||||
x: float,
|
||||
graph: ParametricCurve,
|
||||
**kwargs
|
||||
) -> float:
|
||||
return np.tan(self.angle_of_tangent(x, graph, **kwargs))
|
||||
|
||||
def get_tangent_line(self, x, graph, length=5, line_func=Line):
|
||||
def get_tangent_line(
|
||||
self,
|
||||
x: float,
|
||||
graph: ParametricCurve,
|
||||
length: float = 5,
|
||||
line_func: Type[T] = Line
|
||||
) -> T:
|
||||
line = line_func(LEFT, RIGHT)
|
||||
line.set_width(length)
|
||||
line.rotate(self.angle_of_tangent(x, graph))
|
||||
line.move_to(self.input_to_graph_point(x, graph))
|
||||
return line
|
||||
|
||||
def get_riemann_rectangles(self,
|
||||
graph,
|
||||
x_range=None,
|
||||
dx=None,
|
||||
input_sample_type="left",
|
||||
stroke_width=1,
|
||||
stroke_color=BLACK,
|
||||
fill_opacity=1,
|
||||
colors=(BLUE, GREEN),
|
||||
show_signed_area=True):
|
||||
def get_riemann_rectangles(
|
||||
self,
|
||||
graph: ParametricCurve,
|
||||
x_range: Sequence[float] = None,
|
||||
dx: float | None = None,
|
||||
input_sample_type: str = "left",
|
||||
stroke_width: float = 1,
|
||||
stroke_color: ManimColor = BLACK,
|
||||
fill_opacity: float = 1,
|
||||
colors: Iterable[ManimColor] = (BLUE, GREEN),
|
||||
stroke_background: bool = True,
|
||||
show_signed_area: bool = True
|
||||
) -> VGroup:
|
||||
if x_range is None:
|
||||
x_range = self.x_range[:2]
|
||||
if dx is None:
|
||||
@@ -241,7 +333,8 @@ class CoordinateSystem():
|
||||
height = get_norm(
|
||||
self.i2gp(sample, graph) - self.c2p(sample, 0)
|
||||
)
|
||||
rect = Rectangle(width=x1 - x0, height=height)
|
||||
rect = Rectangle(width=self.x_axis.n2p(x1)[0] - self.x_axis.n2p(x0)[0],
|
||||
height=height)
|
||||
rect.move_to(self.c2p(x0, 0), DL)
|
||||
rects.append(rect)
|
||||
result = VGroup(*rects)
|
||||
@@ -250,6 +343,7 @@ class CoordinateSystem():
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_background=stroke_background
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -261,7 +355,7 @@ class CoordinateSystem():
|
||||
class Axes(VGroup, CoordinateSystem):
|
||||
CONFIG = {
|
||||
"axis_config": {
|
||||
"include_tip": True,
|
||||
"include_tip": False,
|
||||
"numbers_to_exclude": [0],
|
||||
},
|
||||
"x_axis_config": {},
|
||||
@@ -272,11 +366,15 @@ class Axes(VGroup, CoordinateSystem):
|
||||
"width": FRAME_WIDTH - 2,
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
x_range=None,
|
||||
y_range=None,
|
||||
**kwargs):
|
||||
super().__init__(**kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
x_range: Sequence[float] | None = None,
|
||||
y_range: Sequence[float] | None = None,
|
||||
**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:
|
||||
@@ -296,33 +394,43 @@ class Axes(VGroup, CoordinateSystem):
|
||||
self.add(*self.axes)
|
||||
self.center()
|
||||
|
||||
def create_axis(self, range_terms, axis_config, length):
|
||||
def create_axis(
|
||||
self,
|
||||
range_terms: Sequence[float],
|
||||
axis_config: dict[str],
|
||||
length: float
|
||||
) -> NumberLine:
|
||||
new_config = merge_dicts_recursively(self.axis_config, axis_config)
|
||||
new_config["width"] = length
|
||||
axis = NumberLine(range_terms, **new_config)
|
||||
axis.shift(-axis.n2p(0))
|
||||
return axis
|
||||
|
||||
def coords_to_point(self, *coords):
|
||||
def coords_to_point(self, *coords: float) -> np.ndarray:
|
||||
origin = self.x_axis.number_to_point(0)
|
||||
result = origin.copy()
|
||||
for axis, coord in zip(self.get_axes(), coords):
|
||||
result += (axis.number_to_point(coord) - origin)
|
||||
return result
|
||||
return origin + sum(
|
||||
axis.number_to_point(coord) - origin
|
||||
for axis, coord in zip(self.get_axes(), coords)
|
||||
)
|
||||
|
||||
def point_to_coords(self, point):
|
||||
def point_to_coords(self, point: np.ndarray) -> tuple[float, ...]:
|
||||
return tuple([
|
||||
axis.point_to_number(point)
|
||||
for axis in self.get_axes()
|
||||
])
|
||||
|
||||
def get_axes(self):
|
||||
def get_axes(self) -> VGroup:
|
||||
return self.axes
|
||||
|
||||
def add_coordinate_labels(self,
|
||||
x_values=None,
|
||||
y_values=None,
|
||||
**kwargs):
|
||||
def get_all_ranges(self) -> list[Sequence[float]]:
|
||||
return [self.x_range, self.y_range]
|
||||
|
||||
def add_coordinate_labels(
|
||||
self,
|
||||
x_values: Iterable[float] | None = None,
|
||||
y_values: Iterable[float] | None = None,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
axes = self.get_axes()
|
||||
self.coordinate_labels = VGroup()
|
||||
for axis, values in zip(axes, [x_values, y_values]):
|
||||
@@ -334,21 +442,31 @@ 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,
|
||||
}
|
||||
|
||||
def __init__(self, x_range=None, y_range=None, z_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
x_range: Sequence[float] | None = None,
|
||||
y_range: Sequence[float] | None = None,
|
||||
z_range: Sequence[float] | None = 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 +483,9 @@ class ThreeDAxes(Axes):
|
||||
for axis in self.axes:
|
||||
axis.insert_n_curves(self.num_axis_pieces - 1)
|
||||
|
||||
def get_all_ranges(self) -> list[Sequence[float]]:
|
||||
return [self.x_range, self.y_range, self.z_range]
|
||||
|
||||
|
||||
class NumberPlane(Axes):
|
||||
CONFIG = {
|
||||
@@ -388,15 +509,20 @@ 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,
|
||||
}
|
||||
|
||||
def __init__(self, x_range=None, y_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
x_range: Sequence[float] | None = None,
|
||||
y_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(x_range, y_range, **kwargs)
|
||||
self.init_background_lines()
|
||||
|
||||
def init_background_lines(self):
|
||||
def init_background_lines(self) -> None:
|
||||
if self.faded_line_style is None:
|
||||
style = dict(self.background_line_style)
|
||||
# For anything numerical, like stroke_width
|
||||
@@ -414,7 +540,7 @@ class NumberPlane(Axes):
|
||||
self.background_lines,
|
||||
)
|
||||
|
||||
def get_lines(self):
|
||||
def get_lines(self) -> tuple[VGroup, VGroup]:
|
||||
x_axis = self.get_x_axis()
|
||||
y_axis = self.get_y_axis()
|
||||
|
||||
@@ -424,8 +550,12 @@ class NumberPlane(Axes):
|
||||
lines2 = VGroup(*x_lines2, *y_lines2)
|
||||
return lines1, lines2
|
||||
|
||||
def get_lines_parallel_to_axis(self, axis1, axis2):
|
||||
freq = axis1.x_step
|
||||
def get_lines_parallel_to_axis(
|
||||
self,
|
||||
axis1: NumberLine,
|
||||
axis2: NumberLine
|
||||
) -> tuple[VGroup, VGroup]:
|
||||
freq = axis2.x_step
|
||||
ratio = self.faded_line_ratio
|
||||
line = Line(axis1.get_start(), axis1.get_end())
|
||||
dense_freq = (1 + ratio)
|
||||
@@ -443,20 +573,20 @@ class NumberPlane(Axes):
|
||||
lines2.add(new_line)
|
||||
return lines1, lines2
|
||||
|
||||
def get_x_unit_size(self):
|
||||
def get_x_unit_size(self) -> float:
|
||||
return self.get_x_axis().get_unit_size()
|
||||
|
||||
def get_y_unit_size(self):
|
||||
def get_y_unit_size(self) -> list:
|
||||
return self.get_x_axis().get_unit_size()
|
||||
|
||||
def get_axes(self):
|
||||
def get_axes(self) -> VGroup:
|
||||
return self.axes
|
||||
|
||||
def get_vector(self, coords, **kwargs):
|
||||
def get_vector(self, coords: Iterable[float], **kwargs) -> Arrow:
|
||||
kwargs["buff"] = 0
|
||||
return Arrow(self.c2p(0, 0), self.c2p(*coords), **kwargs)
|
||||
|
||||
def prepare_for_nonlinear_transform(self, num_inserted_curves=50):
|
||||
def prepare_for_nonlinear_transform(self, num_inserted_curves: int = 50):
|
||||
for mob in self.family_members_with_points():
|
||||
num_curves = mob.get_num_curves()
|
||||
if num_inserted_curves > num_curves:
|
||||
@@ -471,29 +601,37 @@ class ComplexPlane(NumberPlane):
|
||||
"line_frequency": 1,
|
||||
}
|
||||
|
||||
def number_to_point(self, number):
|
||||
def number_to_point(self, number: complex | float) -> np.ndarray:
|
||||
number = complex(number)
|
||||
return self.coords_to_point(number.real, number.imag)
|
||||
|
||||
def n2p(self, number):
|
||||
def n2p(self, number: complex | float) -> np.ndarray:
|
||||
return self.number_to_point(number)
|
||||
|
||||
def point_to_number(self, point):
|
||||
def point_to_number(self, point: np.ndarray) -> complex:
|
||||
x, y = self.point_to_coords(point)
|
||||
return complex(x, y)
|
||||
|
||||
def p2n(self, point):
|
||||
def p2n(self, point: np.ndarray) -> complex:
|
||||
return self.point_to_number(point)
|
||||
|
||||
def get_default_coordinate_values(self):
|
||||
def get_default_coordinate_values(
|
||||
self,
|
||||
skip_first: bool = True
|
||||
) -> list[complex]:
|
||||
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: list[complex] | None = None,
|
||||
skip_first: bool = 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 +644,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,3 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Sequence
|
||||
|
||||
from isosurfaces import plot_isoline
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
@@ -12,7 +18,12 @@ class ParametricCurve(VMobject):
|
||||
"use_smoothing": True,
|
||||
}
|
||||
|
||||
def __init__(self, t_func, t_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
t_func: Callable[[float], np.ndarray],
|
||||
t_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
if t_range is not None:
|
||||
self.t_range[:len(t_range)] = t_range
|
||||
@@ -25,7 +36,7 @@ class ParametricCurve(VMobject):
|
||||
self.t_func = t_func
|
||||
VMobject.__init__(self, **kwargs)
|
||||
|
||||
def get_point_from_function(self, t):
|
||||
def get_point_from_function(self, t: float) -> np.ndarray:
|
||||
return self.t_func(t)
|
||||
|
||||
def init_points(self):
|
||||
@@ -42,8 +53,22 @@ class ParametricCurve(VMobject):
|
||||
self.add_points_as_corners(points[1:])
|
||||
if self.use_smoothing:
|
||||
self.make_approximately_smooth()
|
||||
if not self.has_points():
|
||||
self.set_points([self.t_func(t_min)])
|
||||
return self
|
||||
|
||||
def get_t_func(self):
|
||||
return self.t_func
|
||||
|
||||
def get_function(self):
|
||||
if hasattr(self, "underlying_function"):
|
||||
return self.underlying_function
|
||||
if hasattr(self, "function"):
|
||||
return self.function
|
||||
|
||||
def get_x_range(self):
|
||||
if hasattr(self, "x_range"):
|
||||
return self.x_range
|
||||
|
||||
class FunctionGraph(ParametricCurve):
|
||||
CONFIG = {
|
||||
@@ -51,7 +76,12 @@ class FunctionGraph(ParametricCurve):
|
||||
"x_range": [-8, 8, 0.25],
|
||||
}
|
||||
|
||||
def __init__(self, function, x_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[float], float],
|
||||
x_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
self.function = function
|
||||
|
||||
@@ -63,8 +93,43 @@ class FunctionGraph(ParametricCurve):
|
||||
|
||||
super().__init__(parametric_function, self.x_range, **kwargs)
|
||||
|
||||
def get_function(self):
|
||||
return self.function
|
||||
|
||||
def get_point_from_function(self, x):
|
||||
return self.t_func(x)
|
||||
class ImplicitFunction(VMobject):
|
||||
CONFIG = {
|
||||
"x_range": [-FRAME_X_RADIUS, FRAME_X_RADIUS],
|
||||
"y_range": [-FRAME_Y_RADIUS, FRAME_Y_RADIUS],
|
||||
"min_depth": 5,
|
||||
"max_quads": 1500,
|
||||
"use_smoothing": True
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], float],
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
self.function = func
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
p_min, p_max = (
|
||||
np.array([self.x_range[0], self.y_range[0]]),
|
||||
np.array([self.x_range[1], self.y_range[1]]),
|
||||
)
|
||||
curves = plot_isoline(
|
||||
fn=lambda u: self.function(u[0], u[1]),
|
||||
pmin=p_min,
|
||||
pmax=p_max,
|
||||
min_depth=self.min_depth,
|
||||
max_quads=self.max_quads,
|
||||
) # returns a list of lists of 2D points
|
||||
curves = [
|
||||
np.pad(curve, [(0, 0), (0, 1)]) for curve in curves if curve != []
|
||||
] # add z coord as 0
|
||||
for curve in curves:
|
||||
self.start_new_path(curve[0])
|
||||
self.add_points_as_corners(curve[1:])
|
||||
if self.use_smoothing:
|
||||
self.make_smooth()
|
||||
return self
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import numbers
|
||||
from typing import Sequence, Union
|
||||
|
||||
import colour
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import *
|
||||
@@ -19,6 +26,8 @@ from manimlib.utils.space_ops import normalize
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
from manimlib.utils.space_ops import rotation_matrix_transpose
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
DEFAULT_DOT_RADIUS = 0.08
|
||||
DEFAULT_SMALL_DOT_RADIUS = 0.04
|
||||
@@ -27,6 +36,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,12 +59,13 @@ class TipableVMobject(VMobject):
|
||||
"tip_config": {
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
"tip_style": 0, # triangle=0, inner_smooth=1, dot=2
|
||||
},
|
||||
"normal_vector": OUT,
|
||||
}
|
||||
|
||||
# Adding, Creating, Modifying tips
|
||||
def add_tip(self, at_start=False, **kwargs):
|
||||
def add_tip(self, at_start: bool = False, **kwargs):
|
||||
"""
|
||||
Adds a tip to the TipableVMobject instance, recognising
|
||||
that the endpoints might need to be switched if it's
|
||||
@@ -63,10 +74,11 @@ 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
|
||||
|
||||
def create_tip(self, at_start=False, **kwargs):
|
||||
def create_tip(self, at_start: bool = False, **kwargs) -> ArrowTip:
|
||||
"""
|
||||
Stylises the tip, positions it spacially, and returns
|
||||
the newly instantiated tip to the caller.
|
||||
@@ -75,7 +87,7 @@ class TipableVMobject(VMobject):
|
||||
self.position_tip(tip, at_start)
|
||||
return tip
|
||||
|
||||
def get_unpositioned_tip(self, **kwargs):
|
||||
def get_unpositioned_tip(self, **kwargs) -> ArrowTip:
|
||||
"""
|
||||
Returns a tip that has been stylistically configured,
|
||||
but has not yet been given a position in space.
|
||||
@@ -85,7 +97,7 @@ class TipableVMobject(VMobject):
|
||||
config.update(kwargs)
|
||||
return ArrowTip(**config)
|
||||
|
||||
def position_tip(self, tip, at_start=False):
|
||||
def position_tip(self, tip: ArrowTip, at_start: bool = False) -> ArrowTip:
|
||||
# Last two control points, defining both
|
||||
# the end, and the tangency direction
|
||||
if at_start:
|
||||
@@ -98,7 +110,7 @@ class TipableVMobject(VMobject):
|
||||
tip.shift(anchor - tip.get_tip_point())
|
||||
return tip
|
||||
|
||||
def reset_endpoints_based_on_tip(self, tip, at_start):
|
||||
def reset_endpoints_based_on_tip(self, tip: ArrowTip, at_start: bool):
|
||||
if self.get_length() == 0:
|
||||
# Zero length, put_start_and_end_on wouldn't
|
||||
# work
|
||||
@@ -113,7 +125,7 @@ class TipableVMobject(VMobject):
|
||||
self.put_start_and_end_on(start, end)
|
||||
return self
|
||||
|
||||
def asign_tip_attr(self, tip, at_start):
|
||||
def asign_tip_attr(self, tip: ArrowTip, at_start: bool):
|
||||
if at_start:
|
||||
self.start_tip = tip
|
||||
else:
|
||||
@@ -121,14 +133,14 @@ class TipableVMobject(VMobject):
|
||||
return self
|
||||
|
||||
# Checking for tips
|
||||
def has_tip(self):
|
||||
def has_tip(self) -> bool:
|
||||
return hasattr(self, "tip") and self.tip in self
|
||||
|
||||
def has_start_tip(self):
|
||||
def has_start_tip(self) -> bool:
|
||||
return hasattr(self, "start_tip") and self.start_tip in self
|
||||
|
||||
# Getters
|
||||
def pop_tips(self):
|
||||
def pop_tips(self) -> VGroup:
|
||||
start, end = self.get_start_and_end()
|
||||
result = VGroup()
|
||||
if self.has_tip():
|
||||
@@ -140,7 +152,7 @@ class TipableVMobject(VMobject):
|
||||
self.put_start_and_end_on(start, end)
|
||||
return result
|
||||
|
||||
def get_tips(self):
|
||||
def get_tips(self) -> VGroup:
|
||||
"""
|
||||
Returns a VGroup (collection of VMobjects) containing
|
||||
the TipableVMObject instance's tips.
|
||||
@@ -152,7 +164,7 @@ class TipableVMobject(VMobject):
|
||||
result.add(self.start_tip)
|
||||
return result
|
||||
|
||||
def get_tip(self):
|
||||
def get_tip(self) -> ArrowTip:
|
||||
"""Returns the TipableVMobject instance's (first) tip,
|
||||
otherwise throws an exception."""
|
||||
tips = self.get_tips()
|
||||
@@ -161,28 +173,28 @@ class TipableVMobject(VMobject):
|
||||
else:
|
||||
return tips[0]
|
||||
|
||||
def get_default_tip_length(self):
|
||||
def get_default_tip_length(self) -> float:
|
||||
return self.tip_length
|
||||
|
||||
def get_first_handle(self):
|
||||
def get_first_handle(self) -> np.ndarray:
|
||||
return self.get_points()[1]
|
||||
|
||||
def get_last_handle(self):
|
||||
def get_last_handle(self) -> np.ndarray:
|
||||
return self.get_points()[-2]
|
||||
|
||||
def get_end(self):
|
||||
def get_end(self) -> np.ndarray:
|
||||
if self.has_tip():
|
||||
return self.tip.get_start()
|
||||
else:
|
||||
return VMobject.get_end(self)
|
||||
|
||||
def get_start(self):
|
||||
def get_start(self) -> np.ndarray:
|
||||
if self.has_start_tip():
|
||||
return self.start_tip.get_start()
|
||||
else:
|
||||
return VMobject.get_start(self)
|
||||
|
||||
def get_length(self):
|
||||
def get_length(self) -> float:
|
||||
start, end = self.get_start_and_end()
|
||||
return get_norm(start - end)
|
||||
|
||||
@@ -195,12 +207,17 @@ class Arc(TipableVMobject):
|
||||
"arc_center": ORIGIN,
|
||||
}
|
||||
|
||||
def __init__(self, start_angle=0, angle=TAU / 4, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start_angle: float = 0,
|
||||
angle: float = TAU / 4,
|
||||
**kwargs
|
||||
):
|
||||
self.start_angle = start_angle
|
||||
self.angle = angle
|
||||
VMobject.__init__(self, **kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.set_points(Arc.create_quadratic_bezier_points(
|
||||
angle=self.angle,
|
||||
start_angle=self.start_angle,
|
||||
@@ -210,7 +227,11 @@ class Arc(TipableVMobject):
|
||||
self.shift(self.arc_center)
|
||||
|
||||
@staticmethod
|
||||
def create_quadratic_bezier_points(angle, start_angle=0, n_components=8):
|
||||
def create_quadratic_bezier_points(
|
||||
angle: float,
|
||||
start_angle: float = 0,
|
||||
n_components: int = 8
|
||||
) -> np.ndarray:
|
||||
samples = np.array([
|
||||
[np.cos(a), np.sin(a), 0]
|
||||
for a in np.linspace(
|
||||
@@ -228,7 +249,7 @@ class Arc(TipableVMobject):
|
||||
points[2::3] = samples[2::2]
|
||||
return points
|
||||
|
||||
def get_arc_center(self):
|
||||
def get_arc_center(self) -> np.ndarray:
|
||||
"""
|
||||
Looks at the normals to the first two
|
||||
anchors, and finds their intersection points
|
||||
@@ -243,21 +264,27 @@ class Arc(TipableVMobject):
|
||||
n2 = rotate_vector(t2, TAU / 4)
|
||||
return find_intersection(a1, n1, a2, n2)
|
||||
|
||||
def get_start_angle(self):
|
||||
def get_start_angle(self) -> float:
|
||||
angle = angle_of_vector(self.get_start() - self.get_arc_center())
|
||||
return angle % TAU
|
||||
|
||||
def get_stop_angle(self):
|
||||
def get_stop_angle(self) -> float:
|
||||
angle = angle_of_vector(self.get_end() - self.get_arc_center())
|
||||
return angle % TAU
|
||||
|
||||
def move_arc_center_to(self, point):
|
||||
def move_arc_center_to(self, point: np.ndarray):
|
||||
self.shift(point - self.get_arc_center())
|
||||
return self
|
||||
|
||||
|
||||
class ArcBetweenPoints(Arc):
|
||||
def __init__(self, start, end, angle=TAU / 4, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start: np.ndarray,
|
||||
end: np.ndarray,
|
||||
angle: float = TAU / 4,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(angle=angle, **kwargs)
|
||||
if angle == 0:
|
||||
self.set_points_as_corners([LEFT, RIGHT])
|
||||
@@ -265,13 +292,23 @@ class ArcBetweenPoints(Arc):
|
||||
|
||||
|
||||
class CurvedArrow(ArcBetweenPoints):
|
||||
def __init__(self, start_point, end_point, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start_point: np.ndarray,
|
||||
end_point: np.ndarray,
|
||||
**kwargs
|
||||
):
|
||||
ArcBetweenPoints.__init__(self, start_point, end_point, **kwargs)
|
||||
self.add_tip()
|
||||
|
||||
|
||||
class CurvedDoubleArrow(CurvedArrow):
|
||||
def __init__(self, start_point, end_point, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start_point: np.ndarray,
|
||||
end_point: np.ndarray,
|
||||
**kwargs
|
||||
):
|
||||
CurvedArrow.__init__(self, start_point, end_point, **kwargs)
|
||||
self.add_tip(at_start=True)
|
||||
|
||||
@@ -286,7 +323,13 @@ class Circle(Arc):
|
||||
def __init__(self, **kwargs):
|
||||
Arc.__init__(self, 0, TAU, **kwargs)
|
||||
|
||||
def surround(self, mobject, dim_to_match=0, stretch=False, buff=MED_SMALL_BUFF):
|
||||
def surround(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
dim_to_match: int = 0,
|
||||
stretch: bool = False,
|
||||
buff: float = MED_SMALL_BUFF
|
||||
):
|
||||
# Ignores dim_to_match and stretch; result will always be a circle
|
||||
# TODO: Perhaps create an ellipse class to handle singele-dimension stretching
|
||||
|
||||
@@ -294,12 +337,15 @@ class Circle(Arc):
|
||||
self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0)
|
||||
self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1)
|
||||
|
||||
def point_at_angle(self, angle):
|
||||
def point_at_angle(self, angle: float) -> np.ndarray:
|
||||
start_angle = self.get_start_angle()
|
||||
return self.point_from_proportion(
|
||||
(angle - start_angle) / TAU
|
||||
)
|
||||
|
||||
def get_radius(self) -> float:
|
||||
return get_norm(self.get_start() - self.get_center())
|
||||
|
||||
|
||||
class Dot(Circle):
|
||||
CONFIG = {
|
||||
@@ -309,7 +355,7 @@ class Dot(Circle):
|
||||
"color": WHITE
|
||||
}
|
||||
|
||||
def __init__(self, point=ORIGIN, **kwargs):
|
||||
def __init__(self, point: np.ndarray = ORIGIN, **kwargs):
|
||||
super().__init__(arc_center=point, **kwargs)
|
||||
|
||||
|
||||
@@ -393,42 +439,59 @@ class Line(TipableVMobject):
|
||||
"path_arc": 0,
|
||||
}
|
||||
|
||||
def __init__(self, start=LEFT, end=RIGHT, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start: np.ndarray = LEFT,
|
||||
end: np.ndarray = RIGHT,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
self.set_start_and_end_attrs(start, end)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
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:
|
||||
def set_points_by_ends(
|
||||
self,
|
||||
start: np.ndarray,
|
||||
end: np.ndarray,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0
|
||||
):
|
||||
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))
|
||||
|
||||
def set_path_arc(self, new_value):
|
||||
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: float) -> None:
|
||||
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):
|
||||
def set_start_and_end_attrs(self, start: np.ndarray, end: np.ndarray):
|
||||
# If either start or end are Mobjects, this
|
||||
# gives their centers
|
||||
rough_start = self.pointify(start)
|
||||
@@ -437,10 +500,14 @@ 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):
|
||||
def pointify(
|
||||
self,
|
||||
mob_or_point: Mobject | np.ndarray,
|
||||
direction: np.ndarray | None = None
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Take an argument passed into Line (or subclass) and turn
|
||||
it into a 3d point.
|
||||
@@ -457,22 +524,24 @@ class Line(TipableVMobject):
|
||||
result[:len(point)] = point
|
||||
return result
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
def put_start_and_end_on(self, start: np.ndarray, end: np.ndarray):
|
||||
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):
|
||||
def get_vector(self) -> np.ndarray:
|
||||
return self.get_end() - self.get_start()
|
||||
|
||||
def get_unit_vector(self):
|
||||
def get_unit_vector(self) -> np.ndarray:
|
||||
return normalize(self.get_vector())
|
||||
|
||||
def get_angle(self):
|
||||
def get_angle(self) -> float:
|
||||
return angle_of_vector(self.get_vector())
|
||||
|
||||
def get_projection(self, point):
|
||||
def get_projection(self, point: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Return projection of a point onto the line
|
||||
"""
|
||||
@@ -480,10 +549,10 @@ class Line(TipableVMobject):
|
||||
start = self.get_start()
|
||||
return start + np.dot(point - start, unit_vect) * unit_vect
|
||||
|
||||
def get_slope(self):
|
||||
def get_slope(self) -> float:
|
||||
return np.tan(self.get_angle())
|
||||
|
||||
def set_angle(self, angle, about_point=None):
|
||||
def set_angle(self, angle: float, about_point: np.ndarray | None = None):
|
||||
if about_point is None:
|
||||
about_point = self.get_start()
|
||||
self.rotate(
|
||||
@@ -492,8 +561,9 @@ class Line(TipableVMobject):
|
||||
)
|
||||
return self
|
||||
|
||||
def set_length(self, length):
|
||||
self.scale(length / self.get_length())
|
||||
def set_length(self, length: float, **kwargs):
|
||||
self.scale(length / self.get_length(), **kwargs)
|
||||
return self
|
||||
|
||||
|
||||
class DashedLine(Line):
|
||||
@@ -515,35 +585,35 @@ class DashedLine(Line):
|
||||
self.clear_points()
|
||||
self.add(*dashes)
|
||||
|
||||
def calculate_num_dashes(self, positive_space_ratio):
|
||||
def calculate_num_dashes(self, positive_space_ratio: float) -> int:
|
||||
try:
|
||||
full_length = self.dash_length / positive_space_ratio
|
||||
return int(np.ceil(self.get_length() / full_length))
|
||||
except ZeroDivisionError:
|
||||
return 1
|
||||
|
||||
def calculate_positive_space_ratio(self):
|
||||
def calculate_positive_space_ratio(self) -> float:
|
||||
return fdiv(
|
||||
self.dash_length,
|
||||
self.dash_length + self.dash_spacing,
|
||||
)
|
||||
|
||||
def get_start(self):
|
||||
def get_start(self) -> np.ndarray:
|
||||
if len(self.submobjects) > 0:
|
||||
return self.submobjects[0].get_start()
|
||||
else:
|
||||
return Line.get_start(self)
|
||||
|
||||
def get_end(self):
|
||||
def get_end(self) -> np.ndarray:
|
||||
if len(self.submobjects) > 0:
|
||||
return self.submobjects[-1].get_end()
|
||||
else:
|
||||
return Line.get_end(self)
|
||||
|
||||
def get_first_handle(self):
|
||||
def get_first_handle(self) -> np.ndarray:
|
||||
return self.submobjects[0].get_points()[1]
|
||||
|
||||
def get_last_handle(self):
|
||||
def get_last_handle(self) -> np.ndarray:
|
||||
return self.submobjects[-1].get_points()[-2]
|
||||
|
||||
|
||||
@@ -553,7 +623,7 @@ class TangentLine(Line):
|
||||
"d_alpha": 1e-6
|
||||
}
|
||||
|
||||
def __init__(self, vmob, alpha, **kwargs):
|
||||
def __init__(self, vmob: VMobject, alpha: float, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
da = self.d_alpha
|
||||
a1 = clip(alpha - da, 0, 1)
|
||||
@@ -569,13 +639,98 @@ 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 = {
|
||||
"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: np.ndarray,
|
||||
end: np.ndarray,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0
|
||||
):
|
||||
super().set_points_by_ends(start, end, buff, path_arc)
|
||||
self.insert_tip_anchor()
|
||||
return self
|
||||
|
||||
def init_colors(self) -> None:
|
||||
super().init_colors()
|
||||
self.create_tip_with_stroke_width()
|
||||
|
||||
def get_arc_length(self) -> float:
|
||||
# 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: ManimColor | None = None,
|
||||
width: float | None = 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: float):
|
||||
return self.reset_tip()
|
||||
|
||||
|
||||
class FillArrow(Line):
|
||||
CONFIG = {
|
||||
"fill_color": GREY_A,
|
||||
"fill_opacity": 1,
|
||||
@@ -588,7 +743,13 @@ class Arrow(Line):
|
||||
"max_width_to_length_ratio": 0.1,
|
||||
}
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
def set_points_by_ends(
|
||||
self,
|
||||
start: np.ndarray,
|
||||
end: np.ndarray,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0
|
||||
) -> None:
|
||||
# Find the right tip length and thickness
|
||||
vect = end - start
|
||||
length = max(get_norm(vect), 1e-8)
|
||||
@@ -657,15 +818,15 @@ class Arrow(Line):
|
||||
)
|
||||
return self
|
||||
|
||||
def get_start(self):
|
||||
def get_start(self) -> np.ndarray:
|
||||
nppc = self.n_points_per_curve
|
||||
points = self.get_points()
|
||||
return (points[0] + points[-nppc]) / 2
|
||||
|
||||
def get_end(self):
|
||||
def get_end(self) -> np.ndarray:
|
||||
return self.get_points()[self.tip_index]
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
def put_start_and_end_on(self, start: np.ndarray, end: np.ndarray):
|
||||
self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
|
||||
return self
|
||||
|
||||
@@ -674,12 +835,12 @@ class Arrow(Line):
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
|
||||
def set_thickness(self, thickness):
|
||||
def set_thickness(self, thickness: float):
|
||||
self.thickness = thickness
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
|
||||
def set_path_arc(self, path_arc):
|
||||
def set_path_arc(self, path_arc: float):
|
||||
self.path_arc = path_arc
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
@@ -690,7 +851,7 @@ class Vector(Arrow):
|
||||
"buff": 0,
|
||||
}
|
||||
|
||||
def __init__(self, direction=RIGHT, **kwargs):
|
||||
def __init__(self, direction: np.ndarray = RIGHT, **kwargs):
|
||||
if len(direction) == 2:
|
||||
direction = np.hstack([direction, 0])
|
||||
super().__init__(ORIGIN, direction, **kwargs)
|
||||
@@ -703,24 +864,31 @@ class DoubleArrow(Arrow):
|
||||
|
||||
|
||||
class CubicBezier(VMobject):
|
||||
def __init__(self, a0, h0, h1, a1, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
a0: np.ndarray,
|
||||
h0: np.ndarray,
|
||||
h1: np.ndarray,
|
||||
a1: np.ndarray,
|
||||
**kwargs
|
||||
):
|
||||
VMobject.__init__(self, **kwargs)
|
||||
self.add_cubic_bezier_curve(a0, h0, h1, a1)
|
||||
|
||||
|
||||
class Polygon(VMobject):
|
||||
def __init__(self, *vertices, **kwargs):
|
||||
def __init__(self, *vertices: np.ndarray, **kwargs):
|
||||
self.vertices = vertices
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
verts = self.vertices
|
||||
self.set_points_as_corners([*verts, verts[0]])
|
||||
|
||||
def get_vertices(self):
|
||||
def get_vertices(self) -> list[np.ndarray]:
|
||||
return self.get_start_anchors()
|
||||
|
||||
def round_corners(self, radius=0.5):
|
||||
def round_corners(self, radius: float = 0.5):
|
||||
vertices = self.get_vertices()
|
||||
arcs = []
|
||||
for v1, v2, v3 in adjacent_n_tuples(vertices, 3):
|
||||
@@ -758,12 +926,17 @@ class Polygon(VMobject):
|
||||
return self
|
||||
|
||||
|
||||
class Polyline(Polygon):
|
||||
def init_points(self) -> None:
|
||||
self.set_points_as_corners(self.vertices)
|
||||
|
||||
|
||||
class RegularPolygon(Polygon):
|
||||
CONFIG = {
|
||||
"start_angle": None,
|
||||
}
|
||||
|
||||
def __init__(self, n=6, **kwargs):
|
||||
def __init__(self, n: int = 6, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
if self.start_angle is None:
|
||||
# 0 for odd, 90 for even
|
||||
@@ -786,27 +959,35 @@ 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):
|
||||
def get_base(self) -> np.ndarray:
|
||||
return self.point_from_proportion(0.5)
|
||||
|
||||
def get_tip_point(self):
|
||||
def get_tip_point(self) -> np.ndarray:
|
||||
return self.get_points()[0]
|
||||
|
||||
def get_vector(self):
|
||||
def get_vector(self) -> np.ndarray:
|
||||
return self.get_tip_point() - self.get_base()
|
||||
|
||||
def get_angle(self):
|
||||
def get_angle(self) -> float:
|
||||
return angle_of_vector(self.get_vector())
|
||||
|
||||
def get_length(self):
|
||||
def get_length(self) -> float:
|
||||
return get_norm(self.get_vector())
|
||||
|
||||
|
||||
@@ -819,7 +1000,12 @@ class Rectangle(Polygon):
|
||||
"close_new_points": True,
|
||||
}
|
||||
|
||||
def __init__(self, width=None, height=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
width: float | None = None,
|
||||
height: float | None = None,
|
||||
**kwargs
|
||||
):
|
||||
Polygon.__init__(self, UR, UL, DL, DR, **kwargs)
|
||||
|
||||
if width is None:
|
||||
@@ -832,16 +1018,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: float = 2.0, **kwargs):
|
||||
self.side_length = side_length
|
||||
super().__init__(side_length, side_length, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
from pyglet.window import key as PygletWindowKeys
|
||||
|
||||
@@ -21,8 +25,7 @@ class MotionMobject(Mobject):
|
||||
"""
|
||||
You could hold and drag this object to any position
|
||||
"""
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
assert(isinstance(mobject, Mobject))
|
||||
self.mobject = mobject
|
||||
@@ -31,7 +34,7 @@ class MotionMobject(Mobject):
|
||||
self.mobject.add_updater(lambda mob: None)
|
||||
self.add(mobject)
|
||||
|
||||
def mob_on_mouse_drag(self, mob, event_data):
|
||||
def mob_on_mouse_drag(self, mob: Mobject, event_data: dict[str, np.ndarray]) -> bool:
|
||||
mob.move_to(event_data["point"])
|
||||
return False
|
||||
|
||||
@@ -43,7 +46,7 @@ class Button(Mobject):
|
||||
The on_click method takes mobject as argument like updater
|
||||
"""
|
||||
|
||||
def __init__(self, mobject, on_click, **kwargs):
|
||||
def __init__(self, mobject: Mobject, on_click: Callable[[Mobject]], **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
assert(isinstance(mobject, Mobject))
|
||||
self.on_click = on_click
|
||||
@@ -51,7 +54,7 @@ class Button(Mobject):
|
||||
self.mobject.add_mouse_press_listner(self.mob_on_mouse_press)
|
||||
self.add(self.mobject)
|
||||
|
||||
def mob_on_mouse_press(self, mob, event_data):
|
||||
def mob_on_mouse_press(self, mob: Mobject, event_data) -> bool:
|
||||
self.on_click(mob)
|
||||
return False
|
||||
|
||||
@@ -59,7 +62,7 @@ class Button(Mobject):
|
||||
# Controls
|
||||
|
||||
class ControlMobject(ValueTracker):
|
||||
def __init__(self, value, *mobjects, **kwargs):
|
||||
def __init__(self, value: float, *mobjects: Mobject, **kwargs):
|
||||
super().__init__(value=value, **kwargs)
|
||||
self.add(*mobjects)
|
||||
|
||||
@@ -67,7 +70,7 @@ class ControlMobject(ValueTracker):
|
||||
self.add_updater(lambda mob: None)
|
||||
self.fix_in_frame()
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: float):
|
||||
self.assert_value(value)
|
||||
self.set_value_anim(value)
|
||||
return ValueTracker.set_value(self, value)
|
||||
@@ -93,25 +96,25 @@ class EnableDisableButton(ControlMobject):
|
||||
"disable_color": RED
|
||||
}
|
||||
|
||||
def __init__(self, value=True, **kwargs):
|
||||
def __init__(self, value: bool = True, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.box = Rectangle(**self.rect_kwargs)
|
||||
super().__init__(value, self.box, **kwargs)
|
||||
self.add_mouse_press_listner(self.on_mouse_press)
|
||||
|
||||
def assert_value(self, value):
|
||||
def assert_value(self, value: bool) -> None:
|
||||
assert(isinstance(value, bool))
|
||||
|
||||
def set_value_anim(self, value):
|
||||
def set_value_anim(self, value: bool) -> None:
|
||||
if value:
|
||||
self.box.set_fill(self.enable_color)
|
||||
else:
|
||||
self.box.set_fill(self.disable_color)
|
||||
|
||||
def toggle_value(self):
|
||||
def toggle_value(self) -> None:
|
||||
super().set_value(not self.get_value())
|
||||
|
||||
def on_mouse_press(self, mob, event_data):
|
||||
def on_mouse_press(self, mob: Mobject, event_data) -> bool:
|
||||
mob.toggle_value()
|
||||
return False
|
||||
|
||||
@@ -136,32 +139,32 @@ class Checkbox(ControlMobject):
|
||||
"box_content_buff": SMALL_BUFF
|
||||
}
|
||||
|
||||
def __init__(self, value=True, **kwargs):
|
||||
def __init__(self, value: bool = True, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.box = Rectangle(**self.rect_kwargs)
|
||||
self.box_content = self.get_checkmark() if value else self.get_cross()
|
||||
super().__init__(value, self.box, self.box_content, **kwargs)
|
||||
self.add_mouse_press_listner(self.on_mouse_press)
|
||||
|
||||
def assert_value(self, value):
|
||||
def assert_value(self, value: bool) -> None:
|
||||
assert(isinstance(value, bool))
|
||||
|
||||
def toggle_value(self):
|
||||
def toggle_value(self) -> None:
|
||||
super().set_value(not self.get_value())
|
||||
|
||||
def set_value_anim(self, value):
|
||||
def set_value_anim(self, value: bool) -> None:
|
||||
if value:
|
||||
self.box_content.become(self.get_checkmark())
|
||||
else:
|
||||
self.box_content.become(self.get_cross())
|
||||
|
||||
def on_mouse_press(self, mob, event_data):
|
||||
def on_mouse_press(self, mob: Mobject, event_data) -> None:
|
||||
mob.toggle_value()
|
||||
return False
|
||||
|
||||
# Helper methods
|
||||
|
||||
def get_checkmark(self):
|
||||
def get_checkmark(self) -> VGroup:
|
||||
checkmark = VGroup(
|
||||
Line(UP / 2 + 2 * LEFT, DOWN + LEFT, **self.checkmark_kwargs),
|
||||
Line(DOWN + LEFT, UP + RIGHT, **self.checkmark_kwargs)
|
||||
@@ -173,7 +176,7 @@ class Checkbox(ControlMobject):
|
||||
checkmark.move_to(self.box)
|
||||
return checkmark
|
||||
|
||||
def get_cross(self):
|
||||
def get_cross(self) -> VGroup:
|
||||
cross = VGroup(
|
||||
Line(UP + LEFT, DOWN + RIGHT, **self.cross_kwargs),
|
||||
Line(UP + RIGHT, DOWN + LEFT, **self.cross_kwargs)
|
||||
@@ -206,7 +209,7 @@ class LinearNumberSlider(ControlMobject):
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
def __init__(self, value: float = 0, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.bar = RoundedRectangle(**self.rounded_rect_kwargs)
|
||||
self.slider = Circle(**self.circle_kwargs)
|
||||
@@ -219,22 +222,22 @@ class LinearNumberSlider(ControlMobject):
|
||||
|
||||
self.slider.add_mouse_drag_listner(self.slider_on_mouse_drag)
|
||||
|
||||
super().__init__(value, self.bar, self.slider, self.slider_axis, ** kwargs)
|
||||
super().__init__(value, self.bar, self.slider, self.slider_axis, **kwargs)
|
||||
|
||||
def assert_value(self, value):
|
||||
def assert_value(self, value: float) -> None:
|
||||
assert(self.min_value <= value <= self.max_value)
|
||||
|
||||
def set_value_anim(self, value):
|
||||
def set_value_anim(self, value: float) -> None:
|
||||
prop = (value - self.min_value) / (self.max_value - self.min_value)
|
||||
self.slider.move_to(self.slider_axis.point_from_proportion(prop))
|
||||
|
||||
def slider_on_mouse_drag(self, mob, event_data):
|
||||
def slider_on_mouse_drag(self, mob, event_data: dict[str, np.ndarray]) -> bool:
|
||||
self.set_value(self.get_value_from_point(event_data["point"]))
|
||||
return False
|
||||
|
||||
# Helper Methods
|
||||
|
||||
def get_value_from_point(self, point):
|
||||
def get_value_from_point(self, point: np.ndarray) -> float:
|
||||
start, end = self.slider_axis.get_start_and_end()
|
||||
point_on_line = get_closest_point_on_line(start, end, point)
|
||||
prop = get_norm(point_on_line - start) / get_norm(end - start)
|
||||
@@ -300,7 +303,7 @@ class ColorSliders(Group):
|
||||
|
||||
self.arrange(DOWN)
|
||||
|
||||
def get_background(self):
|
||||
def get_background(self) -> VGroup:
|
||||
single_square_len = self.background_grid_kwargs["single_square_len"]
|
||||
colors = self.background_grid_kwargs["colors"]
|
||||
width = self.rect_kwargs["width"]
|
||||
@@ -322,24 +325,24 @@ class ColorSliders(Group):
|
||||
|
||||
return grid
|
||||
|
||||
def set_value(self, r, g, b, a):
|
||||
def set_value(self, r: float, g: float, b: float, a: float):
|
||||
self.r_slider.set_value(r)
|
||||
self.g_slider.set_value(g)
|
||||
self.b_slider.set_value(b)
|
||||
self.a_slider.set_value(a)
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> np.ndarary:
|
||||
r = self.r_slider.get_value() / 255
|
||||
g = self.g_slider.get_value() / 255
|
||||
b = self.b_slider.get_value() / 255
|
||||
alpha = self.a_slider.get_value()
|
||||
return color_to_rgba(rgb_to_color((r, g, b)), alpha=alpha)
|
||||
|
||||
def get_picked_color(self):
|
||||
def get_picked_color(self) -> str:
|
||||
rgba = self.get_value()
|
||||
return rgb_to_hex(rgba[:3])
|
||||
|
||||
def get_picked_opacity(self):
|
||||
def get_picked_opacity(self) -> float:
|
||||
rgba = self.get_value()
|
||||
return rgba[3]
|
||||
|
||||
@@ -363,7 +366,7 @@ class Textbox(ControlMobject):
|
||||
"deactive_color": RED,
|
||||
}
|
||||
|
||||
def __init__(self, value="", **kwargs):
|
||||
def __init__(self, value: str = "", **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.isActive = self.isInitiallyActive
|
||||
self.box = Rectangle(**self.box_kwargs)
|
||||
@@ -374,10 +377,10 @@ class Textbox(ControlMobject):
|
||||
self.active_anim(self.isActive)
|
||||
self.add_key_press_listner(self.on_key_press)
|
||||
|
||||
def set_value_anim(self, value):
|
||||
def set_value_anim(self, value: str) -> None:
|
||||
self.update_text(value)
|
||||
|
||||
def update_text(self, value):
|
||||
def update_text(self, value: str) -> None:
|
||||
text = self.text
|
||||
self.remove(text)
|
||||
text.__init__(value, **self.text_kwargs)
|
||||
@@ -389,18 +392,18 @@ class Textbox(ControlMobject):
|
||||
text.fix_in_frame()
|
||||
self.add(text)
|
||||
|
||||
def active_anim(self, isActive):
|
||||
def active_anim(self, isActive: bool) -> None:
|
||||
if isActive:
|
||||
self.box.set_stroke(self.active_color)
|
||||
else:
|
||||
self.box.set_stroke(self.deactive_color)
|
||||
|
||||
def box_on_mouse_press(self, mob, event_data):
|
||||
def box_on_mouse_press(self, mob, event_data) -> bool:
|
||||
self.isActive = not self.isActive
|
||||
self.active_anim(self.isActive)
|
||||
return False
|
||||
|
||||
def on_key_press(self, mob, event_data):
|
||||
def on_key_press(self, mob: Mobject, event_data: dict[str, int]) -> bool | None:
|
||||
symbol = event_data["symbol"]
|
||||
modifiers = event_data["modifiers"]
|
||||
char = chr(symbol)
|
||||
@@ -439,11 +442,11 @@ class ControlPanel(Group):
|
||||
},
|
||||
"opener_text_kwargs": {
|
||||
"text": "Control Panel",
|
||||
"size": 0.4
|
||||
"font_size": 20
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, *controls, **kwargs):
|
||||
def __init__(self, *controls: ControlMobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
|
||||
self.panel = Rectangle(**self.panel_kwargs)
|
||||
@@ -472,7 +475,7 @@ class ControlPanel(Group):
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
self.fix_in_frame()
|
||||
|
||||
def move_panel_and_controls_to_panel_opener(self):
|
||||
def move_panel_and_controls_to_panel_opener(self) -> None:
|
||||
self.panel.next_to(
|
||||
self.panel_opener_rect,
|
||||
direction=UP,
|
||||
@@ -488,11 +491,11 @@ class ControlPanel(Group):
|
||||
|
||||
self.controls.set_x(controls_old_x)
|
||||
|
||||
def add_controls(self, *new_controls):
|
||||
def add_controls(self, *new_controls: ControlMobject) -> None:
|
||||
self.controls.add(*new_controls)
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
|
||||
def remove_controls(self, *controls_to_remove):
|
||||
def remove_controls(self, *controls_to_remove: ControlMobject) -> None:
|
||||
self.controls.remove(*controls_to_remove)
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
|
||||
@@ -510,13 +513,13 @@ class ControlPanel(Group):
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
return self
|
||||
|
||||
def panel_opener_on_mouse_drag(self, mob, event_data):
|
||||
def panel_opener_on_mouse_drag(self, mob, event_data: dict[str, np.ndarray]) -> bool:
|
||||
point = event_data["point"]
|
||||
self.panel_opener.match_y(Dot(point))
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
return False
|
||||
|
||||
def panel_on_mouse_scroll(self, mob, event_data):
|
||||
def panel_on_mouse_scroll(self, mob, event_data: dict[str, np.ndarray]) -> bool:
|
||||
offset = event_data["offset"]
|
||||
factor = 10 * offset[1]
|
||||
self.controls.set_y(self.controls.get_y() + factor)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
from typing import Union, Sequence
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
@@ -10,10 +15,18 @@ from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import colour
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
VECTOR_LABEL_SCALE_FACTOR = 0.8
|
||||
|
||||
|
||||
def matrix_to_tex_string(matrix):
|
||||
def matrix_to_tex_string(matrix: npt.ArrayLike) -> str:
|
||||
matrix = np.array(matrix).astype("str")
|
||||
if matrix.ndim == 1:
|
||||
matrix = matrix.reshape((matrix.size, 1))
|
||||
@@ -27,12 +40,16 @@ def matrix_to_tex_string(matrix):
|
||||
return prefix + " \\\\ ".join(rows) + suffix
|
||||
|
||||
|
||||
def matrix_to_mobject(matrix):
|
||||
def matrix_to_mobject(matrix: npt.ArrayLike) -> Tex:
|
||||
return Tex(matrix_to_tex_string(matrix))
|
||||
|
||||
|
||||
def vector_coordinate_label(vector_mob, integer_labels=True,
|
||||
n_dim=2, color=WHITE):
|
||||
def vector_coordinate_label(
|
||||
vector_mob: VMobject,
|
||||
integer_labels: bool = True,
|
||||
n_dim: int = 2,
|
||||
color: ManimColor = WHITE
|
||||
) -> Matrix:
|
||||
vect = np.array(vector_mob.get_end())
|
||||
if integer_labels:
|
||||
vect = np.round(vect).astype(int)
|
||||
@@ -57,18 +74,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):
|
||||
def __init__(self, matrix: npt.ArrayLike, **kwargs):
|
||||
"""
|
||||
Matrix can either either include numbres, tex_strings,
|
||||
Matrix can either include numbers, tex_strings,
|
||||
or mobjects
|
||||
"""
|
||||
VMobject.__init__(self, **kwargs)
|
||||
@@ -87,7 +104,7 @@ class Matrix(VMobject):
|
||||
if self.include_background_rectangle:
|
||||
self.add_background_rectangle()
|
||||
|
||||
def matrix_to_mob_matrix(self, matrix):
|
||||
def matrix_to_mob_matrix(self, matrix: npt.ArrayLike) -> list[list[Mobject]]:
|
||||
return [
|
||||
[
|
||||
self.element_to_mobject(item, **self.element_to_mobject_config)
|
||||
@@ -96,7 +113,7 @@ class Matrix(VMobject):
|
||||
for row in matrix
|
||||
]
|
||||
|
||||
def organize_mob_matrix(self, matrix):
|
||||
def organize_mob_matrix(self, matrix: npt.ArrayLike):
|
||||
for i, row in enumerate(matrix):
|
||||
for j, elem in enumerate(row):
|
||||
mob = matrix[i][j]
|
||||
@@ -112,7 +129,7 @@ class Matrix(VMobject):
|
||||
"\\left[",
|
||||
"\\begin{array}{c}",
|
||||
*height * ["\\quad \\\\"],
|
||||
"\\end{array}"
|
||||
"\\end{array}",
|
||||
"\\right]",
|
||||
]))[0]
|
||||
bracket_pair.set_height(
|
||||
@@ -126,13 +143,19 @@ class Matrix(VMobject):
|
||||
self.brackets = VGroup(l_bracket, r_bracket)
|
||||
return self
|
||||
|
||||
def get_columns(self):
|
||||
def get_columns(self) -> VGroup:
|
||||
return VGroup(*[
|
||||
VGroup(*[row[i] for row in self.mob_matrix])
|
||||
for i in range(len(self.mob_matrix[0]))
|
||||
])
|
||||
|
||||
def set_column_colors(self, *colors):
|
||||
def get_rows(self) -> VGroup:
|
||||
return VGroup(*[
|
||||
VGroup(*row)
|
||||
for row in self.mob_matrix
|
||||
])
|
||||
|
||||
def set_column_colors(self, *colors: ManimColor):
|
||||
columns = self.get_columns()
|
||||
for color, column in zip(colors, columns):
|
||||
column.set_color(color)
|
||||
@@ -143,13 +166,13 @@ class Matrix(VMobject):
|
||||
mob.add_background_rectangle()
|
||||
return self
|
||||
|
||||
def get_mob_matrix(self):
|
||||
def get_mob_matrix(self) -> list[list[Mobject]]:
|
||||
return self.mob_matrix
|
||||
|
||||
def get_entries(self):
|
||||
def get_entries(self) -> VGroup:
|
||||
return self.elements
|
||||
|
||||
def get_brackets(self):
|
||||
def get_brackets(self) -> VGroup:
|
||||
return self.brackets
|
||||
|
||||
|
||||
@@ -163,6 +186,7 @@ class DecimalMatrix(Matrix):
|
||||
class IntegerMatrix(Matrix):
|
||||
CONFIG = {
|
||||
"element_to_mobject": Integer,
|
||||
"element_alignment_corner": UP,
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +196,12 @@ class MobjectMatrix(Matrix):
|
||||
}
|
||||
|
||||
|
||||
def get_det_text(matrix, determinant=None, background_rect=False, initial_scale_factor=2):
|
||||
def get_det_text(
|
||||
matrix: Matrix,
|
||||
determinant: int | str | None = None,
|
||||
background_rect: bool = False,
|
||||
initial_scale_factor: int = 2
|
||||
) -> VGroup:
|
||||
parens = Tex("(", ")")
|
||||
parens.scale(initial_scale_factor)
|
||||
parens.stretch_to_fit_height(matrix.get_height())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
|
||||
def assert_is_mobject_method(method):
|
||||
assert(inspect.ismethod(method))
|
||||
@@ -23,7 +32,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)
|
||||
@@ -41,27 +50,39 @@ def f_always(method, *arg_generators, **kwargs):
|
||||
return mobject
|
||||
|
||||
|
||||
def always_redraw(func, *args, **kwargs):
|
||||
def always_redraw(func: Callable[..., Mobject], *args, **kwargs) -> Mobject:
|
||||
mob = func(*args, **kwargs)
|
||||
mob.add_updater(lambda m: mob.become(func(*args, **kwargs)))
|
||||
return mob
|
||||
|
||||
|
||||
def always_shift(mobject, direction=RIGHT, rate=0.1):
|
||||
def always_shift(
|
||||
mobject: Mobject,
|
||||
direction: np.ndarray = RIGHT,
|
||||
rate: float = 0.1
|
||||
) -> Mobject:
|
||||
mobject.add_updater(
|
||||
lambda m, dt: m.shift(dt * rate * direction)
|
||||
)
|
||||
return mobject
|
||||
|
||||
|
||||
def always_rotate(mobject, rate=20 * DEGREES, **kwargs):
|
||||
def always_rotate(
|
||||
mobject: Mobject,
|
||||
rate: float = 20 * DEGREES,
|
||||
**kwargs
|
||||
) -> Mobject:
|
||||
mobject.add_updater(
|
||||
lambda m, dt: m.rotate(dt * rate, **kwargs)
|
||||
)
|
||||
return mobject
|
||||
|
||||
|
||||
def turn_animation_into_updater(animation, cycle=False, **kwargs):
|
||||
def turn_animation_into_updater(
|
||||
animation: Animation,
|
||||
cycle: bool = False,
|
||||
**kwargs
|
||||
) -> Mobject:
|
||||
"""
|
||||
Add an updater to the animation's mobject which applies
|
||||
the interpolation and update functions of the animation
|
||||
@@ -94,7 +115,7 @@ def turn_animation_into_updater(animation, cycle=False, **kwargs):
|
||||
return mobject
|
||||
|
||||
|
||||
def cycle_animation(animation, **kwargs):
|
||||
def cycle_animation(animation: Animation, **kwargs) -> Mobject:
|
||||
return turn_animation_into_updater(
|
||||
animation, cycle=True, **kwargs
|
||||
)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Sequence
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import outer_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):
|
||||
@@ -40,7 +43,7 @@ class NumberLine(Line):
|
||||
"numbers_to_exclude": None
|
||||
}
|
||||
|
||||
def __init__(self, x_range=None, **kwargs):
|
||||
def __init__(self, x_range: Sequence[float] | None = None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
if x_range is None:
|
||||
x_range = self.x_range
|
||||
@@ -50,9 +53,9 @@ class NumberLine(Line):
|
||||
x_min, x_max, x_step = x_range
|
||||
# A lot of old scenes pass in x_min or x_max explicitly,
|
||||
# so this is just here to keep those workin
|
||||
self.x_min = kwargs.get("x_min", x_min)
|
||||
self.x_max = kwargs.get("x_max", x_max)
|
||||
self.x_step = kwargs.get("x_step", x_step)
|
||||
self.x_min: float = kwargs.get("x_min", x_min)
|
||||
self.x_max: float = kwargs.get("x_max", x_max)
|
||||
self.x_step: float = kwargs.get("x_step", x_step)
|
||||
|
||||
super().__init__(self.x_min * RIGHT, self.x_max * RIGHT, **kwargs)
|
||||
if self.width:
|
||||
@@ -73,24 +76,24 @@ class NumberLine(Line):
|
||||
if self.include_numbers:
|
||||
self.add_numbers(excluding=self.numbers_to_exclude)
|
||||
|
||||
def get_tick_range(self):
|
||||
def get_tick_range(self) -> np.ndarray:
|
||||
if self.include_tip:
|
||||
x_max = self.x_max
|
||||
else:
|
||||
x_max = self.x_max + self.x_step
|
||||
return np.arange(self.x_min, x_max, self.x_step)
|
||||
|
||||
def add_ticks(self):
|
||||
def add_ticks(self) -> None:
|
||||
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)
|
||||
self.ticks = ticks
|
||||
|
||||
def get_tick(self, x, size=None):
|
||||
def get_tick(self, x: float, size: float | None = None) -> Line:
|
||||
if size is None:
|
||||
size = self.tick_size
|
||||
result = Line(size * DOWN, size * UP)
|
||||
@@ -99,37 +102,42 @@ class NumberLine(Line):
|
||||
result.match_style(self)
|
||||
return result
|
||||
|
||||
def get_tick_marks(self):
|
||||
def get_tick_marks(self) -> VGroup:
|
||||
return self.ticks
|
||||
|
||||
def number_to_point(self, number):
|
||||
alpha = float(number - self.x_min) / (self.x_max - self.x_min)
|
||||
return interpolate(self.get_start(), self.get_end(), alpha)
|
||||
def number_to_point(self, number: float | np.ndarray) -> np.ndarray:
|
||||
alpha = (number - self.x_min) / (self.x_max - self.x_min)
|
||||
return outer_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)
|
||||
def point_to_number(self, point: np.ndarray) -> float:
|
||||
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)
|
||||
|
||||
def n2p(self, number):
|
||||
def n2p(self, number: float) -> np.ndarray:
|
||||
"""Abbreviation for number_to_point"""
|
||||
return self.number_to_point(number)
|
||||
|
||||
def p2n(self, point):
|
||||
def p2n(self, point: np.ndarray) -> float:
|
||||
"""Abbreviation for point_to_number"""
|
||||
return self.point_to_number(point)
|
||||
|
||||
def get_unit_size(self):
|
||||
def get_unit_size(self) -> float:
|
||||
return self.get_length() / (self.x_max - self.x_min)
|
||||
|
||||
def get_number_mobject(self, x,
|
||||
direction=None,
|
||||
buff=None,
|
||||
**number_config):
|
||||
def get_number_mobject(
|
||||
self,
|
||||
x: float,
|
||||
direction: np.ndarray | None = None,
|
||||
buff: float | None = None,
|
||||
**number_config
|
||||
) -> DecimalNumber:
|
||||
number_config = merge_dicts_recursively(
|
||||
self.decimal_number_config, number_config
|
||||
)
|
||||
@@ -144,21 +152,28 @@ 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
|
||||
|
||||
def add_numbers(self, x_values=None, excluding=None, font_size=24, **kwargs):
|
||||
def add_numbers(
|
||||
self,
|
||||
x_values: Iterable[float] | None = None,
|
||||
excluding: Iterable[float] | None = None,
|
||||
font_size: int = 24,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
if x_values is None:
|
||||
x_values = self.get_tick_range()
|
||||
|
||||
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))
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar, Type
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.svg.tex_mobject import SingleStringTex
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
string_to_mob_map = {}
|
||||
T = TypeVar("T", bound=VMobject)
|
||||
|
||||
|
||||
class DecimalNumber(VMobject):
|
||||
@@ -20,23 +23,25 @@ class DecimalNumber(VMobject):
|
||||
"include_background_rectangle": False,
|
||||
"edge_to_fix": LEFT,
|
||||
"font_size": 48,
|
||||
"text_config": {} # Do not pass in font_size here
|
||||
}
|
||||
|
||||
def __init__(self, number=0, **kwargs):
|
||||
def __init__(self, number: float | complex = 0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_submobjects_from_number(number)
|
||||
self.init_colors()
|
||||
|
||||
def set_submobjects_from_number(self, number):
|
||||
def set_submobjects_from_number(self, number: float | complex) -> None:
|
||||
self.number = number
|
||||
self.set_submobjects([])
|
||||
|
||||
string_to_mob_ = lambda s: self.string_to_mob(s, **self.text_config)
|
||||
num_string = self.get_num_string(number)
|
||||
self.add(*map(self.string_to_mob, num_string))
|
||||
self.add(*map(string_to_mob_, num_string))
|
||||
|
||||
# Add non-numerical bits
|
||||
if self.show_ellipsis:
|
||||
self.add(self.string_to_mob("..."))
|
||||
dots = 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)
|
||||
@@ -60,7 +65,7 @@ class DecimalNumber(VMobject):
|
||||
if self.include_background_rectangle:
|
||||
self.add_background_rectangle()
|
||||
|
||||
def get_num_string(self, number):
|
||||
def get_num_string(self, number: float | complex) -> str:
|
||||
if isinstance(number, complex):
|
||||
formatter = self.get_complex_formatter()
|
||||
else:
|
||||
@@ -76,21 +81,19 @@ class DecimalNumber(VMobject):
|
||||
num_string = num_string.replace("-", "–")
|
||||
return num_string
|
||||
|
||||
def init_data(self):
|
||||
def init_data(self) -> None:
|
||||
super().init_data()
|
||||
self.data["font_size"] = np.array([self.font_size], dtype=float)
|
||||
|
||||
def get_font_size(self):
|
||||
def get_font_size(self) -> float:
|
||||
return self.data["font_size"][0]
|
||||
|
||||
def string_to_mob(self, string, mob_class=Text):
|
||||
if string not in string_to_mob_map:
|
||||
string_to_mob_map[string] = mob_class(string, font_size=1)
|
||||
mob = string_to_mob_map[string].copy()
|
||||
def string_to_mob(self, string: str, mob_class: Type[T] = Text, **kwargs) -> T:
|
||||
mob = mob_class(string, font_size=1, **kwargs)
|
||||
mob.scale(self.get_font_size())
|
||||
return mob
|
||||
|
||||
def get_formatter(self, **kwargs):
|
||||
def get_formatter(self, **kwargs) -> str:
|
||||
"""
|
||||
Configuration is based first off instance attributes,
|
||||
but overwritten by any kew word argument. Relevant
|
||||
@@ -119,30 +122,29 @@ class DecimalNumber(VMobject):
|
||||
"}",
|
||||
])
|
||||
|
||||
def get_complex_formatter(self, **kwargs):
|
||||
def get_complex_formatter(self, **kwargs) -> str:
|
||||
return "".join([
|
||||
self.get_formatter(field_name="0.real"),
|
||||
self.get_formatter(field_name="0.imag", include_sign=True),
|
||||
"i"
|
||||
])
|
||||
|
||||
def set_value(self, number):
|
||||
def set_value(self, number: float | complex):
|
||||
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: float) -> None:
|
||||
self.data["font_size"] *= scale_factor
|
||||
return self
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> float | complex:
|
||||
return self.number
|
||||
|
||||
def increment_value(self, delta_t=1):
|
||||
def increment_value(self, delta_t: float | complex = 1) -> None:
|
||||
self.set_value(self.get_value() + delta_t)
|
||||
|
||||
|
||||
@@ -151,5 +153,5 @@ class Integer(DecimalNumber):
|
||||
"num_decimal_places": 0,
|
||||
}
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> int:
|
||||
return int(np.round(super().get_value()))
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Union, Sequence
|
||||
import colour
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
@@ -9,6 +14,8 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.color import color_gradient
|
||||
from manimlib.utils.iterables import listify
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
EPSILON = 0.0001
|
||||
|
||||
|
||||
@@ -24,7 +31,11 @@ class SampleSpace(Rectangle):
|
||||
"default_label_scale_val": 1,
|
||||
}
|
||||
|
||||
def add_title(self, title="Sample space", buff=MED_SMALL_BUFF):
|
||||
def add_title(
|
||||
self,
|
||||
title: str = "Sample space",
|
||||
buff: float = MED_SMALL_BUFF
|
||||
) -> None:
|
||||
# TODO, should this really exist in SampleSpaceScene
|
||||
title_mob = TexText(title)
|
||||
if title_mob.get_width() > self.get_width():
|
||||
@@ -33,17 +44,23 @@ class SampleSpace(Rectangle):
|
||||
self.title = title_mob
|
||||
self.add(title_mob)
|
||||
|
||||
def add_label(self, label):
|
||||
def add_label(self, label: str) -> None:
|
||||
self.label = label
|
||||
|
||||
def complete_p_list(self, p_list):
|
||||
def complete_p_list(self, p_list: list[float]) -> list[float]:
|
||||
new_p_list = listify(p_list)
|
||||
remainder = 1.0 - sum(new_p_list)
|
||||
if abs(remainder) > EPSILON:
|
||||
new_p_list.append(remainder)
|
||||
return new_p_list
|
||||
|
||||
def get_division_along_dimension(self, p_list, dim, colors, vect):
|
||||
def get_division_along_dimension(
|
||||
self,
|
||||
p_list: list[float],
|
||||
dim: int,
|
||||
colors: Iterable[ManimColor],
|
||||
vect: np.ndarray
|
||||
) -> VGroup:
|
||||
p_list = self.complete_p_list(p_list)
|
||||
colors = color_gradient(colors, len(p_list))
|
||||
|
||||
@@ -60,38 +77,41 @@ class SampleSpace(Rectangle):
|
||||
return parts
|
||||
|
||||
def get_horizontal_division(
|
||||
self, p_list,
|
||||
colors=[GREEN_E, BLUE_E],
|
||||
vect=DOWN
|
||||
):
|
||||
self,
|
||||
p_list: list[float],
|
||||
colors: Iterable[ManimColor] = [GREEN_E, BLUE_E],
|
||||
vect: np.ndarray = DOWN
|
||||
) -> VGroup:
|
||||
return self.get_division_along_dimension(p_list, 1, colors, vect)
|
||||
|
||||
def get_vertical_division(
|
||||
self, p_list,
|
||||
colors=[MAROON_B, YELLOW],
|
||||
vect=RIGHT
|
||||
):
|
||||
self,
|
||||
p_list: list[float],
|
||||
colors: Iterable[ManimColor] = [MAROON_B, YELLOW],
|
||||
vect: np.ndarray = RIGHT
|
||||
) -> VGroup:
|
||||
return self.get_division_along_dimension(p_list, 0, colors, vect)
|
||||
|
||||
def divide_horizontally(self, *args, **kwargs):
|
||||
def divide_horizontally(self, *args, **kwargs) -> None:
|
||||
self.horizontal_parts = self.get_horizontal_division(*args, **kwargs)
|
||||
self.add(self.horizontal_parts)
|
||||
|
||||
def divide_vertically(self, *args, **kwargs):
|
||||
def divide_vertically(self, *args, **kwargs) -> None:
|
||||
self.vertical_parts = self.get_vertical_division(*args, **kwargs)
|
||||
self.add(self.vertical_parts)
|
||||
|
||||
def get_subdivision_braces_and_labels(
|
||||
self, parts, labels, direction,
|
||||
buff=SMALL_BUFF,
|
||||
min_num_quads=1
|
||||
):
|
||||
self,
|
||||
parts: VGroup,
|
||||
labels: str,
|
||||
direction: np.ndarray,
|
||||
buff: float = SMALL_BUFF,
|
||||
) -> VGroup:
|
||||
label_mobs = VGroup()
|
||||
braces = VGroup()
|
||||
for label, part in zip(labels, parts):
|
||||
brace = Brace(
|
||||
part, direction,
|
||||
min_num_quads=min_num_quads,
|
||||
buff=buff
|
||||
)
|
||||
if isinstance(label, Mobject):
|
||||
@@ -112,22 +132,35 @@ class SampleSpace(Rectangle):
|
||||
}
|
||||
return VGroup(parts.braces, parts.labels)
|
||||
|
||||
def get_side_braces_and_labels(self, labels, direction=LEFT, **kwargs):
|
||||
def get_side_braces_and_labels(
|
||||
self,
|
||||
labels: str,
|
||||
direction: np.ndarray = LEFT,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
assert(hasattr(self, "horizontal_parts"))
|
||||
parts = self.horizontal_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, direction, **kwargs)
|
||||
|
||||
def get_top_braces_and_labels(self, labels, **kwargs):
|
||||
def get_top_braces_and_labels(
|
||||
self,
|
||||
labels: str,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
assert(hasattr(self, "vertical_parts"))
|
||||
parts = self.vertical_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, UP, **kwargs)
|
||||
|
||||
def get_bottom_braces_and_labels(self, labels, **kwargs):
|
||||
def get_bottom_braces_and_labels(
|
||||
self,
|
||||
labels: str,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
assert(hasattr(self, "vertical_parts"))
|
||||
parts = self.vertical_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, DOWN, **kwargs)
|
||||
|
||||
def add_braces_and_labels(self):
|
||||
def add_braces_and_labels(self) -> None:
|
||||
for attr in "horizontal_parts", "vertical_parts":
|
||||
if not hasattr(self, attr):
|
||||
continue
|
||||
@@ -136,7 +169,7 @@ class SampleSpace(Rectangle):
|
||||
if hasattr(parts, subattr):
|
||||
self.add(getattr(parts, subattr))
|
||||
|
||||
def __getitem__(self, index):
|
||||
def __getitem__(self, index: int | slice) -> VGroup:
|
||||
if hasattr(self, "horizontal_parts"):
|
||||
return self.horizontal_parts[index]
|
||||
elif hasattr(self, "vertical_parts"):
|
||||
@@ -149,7 +182,9 @@ class BarChart(VGroup):
|
||||
"height": 4,
|
||||
"width": 6,
|
||||
"n_ticks": 4,
|
||||
"include_x_ticks": False,
|
||||
"tick_width": 0.2,
|
||||
"tick_height": 0.15,
|
||||
"label_y_axis": True,
|
||||
"y_axis_label_height": 0.25,
|
||||
"max_value": 1,
|
||||
@@ -160,43 +195,55 @@ class BarChart(VGroup):
|
||||
"bar_label_scale_val": 0.75,
|
||||
}
|
||||
|
||||
def __init__(self, values, **kwargs):
|
||||
def __init__(self, values: Iterable[float], **kwargs):
|
||||
VGroup.__init__(self, **kwargs)
|
||||
if self.max_value is None:
|
||||
self.max_value = max(values)
|
||||
|
||||
self.n_ticks_x = len(values)
|
||||
self.add_axes()
|
||||
self.add_bars(values)
|
||||
self.center()
|
||||
|
||||
def add_axes(self):
|
||||
def add_axes(self) -> None:
|
||||
x_axis = Line(self.tick_width * LEFT / 2, self.width * RIGHT)
|
||||
y_axis = Line(MED_LARGE_BUFF * DOWN, self.height * UP)
|
||||
ticks = VGroup()
|
||||
y_ticks = VGroup()
|
||||
heights = np.linspace(0, self.height, self.n_ticks + 1)
|
||||
values = np.linspace(0, self.max_value, self.n_ticks + 1)
|
||||
for y, value in zip(heights, values):
|
||||
tick = Line(LEFT, RIGHT)
|
||||
tick.set_width(self.tick_width)
|
||||
tick.move_to(y * UP)
|
||||
ticks.add(tick)
|
||||
y_axis.add(ticks)
|
||||
y_tick = Line(LEFT, RIGHT)
|
||||
y_tick.set_width(self.tick_width)
|
||||
y_tick.move_to(y * UP)
|
||||
y_ticks.add(y_tick)
|
||||
y_axis.add(y_ticks)
|
||||
|
||||
if self.include_x_ticks == True:
|
||||
x_ticks = VGroup()
|
||||
widths = np.linspace(0, self.width, self.n_ticks_x + 1)
|
||||
label_values = np.linspace(0, len(self.bar_names), self.n_ticks_x + 1)
|
||||
for x, value in zip(widths, label_values):
|
||||
x_tick = Line(UP, DOWN)
|
||||
x_tick.set_height(self.tick_height)
|
||||
x_tick.move_to(x * RIGHT)
|
||||
x_ticks.add(x_tick)
|
||||
x_axis.add(x_ticks)
|
||||
|
||||
self.add(x_axis, y_axis)
|
||||
self.x_axis, self.y_axis = x_axis, y_axis
|
||||
|
||||
if self.label_y_axis:
|
||||
labels = VGroup()
|
||||
for tick, value in zip(ticks, values):
|
||||
for y_tick, value in zip(y_ticks, values):
|
||||
label = Tex(str(np.round(value, 2)))
|
||||
label.set_height(self.y_axis_label_height)
|
||||
label.next_to(tick, LEFT, SMALL_BUFF)
|
||||
label.next_to(y_tick, LEFT, SMALL_BUFF)
|
||||
labels.add(label)
|
||||
self.y_axis_labels = labels
|
||||
self.add(labels)
|
||||
|
||||
def add_bars(self, values):
|
||||
buff = float(self.width) / (2 * len(values) + 1)
|
||||
def add_bars(self, values: Iterable[float]) -> None:
|
||||
buff = float(self.width) / (2 * len(values))
|
||||
bars = VGroup()
|
||||
for i, value in enumerate(values):
|
||||
bar = Rectangle(
|
||||
@@ -205,7 +252,7 @@ class BarChart(VGroup):
|
||||
stroke_width=self.bar_stroke_width,
|
||||
fill_opacity=self.bar_fill_opacity,
|
||||
)
|
||||
bar.move_to((2 * i + 1) * buff * RIGHT, DOWN + LEFT)
|
||||
bar.move_to((2 * i + 0.5) * buff * RIGHT, DOWN + LEFT * 5)
|
||||
bars.add(bar)
|
||||
bars.set_color_by_gradient(*self.bar_colors)
|
||||
|
||||
@@ -220,7 +267,7 @@ class BarChart(VGroup):
|
||||
self.bars = bars
|
||||
self.bar_labels = bar_labels
|
||||
|
||||
def change_bar_values(self, values):
|
||||
def change_bar_values(self, values: Iterable[float]) -> None:
|
||||
for bar, value in zip(self.bars, values):
|
||||
bar_bottom = bar.get_bottom()
|
||||
bar.stretch_to_fit_height(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
@@ -7,6 +9,13 @@ from manimlib.utils.color import Color
|
||||
from manimlib.utils.customization import get_customization
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Union, Sequence
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
ManimColor = Union[str, Color, Sequence[float]]
|
||||
|
||||
|
||||
class SurroundingRectangle(Rectangle):
|
||||
CONFIG = {
|
||||
@@ -14,7 +23,7 @@ class SurroundingRectangle(Rectangle):
|
||||
"buff": SMALL_BUFF,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
kwargs["width"] = mobject.get_width() + 2 * self.buff
|
||||
kwargs["height"] = mobject.get_height() + 2 * self.buff
|
||||
@@ -30,24 +39,25 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||
"buff": 0
|
||||
}
|
||||
|
||||
def __init__(self, mobject, color=None, **kwargs):
|
||||
def __init__(self, mobject: Mobject, color: ManimColor = None, **kwargs):
|
||||
if color is None:
|
||||
color = get_customization()['style']['background_color']
|
||||
SurroundingRectangle.__init__(self, mobject, color=color, **kwargs)
|
||||
self.original_fill_opacity = self.fill_opacity
|
||||
|
||||
def pointwise_become_partial(self, mobject, a, b):
|
||||
def pointwise_become_partial(self, mobject: Mobject, a: float, b: float):
|
||||
self.set_fill(opacity=b * self.original_fill_opacity)
|
||||
return self
|
||||
|
||||
def set_style_data(self,
|
||||
stroke_color=None,
|
||||
stroke_width=None,
|
||||
fill_color=None,
|
||||
fill_opacity=None,
|
||||
family=True
|
||||
):
|
||||
# Unchangable style, except for fill_opacity
|
||||
def set_style_data(
|
||||
self,
|
||||
stroke_color: ManimColor | None = None,
|
||||
stroke_width: float | None = None,
|
||||
fill_color: ManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
family: bool = True
|
||||
):
|
||||
# Unchangeable style, except for fill_opacity
|
||||
VMobject.set_style_data(
|
||||
self,
|
||||
stroke_color=BLACK,
|
||||
@@ -57,23 +67,24 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||
)
|
||||
return self
|
||||
|
||||
def get_fill_color(self):
|
||||
def get_fill_color(self) -> Color:
|
||||
return Color(self.color)
|
||||
|
||||
|
||||
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),
|
||||
)
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
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):
|
||||
@@ -81,7 +92,7 @@ class Underline(Line):
|
||||
"buff": SMALL_BUFF,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(LEFT, RIGHT, **kwargs)
|
||||
self.match_width(mobject)
|
||||
self.next_to(mobject, DOWN, buff=self.buff)
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import numpy as np
|
||||
import math
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import copy
|
||||
from typing import Iterable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.constants import *
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.animation.growing import GrowFromCenter
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
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
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
class Brace(SingleStringTex):
|
||||
CONFIG = {
|
||||
@@ -19,7 +30,12 @@ class Brace(SingleStringTex):
|
||||
"tex_string": r"\underbrace{\qquad}"
|
||||
}
|
||||
|
||||
def __init__(self, mobject, direction=DOWN, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
direction: np.ndarray = DOWN,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs, locals())
|
||||
angle = -math.atan2(*direction[:2]) + PI
|
||||
mobject.rotate(-angle, about_point=ORIGIN)
|
||||
@@ -34,7 +50,7 @@ class Brace(SingleStringTex):
|
||||
for mob in mobject, self:
|
||||
mob.rotate(angle, about_point=ORIGIN)
|
||||
|
||||
def set_initial_width(self, width):
|
||||
def set_initial_width(self, width: float):
|
||||
width_diff = width - self.get_width()
|
||||
if width_diff > 0:
|
||||
for tip, rect, vect in [(self[0], self[1], RIGHT), (self[5], self[4], LEFT)]:
|
||||
@@ -47,7 +63,12 @@ class Brace(SingleStringTex):
|
||||
self.set_width(width, stretch=True)
|
||||
return self
|
||||
|
||||
def put_at_tip(self, mob, use_next_to=True, **kwargs):
|
||||
def put_at_tip(
|
||||
self,
|
||||
mob: Mobject,
|
||||
use_next_to: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
if use_next_to:
|
||||
mob.next_to(
|
||||
self.get_tip(),
|
||||
@@ -61,23 +82,24 @@ 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: str, **kwargs) -> Text:
|
||||
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):
|
||||
def get_tex(self, *tex: str, **kwargs) -> Tex:
|
||||
tex_mob = Tex(*tex)
|
||||
self.put_at_tip(tex_mob, **kwargs)
|
||||
return tex_mob
|
||||
|
||||
def get_tip(self):
|
||||
def get_tip(self) -> np.ndarray:
|
||||
# Very specific to the LaTeX representation
|
||||
# of a brace, but it's the only way I can think
|
||||
# of to get the tip regardless of orientation.
|
||||
return self.get_all_points()[self.tip_point_index]
|
||||
|
||||
def get_direction(self):
|
||||
def get_direction(self) -> np.ndarray:
|
||||
vect = self.get_tip() - self.get_center()
|
||||
return vect / get_norm(vect)
|
||||
|
||||
@@ -86,29 +108,40 @@ class BraceLabel(VMobject):
|
||||
CONFIG = {
|
||||
"label_constructor": Tex,
|
||||
"label_scale": 1,
|
||||
"label_buff": DEFAULT_MOBJECT_TO_MOBJECT_BUFFER
|
||||
}
|
||||
|
||||
def __init__(self, obj, text, brace_direction=DOWN, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
obj: list[VMobject] | Mobject,
|
||||
text: Iterable[str] | str,
|
||||
brace_direction: np.ndarray = DOWN,
|
||||
**kwargs
|
||||
) -> None:
|
||||
VMobject.__init__(self, **kwargs)
|
||||
self.brace_direction = brace_direction
|
||||
if isinstance(obj, list):
|
||||
obj = VMobject(*obj)
|
||||
self.brace = Brace(obj, brace_direction, **kwargs)
|
||||
|
||||
if isinstance(text, tuple) or isinstance(text, list):
|
||||
if isinstance(text, Iterable):
|
||||
self.label = self.label_constructor(*text, **kwargs)
|
||||
else:
|
||||
self.label = self.label_constructor(str(text))
|
||||
if self.label_scale != 1:
|
||||
self.label.scale(self.label_scale)
|
||||
|
||||
self.brace.put_at_tip(self.label)
|
||||
self.brace.put_at_tip(self.label, buff=self.label_buff)
|
||||
self.set_submobjects([self.brace, self.label])
|
||||
|
||||
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):
|
||||
def creation_anim(
|
||||
self,
|
||||
label_anim: Animation = FadeIn,
|
||||
brace_anim: Animation=GrowFromCenter
|
||||
) -> AnimationGroup:
|
||||
return AnimationGroup(brace_anim(self.brace), label_anim(self.label))
|
||||
|
||||
def shift_brace(self, obj, **kwargs):
|
||||
def shift_brace(self, obj: list[VMobject] | Mobject, **kwargs):
|
||||
if isinstance(obj, list):
|
||||
obj = VMobject(*obj)
|
||||
self.brace = Brace(obj, self.brace_direction, **kwargs)
|
||||
@@ -116,7 +149,7 @@ class BraceLabel(VMobject):
|
||||
self.submobjects[0] = self.brace
|
||||
return self
|
||||
|
||||
def change_label(self, *text, **kwargs):
|
||||
def change_label(self, *text: str, **kwargs):
|
||||
self.label = self.label_constructor(*text, **kwargs)
|
||||
if self.label_scale != 1:
|
||||
self.label.scale(self.label_scale)
|
||||
@@ -125,7 +158,7 @@ class BraceLabel(VMobject):
|
||||
self.submobjects[1] = self.label
|
||||
return self
|
||||
|
||||
def change_brace_label(self, obj, *text):
|
||||
def change_brace_label(self, obj: list[VMobject] | Mobject, *text: str):
|
||||
self.shift_brace(obj)
|
||||
self.change_label(*text)
|
||||
return self
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.rotation import Rotating
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.boolean_ops import Difference
|
||||
from manimlib.mobject.geometry import Arc
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Line
|
||||
@@ -12,6 +13,7 @@ from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.three_dimensions import Cube
|
||||
from manimlib.mobject.three_dimensions import Prismify
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
@@ -19,6 +21,7 @@ from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
from manimlib.utils.space_ops import complex_to_R3
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
from manimlib.utils.space_ops import midpoint
|
||||
|
||||
|
||||
class Checkmark(TexText):
|
||||
@@ -41,7 +44,6 @@ class Exmark(TexText):
|
||||
|
||||
class Lightbulb(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name": "lightbulb",
|
||||
"height": 1,
|
||||
"stroke_color": YELLOW,
|
||||
"stroke_width": 3,
|
||||
@@ -49,6 +51,10 @@ class Lightbulb(SVGMobject):
|
||||
"fill_opacity": 0,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("lightbulb", **kwargs)
|
||||
self.insert_n_curves(25)
|
||||
|
||||
|
||||
class Speedometer(VMobject):
|
||||
CONFIG = {
|
||||
@@ -198,12 +204,11 @@ class Laptop(VGroup):
|
||||
|
||||
class VideoIcon(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name": "video_icon",
|
||||
"width": FRAME_WIDTH / 12.,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
SVGMobject.__init__(self, **kwargs)
|
||||
super().__init__(file_name="video_icon", **kwargs)
|
||||
self.center()
|
||||
self.set_width(self.width)
|
||||
self.set_stroke(color=WHITE, width=0)
|
||||
@@ -431,3 +436,84 @@ class VectorizedEarth(SVGMobject):
|
||||
)
|
||||
circle.replace(self)
|
||||
self.add_to_back(circle)
|
||||
|
||||
|
||||
class Piano(VGroup):
|
||||
n_white_keys = 52
|
||||
black_pattern = [0, 2, 3, 5, 6]
|
||||
white_keys_per_octave = 7
|
||||
white_key_dims = (0.15, 1.0)
|
||||
black_key_dims = (0.1, 0.66)
|
||||
key_buff = 0.02
|
||||
white_key_color = WHITE
|
||||
black_key_color = GREY_E
|
||||
total_width = 13
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.add_white_keys()
|
||||
self.add_black_keys()
|
||||
self.sort_keys()
|
||||
self[:-1].reverse_points()
|
||||
self.set_width(self.total_width)
|
||||
|
||||
def add_white_keys(self):
|
||||
key = Rectangle(*self.white_key_dims)
|
||||
key.set_fill(self.white_key_color, 1)
|
||||
key.set_stroke(width=0)
|
||||
self.white_keys = key.get_grid(1, self.n_white_keys, buff=self.key_buff)
|
||||
self.add(*self.white_keys)
|
||||
|
||||
def add_black_keys(self):
|
||||
key = Rectangle(*self.black_key_dims)
|
||||
key.set_fill(self.black_key_color, 1)
|
||||
key.set_stroke(width=0)
|
||||
|
||||
self.black_keys = VGroup()
|
||||
for i in range(len(self.white_keys) - 1):
|
||||
if i % self.white_keys_per_octave not in self.black_pattern:
|
||||
continue
|
||||
wk1 = self.white_keys[i]
|
||||
wk2 = self.white_keys[i + 1]
|
||||
bk = key.copy()
|
||||
bk.move_to(midpoint(wk1.get_top(), wk2.get_top()), UP)
|
||||
big_bk = bk.copy()
|
||||
big_bk.stretch((bk.get_width() + self.key_buff) / bk.get_width(), 0)
|
||||
big_bk.stretch((bk.get_height() + self.key_buff) / bk.get_height(), 1)
|
||||
big_bk.move_to(bk, UP)
|
||||
for wk in wk1, wk2:
|
||||
wk.become(Difference(wk, big_bk).match_style(wk))
|
||||
self.black_keys.add(bk)
|
||||
self.add(*self.black_keys)
|
||||
|
||||
def sort_keys(self):
|
||||
self.sort(lambda p: p[0])
|
||||
|
||||
|
||||
class Piano3D(VGroup):
|
||||
CONFIG = {
|
||||
"depth_test": True,
|
||||
"reflectiveness": 1.0,
|
||||
"stroke_width": 0.25,
|
||||
"stroke_color": BLACK,
|
||||
"key_depth": 0.1,
|
||||
"black_key_shift": 0.05,
|
||||
}
|
||||
piano_2d_config = {
|
||||
"white_key_color": GREY_A,
|
||||
"key_buff": 0.001
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
piano_2d = Piano(**self.piano_2d_config)
|
||||
super().__init__(*(
|
||||
Prismify(key, self.key_depth)
|
||||
for key in piano_2d
|
||||
))
|
||||
self.set_stroke(self.stroke_color, self.stroke_width)
|
||||
self.apply_depth_test()
|
||||
# Elevate black keys
|
||||
for i, key in enumerate(self):
|
||||
if piano_2d[i] in piano_2d.black_keys:
|
||||
key.shift(self.black_key_shift * OUT)
|
||||
|
||||
546
manimlib/mobject/svg/labelled_string.py
Normal file
546
manimlib/mobject/svg/labelled_string.py
Normal file
@@ -0,0 +1,546 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import colour
|
||||
import itertools as it
|
||||
from typing import Iterable, Union, Sequence
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from manimlib.constants import BLACK, WHITE
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.color import color_to_int_rgb
|
||||
from manimlib.utils.color import color_to_rgb
|
||||
from manimlib.utils.color import rgb_to_hex
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
Span = tuple[int, int]
|
||||
|
||||
|
||||
class _StringSVG(SVGMobject):
|
||||
CONFIG = {
|
||||
"height": None,
|
||||
"stroke_width": 0,
|
||||
"stroke_color": WHITE,
|
||||
"path_string_config": {
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class LabelledString(_StringSVG, ABC):
|
||||
"""
|
||||
An abstract base class for `MTex` and `MarkupText`
|
||||
"""
|
||||
CONFIG = {
|
||||
"base_color": WHITE,
|
||||
"use_plain_file": False,
|
||||
"isolate": [],
|
||||
}
|
||||
|
||||
def __init__(self, string: str, **kwargs):
|
||||
self.string = string
|
||||
digest_config(self, kwargs)
|
||||
|
||||
# Convert `base_color` to hex code.
|
||||
self.base_color = rgb_to_hex(color_to_rgb(
|
||||
self.base_color \
|
||||
or self.svg_default.get("color", None) \
|
||||
or self.svg_default.get("fill_color", None) \
|
||||
or WHITE
|
||||
))
|
||||
self.svg_default["fill_color"] = BLACK
|
||||
|
||||
self.pre_parse()
|
||||
self.parse()
|
||||
super().__init__()
|
||||
self.post_parse()
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
return self.get_file_path_(use_plain_file=False)
|
||||
|
||||
def get_file_path_(self, use_plain_file: bool) -> str:
|
||||
content = self.get_content(use_plain_file)
|
||||
return self.get_file_path_by_content(content)
|
||||
|
||||
@abstractmethod
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
return ""
|
||||
|
||||
def generate_mobject(self) -> None:
|
||||
super().generate_mobject()
|
||||
|
||||
submob_labels = [
|
||||
self.color_to_label(submob.get_fill_color())
|
||||
for submob in self.submobjects
|
||||
]
|
||||
if self.use_plain_file or self.has_predefined_local_colors:
|
||||
file_path = self.get_file_path_(use_plain_file=True)
|
||||
plain_svg = _StringSVG(
|
||||
file_path,
|
||||
svg_default=self.svg_default,
|
||||
path_string_config=self.path_string_config
|
||||
)
|
||||
self.set_submobjects(plain_svg.submobjects)
|
||||
else:
|
||||
self.set_fill(self.base_color)
|
||||
for submob, label in zip(self.submobjects, submob_labels):
|
||||
submob.label = label
|
||||
|
||||
def pre_parse(self) -> None:
|
||||
self.string_len = len(self.string)
|
||||
self.full_span = (0, self.string_len)
|
||||
|
||||
def parse(self) -> None:
|
||||
self.command_repl_items = self.get_command_repl_items()
|
||||
self.command_spans = self.get_command_spans()
|
||||
self.extra_entity_spans = self.get_extra_entity_spans()
|
||||
self.entity_spans = self.get_entity_spans()
|
||||
self.extra_ignored_spans = self.get_extra_ignored_spans()
|
||||
self.skipped_spans = self.get_skipped_spans()
|
||||
self.internal_specified_spans = self.get_internal_specified_spans()
|
||||
self.external_specified_spans = self.get_external_specified_spans()
|
||||
self.specified_spans = self.get_specified_spans()
|
||||
self.label_span_list = self.get_label_span_list()
|
||||
self.check_overlapping()
|
||||
|
||||
def post_parse(self) -> None:
|
||||
self.labelled_submobject_items = [
|
||||
(submob.label, submob)
|
||||
for submob in self.submobjects
|
||||
]
|
||||
self.labelled_submobjects = self.get_labelled_submobjects()
|
||||
self.specified_substrs = self.get_specified_substrs()
|
||||
self.group_items = self.get_group_items()
|
||||
self.group_substrs = self.get_group_substrs()
|
||||
self.submob_groups = self.get_submob_groups()
|
||||
|
||||
def copy(self):
|
||||
return self.deepcopy()
|
||||
|
||||
# Toolkits
|
||||
|
||||
def get_substr(self, span: Span) -> str:
|
||||
return self.string[slice(*span)]
|
||||
|
||||
def finditer(
|
||||
self, pattern: str, flags: int = 0, **kwargs
|
||||
) -> Iterable[re.Match]:
|
||||
return re.compile(pattern, flags).finditer(self.string, **kwargs)
|
||||
|
||||
def search(
|
||||
self, pattern: str, flags: int = 0, **kwargs
|
||||
) -> re.Match | None:
|
||||
return re.compile(pattern, flags).search(self.string, **kwargs)
|
||||
|
||||
def match(
|
||||
self, pattern: str, flags: int = 0, **kwargs
|
||||
) -> re.Match | None:
|
||||
return re.compile(pattern, flags).match(self.string, **kwargs)
|
||||
|
||||
def find_spans(self, pattern: str, **kwargs) -> list[Span]:
|
||||
return [
|
||||
match_obj.span()
|
||||
for match_obj in self.finditer(pattern, **kwargs)
|
||||
]
|
||||
|
||||
def find_substr(self, substr: str, **kwargs) -> list[Span]:
|
||||
if not substr:
|
||||
return []
|
||||
return self.find_spans(re.escape(substr), **kwargs)
|
||||
|
||||
def find_substrs(self, substrs: list[str], **kwargs) -> list[Span]:
|
||||
return list(it.chain(*[
|
||||
self.find_substr(substr, **kwargs)
|
||||
for substr in remove_list_redundancies(substrs)
|
||||
]))
|
||||
|
||||
@staticmethod
|
||||
def get_neighbouring_pairs(iterable: list) -> list[tuple]:
|
||||
return list(zip(iterable[:-1], iterable[1:]))
|
||||
|
||||
@staticmethod
|
||||
def span_contains(span_0: Span, span_1: Span) -> bool:
|
||||
return span_0[0] <= span_1[0] and span_0[1] >= span_1[1]
|
||||
|
||||
@staticmethod
|
||||
def get_complement_spans(
|
||||
interval_spans: list[Span], universal_span: Span
|
||||
) -> list[Span]:
|
||||
if not interval_spans:
|
||||
return [universal_span]
|
||||
|
||||
span_ends, span_begins = zip(*interval_spans)
|
||||
return list(zip(
|
||||
(universal_span[0], *span_begins),
|
||||
(*span_ends, universal_span[1])
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def compress_neighbours(vals: list[int]) -> list[tuple[int, Span]]:
|
||||
if not vals:
|
||||
return []
|
||||
|
||||
unique_vals = [vals[0]]
|
||||
indices = [0]
|
||||
for index, val in enumerate(vals):
|
||||
if val == unique_vals[-1]:
|
||||
continue
|
||||
unique_vals.append(val)
|
||||
indices.append(index)
|
||||
indices.append(len(vals))
|
||||
spans = LabelledString.get_neighbouring_pairs(indices)
|
||||
return list(zip(unique_vals, spans))
|
||||
|
||||
@staticmethod
|
||||
def find_region_index(seq: list[int], val: int) -> int:
|
||||
# Returns an integer in `range(-1, len(seq))` satisfying
|
||||
# `seq[result] <= val < seq[result + 1]`.
|
||||
# `seq` should be sorted in ascending order.
|
||||
if not seq or val < seq[0]:
|
||||
return -1
|
||||
result = len(seq) - 1
|
||||
while val < seq[result]:
|
||||
result -= 1
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def take_nearest_value(seq: list[int], val: int, index_shift: int) -> int:
|
||||
sorted_seq = sorted(seq)
|
||||
index = LabelledString.find_region_index(sorted_seq, val)
|
||||
return sorted_seq[index + index_shift]
|
||||
|
||||
@staticmethod
|
||||
def generate_span_repl_dict(
|
||||
inserted_string_pairs: list[tuple[Span, tuple[str, str]]],
|
||||
other_repl_items: list[tuple[Span, str]]
|
||||
) -> dict[Span, str]:
|
||||
result = dict(other_repl_items)
|
||||
if not inserted_string_pairs:
|
||||
return result
|
||||
|
||||
indices, _, _, inserted_strings = zip(*sorted([
|
||||
(
|
||||
span[flag],
|
||||
-flag,
|
||||
-span[1 - flag],
|
||||
str_pair[flag]
|
||||
)
|
||||
for span, str_pair in inserted_string_pairs
|
||||
for flag in range(2)
|
||||
]))
|
||||
result.update({
|
||||
(index, index): "".join(inserted_strings[slice(*item_span)])
|
||||
for index, item_span
|
||||
in LabelledString.compress_neighbours(indices)
|
||||
})
|
||||
return result
|
||||
|
||||
def get_replaced_substr(
|
||||
self, span: Span, span_repl_dict: dict[Span, str]
|
||||
) -> str:
|
||||
repl_spans = sorted(filter(
|
||||
lambda repl_span: self.span_contains(span, repl_span),
|
||||
span_repl_dict.keys()
|
||||
))
|
||||
if not all(
|
||||
span_0[1] <= span_1[0]
|
||||
for span_0, span_1 in self.get_neighbouring_pairs(repl_spans)
|
||||
):
|
||||
raise ValueError("Overlapping replacement")
|
||||
|
||||
pieces = [
|
||||
self.get_substr(piece_span)
|
||||
for piece_span in self.get_complement_spans(repl_spans, span)
|
||||
]
|
||||
repl_strs = [span_repl_dict[repl_span] for repl_span in repl_spans]
|
||||
repl_strs.append("")
|
||||
return "".join(it.chain(*zip(pieces, repl_strs)))
|
||||
|
||||
@staticmethod
|
||||
def rslide(index: int, skipped: list[Span]) -> int:
|
||||
transfer_dict = dict(sorted(skipped))
|
||||
while index in transfer_dict.keys():
|
||||
index = transfer_dict[index]
|
||||
return index
|
||||
|
||||
@staticmethod
|
||||
def lslide(index: int, skipped: list[Span]) -> int:
|
||||
transfer_dict = dict(sorted([
|
||||
skipped_span[::-1] for skipped_span in skipped
|
||||
], reverse=True))
|
||||
while index in transfer_dict.keys():
|
||||
index = transfer_dict[index]
|
||||
return index
|
||||
|
||||
@staticmethod
|
||||
def rgb_to_int(rgb_tuple: tuple[int, int, int]) -> int:
|
||||
r, g, b = rgb_tuple
|
||||
rg = r * 256 + g
|
||||
return rg * 256 + b
|
||||
|
||||
@staticmethod
|
||||
def int_to_rgb(rgb_int: int) -> tuple[int, int, int]:
|
||||
rg, b = divmod(rgb_int, 256)
|
||||
r, g = divmod(rg, 256)
|
||||
return r, g, b
|
||||
|
||||
@staticmethod
|
||||
def int_to_hex(rgb_int: int) -> str:
|
||||
return "#{:06x}".format(rgb_int).upper()
|
||||
|
||||
@staticmethod
|
||||
def hex_to_int(rgb_hex: str) -> int:
|
||||
return int(rgb_hex[1:], 16)
|
||||
|
||||
@staticmethod
|
||||
def color_to_label(color: ManimColor) -> int:
|
||||
rgb_tuple = color_to_int_rgb(color)
|
||||
rgb = LabelledString.rgb_to_int(rgb_tuple)
|
||||
return rgb - 1
|
||||
|
||||
# Parsing
|
||||
|
||||
@abstractmethod
|
||||
def get_command_repl_items(self) -> list[tuple[Span, str]]:
|
||||
return []
|
||||
|
||||
def get_command_spans(self) -> list[Span]:
|
||||
return [cmd_span for cmd_span, _ in self.command_repl_items]
|
||||
|
||||
@abstractmethod
|
||||
def get_extra_entity_spans(self) -> list[Span]:
|
||||
return []
|
||||
|
||||
def get_entity_spans(self) -> list[Span]:
|
||||
return list(it.chain(
|
||||
self.command_spans,
|
||||
self.extra_entity_spans
|
||||
))
|
||||
|
||||
@abstractmethod
|
||||
def get_extra_ignored_spans(self) -> list[int]:
|
||||
return []
|
||||
|
||||
def get_skipped_spans(self) -> list[Span]:
|
||||
return list(it.chain(
|
||||
self.find_spans(r"\s"),
|
||||
self.command_spans,
|
||||
self.extra_ignored_spans
|
||||
))
|
||||
|
||||
def shrink_span(self, span: Span) -> Span:
|
||||
return (
|
||||
self.rslide(span[0], self.skipped_spans),
|
||||
self.lslide(span[1], self.skipped_spans)
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_internal_specified_spans(self) -> list[Span]:
|
||||
return []
|
||||
|
||||
@abstractmethod
|
||||
def get_external_specified_spans(self) -> list[Span]:
|
||||
return []
|
||||
|
||||
def get_specified_spans(self) -> list[Span]:
|
||||
spans = list(it.chain(
|
||||
self.internal_specified_spans,
|
||||
self.external_specified_spans,
|
||||
self.find_substrs(self.isolate)
|
||||
))
|
||||
shrinked_spans = list(filter(
|
||||
lambda span: span[0] < span[1] and not any([
|
||||
entity_span[0] < index < entity_span[1]
|
||||
for index in span
|
||||
for entity_span in self.entity_spans
|
||||
]),
|
||||
[self.shrink_span(span) for span in spans]
|
||||
))
|
||||
return remove_list_redundancies(shrinked_spans)
|
||||
|
||||
@abstractmethod
|
||||
def get_label_span_list(self) -> list[Span]:
|
||||
return []
|
||||
|
||||
def check_overlapping(self) -> None:
|
||||
for span_0, span_1 in it.product(self.label_span_list, repeat=2):
|
||||
if not span_0[0] < span_1[0] < span_0[1] < span_1[1]:
|
||||
continue
|
||||
raise ValueError(
|
||||
"Partially overlapping substrings detected: "
|
||||
f"'{self.get_substr(span_0)}' and '{self.get_substr(span_1)}'"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_content(self, use_plain_file: bool) -> str:
|
||||
return ""
|
||||
|
||||
@abstractmethod
|
||||
def has_predefined_local_colors(self) -> bool:
|
||||
return False
|
||||
|
||||
# Post-parsing
|
||||
|
||||
def get_labelled_submobjects(self) -> list[VMobject]:
|
||||
return [submob for _, submob in self.labelled_submobject_items]
|
||||
|
||||
def get_cleaned_substr(self, span: Span) -> str:
|
||||
span_repl_dict = dict.fromkeys(self.command_spans, "")
|
||||
return self.get_replaced_substr(span, span_repl_dict)
|
||||
|
||||
def get_specified_substrs(self) -> list[str]:
|
||||
return remove_list_redundancies([
|
||||
self.get_cleaned_substr(span)
|
||||
for span in self.specified_spans
|
||||
])
|
||||
|
||||
def get_group_items(self) -> list[tuple[str, VGroup]]:
|
||||
if not self.labelled_submobject_items:
|
||||
return []
|
||||
|
||||
labels, labelled_submobjects = zip(*self.labelled_submobject_items)
|
||||
group_labels, labelled_submob_spans = zip(
|
||||
*self.compress_neighbours(labels)
|
||||
)
|
||||
ordered_spans = [
|
||||
self.label_span_list[label] if label != -1 else self.full_span
|
||||
for label in group_labels
|
||||
]
|
||||
interval_spans = [
|
||||
(
|
||||
next_span[0]
|
||||
if self.span_contains(prev_span, next_span)
|
||||
else prev_span[1],
|
||||
prev_span[1]
|
||||
if self.span_contains(next_span, prev_span)
|
||||
else next_span[0]
|
||||
)
|
||||
for prev_span, next_span in self.get_neighbouring_pairs(
|
||||
ordered_spans
|
||||
)
|
||||
]
|
||||
shrinked_spans = [
|
||||
self.shrink_span(span)
|
||||
for span in self.get_complement_spans(
|
||||
interval_spans, (ordered_spans[0][0], ordered_spans[-1][1])
|
||||
)
|
||||
]
|
||||
group_substrs = [
|
||||
self.get_cleaned_substr(span) if span[0] < span[1] else ""
|
||||
for span in shrinked_spans
|
||||
]
|
||||
submob_groups = VGroup(*[
|
||||
VGroup(*labelled_submobjects[slice(*submob_span)])
|
||||
for submob_span in labelled_submob_spans
|
||||
])
|
||||
return list(zip(group_substrs, submob_groups))
|
||||
|
||||
def get_group_substrs(self) -> list[str]:
|
||||
return [group_substr for group_substr, _ in self.group_items]
|
||||
|
||||
def get_submob_groups(self) -> list[VGroup]:
|
||||
return [submob_group for _, submob_group in self.group_items]
|
||||
|
||||
def get_parts_by_group_substr(self, substr: str) -> VGroup:
|
||||
return VGroup(*[
|
||||
group
|
||||
for group_substr, group in self.group_items
|
||||
if group_substr == substr
|
||||
])
|
||||
|
||||
# Selector
|
||||
|
||||
def find_span_components(
|
||||
self, custom_span: Span, substring: bool = True
|
||||
) -> list[Span]:
|
||||
shrinked_span = self.shrink_span(custom_span)
|
||||
if shrinked_span[0] >= shrinked_span[1]:
|
||||
return []
|
||||
|
||||
if substring:
|
||||
indices = remove_list_redundancies(list(it.chain(
|
||||
self.full_span,
|
||||
*self.label_span_list
|
||||
)))
|
||||
span_begin = self.take_nearest_value(
|
||||
indices, shrinked_span[0], 0
|
||||
)
|
||||
span_end = self.take_nearest_value(
|
||||
indices, shrinked_span[1] - 1, 1
|
||||
)
|
||||
else:
|
||||
span_begin, span_end = shrinked_span
|
||||
|
||||
span_choices = sorted(filter(
|
||||
lambda span: self.span_contains((span_begin, span_end), span),
|
||||
self.label_span_list
|
||||
))
|
||||
# Choose spans that reach the farthest.
|
||||
span_choices_dict = dict(span_choices)
|
||||
|
||||
result = []
|
||||
while span_begin < span_end:
|
||||
if span_begin not in span_choices_dict.keys():
|
||||
span_begin += 1
|
||||
continue
|
||||
next_begin = span_choices_dict[span_begin]
|
||||
result.append((span_begin, next_begin))
|
||||
span_begin = next_begin
|
||||
return result
|
||||
|
||||
def get_part_by_custom_span(self, custom_span: Span, **kwargs) -> VGroup:
|
||||
labels = [
|
||||
label for label, span in enumerate(self.label_span_list)
|
||||
if any([
|
||||
self.span_contains(span_component, span)
|
||||
for span_component in self.find_span_components(
|
||||
custom_span, **kwargs
|
||||
)
|
||||
])
|
||||
]
|
||||
return VGroup(*[
|
||||
submob for label, submob in self.labelled_submobject_items
|
||||
if label in labels
|
||||
])
|
||||
|
||||
def get_parts_by_string(
|
||||
self, substr: str,
|
||||
case_sensitive: bool = True, regex: bool = False, **kwargs
|
||||
) -> VGroup:
|
||||
flags = 0
|
||||
if not case_sensitive:
|
||||
flags |= re.I
|
||||
pattern = substr if regex else re.escape(substr)
|
||||
return VGroup(*[
|
||||
self.get_part_by_custom_span(span, **kwargs)
|
||||
for span in self.find_spans(pattern, flags=flags)
|
||||
if span[0] < span[1]
|
||||
])
|
||||
|
||||
def get_part_by_string(
|
||||
self, substr: str, index: int = 0, **kwargs
|
||||
) -> VMobject:
|
||||
return self.get_parts_by_string(substr, **kwargs)[index]
|
||||
|
||||
def set_color_by_string(self, substr: str, color: ManimColor, **kwargs):
|
||||
self.get_parts_by_string(substr, **kwargs).set_color(color)
|
||||
return self
|
||||
|
||||
def set_color_by_string_to_color_map(
|
||||
self, string_to_color_map: dict[str, ManimColor], **kwargs
|
||||
):
|
||||
for substr, color in string_to_color_map.items():
|
||||
self.set_color_by_string(substr, color, **kwargs)
|
||||
return self
|
||||
|
||||
def get_string(self) -> str:
|
||||
return self.string
|
||||
336
manimlib/mobject/svg/mtex_mobject.py
Normal file
336
manimlib/mobject/svg/mtex_mobject.py
Normal file
@@ -0,0 +1,336 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
import colour
|
||||
from typing import Union, Sequence
|
||||
|
||||
from manimlib.mobject.svg.labelled_string import LabelledString
|
||||
from manimlib.utils.tex_file_writing import tex_to_svg_file
|
||||
from manimlib.utils.tex_file_writing import get_tex_config
|
||||
from manimlib.utils.tex_file_writing import display_during_execution
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
Span = tuple[int, int]
|
||||
|
||||
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
|
||||
|
||||
class MTex(LabelledString):
|
||||
CONFIG = {
|
||||
"font_size": 48,
|
||||
"alignment": "\\centering",
|
||||
"tex_environment": "align*",
|
||||
"tex_to_color_map": {},
|
||||
}
|
||||
|
||||
def __init__(self, tex_string: str, **kwargs):
|
||||
# Prevent from passing an empty string.
|
||||
if not tex_string:
|
||||
tex_string = "\\\\"
|
||||
self.tex_string = tex_string
|
||||
super().__init__(tex_string, **kwargs)
|
||||
|
||||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
||||
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.base_color,
|
||||
self.use_plain_file,
|
||||
self.isolate,
|
||||
self.tex_string,
|
||||
self.alignment,
|
||||
self.tex_environment,
|
||||
self.tex_to_color_map
|
||||
)
|
||||
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
tex_config = get_tex_config()
|
||||
full_tex = tex_config["tex_body"].replace(
|
||||
tex_config["text_to_replace"],
|
||||
content
|
||||
)
|
||||
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||
file_path = tex_to_svg_file(full_tex)
|
||||
return file_path
|
||||
|
||||
def pre_parse(self) -> None:
|
||||
super().pre_parse()
|
||||
self.backslash_indices = self.get_backslash_indices()
|
||||
self.brace_index_pairs = self.get_brace_index_pairs()
|
||||
self.script_char_spans = self.get_script_char_spans()
|
||||
self.script_content_spans = self.get_script_content_spans()
|
||||
self.script_spans = self.get_script_spans()
|
||||
|
||||
# Toolkits
|
||||
|
||||
@staticmethod
|
||||
def get_color_command_str(rgb_int: int) -> str:
|
||||
rgb_tuple = MTex.int_to_rgb(rgb_int)
|
||||
return "".join([
|
||||
"\\color[RGB]",
|
||||
"{",
|
||||
",".join(map(str, rgb_tuple)),
|
||||
"}"
|
||||
])
|
||||
|
||||
# Pre-parsing
|
||||
|
||||
def get_backslash_indices(self) -> list[int]:
|
||||
# The latter of `\\` doesn't count.
|
||||
return list(it.chain(*[
|
||||
range(span[0], span[1], 2)
|
||||
for span in self.find_spans(r"\\+")
|
||||
]))
|
||||
|
||||
def get_unescaped_char_spans(self, chars: str):
|
||||
return sorted(filter(
|
||||
lambda span: span[0] - 1 not in self.backslash_indices,
|
||||
self.find_substrs(list(chars))
|
||||
))
|
||||
|
||||
def get_brace_index_pairs(self) -> list[Span]:
|
||||
left_brace_indices = []
|
||||
right_brace_indices = []
|
||||
left_brace_indices_stack = []
|
||||
for span in self.get_unescaped_char_spans("{}"):
|
||||
index = span[0]
|
||||
if self.get_substr(span) == "{":
|
||||
left_brace_indices_stack.append(index)
|
||||
else:
|
||||
if not left_brace_indices_stack:
|
||||
raise ValueError("Missing '{' inserted")
|
||||
left_brace_index = left_brace_indices_stack.pop()
|
||||
left_brace_indices.append(left_brace_index)
|
||||
right_brace_indices.append(index)
|
||||
if left_brace_indices_stack:
|
||||
raise ValueError("Missing '}' inserted")
|
||||
return list(zip(left_brace_indices, right_brace_indices))
|
||||
|
||||
def get_script_char_spans(self) -> list[int]:
|
||||
return self.get_unescaped_char_spans("_^")
|
||||
|
||||
def get_script_content_spans(self) -> list[Span]:
|
||||
result = []
|
||||
brace_indices_dict = dict(self.brace_index_pairs)
|
||||
script_pattern = r"[a-zA-Z0-9]|\\[a-zA-Z]+"
|
||||
for script_char_span in self.script_char_spans:
|
||||
span_begin = self.match(r"\s*", pos=script_char_span[1]).end()
|
||||
if span_begin in brace_indices_dict.keys():
|
||||
span_end = brace_indices_dict[span_begin] + 1
|
||||
else:
|
||||
match_obj = self.match(script_pattern, pos=span_begin)
|
||||
if not match_obj:
|
||||
script_name = {
|
||||
"_": "subscript",
|
||||
"^": "superscript"
|
||||
}[script_char]
|
||||
raise ValueError(
|
||||
f"Unclear {script_name} detected while parsing. "
|
||||
"Please use braces to clarify"
|
||||
)
|
||||
span_end = match_obj.end()
|
||||
result.append((span_begin, span_end))
|
||||
return result
|
||||
|
||||
def get_script_spans(self) -> list[Span]:
|
||||
return [
|
||||
(
|
||||
self.search(r"\s*$", endpos=script_char_span[0]).start(),
|
||||
script_content_span[1]
|
||||
)
|
||||
for script_char_span, script_content_span in zip(
|
||||
self.script_char_spans, self.script_content_spans
|
||||
)
|
||||
]
|
||||
|
||||
# Parsing
|
||||
|
||||
def get_command_repl_items(self) -> list[tuple[Span, str]]:
|
||||
color_related_command_dict = {
|
||||
"color": (1, False),
|
||||
"textcolor": (1, False),
|
||||
"pagecolor": (1, True),
|
||||
"colorbox": (1, True),
|
||||
"fcolorbox": (2, True),
|
||||
}
|
||||
result = []
|
||||
backslash_indices = self.backslash_indices
|
||||
right_brace_indices = [
|
||||
right_index
|
||||
for left_index, right_index in self.brace_index_pairs
|
||||
]
|
||||
pattern = "".join([
|
||||
r"\\",
|
||||
"(",
|
||||
"|".join(color_related_command_dict.keys()),
|
||||
")",
|
||||
r"(?![a-zA-Z])"
|
||||
])
|
||||
for match_obj in self.finditer(pattern):
|
||||
span_begin, cmd_end = match_obj.span()
|
||||
if span_begin not in backslash_indices:
|
||||
continue
|
||||
cmd_name = match_obj.group(1)
|
||||
n_braces, substitute_cmd = color_related_command_dict[cmd_name]
|
||||
span_end = self.take_nearest_value(
|
||||
right_brace_indices, cmd_end, n_braces
|
||||
) + 1
|
||||
if substitute_cmd:
|
||||
repl_str = "\\" + cmd_name + n_braces * "{black}"
|
||||
else:
|
||||
repl_str = ""
|
||||
result.append(((span_begin, span_end), repl_str))
|
||||
return result
|
||||
|
||||
def get_extra_entity_spans(self) -> list[Span]:
|
||||
return [
|
||||
self.match(r"\\([a-zA-Z]+|.)", pos=index).span()
|
||||
for index in self.backslash_indices
|
||||
]
|
||||
|
||||
def get_extra_ignored_spans(self) -> list[int]:
|
||||
return self.script_char_spans.copy()
|
||||
|
||||
def get_internal_specified_spans(self) -> list[Span]:
|
||||
# Match paired double braces (`{{...}}`).
|
||||
result = []
|
||||
reversed_brace_indices_dict = dict([
|
||||
pair[::-1] for pair in self.brace_index_pairs
|
||||
])
|
||||
skip = False
|
||||
for prev_right_index, right_index in self.get_neighbouring_pairs(
|
||||
list(reversed_brace_indices_dict.keys())
|
||||
):
|
||||
if skip:
|
||||
skip = False
|
||||
continue
|
||||
if right_index != prev_right_index + 1:
|
||||
continue
|
||||
left_index = reversed_brace_indices_dict[right_index]
|
||||
prev_left_index = reversed_brace_indices_dict[prev_right_index]
|
||||
if left_index != prev_left_index - 1:
|
||||
continue
|
||||
result.append((left_index, right_index + 1))
|
||||
skip = True
|
||||
return result
|
||||
|
||||
def get_external_specified_spans(self) -> list[Span]:
|
||||
return self.find_substrs(list(self.tex_to_color_map.keys()))
|
||||
|
||||
def get_label_span_list(self) -> list[Span]:
|
||||
result = self.script_content_spans.copy()
|
||||
for span_begin, span_end in self.specified_spans:
|
||||
shrinked_end = self.lslide(span_end, self.script_spans)
|
||||
if span_begin >= shrinked_end:
|
||||
continue
|
||||
shrinked_span = (span_begin, shrinked_end)
|
||||
if shrinked_span in result:
|
||||
continue
|
||||
result.append(shrinked_span)
|
||||
return result
|
||||
|
||||
def get_content(self, use_plain_file: bool) -> str:
|
||||
if use_plain_file:
|
||||
span_repl_dict = {}
|
||||
else:
|
||||
extended_label_span_list = [
|
||||
span
|
||||
if span in self.script_content_spans
|
||||
else (span[0], self.rslide(span[1], self.script_spans))
|
||||
for span in self.label_span_list
|
||||
]
|
||||
inserted_string_pairs = [
|
||||
(span, (
|
||||
"{{" + self.get_color_command_str(label + 1),
|
||||
"}}"
|
||||
))
|
||||
for label, span in enumerate(extended_label_span_list)
|
||||
]
|
||||
span_repl_dict = self.generate_span_repl_dict(
|
||||
inserted_string_pairs,
|
||||
self.command_repl_items
|
||||
)
|
||||
result = self.get_replaced_substr(self.full_span, span_repl_dict)
|
||||
|
||||
if self.tex_environment:
|
||||
result = "\n".join([
|
||||
f"\\begin{{{self.tex_environment}}}",
|
||||
result,
|
||||
f"\\end{{{self.tex_environment}}}"
|
||||
])
|
||||
if self.alignment:
|
||||
result = "\n".join([self.alignment, result])
|
||||
if use_plain_file:
|
||||
result = "\n".join([
|
||||
self.get_color_command_str(self.hex_to_int(self.base_color)),
|
||||
result
|
||||
])
|
||||
return result
|
||||
|
||||
@property
|
||||
def has_predefined_local_colors(self) -> bool:
|
||||
return bool(self.command_repl_items)
|
||||
|
||||
# Post-parsing
|
||||
|
||||
def get_cleaned_substr(self, span: Span) -> str:
|
||||
substr = super().get_cleaned_substr(span)
|
||||
if not self.brace_index_pairs:
|
||||
return substr
|
||||
|
||||
# Balance braces.
|
||||
left_brace_indices, right_brace_indices = zip(*self.brace_index_pairs)
|
||||
unclosed_left_braces = 0
|
||||
unclosed_right_braces = 0
|
||||
for index in range(*span):
|
||||
if index in left_brace_indices:
|
||||
unclosed_left_braces += 1
|
||||
elif index in right_brace_indices:
|
||||
if unclosed_left_braces == 0:
|
||||
unclosed_right_braces += 1
|
||||
else:
|
||||
unclosed_left_braces -= 1
|
||||
return "".join([
|
||||
unclosed_right_braces * "{",
|
||||
substr,
|
||||
unclosed_left_braces * "}"
|
||||
])
|
||||
|
||||
# Method alias
|
||||
|
||||
def get_parts_by_tex(self, tex: str, **kwargs) -> VGroup:
|
||||
return self.get_parts_by_string(tex, **kwargs)
|
||||
|
||||
def get_part_by_tex(self, tex: str, **kwargs) -> VMobject:
|
||||
return self.get_part_by_string(tex, **kwargs)
|
||||
|
||||
def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
|
||||
return self.set_color_by_string(tex, color, **kwargs)
|
||||
|
||||
def set_color_by_tex_to_color_map(
|
||||
self, tex_to_color_map: dict[str, ManimColor], **kwargs
|
||||
):
|
||||
return self.set_color_by_string_to_color_map(
|
||||
tex_to_color_map, **kwargs
|
||||
)
|
||||
|
||||
def get_tex(self) -> str:
|
||||
return self.get_string()
|
||||
|
||||
|
||||
class MTexText(MTex):
|
||||
CONFIG = {
|
||||
"tex_environment": None,
|
||||
}
|
||||
@@ -1,36 +1,34 @@
|
||||
import itertools as it
|
||||
import re
|
||||
import string
|
||||
import warnings
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import itertools as it
|
||||
from typing import Callable
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from xml.dom import minidom
|
||||
|
||||
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
|
||||
import svgelements as se
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Polygon
|
||||
from manimlib.mobject.geometry import Polyline
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import RoundedRectangle
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
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.iterables import hash_obj
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
def string_to_numbers(num_string):
|
||||
num_string = num_string.replace("-", ",-")
|
||||
num_string = num_string.replace("e,-", "e-")
|
||||
return [
|
||||
float(s)
|
||||
for s in re.split("[ ,]", num_string)
|
||||
if s != ""
|
||||
]
|
||||
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
|
||||
|
||||
|
||||
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
|
||||
return np.array([x, y, 0.0])
|
||||
|
||||
|
||||
class SVGMobject(VMobject):
|
||||
@@ -38,26 +36,249 @@ class SVGMobject(VMobject):
|
||||
"should_center": True,
|
||||
"height": 2,
|
||||
"width": None,
|
||||
# Must be filled in in a subclass, or when called
|
||||
"file_name": None,
|
||||
"unpack_groups": True, # if False, creates a hierarchy of VGroups
|
||||
# TODO, style components should be read in, not defaulted
|
||||
"stroke_width": DEFAULT_STROKE_WIDTH,
|
||||
"fill_opacity": 1.0,
|
||||
"path_string_config": {}
|
||||
# Style that overrides the original svg
|
||||
"color": None,
|
||||
"opacity": None,
|
||||
"fill_color": None,
|
||||
"fill_opacity": None,
|
||||
"stroke_width": None,
|
||||
"stroke_color": None,
|
||||
"stroke_opacity": None,
|
||||
# Style that fills only when not specified
|
||||
# If None, regarded as default values from svg standard
|
||||
"svg_default": {
|
||||
"color": None,
|
||||
"opacity": None,
|
||||
"fill_color": None,
|
||||
"fill_opacity": None,
|
||||
"stroke_width": None,
|
||||
"stroke_color": None,
|
||||
"stroke_opacity": None,
|
||||
},
|
||||
"path_string_config": {},
|
||||
}
|
||||
|
||||
def __init__(self, file_name=None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.file_name = file_name or self.file_name
|
||||
if file_name is None:
|
||||
raise Exception("Must specify file for SVGMobject")
|
||||
self.file_path = get_full_vector_image_path(file_name)
|
||||
|
||||
def __init__(self, file_name: str | None = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.file_name = file_name or self.file_name
|
||||
self.init_svg_mobject()
|
||||
self.init_colors()
|
||||
self.move_into_position()
|
||||
|
||||
def move_into_position(self):
|
||||
def init_svg_mobject(self) -> None:
|
||||
hash_val = hash_obj(self.hash_seed)
|
||||
if hash_val in SVG_HASH_TO_MOB_MAP:
|
||||
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
|
||||
self.add(*mob)
|
||||
return
|
||||
|
||||
self.generate_mobject()
|
||||
SVG_HASH_TO_MOB_MAP[hash_val] = self.copy()
|
||||
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
# Returns data which can uniquely represent the result of `init_points`.
|
||||
# The hashed value of it is stored as a key in `SVG_HASH_TO_MOB_MAP`.
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.file_name
|
||||
)
|
||||
|
||||
def generate_mobject(self) -> None:
|
||||
file_path = self.get_file_path()
|
||||
element_tree = ET.parse(file_path)
|
||||
new_tree = self.modify_xml_tree(element_tree)
|
||||
# Create a temporary svg file to dump modified svg to be parsed
|
||||
root, ext = os.path.splitext(file_path)
|
||||
modified_file_path = root + "_" + ext
|
||||
new_tree.write(modified_file_path)
|
||||
|
||||
svg = se.SVG.parse(modified_file_path)
|
||||
os.remove(modified_file_path)
|
||||
|
||||
mobjects = self.get_mobjects_from(svg)
|
||||
self.add(*mobjects)
|
||||
self.flip(RIGHT) # Flip y
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
if self.file_name is None:
|
||||
raise Exception("Must specify file for SVGMobject")
|
||||
return get_full_vector_image_path(self.file_name)
|
||||
|
||||
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
|
||||
config_style_dict = self.generate_config_style_dict()
|
||||
style_keys = (
|
||||
"fill",
|
||||
"fill-opacity",
|
||||
"stroke",
|
||||
"stroke-opacity",
|
||||
"stroke-width",
|
||||
"style"
|
||||
)
|
||||
root = element_tree.getroot()
|
||||
root_style_dict = {
|
||||
k: v for k, v in root.attrib.items()
|
||||
if k in style_keys
|
||||
}
|
||||
|
||||
new_root = ET.Element("svg", {})
|
||||
config_style_node = ET.SubElement(new_root, "g", config_style_dict)
|
||||
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
|
||||
root_style_node.extend(root)
|
||||
return ET.ElementTree(new_root)
|
||||
|
||||
def generate_config_style_dict(self) -> dict[str, str]:
|
||||
keys_converting_dict = {
|
||||
"fill": ("color", "fill_color"),
|
||||
"fill-opacity": ("opacity", "fill_opacity"),
|
||||
"stroke": ("color", "stroke_color"),
|
||||
"stroke-opacity": ("opacity", "stroke_opacity"),
|
||||
"stroke-width": ("stroke_width",)
|
||||
}
|
||||
svg_default_dict = self.svg_default
|
||||
result = {}
|
||||
for svg_key, style_keys in keys_converting_dict.items():
|
||||
for style_key in style_keys:
|
||||
if svg_default_dict[style_key] is None:
|
||||
continue
|
||||
result[svg_key] = str(svg_default_dict[style_key])
|
||||
return result
|
||||
|
||||
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
|
||||
result = []
|
||||
for shape in svg.elements():
|
||||
if isinstance(shape, se.Group):
|
||||
continue
|
||||
elif isinstance(shape, se.Path):
|
||||
mob = self.path_to_mobject(shape)
|
||||
elif isinstance(shape, se.SimpleLine):
|
||||
mob = self.line_to_mobject(shape)
|
||||
elif isinstance(shape, se.Rect):
|
||||
mob = self.rect_to_mobject(shape)
|
||||
elif isinstance(shape, se.Circle):
|
||||
mob = self.circle_to_mobject(shape)
|
||||
elif isinstance(shape, se.Ellipse):
|
||||
mob = self.ellipse_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polygon):
|
||||
mob = self.polygon_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polyline):
|
||||
mob = self.polyline_to_mobject(shape)
|
||||
# elif isinstance(shape, se.Text):
|
||||
# mob = self.text_to_mobject(shape)
|
||||
elif type(shape) == se.SVGElement:
|
||||
continue
|
||||
else:
|
||||
log.warning(f"Unsupported element type: {type(shape)}")
|
||||
continue
|
||||
if not mob.has_points():
|
||||
continue
|
||||
self.apply_style_to_mobject(mob, shape)
|
||||
if isinstance(shape, se.Transformable) and shape.apply:
|
||||
self.handle_transform(mob, shape.transform)
|
||||
result.append(mob)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:
|
||||
mat = np.array([
|
||||
[matrix.a, matrix.c],
|
||||
[matrix.b, matrix.d]
|
||||
])
|
||||
vec = np.array([matrix.e, matrix.f, 0.0])
|
||||
mob.apply_matrix(mat)
|
||||
mob.shift(vec)
|
||||
return mob
|
||||
|
||||
@staticmethod
|
||||
def apply_style_to_mobject(
|
||||
mob: VMobject,
|
||||
shape: se.GraphicObject
|
||||
) -> VMobject:
|
||||
mob.set_style(
|
||||
stroke_width=shape.stroke_width,
|
||||
stroke_color=shape.stroke.hex,
|
||||
stroke_opacity=shape.stroke.opacity,
|
||||
fill_color=shape.fill.hex,
|
||||
fill_opacity=shape.fill.opacity
|
||||
)
|
||||
return mob
|
||||
|
||||
@staticmethod
|
||||
def handle_transform(mob, matrix):
|
||||
mat = np.array([
|
||||
[matrix.a, matrix.c],
|
||||
[matrix.b, matrix.d]
|
||||
])
|
||||
vec = np.array([matrix.e, matrix.f, 0.0])
|
||||
mob.apply_matrix(mat)
|
||||
mob.shift(vec)
|
||||
return mob
|
||||
|
||||
def path_to_mobject(self, path: se.Path) -> VMobjectFromSVGPath:
|
||||
return VMobjectFromSVGPath(path, **self.path_string_config)
|
||||
|
||||
def line_to_mobject(self, line: se.Line) -> Line:
|
||||
return Line(
|
||||
start=_convert_point_to_3d(line.x1, line.y1),
|
||||
end=_convert_point_to_3d(line.x2, line.y2)
|
||||
)
|
||||
|
||||
def rect_to_mobject(self, rect: se.Rect) -> Rectangle:
|
||||
if rect.rx == 0 or rect.ry == 0:
|
||||
mob = Rectangle(
|
||||
width=rect.width,
|
||||
height=rect.height,
|
||||
)
|
||||
else:
|
||||
mob = RoundedRectangle(
|
||||
width=rect.width,
|
||||
height=rect.height * rect.rx / rect.ry,
|
||||
corner_radius=rect.rx
|
||||
)
|
||||
mob.stretch_to_fit_height(rect.height)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
rect.x + rect.width / 2,
|
||||
rect.y + rect.height / 2
|
||||
))
|
||||
return mob
|
||||
|
||||
def circle_to_mobject(self, circle: se.Circle) -> Circle:
|
||||
# svgelements supports `rx` & `ry` but `r`
|
||||
mob = Circle(radius=circle.rx)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
circle.cx, circle.cy
|
||||
))
|
||||
return mob
|
||||
|
||||
def ellipse_to_mobject(self, ellipse: se.Ellipse) -> Circle:
|
||||
mob = Circle(radius=ellipse.rx)
|
||||
mob.stretch_to_fit_height(2 * ellipse.ry)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
ellipse.cx, ellipse.cy
|
||||
))
|
||||
return mob
|
||||
|
||||
def polygon_to_mobject(self, polygon: se.Polygon) -> Polygon:
|
||||
points = [
|
||||
_convert_point_to_3d(*point)
|
||||
for point in polygon
|
||||
]
|
||||
return Polygon(*points)
|
||||
|
||||
def polyline_to_mobject(self, polyline: se.Polyline) -> Polyline:
|
||||
points = [
|
||||
_convert_point_to_3d(*point)
|
||||
for point in polyline
|
||||
]
|
||||
return Polyline(*points)
|
||||
|
||||
def text_to_mobject(self, text: se.Text):
|
||||
pass
|
||||
|
||||
def move_into_position(self) -> None:
|
||||
if self.should_center:
|
||||
self.center()
|
||||
if self.height is not None:
|
||||
@@ -65,359 +286,63 @@ class SVGMobject(VMobject):
|
||||
if self.width is not None:
|
||||
self.set_width(self.width)
|
||||
|
||||
def init_points(self):
|
||||
doc = minidom.parse(self.file_path)
|
||||
self.ref_to_element = {}
|
||||
|
||||
for svg in doc.getElementsByTagName("svg"):
|
||||
mobjects = self.get_mobjects_from(svg)
|
||||
if self.unpack_groups:
|
||||
self.add(*mobjects)
|
||||
else:
|
||||
self.add(*mobjects[0].submobjects)
|
||||
doc.unlink()
|
||||
|
||||
def get_mobjects_from(self, element):
|
||||
result = []
|
||||
if not isinstance(element, minidom.Element):
|
||||
return result
|
||||
if element.tagName == 'defs':
|
||||
self.update_ref_to_element(element)
|
||||
elif element.tagName == 'style':
|
||||
pass # TODO, handle style
|
||||
elif element.tagName in ['g', 'svg', 'symbol']:
|
||||
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')
|
||||
))
|
||||
elif element.tagName == 'use':
|
||||
result += self.use_to_mobjects(element)
|
||||
elif element.tagName == 'rect':
|
||||
result.append(self.rect_to_mobject(element))
|
||||
elif element.tagName == 'circle':
|
||||
result.append(self.circle_to_mobject(element))
|
||||
elif element.tagName == 'ellipse':
|
||||
result.append(self.ellipse_to_mobject(element))
|
||||
elif element.tagName in ['polygon', 'polyline']:
|
||||
result.append(self.polygon_to_mobject(element))
|
||||
else:
|
||||
pass # TODO
|
||||
# warnings.warn("Unknown element type: " + element.tagName)
|
||||
result = [m for m in result if m is not None]
|
||||
self.handle_transforms(element, VGroup(*result))
|
||||
if len(result) > 1 and not self.unpack_groups:
|
||||
result = [VGroup(*result)]
|
||||
|
||||
return result
|
||||
|
||||
def g_to_mobjects(self, g_element):
|
||||
mob = VGroup(*self.get_mobjects_from(g_element))
|
||||
self.handle_transforms(g_element, mob)
|
||||
return mob.submobjects
|
||||
|
||||
def path_string_to_mobject(self, path_string):
|
||||
return VMobjectFromSVGPathstring(
|
||||
path_string,
|
||||
**self.path_string_config,
|
||||
)
|
||||
|
||||
def use_to_mobjects(self, use_element):
|
||||
# Remove initial "#" character
|
||||
ref = use_element.getAttribute("xlink:href")[1:]
|
||||
if ref not in self.ref_to_element:
|
||||
warnings.warn(f"{ref} not recognized")
|
||||
return VGroup()
|
||||
return self.get_mobjects_from(
|
||||
self.ref_to_element[ref]
|
||||
)
|
||||
|
||||
def attribute_to_float(self, attr):
|
||||
stripped_attr = "".join([
|
||||
char for char in attr
|
||||
if char in string.digits + "." + "-"
|
||||
])
|
||||
return float(stripped_attr)
|
||||
|
||||
def polygon_to_mobject(self, polygon_element):
|
||||
path_string = polygon_element.getAttribute("points")
|
||||
for digit in string.digits:
|
||||
path_string = path_string.replace(f" {digit}", f"L {digit}")
|
||||
path_string = path_string.replace("L", "M", 1)
|
||||
return self.path_string_to_mobject(path_string)
|
||||
|
||||
def circle_to_mobject(self, circle_element):
|
||||
x, y, r = [
|
||||
self.attribute_to_float(
|
||||
circle_element.getAttribute(key)
|
||||
)
|
||||
if circle_element.hasAttribute(key)
|
||||
else 0.0
|
||||
for key in ("cx", "cy", "r")
|
||||
]
|
||||
return Circle(radius=r).shift(x * RIGHT + y * DOWN)
|
||||
|
||||
def ellipse_to_mobject(self, circle_element):
|
||||
x, y, rx, ry = [
|
||||
self.attribute_to_float(
|
||||
circle_element.getAttribute(key)
|
||||
)
|
||||
if circle_element.hasAttribute(key)
|
||||
else 0.0
|
||||
for key in ("cx", "cy", "rx", "ry")
|
||||
]
|
||||
return Circle().scale(rx * RIGHT + ry * UP).shift(x * RIGHT + y * DOWN)
|
||||
|
||||
def rect_to_mobject(self, rect_element):
|
||||
fill_color = rect_element.getAttribute("fill")
|
||||
stroke_color = rect_element.getAttribute("stroke")
|
||||
stroke_width = rect_element.getAttribute("stroke-width")
|
||||
corner_radius = rect_element.getAttribute("rx")
|
||||
|
||||
# input preprocessing
|
||||
if fill_color in ["", "none", "#FFF", "#FFFFFF"] or Color(fill_color) == Color(WHITE):
|
||||
opacity = 0
|
||||
fill_color = BLACK # shdn't be necessary but avoids error msgs
|
||||
if fill_color in ["#000", "#000000"]:
|
||||
fill_color = WHITE
|
||||
if stroke_color in ["", "none", "#FFF", "#FFFFFF"] or Color(stroke_color) == Color(WHITE):
|
||||
stroke_width = 0
|
||||
stroke_color = BLACK
|
||||
if stroke_color in ["#000", "#000000"]:
|
||||
stroke_color = WHITE
|
||||
if stroke_width in ["", "none", "0"]:
|
||||
stroke_width = 0
|
||||
|
||||
if corner_radius in ["", "0", "none"]:
|
||||
corner_radius = 0
|
||||
|
||||
corner_radius = float(corner_radius)
|
||||
|
||||
if corner_radius == 0:
|
||||
mob = Rectangle(
|
||||
width=self.attribute_to_float(
|
||||
rect_element.getAttribute("width")
|
||||
),
|
||||
height=self.attribute_to_float(
|
||||
rect_element.getAttribute("height")
|
||||
),
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=opacity
|
||||
)
|
||||
else:
|
||||
mob = RoundedRectangle(
|
||||
width=self.attribute_to_float(
|
||||
rect_element.getAttribute("width")
|
||||
),
|
||||
height=self.attribute_to_float(
|
||||
rect_element.getAttribute("height")
|
||||
),
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=opacity,
|
||||
corner_radius=corner_radius
|
||||
)
|
||||
|
||||
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
|
||||
return mob
|
||||
|
||||
def handle_transforms(self, element, mobject):
|
||||
# TODO, this could use some cleaning...
|
||||
x, y = 0, 0
|
||||
try:
|
||||
x = self.attribute_to_float(element.getAttribute('x'))
|
||||
# Flip y
|
||||
y = -self.attribute_to_float(element.getAttribute('y'))
|
||||
mobject.shift([x, y, 0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
transform = element.getAttribute('transform')
|
||||
|
||||
try: # transform matrix
|
||||
prefix = "matrix("
|
||||
suffix = ")"
|
||||
if not transform.startswith(prefix) or not transform.endswith(suffix):
|
||||
raise Exception()
|
||||
transform = transform[len(prefix):-len(suffix)]
|
||||
transform = string_to_numbers(transform)
|
||||
transform = np.array(transform).reshape([3, 2])
|
||||
x = transform[2][0]
|
||||
y = -transform[2][1]
|
||||
matrix = np.identity(self.dim)
|
||||
matrix[:2, :2] = transform[:2, :]
|
||||
matrix[1] *= -1
|
||||
matrix[:, 1] *= -1
|
||||
|
||||
for mob in mobject.family_members_with_points():
|
||||
mob.apply_matrix(matrix.T)
|
||||
mobject.shift(x * RIGHT + y * UP)
|
||||
except:
|
||||
pass
|
||||
|
||||
try: # transform scale
|
||||
prefix = "scale("
|
||||
suffix = ")"
|
||||
if not transform.startswith(prefix) or not transform.endswith(suffix):
|
||||
raise Exception()
|
||||
transform = transform[len(prefix):-len(suffix)]
|
||||
scale_values = string_to_numbers(transform)
|
||||
if len(scale_values) == 2:
|
||||
scale_x, scale_y = scale_values
|
||||
mobject.scale(np.array([scale_x, scale_y, 1]), about_point=ORIGIN)
|
||||
elif len(scale_values) == 1:
|
||||
scale = scale_values[0]
|
||||
mobject.scale(np.array([scale, scale, 1]), about_point=ORIGIN)
|
||||
except:
|
||||
pass
|
||||
|
||||
try: # transform translate
|
||||
prefix = "translate("
|
||||
suffix = ")"
|
||||
if not transform.startswith(prefix) or not transform.endswith(suffix):
|
||||
raise Exception()
|
||||
transform = transform[len(prefix):-len(suffix)]
|
||||
x, y = string_to_numbers(transform)
|
||||
mobject.shift(x * RIGHT + y * DOWN)
|
||||
except:
|
||||
pass
|
||||
# TODO, ...
|
||||
|
||||
def flatten(self, input_list):
|
||||
output_list = []
|
||||
for i in input_list:
|
||||
if isinstance(i, list):
|
||||
output_list.extend(self.flatten(i))
|
||||
else:
|
||||
output_list.append(i)
|
||||
return output_list
|
||||
|
||||
def get_all_childNodes_have_id(self, element):
|
||||
all_childNodes_have_id = []
|
||||
if not isinstance(element, minidom.Element):
|
||||
return
|
||||
if element.hasAttribute('id'):
|
||||
return [element]
|
||||
for e in element.childNodes:
|
||||
all_childNodes_have_id.append(self.get_all_childNodes_have_id(e))
|
||||
return self.flatten([e for e in all_childNodes_have_id if e])
|
||||
|
||||
def update_ref_to_element(self, defs):
|
||||
new_refs = dict([(e.getAttribute('id'), e) for e in self.get_all_childNodes_have_id(defs)])
|
||||
self.ref_to_element.update(new_refs)
|
||||
|
||||
|
||||
class VMobjectFromSVGPathstring(VMobject):
|
||||
class VMobjectFromSVGPath(VMobject):
|
||||
CONFIG = {
|
||||
"long_lines": True,
|
||||
"long_lines": False,
|
||||
"should_subdivide_sharp_curves": False,
|
||||
"should_remove_null_curves": False,
|
||||
}
|
||||
|
||||
def __init__(self, path_string, **kwargs):
|
||||
self.path_string = path_string
|
||||
def __init__(self, path_obj: se.Path, **kwargs):
|
||||
# Get rid of arcs
|
||||
path_obj.approximate_arcs_with_quads()
|
||||
self.path_obj = path_obj
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
# After a given svg_path has been converted into points, the result
|
||||
# will be saved to a file so that future calls for the same path
|
||||
# don't need to retrace the same computation.
|
||||
hasher = hashlib.sha256(self.path_string.encode())
|
||||
path_string = self.path_obj.d()
|
||||
hasher = hashlib.sha256(path_string.encode())
|
||||
path_hash = hasher.hexdigest()[:16]
|
||||
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
|
||||
tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy")
|
||||
|
||||
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():
|
||||
new_points = self.string_to_points(command, coord_string)
|
||||
self.handle_command(command, new_points)
|
||||
self.handle_commands()
|
||||
if self.should_subdivide_sharp_curves:
|
||||
# For a healthy triangulation later
|
||||
self.subdivide_sharp_curves()
|
||||
if self.should_remove_null_curves:
|
||||
# Get rid of any null curves
|
||||
self.set_points(self.get_points_without_null_curves())
|
||||
# SVG treats y-coordinate differently
|
||||
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())
|
||||
all_commands += [c.lower() for c in all_commands]
|
||||
pattern = "[{}]".format("".join(all_commands))
|
||||
return zip(
|
||||
re.findall(pattern, self.path_string),
|
||||
re.split(pattern, self.path_string)[1:]
|
||||
)
|
||||
|
||||
def handle_command(self, command, new_points):
|
||||
if command.islower():
|
||||
# Treat it as a relative command
|
||||
new_points += self.relative_point
|
||||
|
||||
func, n_points = self.command_to_function(command)
|
||||
func(*new_points[:n_points])
|
||||
leftover_points = new_points[n_points:]
|
||||
|
||||
# Recursively handle the rest of the points
|
||||
if len(leftover_points) > 0:
|
||||
if command.upper() == "M":
|
||||
# Treat following points as relative line coordinates
|
||||
command = "l"
|
||||
if command.islower():
|
||||
leftover_points -= self.relative_point
|
||||
self.relative_point = self.get_last_point()
|
||||
self.handle_command(command, leftover_points)
|
||||
else:
|
||||
# Command is over, reset for future relative commands
|
||||
self.relative_point = self.get_last_point()
|
||||
|
||||
def string_to_points(self, command, coord_string):
|
||||
numbers = string_to_numbers(coord_string)
|
||||
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))
|
||||
result = np.zeros((xy.shape[0], self.dim))
|
||||
result[:, :2] = xy
|
||||
return result
|
||||
|
||||
def command_to_function(self, command):
|
||||
return self.get_command_to_function_map()[command.upper()]
|
||||
|
||||
def get_command_to_function_map(self):
|
||||
"""
|
||||
Associates svg command to VMobject function, and
|
||||
the number of arguments it takes in
|
||||
"""
|
||||
return {
|
||||
"M": (self.start_new_path, 1),
|
||||
"L": (self.add_line_to, 1),
|
||||
"H": (self.add_line_to, 1),
|
||||
"V": (self.add_line_to, 1),
|
||||
"C": (self.add_cubic_bezier_curve_to, 3),
|
||||
"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
|
||||
"Z": (self.close_path, 0),
|
||||
def handle_commands(self) -> None:
|
||||
segment_class_to_func_map = {
|
||||
se.Move: (self.start_new_path, ("end",)),
|
||||
se.Close: (self.close_path, ()),
|
||||
se.Line: (self.add_line_to, ("end",)),
|
||||
se.QuadraticBezier: (self.add_quadratic_bezier_curve_to, ("control", "end")),
|
||||
se.CubicBezier: (self.add_cubic_bezier_curve_to, ("control1", "control2", "end"))
|
||||
}
|
||||
for segment in self.path_obj:
|
||||
segment_class = segment.__class__
|
||||
func, attr_names = segment_class_to_func_map[segment_class]
|
||||
points = [
|
||||
_convert_point_to_3d(*segment.__getattribute__(attr_name))
|
||||
for attr_name in attr_names
|
||||
]
|
||||
func(*points)
|
||||
|
||||
def get_original_path_string(self):
|
||||
return self.path_string
|
||||
# Get rid of the side effect of trailing "Z M" commands.
|
||||
if self.has_new_path_started():
|
||||
self.resize_points(self.get_num_points() - 1)
|
||||
|
||||
@@ -1,83 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Sequence, Union
|
||||
from functools import reduce
|
||||
import operator as op
|
||||
import colour
|
||||
import re
|
||||
import itertools as it
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.tex_file_writing import tex_to_svg_file
|
||||
from manimlib.utils.tex_file_writing import get_tex_config
|
||||
from manimlib.utils.tex_file_writing import display_during_execution
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
|
||||
|
||||
tex_string_to_mob_map = {}
|
||||
|
||||
|
||||
class SingleStringTex(VMobject):
|
||||
class SingleStringTex(SVGMobject):
|
||||
CONFIG = {
|
||||
"height": None,
|
||||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0,
|
||||
"should_center": True,
|
||||
"svg_default": {
|
||||
"color": WHITE,
|
||||
},
|
||||
"path_string_config": {
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
},
|
||||
"font_size": 48,
|
||||
"height": None,
|
||||
"organize_left_to_right": False,
|
||||
"alignment": "\\centering",
|
||||
"math_mode": True,
|
||||
"organize_left_to_right": False,
|
||||
}
|
||||
|
||||
def __init__(self, tex_string, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
assert(isinstance(tex_string, str))
|
||||
def __init__(self, tex_string: str, **kwargs):
|
||||
assert isinstance(tex_string, str)
|
||||
self.tex_string = tex_string
|
||||
if tex_string not in tex_string_to_mob_map:
|
||||
with display_during_execution(f" Writing \"{tex_string}\""):
|
||||
full_tex = self.get_tex_file_body(tex_string)
|
||||
filename = tex_to_svg_file(full_tex)
|
||||
svg_mob = SVGMobject(
|
||||
filename,
|
||||
height=None,
|
||||
path_string_config={
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
}
|
||||
)
|
||||
tex_string_to_mob_map[tex_string] = svg_mob
|
||||
self.add(*(
|
||||
sm.copy()
|
||||
for sm in tex_string_to_mob_map[tex_string]
|
||||
))
|
||||
self.init_colors()
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if self.height is None:
|
||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
||||
if self.organize_left_to_right:
|
||||
self.organize_submobjects_left_to_right()
|
||||
|
||||
def get_tex_file_body(self, tex_string):
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.tex_string,
|
||||
self.alignment,
|
||||
self.math_mode
|
||||
)
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
full_tex = self.get_tex_file_body(self.tex_string)
|
||||
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||
file_path = tex_to_svg_file(full_tex)
|
||||
return file_path
|
||||
|
||||
def get_tex_file_body(self, tex_string: str) -> str:
|
||||
new_tex = self.get_modified_expression(tex_string)
|
||||
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"],
|
||||
new_tex
|
||||
)
|
||||
|
||||
def get_modified_expression(self, tex_string):
|
||||
result = self.alignment + " " + tex_string
|
||||
result = result.strip()
|
||||
result = self.modify_special_strings(result)
|
||||
return result
|
||||
def get_modified_expression(self, tex_string: str) -> str:
|
||||
return self.modify_special_strings(tex_string.strip())
|
||||
|
||||
def modify_special_strings(self, tex):
|
||||
def modify_special_strings(self, tex: str) -> str:
|
||||
tex = tex.strip()
|
||||
should_add_filler = reduce(op.or_, [
|
||||
# Fraction line needs something to be over
|
||||
@@ -95,6 +100,18 @@ class SingleStringTex(VMobject):
|
||||
filler = "{\\quad}"
|
||||
tex += filler
|
||||
|
||||
should_add_double_filler = reduce(op.or_, [
|
||||
tex == "\\overset",
|
||||
# TODO: these can't be used since they change
|
||||
# the latex draw order.
|
||||
# tex == "\\frac", # you can use \\over as a alternative
|
||||
# tex == "\\dfrac",
|
||||
# tex == "\\binom",
|
||||
])
|
||||
if should_add_double_filler:
|
||||
filler = "{\\quad}{\\quad}"
|
||||
tex += filler
|
||||
|
||||
if tex == "\\substack":
|
||||
tex = "\\quad"
|
||||
|
||||
@@ -129,20 +146,27 @@ class SingleStringTex(VMobject):
|
||||
tex = ""
|
||||
return tex
|
||||
|
||||
def balance_braces(self, tex):
|
||||
def balance_braces(self, tex: str) -> str:
|
||||
"""
|
||||
Makes Tex resiliant to unmatched { at start
|
||||
Makes Tex resiliant to unmatched braces
|
||||
"""
|
||||
num_lefts, num_rights = [tex.count(char) for char in "{}"]
|
||||
while num_rights > num_lefts:
|
||||
tex = "{" + tex
|
||||
num_lefts += 1
|
||||
while num_lefts > num_rights:
|
||||
tex = tex + "}"
|
||||
num_rights += 1
|
||||
num_unclosed_brackets = 0
|
||||
for i in range(len(tex)):
|
||||
if i > 0 and tex[i - 1] == "\\":
|
||||
# So as to not count '\{' type expressions
|
||||
continue
|
||||
char = tex[i]
|
||||
if char == "{":
|
||||
num_unclosed_brackets += 1
|
||||
elif char == "}":
|
||||
if num_unclosed_brackets == 0:
|
||||
tex = "{" + tex
|
||||
else:
|
||||
num_unclosed_brackets -= 1
|
||||
tex += num_unclosed_brackets * "}"
|
||||
return tex
|
||||
|
||||
def get_tex(self):
|
||||
def get_tex(self) -> str:
|
||||
return self.tex_string
|
||||
|
||||
def organize_submobjects_left_to_right(self):
|
||||
@@ -152,15 +176,12 @@ 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": {},
|
||||
}
|
||||
|
||||
def __init__(self, *tex_strings, **kwargs):
|
||||
def __init__(self, *tex_strings: str, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.tex_strings = self.break_up_tex_strings(tex_strings)
|
||||
full_string = self.arg_separator.join(self.tex_strings)
|
||||
@@ -171,19 +192,23 @@ class Tex(SingleStringTex):
|
||||
if self.organize_left_to_right:
|
||||
self.organize_submobjects_left_to_right()
|
||||
|
||||
def break_up_tex_strings(self, tex_strings):
|
||||
# Separate out anything surrounded in double braces
|
||||
patterns = ["{{", "}}"]
|
||||
def break_up_tex_strings(self, tex_strings: Iterable[str]) -> Iterable[str]:
|
||||
# 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):
|
||||
@@ -215,7 +240,12 @@ class Tex(SingleStringTex):
|
||||
self.set_submobjects(new_submobjects)
|
||||
return self
|
||||
|
||||
def get_parts_by_tex(self, tex, substring=True, case_sensitive=True):
|
||||
def get_parts_by_tex(
|
||||
self,
|
||||
tex: str,
|
||||
substring: bool = True,
|
||||
case_sensitive: bool = True
|
||||
) -> VGroup:
|
||||
def test(tex1, tex2):
|
||||
if not case_sensitive:
|
||||
tex1 = tex1.lower()
|
||||
@@ -230,27 +260,36 @@ class Tex(SingleStringTex):
|
||||
self.submobjects
|
||||
))
|
||||
|
||||
def get_part_by_tex(self, tex, **kwargs):
|
||||
def get_part_by_tex(self, tex: str, **kwargs) -> SingleStringTex | None:
|
||||
all_parts = self.get_parts_by_tex(tex, **kwargs)
|
||||
return all_parts[0] if all_parts else None
|
||||
|
||||
def set_color_by_tex(self, tex, color, **kwargs):
|
||||
def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
|
||||
self.get_parts_by_tex(tex, **kwargs).set_color(color)
|
||||
return self
|
||||
|
||||
def set_color_by_tex_to_color_map(self, tex_to_color_map, **kwargs):
|
||||
def set_color_by_tex_to_color_map(
|
||||
self,
|
||||
tex_to_color_map: dict[str, ManimColor],
|
||||
**kwargs
|
||||
):
|
||||
for tex, color in list(tex_to_color_map.items()):
|
||||
self.set_color_by_tex(tex, color, **kwargs)
|
||||
return self
|
||||
|
||||
def index_of_part(self, part, start=0):
|
||||
def index_of_part(self, part: SingleStringTex, start: int = 0) -> int:
|
||||
return self.submobjects.index(part, start)
|
||||
|
||||
def index_of_part_by_tex(self, tex, start=0, **kwargs):
|
||||
def index_of_part_by_tex(self, tex: str, start: int = 0, **kwargs) -> int:
|
||||
part = self.get_part_by_tex(tex, **kwargs)
|
||||
return self.index_of_part(part, start)
|
||||
|
||||
def slice_by_tex(self, start_tex=None, stop_tex=None, **kwargs):
|
||||
def slice_by_tex(
|
||||
self,
|
||||
start_tex: str | None = None,
|
||||
stop_tex: str | None = None,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
if start_tex is None:
|
||||
start_index = 0
|
||||
else:
|
||||
@@ -262,10 +301,10 @@ class Tex(SingleStringTex):
|
||||
stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
|
||||
return self[start_index:stop_index]
|
||||
|
||||
def sort_alphabetically(self):
|
||||
def sort_alphabetically(self) -> None:
|
||||
self.submobjects.sort(key=lambda m: m.get_tex())
|
||||
|
||||
def set_bstroke(self, color=BLACK, width=4):
|
||||
def set_bstroke(self, color: ManimColor = BLACK, width: float = 4):
|
||||
self.set_stroke(color, width, background=True)
|
||||
return self
|
||||
|
||||
@@ -284,7 +323,7 @@ class BulletedList(TexText):
|
||||
"alignment": "",
|
||||
}
|
||||
|
||||
def __init__(self, *items, **kwargs):
|
||||
def __init__(self, *items: str, **kwargs):
|
||||
line_separated_items = [s + "\\\\" for s in items]
|
||||
TexText.__init__(self, *line_separated_items, **kwargs)
|
||||
for part in self:
|
||||
@@ -297,7 +336,7 @@ class BulletedList(TexText):
|
||||
buff=self.buff
|
||||
)
|
||||
|
||||
def fade_all_but(self, index_or_string, opacity=0.5):
|
||||
def fade_all_but(self, index_or_string: int | str, opacity: float = 0.5) -> None:
|
||||
arg = index_or_string
|
||||
if isinstance(arg, str):
|
||||
part = self.get_part_by_tex(arg)
|
||||
@@ -335,7 +374,7 @@ class Title(TexText):
|
||||
"underline_buff": MED_SMALL_BUFF,
|
||||
}
|
||||
|
||||
def __init__(self, *text_parts, **kwargs):
|
||||
def __init__(self, *text_parts: str, **kwargs):
|
||||
TexText.__init__(self, *text_parts, **kwargs)
|
||||
self.scale(self.scale_factor)
|
||||
self.to_edge(UP)
|
||||
|
||||
@@ -1,219 +1,565 @@
|
||||
import copy
|
||||
import hashlib
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import typing
|
||||
from contextlib import contextmanager
|
||||
import itertools as it
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
import typing
|
||||
from typing import Iterable, Sequence, Union
|
||||
|
||||
import manimpango
|
||||
import pygments
|
||||
import pygments.formatters
|
||||
import pygments.lexers
|
||||
|
||||
from manimpango import MarkupUtils
|
||||
|
||||
from manimlib.logger import log
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.mobject.svg.labelled_string import LabelledString
|
||||
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
|
||||
|
||||
TEXT_MOB_SCALE_FACTOR = 0.001048
|
||||
from manimlib.utils.tex_file_writing import tex_hash
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.directories import get_downloads_dir
|
||||
from manimlib.utils.directories import get_text_dir
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
|
||||
|
||||
class Text(SVGMobject):
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
Span = tuple[int, int]
|
||||
|
||||
|
||||
TEXT_MOB_SCALE_FACTOR = 0.0076
|
||||
DEFAULT_LINE_SPACING_SCALE = 0.6
|
||||
|
||||
|
||||
# See https://docs.gtk.org/Pango/pango_markup.html
|
||||
# A tag containing two aliases will cause warning,
|
||||
# so only use the first key of each group of aliases.
|
||||
SPAN_ATTR_KEY_ALIAS_LIST = (
|
||||
("font", "font_desc"),
|
||||
("font_family", "face"),
|
||||
("font_size", "size"),
|
||||
("font_style", "style"),
|
||||
("font_weight", "weight"),
|
||||
("font_variant", "variant"),
|
||||
("font_stretch", "stretch"),
|
||||
("font_features",),
|
||||
("foreground", "fgcolor", "color"),
|
||||
("background", "bgcolor"),
|
||||
("alpha", "fgalpha"),
|
||||
("background_alpha", "bgalpha"),
|
||||
("underline",),
|
||||
("underline_color",),
|
||||
("overline",),
|
||||
("overline_color",),
|
||||
("rise",),
|
||||
("baseline_shift",),
|
||||
("font_scale",),
|
||||
("strikethrough",),
|
||||
("strikethrough_color",),
|
||||
("fallback",),
|
||||
("lang",),
|
||||
("letter_spacing",),
|
||||
("gravity",),
|
||||
("gravity_hint",),
|
||||
("show",),
|
||||
("insert_hyphens",),
|
||||
("allow_breaks",),
|
||||
("line_height",),
|
||||
("text_transform",),
|
||||
("segment",),
|
||||
)
|
||||
COLOR_RELATED_KEYS = (
|
||||
"foreground",
|
||||
"background",
|
||||
"underline_color",
|
||||
"overline_color",
|
||||
"strikethrough_color"
|
||||
)
|
||||
SPAN_ATTR_KEY_CONVERSION = {
|
||||
key: key_alias_list[0]
|
||||
for key_alias_list in SPAN_ATTR_KEY_ALIAS_LIST
|
||||
for key in key_alias_list
|
||||
}
|
||||
TAG_TO_ATTR_DICT = {
|
||||
"b": {"font_weight": "bold"},
|
||||
"big": {"font_size": "larger"},
|
||||
"i": {"font_style": "italic"},
|
||||
"s": {"strikethrough": "true"},
|
||||
"sub": {"baseline_shift": "subscript", "font_scale": "subscript"},
|
||||
"sup": {"baseline_shift": "superscript", "font_scale": "superscript"},
|
||||
"small": {"font_size": "smaller"},
|
||||
"tt": {"font_family": "monospace"},
|
||||
"u": {"underline": "single"},
|
||||
}
|
||||
|
||||
|
||||
# Temporary handler
|
||||
class _Alignment:
|
||||
VAL_DICT = {
|
||||
"LEFT": 0,
|
||||
"CENTER": 1,
|
||||
"RIGHT": 2
|
||||
}
|
||||
|
||||
def __init__(self, s: str):
|
||||
self.value = _Alignment.VAL_DICT[s.upper()]
|
||||
|
||||
|
||||
class MarkupText(LabelledString):
|
||||
CONFIG = {
|
||||
# Mobject
|
||||
"color": WHITE,
|
||||
"height": None,
|
||||
"stroke_width": 0,
|
||||
# Text
|
||||
"font": '',
|
||||
"gradient": None,
|
||||
"lsh": -1,
|
||||
"size": 1,
|
||||
"is_markup": True,
|
||||
"font_size": 48,
|
||||
"tab_width": 4,
|
||||
"lsh": None,
|
||||
"justify": False,
|
||||
"indent": 0,
|
||||
"alignment": "LEFT",
|
||||
"line_width_factor": None,
|
||||
"font": "",
|
||||
"slant": NORMAL,
|
||||
"weight": NORMAL,
|
||||
"gradient": None,
|
||||
"t2c": {},
|
||||
"t2f": {},
|
||||
"t2g": {},
|
||||
"t2s": {},
|
||||
"t2w": {},
|
||||
"disable_ligatures": True,
|
||||
"global_config": {},
|
||||
"local_configs": {},
|
||||
}
|
||||
|
||||
def __init__(self, text, **config):
|
||||
self.full2short(config)
|
||||
digest_config(self, config)
|
||||
self.lsh = self.size if self.lsh == -1 else self.lsh
|
||||
text_without_tabs = text
|
||||
if text.find('\t') != -1:
|
||||
text_without_tabs = text.replace('\t', ' ' * self.tab_width)
|
||||
self.text = text_without_tabs
|
||||
file_name = self.text2svg()
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
self.remove_empty_path(file_name)
|
||||
SVGMobject.__init__(self, file_name, **config)
|
||||
def __init__(self, text: str, **kwargs):
|
||||
self.full2short(kwargs)
|
||||
digest_config(self, kwargs)
|
||||
|
||||
if not self.font:
|
||||
self.font = get_customization()["style"]["font"]
|
||||
if self.is_markup:
|
||||
validate_error = MarkupUtils.validate(text)
|
||||
if validate_error:
|
||||
raise ValueError(validate_error)
|
||||
|
||||
self.text = text
|
||||
if self.disable_ligatures:
|
||||
self.apply_space_chars()
|
||||
if self.t2c:
|
||||
self.set_color_by_t2c()
|
||||
super().__init__(text, **kwargs)
|
||||
|
||||
if self.t2g:
|
||||
log.warning(
|
||||
"Manim currently cannot parse gradient from svg. "
|
||||
"Please set gradient via `set_color_by_gradient`.",
|
||||
)
|
||||
if self.gradient:
|
||||
self.set_color_by_gradient(*self.gradient)
|
||||
if self.t2g:
|
||||
self.set_color_by_t2g()
|
||||
|
||||
# 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:
|
||||
content = fpr.read()
|
||||
content = re.sub(r'<path .*?d=""/>', '', content)
|
||||
with open(file_name, 'w') as fpw:
|
||||
fpw.write(content)
|
||||
|
||||
def apply_space_chars(self):
|
||||
submobs = self.submobjects.copy()
|
||||
for char_index in range(len(self.text)):
|
||||
if self.text[char_index] in [" ", "\t", "\n"]:
|
||||
space = Dot(radius=0, fill_opacity=0, stroke_opacity=0)
|
||||
space.move_to(submobs[max(char_index - 1, 0)].get_center())
|
||||
submobs.insert(char_index, space)
|
||||
self.set_submobjects(submobs)
|
||||
|
||||
def find_indexes(self, word):
|
||||
m = re.match(r'\[([0-9\-]{0,}):([0-9\-]{0,})\]', word)
|
||||
if m:
|
||||
start = int(m.group(1)) if m.group(1) != '' else 0
|
||||
end = int(m.group(2)) if m.group(2) != '' else len(self.text)
|
||||
start = len(self.text) + start if start < 0 else start
|
||||
end = len(self.text) + end if end < 0 else end
|
||||
return [(start, end)]
|
||||
|
||||
indexes = []
|
||||
index = self.text.find(word)
|
||||
while index != -1:
|
||||
indexes.append((index, index + len(word)))
|
||||
index = self.text.find(word, index + len(word))
|
||||
return indexes
|
||||
|
||||
def full2short(self, config):
|
||||
for kwargs in [config, self.CONFIG]:
|
||||
if kwargs.__contains__('line_spacing_height'):
|
||||
kwargs['lsh'] = kwargs.pop('line_spacing_height')
|
||||
if kwargs.__contains__('text2color'):
|
||||
kwargs['t2c'] = kwargs.pop('text2color')
|
||||
if kwargs.__contains__('text2font'):
|
||||
kwargs['t2f'] = kwargs.pop('text2font')
|
||||
if kwargs.__contains__('text2gradient'):
|
||||
kwargs['t2g'] = kwargs.pop('text2gradient')
|
||||
if kwargs.__contains__('text2slant'):
|
||||
kwargs['t2s'] = kwargs.pop('text2slant')
|
||||
if kwargs.__contains__('text2weight'):
|
||||
kwargs['t2w'] = kwargs.pop('text2weight')
|
||||
|
||||
def set_color_by_t2c(self, t2c=None):
|
||||
t2c = t2c if t2c else self.t2c
|
||||
for word, color in list(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 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)
|
||||
id_str = self.text + settings
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update(id_str.encode())
|
||||
return hasher.hexdigest()[:16]
|
||||
|
||||
def text2settings(self):
|
||||
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))
|
||||
|
||||
# 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)
|
||||
|
||||
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
|
||||
|
||||
return settings
|
||||
|
||||
def text2svg(self):
|
||||
# anti-aliasing
|
||||
size = self.size * 10
|
||||
lsh = self.lsh * 10
|
||||
|
||||
if self.font == '':
|
||||
self.font = get_customization()['style']['font']
|
||||
|
||||
dir_name = get_text_dir()
|
||||
hash_name = self.text2hash()
|
||||
file_name = os.path.join(dir_name, hash_name) + '.svg'
|
||||
if os.path.exists(file_name):
|
||||
return file_name
|
||||
settings = self.text2settings()
|
||||
width = 600
|
||||
height = 400
|
||||
disable_liga = self.disable_ligatures
|
||||
return manimpango.text2svg(
|
||||
settings,
|
||||
size,
|
||||
lsh,
|
||||
disable_liga,
|
||||
file_name,
|
||||
START_X,
|
||||
START_Y,
|
||||
width,
|
||||
height,
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.base_color,
|
||||
self.use_plain_file,
|
||||
self.isolate,
|
||||
self.text,
|
||||
self.is_markup,
|
||||
self.font_size,
|
||||
self.lsh,
|
||||
self.justify,
|
||||
self.indent,
|
||||
self.alignment,
|
||||
self.line_width_factor,
|
||||
self.font,
|
||||
self.slant,
|
||||
self.weight,
|
||||
self.t2c,
|
||||
self.t2f,
|
||||
self.t2s,
|
||||
self.t2w,
|
||||
self.global_config,
|
||||
self.local_configs
|
||||
)
|
||||
|
||||
def full2short(self, config: dict) -> None:
|
||||
conversion_dict = {
|
||||
"line_spacing_height": "lsh",
|
||||
"text2color": "t2c",
|
||||
"text2font": "t2f",
|
||||
"text2gradient": "t2g",
|
||||
"text2slant": "t2s",
|
||||
"text2weight": "t2w"
|
||||
}
|
||||
for kwargs in [config, self.CONFIG]:
|
||||
for long_name, short_name in conversion_dict.items():
|
||||
if long_name in kwargs:
|
||||
kwargs[short_name] = kwargs.pop(long_name)
|
||||
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
svg_file = os.path.join(
|
||||
get_text_dir(), tex_hash(content) + ".svg"
|
||||
)
|
||||
if not os.path.exists(svg_file):
|
||||
self.markup_to_svg(content, svg_file)
|
||||
return svg_file
|
||||
|
||||
def markup_to_svg(self, markup_str: str, file_name: str) -> str:
|
||||
# `manimpango` is under construction,
|
||||
# so the following code is intended to suit its interface
|
||||
alignment = _Alignment(self.alignment)
|
||||
if self.line_width_factor is None:
|
||||
pango_width = -1
|
||||
else:
|
||||
pango_width = self.line_width_factor * DEFAULT_PIXEL_WIDTH
|
||||
|
||||
return MarkupUtils.text2svg(
|
||||
text=markup_str,
|
||||
font="", # Already handled
|
||||
slant="NORMAL", # Already handled
|
||||
weight="NORMAL", # Already handled
|
||||
size=1, # Already handled
|
||||
_=0, # Empty parameter
|
||||
disable_liga=False,
|
||||
file_name=file_name,
|
||||
START_X=0,
|
||||
START_Y=0,
|
||||
width=DEFAULT_PIXEL_WIDTH,
|
||||
height=DEFAULT_PIXEL_HEIGHT,
|
||||
justify=self.justify,
|
||||
indent=self.indent,
|
||||
line_spacing=None, # Already handled
|
||||
alignment=alignment,
|
||||
pango_width=pango_width
|
||||
)
|
||||
|
||||
def pre_parse(self) -> None:
|
||||
super().pre_parse()
|
||||
self.tag_items_from_markup = self.get_tag_items_from_markup()
|
||||
self.global_dict_from_config = self.get_global_dict_from_config()
|
||||
self.local_dicts_from_markup = self.get_local_dicts_from_markup()
|
||||
self.local_dicts_from_config = self.get_local_dicts_from_config()
|
||||
self.predefined_attr_dicts = self.get_predefined_attr_dicts()
|
||||
|
||||
# Toolkits
|
||||
|
||||
@staticmethod
|
||||
def get_attr_dict_str(attr_dict: dict[str, str]) -> str:
|
||||
return " ".join([
|
||||
f"{key}='{val}'"
|
||||
for key, val in attr_dict.items()
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def merge_attr_dicts(
|
||||
attr_dict_items: list[Span, str, typing.Any]
|
||||
) -> list[tuple[Span, dict[str, str]]]:
|
||||
index_seq = [0]
|
||||
attr_dict_list = [{}]
|
||||
for span, attr_dict in attr_dict_items:
|
||||
if span[0] >= span[1]:
|
||||
continue
|
||||
region_indices = [
|
||||
MarkupText.find_region_index(index_seq, index)
|
||||
for index in span
|
||||
]
|
||||
for flag in (1, 0):
|
||||
if index_seq[region_indices[flag]] == span[flag]:
|
||||
continue
|
||||
region_index = region_indices[flag]
|
||||
index_seq.insert(region_index + 1, span[flag])
|
||||
attr_dict_list.insert(
|
||||
region_index + 1, attr_dict_list[region_index].copy()
|
||||
)
|
||||
region_indices[flag] += 1
|
||||
if flag == 0:
|
||||
region_indices[1] += 1
|
||||
for key, val in attr_dict.items():
|
||||
if not key:
|
||||
continue
|
||||
for mid_dict in attr_dict_list[slice(*region_indices)]:
|
||||
mid_dict[key] = val
|
||||
return list(zip(
|
||||
MarkupText.get_neighbouring_pairs(index_seq), attr_dict_list[:-1]
|
||||
))
|
||||
|
||||
def find_substr_or_span(
|
||||
self, substr_or_span: str | tuple[int | None, int | None]
|
||||
) -> list[Span]:
|
||||
if isinstance(substr_or_span, str):
|
||||
return self.find_substr(substr_or_span)
|
||||
|
||||
span = tuple([
|
||||
(
|
||||
min(index, self.string_len)
|
||||
if index >= 0
|
||||
else max(index + self.string_len, 0)
|
||||
)
|
||||
if index is not None else default_index
|
||||
for index, default_index in zip(substr_or_span, self.full_span)
|
||||
])
|
||||
if span[0] >= span[1]:
|
||||
return []
|
||||
return [span]
|
||||
|
||||
# Pre-parsing
|
||||
|
||||
def get_tag_items_from_markup(
|
||||
self
|
||||
) -> list[tuple[Span, Span, dict[str, str]]]:
|
||||
if not self.is_markup:
|
||||
return []
|
||||
|
||||
tag_pattern = r"""<(/?)(\w+)\s*((?:\w+\s*\=\s*(['"]).*?\4\s*)*)>"""
|
||||
attr_pattern = r"""(\w+)\s*\=\s*(['"])(.*?)\2"""
|
||||
begin_match_obj_stack = []
|
||||
match_obj_pairs = []
|
||||
for match_obj in self.finditer(tag_pattern):
|
||||
if not match_obj.group(1):
|
||||
begin_match_obj_stack.append(match_obj)
|
||||
else:
|
||||
match_obj_pairs.append(
|
||||
(begin_match_obj_stack.pop(), match_obj)
|
||||
)
|
||||
if begin_match_obj_stack:
|
||||
raise ValueError("Unclosed tag(s) detected")
|
||||
|
||||
result = []
|
||||
for begin_match_obj, end_match_obj in match_obj_pairs:
|
||||
tag_name = begin_match_obj.group(2)
|
||||
if tag_name != end_match_obj.group(2):
|
||||
raise ValueError("Unmatched tag names")
|
||||
if end_match_obj.group(3):
|
||||
raise ValueError("Attributes shan't exist in ending tags")
|
||||
if tag_name == "span":
|
||||
attr_dict = {
|
||||
match.group(1): match.group(3)
|
||||
for match in re.finditer(
|
||||
attr_pattern, begin_match_obj.group(3)
|
||||
)
|
||||
}
|
||||
elif tag_name in TAG_TO_ATTR_DICT.keys():
|
||||
if begin_match_obj.group(3):
|
||||
raise ValueError(
|
||||
f"Attributes shan't exist in tag '{tag_name}'"
|
||||
)
|
||||
attr_dict = TAG_TO_ATTR_DICT[tag_name].copy()
|
||||
else:
|
||||
raise ValueError(f"Unknown tag: '{tag_name}'")
|
||||
|
||||
result.append(
|
||||
(begin_match_obj.span(), end_match_obj.span(), attr_dict)
|
||||
)
|
||||
return result
|
||||
|
||||
def get_global_dict_from_config(self) -> dict[str, typing.Any]:
|
||||
result = {
|
||||
"line_height": (
|
||||
(self.lsh or DEFAULT_LINE_SPACING_SCALE) + 1
|
||||
) * 0.6,
|
||||
"font_family": self.font,
|
||||
"font_size": self.font_size * 1024,
|
||||
"font_style": self.slant,
|
||||
"font_weight": self.weight
|
||||
}
|
||||
result.update(self.global_config)
|
||||
return result
|
||||
|
||||
def get_local_dicts_from_markup(
|
||||
self
|
||||
) -> list[Span, dict[str, str]]:
|
||||
return sorted([
|
||||
((begin_tag_span[0], end_tag_span[1]), attr_dict)
|
||||
for begin_tag_span, end_tag_span, attr_dict
|
||||
in self.tag_items_from_markup
|
||||
])
|
||||
|
||||
def get_local_dicts_from_config(
|
||||
self
|
||||
) -> list[Span, dict[str, typing.Any]]:
|
||||
return [
|
||||
(span, {key: val})
|
||||
for t2x_dict, key in (
|
||||
(self.t2c, "foreground"),
|
||||
(self.t2f, "font_family"),
|
||||
(self.t2s, "font_style"),
|
||||
(self.t2w, "font_weight")
|
||||
)
|
||||
for substr_or_span, val in t2x_dict.items()
|
||||
for span in self.find_substr_or_span(substr_or_span)
|
||||
] + [
|
||||
(span, local_config)
|
||||
for substr_or_span, local_config in self.local_configs.items()
|
||||
for span in self.find_substr_or_span(substr_or_span)
|
||||
]
|
||||
|
||||
def get_predefined_attr_dicts(self) -> list[Span, dict[str, str]]:
|
||||
attr_dict_items = [
|
||||
(self.full_span, self.global_dict_from_config),
|
||||
*self.local_dicts_from_markup,
|
||||
*self.local_dicts_from_config
|
||||
]
|
||||
return [
|
||||
(span, {
|
||||
SPAN_ATTR_KEY_CONVERSION[key.lower()]: str(val)
|
||||
for key, val in attr_dict.items()
|
||||
})
|
||||
for span, attr_dict in attr_dict_items
|
||||
]
|
||||
|
||||
# Parsing
|
||||
|
||||
def get_command_repl_items(self) -> list[tuple[Span, str]]:
|
||||
result = [
|
||||
(tag_span, "")
|
||||
for begin_tag, end_tag, _ in self.tag_items_from_markup
|
||||
for tag_span in (begin_tag, end_tag)
|
||||
]
|
||||
if not self.is_markup:
|
||||
result += [
|
||||
(span, escaped)
|
||||
for char, escaped in (
|
||||
("&", "&"),
|
||||
(">", ">"),
|
||||
("<", "<")
|
||||
)
|
||||
for span in self.find_substr(char)
|
||||
]
|
||||
return result
|
||||
|
||||
def get_extra_entity_spans(self) -> list[Span]:
|
||||
if not self.is_markup:
|
||||
return []
|
||||
return self.find_spans(r"&.*?;")
|
||||
|
||||
def get_extra_ignored_spans(self) -> list[int]:
|
||||
return []
|
||||
|
||||
def get_internal_specified_spans(self) -> list[Span]:
|
||||
return [span for span, _ in self.local_dicts_from_markup]
|
||||
|
||||
def get_external_specified_spans(self) -> list[Span]:
|
||||
return [span for span, _ in self.local_dicts_from_config]
|
||||
|
||||
def get_label_span_list(self) -> list[Span]:
|
||||
breakup_indices = remove_list_redundancies(list(it.chain(*it.chain(
|
||||
self.find_spans(r"\s+"),
|
||||
self.find_spans(r"\b"),
|
||||
self.specified_spans
|
||||
))))
|
||||
breakup_indices = sorted(filter(
|
||||
lambda index: not any([
|
||||
span[0] < index < span[1]
|
||||
for span in self.entity_spans
|
||||
]),
|
||||
breakup_indices
|
||||
))
|
||||
return list(filter(
|
||||
lambda span: self.get_substr(span).strip(),
|
||||
self.get_neighbouring_pairs(breakup_indices)
|
||||
))
|
||||
|
||||
def get_content(self, use_plain_file: bool) -> str:
|
||||
if use_plain_file:
|
||||
attr_dict_items = [
|
||||
(self.full_span, {"foreground": self.base_color}),
|
||||
*self.predefined_attr_dicts,
|
||||
*[
|
||||
(span, {})
|
||||
for span in self.label_span_list
|
||||
]
|
||||
]
|
||||
else:
|
||||
attr_dict_items = [
|
||||
(self.full_span, {"foreground": BLACK}),
|
||||
*[
|
||||
(span, {
|
||||
key: BLACK if key in COLOR_RELATED_KEYS else val
|
||||
for key, val in attr_dict.items()
|
||||
})
|
||||
for span, attr_dict in self.predefined_attr_dicts
|
||||
],
|
||||
*[
|
||||
(span, {"foreground": self.int_to_hex(label + 1)})
|
||||
for label, span in enumerate(self.label_span_list)
|
||||
]
|
||||
]
|
||||
inserted_string_pairs = [
|
||||
(span, (
|
||||
f"<span {self.get_attr_dict_str(attr_dict)}>",
|
||||
"</span>"
|
||||
))
|
||||
for span, attr_dict in self.merge_attr_dicts(attr_dict_items)
|
||||
]
|
||||
span_repl_dict = self.generate_span_repl_dict(
|
||||
inserted_string_pairs, self.command_repl_items
|
||||
)
|
||||
return self.get_replaced_substr(self.full_span, span_repl_dict)
|
||||
|
||||
@property
|
||||
def has_predefined_local_colors(self) -> bool:
|
||||
return any([
|
||||
key in COLOR_RELATED_KEYS
|
||||
for _, attr_dict in self.predefined_attr_dicts
|
||||
for key in attr_dict.keys()
|
||||
])
|
||||
|
||||
# Method alias
|
||||
|
||||
def get_parts_by_text(self, text: str, **kwargs) -> VGroup:
|
||||
return self.get_parts_by_string(text, **kwargs)
|
||||
|
||||
def get_part_by_text(self, text: str, **kwargs) -> VMobject:
|
||||
return self.get_part_by_string(text, **kwargs)
|
||||
|
||||
def set_color_by_text(self, text: str, color: ManimColor, **kwargs):
|
||||
return self.set_color_by_string(text, color, **kwargs)
|
||||
|
||||
def set_color_by_text_to_color_map(
|
||||
self, text_to_color_map: dict[str, ManimColor], **kwargs
|
||||
):
|
||||
return self.set_color_by_string_to_color_map(
|
||||
text_to_color_map, **kwargs
|
||||
)
|
||||
|
||||
def get_text(self) -> str:
|
||||
return self.get_string()
|
||||
|
||||
|
||||
class Text(MarkupText):
|
||||
CONFIG = {
|
||||
"is_markup": False,
|
||||
}
|
||||
|
||||
|
||||
class Code(MarkupText):
|
||||
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",
|
||||
}
|
||||
|
||||
def __init__(self, code: str, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.code = code
|
||||
lexer = pygments.lexers.get_lexer_by_name(self.language)
|
||||
formatter = pygments.formatters.PangoMarkupFormatter(
|
||||
style=self.code_style
|
||||
)
|
||||
markup = pygments.highlight(code, lexer, formatter)
|
||||
markup = re.sub(r"</?tt>", "", markup)
|
||||
super().__init__(markup, **kwargs)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def register_font(font_file: typing.Union[str, Path]):
|
||||
def register_font(font_file: str | Path):
|
||||
"""Temporarily add a font file to Pango's search path.
|
||||
This searches for the font_file at various places. The order it searches it described below.
|
||||
1. Absolute path.
|
||||
@@ -240,8 +586,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()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from manimlib.constants import *
|
||||
@@ -5,47 +7,64 @@ 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.mobject.geometry import Polygon
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.iterables import adjacent_pairs
|
||||
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),
|
||||
"resolution": (21, 11),
|
||||
"stroke_width": 1,
|
||||
"normal_nudge": 1e-2,
|
||||
"depth_test": True,
|
||||
"flat_stroke": False,
|
||||
}
|
||||
|
||||
def __init__(self, uv_surface, **kwargs):
|
||||
def __init__(self, uv_surface: Surface, **kwargs):
|
||||
if not isinstance(uv_surface, Surface):
|
||||
raise Exception("uv_surface must be of type Surface")
|
||||
self.uv_surface = uv_surface
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
uv_surface = self.uv_surface
|
||||
|
||||
full_nu, full_nv = uv_surface.resolution
|
||||
part_nu, part_nv = self.resolution
|
||||
u_indices = np.linspace(0, full_nu, part_nu).astype(int)
|
||||
v_indices = np.linspace(0, full_nv, part_nv).astype(int)
|
||||
# 'indices' are treated as floats. Later, there will be
|
||||
# an interpolation between the floor and ceiling of these
|
||||
# indices
|
||||
u_indices = np.linspace(0, full_nu - 1, part_nu)
|
||||
v_indices = np.linspace(0, full_nv - 1, part_nv)
|
||||
|
||||
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:
|
||||
path = VMobject()
|
||||
full_ui = full_nv * ui
|
||||
path.set_points_smoothly(nudged_points[full_ui:full_ui + full_nv])
|
||||
low_ui = full_nv * int(math.floor(ui))
|
||||
high_ui = full_nv * int(math.ceil(ui))
|
||||
path.set_points_smoothly(interpolate(
|
||||
nudged_points[low_ui:low_ui + full_nv],
|
||||
nudged_points[high_ui:high_ui + full_nv],
|
||||
ui % 1
|
||||
))
|
||||
self.add(path)
|
||||
for vi in v_indices:
|
||||
path = VMobject()
|
||||
path.set_points_smoothly(nudged_points[vi::full_nv])
|
||||
path.set_points_smoothly(interpolate(
|
||||
nudged_points[int(math.floor(vi))::full_nv],
|
||||
nudged_points[int(math.ceil(vi))::full_nv],
|
||||
vi % 1
|
||||
))
|
||||
self.add(path)
|
||||
|
||||
|
||||
@@ -59,7 +78,7 @@ class Sphere(Surface):
|
||||
"v_range": (0, PI),
|
||||
}
|
||||
|
||||
def uv_func(self, u, v):
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return self.radius * np.array([
|
||||
np.cos(u) * np.sin(v),
|
||||
np.sin(u) * np.sin(v),
|
||||
@@ -75,7 +94,7 @@ class Torus(Surface):
|
||||
"r2": 1,
|
||||
}
|
||||
|
||||
def uv_func(self, u, v):
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
P = np.array([math.cos(u), math.sin(u), 0])
|
||||
return (self.r1 - self.r2 * math.cos(v)) * P - math.sin(v) * OUT
|
||||
|
||||
@@ -97,8 +116,8 @@ class Cylinder(Surface):
|
||||
self.apply_matrix(z_to_vector(self.axis))
|
||||
return self
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return [np.cos(u), np.sin(u), v]
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([np.cos(u), np.sin(u), v])
|
||||
|
||||
|
||||
class Line3D(Cylinder):
|
||||
@@ -107,7 +126,7 @@ class Line3D(Cylinder):
|
||||
"resolution": (21, 25)
|
||||
}
|
||||
|
||||
def __init__(self, start, end, **kwargs):
|
||||
def __init__(self, start: np.ndarray, end: np.ndarray, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
axis = end - start
|
||||
super().__init__(
|
||||
@@ -126,16 +145,16 @@ class Disk3D(Surface):
|
||||
"resolution": (2, 25),
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
super().init_points()
|
||||
self.scale(self.radius)
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return [
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([
|
||||
u * np.cos(v),
|
||||
u * np.sin(v),
|
||||
0
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
class Square3D(Surface):
|
||||
@@ -146,12 +165,12 @@ class Square3D(Surface):
|
||||
"resolution": (2, 2),
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
super().init_points()
|
||||
self.scale(self.side_length / 2)
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return [u, v, 0]
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([u, v, 0])
|
||||
|
||||
|
||||
class Cube(SGroup):
|
||||
@@ -161,23 +180,126 @@ 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)
|
||||
def init_points(self) -> None:
|
||||
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: Square3D) -> list[Square3D]:
|
||||
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) -> Square3D:
|
||||
return Square3D(resolution=self.square_resolution)
|
||||
|
||||
|
||||
class Prism(Cube):
|
||||
def __init__(self, width: float = 3.0, height: float = 2.0, depth: float = 1.0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
for dim, value in enumerate([width, height, depth]):
|
||||
self.rescale_to_fit(value, dim, stretch=True)
|
||||
|
||||
|
||||
class VCube(VGroup):
|
||||
CONFIG = {
|
||||
"dimensions": [3, 2, 1]
|
||||
"fill_color": BLUE_D,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
"gloss": 0.5,
|
||||
"shadow": 0.5,
|
||||
"joint_type": "round",
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
Cube.init_points(self)
|
||||
for dim, value in enumerate(self.dimensions):
|
||||
def __init__(self, side_length: float = 2.0, **kwargs):
|
||||
face = Square(side_length=side_length)
|
||||
super().__init__(*Cube.square_to_cube_faces(face), **kwargs)
|
||||
self.init_colors()
|
||||
self.set_joint_type(self.joint_type)
|
||||
self.apply_depth_test()
|
||||
self.refresh_unit_normal()
|
||||
|
||||
|
||||
class VPrism(VCube):
|
||||
def __init__(self, width: float = 3.0, height: float = 2.0, depth: float = 1.0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
for dim, value in enumerate([width, height, depth]):
|
||||
self.rescale_to_fit(value, dim, stretch=True)
|
||||
|
||||
|
||||
class Dodecahedron(VGroup):
|
||||
CONFIG = {
|
||||
"fill_color": BLUE_E,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 1,
|
||||
"reflectiveness": 0.2,
|
||||
"gloss": 0.3,
|
||||
"shadow": 0.2,
|
||||
"depth_test": True,
|
||||
}
|
||||
|
||||
def init_points(self) -> None:
|
||||
# Star by creating two of the pentagons, meeting
|
||||
# back to back on the positive x-axis
|
||||
phi = (1 + math.sqrt(5)) / 2
|
||||
x, y, z = np.identity(3)
|
||||
pentagon1 = Polygon(
|
||||
[phi, 1 / phi, 0],
|
||||
[1, 1, 1],
|
||||
[1 / phi, 0, phi],
|
||||
[1, -1, 1],
|
||||
[phi, -1 / phi, 0],
|
||||
)
|
||||
pentagon2 = pentagon1.copy().stretch(-1, 2, about_point=ORIGIN)
|
||||
pentagon2.reverse_points()
|
||||
x_pair = VGroup(pentagon1, pentagon2)
|
||||
z_pair = x_pair.copy().apply_matrix(np.array([z, -x, -y]).T)
|
||||
y_pair = x_pair.copy().apply_matrix(np.array([y, z, x]).T)
|
||||
|
||||
self.add(*x_pair, *y_pair, *z_pair)
|
||||
for pentagon in list(self):
|
||||
pc = pentagon.copy()
|
||||
pc.apply_function(lambda p: -p)
|
||||
pc.reverse_points()
|
||||
self.add(pc)
|
||||
|
||||
# # Rotate those two pentagons by all the axis permuations to fill
|
||||
# # out the dodecahedron
|
||||
# Id = np.identity(3)
|
||||
# for i in range(3):
|
||||
# perm = [j % 3 for j in range(i, i + 3)]
|
||||
# for b in [1, -1]:
|
||||
# matrix = b * np.array([Id[0][perm], Id[1][perm], Id[2][perm]])
|
||||
# self.add(pentagon1.copy().apply_matrix(matrix, about_point=ORIGIN))
|
||||
# self.add(pentagon2.copy().apply_matrix(matrix, about_point=ORIGIN))
|
||||
|
||||
|
||||
class Prismify(VGroup):
|
||||
CONFIG = {
|
||||
"apply_depth_test": True,
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):
|
||||
# At the moment, this assume stright edges
|
||||
super().__init__(**kwargs)
|
||||
vect = depth * direction
|
||||
self.add(vmobject.copy())
|
||||
points = vmobject.get_points()[::vmobject.n_points_per_curve]
|
||||
for p1, p2 in adjacent_pairs(points):
|
||||
wall = VMobject()
|
||||
wall.match_style(vmobject)
|
||||
wall.set_points_as_corners([p1, p2, p2 + vect, p1 + vect])
|
||||
self.add(wall)
|
||||
self.add(vmobject.copy().shift(vect).reverse_points())
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import moderngl
|
||||
|
||||
from manimlib.constants import GREY_C
|
||||
from manimlib.constants import YELLOW
|
||||
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_GLOW_DOT_RADIUS = 0.2
|
||||
DEFAULT_GRID_HEIGHT = 6
|
||||
DEFAULT_BUFF_RATIO = 0.5
|
||||
|
||||
@@ -15,7 +21,8 @@ class DotCloud(PMobject):
|
||||
CONFIG = {
|
||||
"color": GREY_C,
|
||||
"opacity": 1,
|
||||
"radius": DEFAULT_DOT_CLOUD_RADIUS,
|
||||
"radius": DEFAULT_DOT_RADIUS,
|
||||
"glow_factor": 0,
|
||||
"shader_folder": "true_dot",
|
||||
"render_primitive": moderngl.POINTS,
|
||||
"shader_dtype": [
|
||||
@@ -25,23 +32,31 @@ class DotCloud(PMobject):
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, points=None, **kwargs):
|
||||
def __init__(self, points: npt.ArrayLike = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if points is not None:
|
||||
self.set_points(points)
|
||||
|
||||
def init_data(self):
|
||||
def init_data(self) -> None:
|
||||
super().init_data()
|
||||
self.data["radii"] = np.zeros((1, 1))
|
||||
self.set_radius(self.radius)
|
||||
|
||||
def to_grid(self, n_rows, n_cols, n_layers=1,
|
||||
buff_ratio=None,
|
||||
h_buff_ratio=1.0,
|
||||
v_buff_ratio=1.0,
|
||||
d_buff_ratio=1.0,
|
||||
height=DEFAULT_GRID_HEIGHT,
|
||||
):
|
||||
def init_uniforms(self) -> None:
|
||||
super().init_uniforms()
|
||||
self.uniforms["glow_factor"] = self.glow_factor
|
||||
|
||||
def to_grid(
|
||||
self,
|
||||
n_rows: int,
|
||||
n_cols: int,
|
||||
n_layers: int = 1,
|
||||
buff_ratio: float | None = None,
|
||||
h_buff_ratio: float = 1.0,
|
||||
v_buff_ratio: float = 1.0,
|
||||
d_buff_ratio: float = 1.0,
|
||||
height: float = DEFAULT_GRID_HEIGHT,
|
||||
):
|
||||
n_points = n_rows * n_cols * n_layers
|
||||
points = np.repeat(range(n_points), 3, axis=0).reshape((n_points, 3))
|
||||
points[:, 0] = points[:, 0] % n_cols
|
||||
@@ -66,43 +81,73 @@ class DotCloud(PMobject):
|
||||
self.center()
|
||||
return self
|
||||
|
||||
def set_radii(self, radii):
|
||||
self.data["radii"][:] = resize_preserving_order(radii, len(self.data["radii"]))
|
||||
def set_radii(self, radii: npt.ArrayLike):
|
||||
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
|
||||
|
||||
def get_radii(self):
|
||||
def get_radii(self) -> np.ndarray:
|
||||
return self.data["radii"]
|
||||
|
||||
def set_radius(self, radius):
|
||||
def set_radius(self, radius: float):
|
||||
self.data["radii"][:] = radius
|
||||
self.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
def get_radius(self):
|
||||
def get_radius(self) -> float:
|
||||
return self.get_radii().max()
|
||||
|
||||
def compute_bounding_box(self):
|
||||
def set_glow_factor(self, glow_factor: float) -> None:
|
||||
self.uniforms["glow_factor"] = glow_factor
|
||||
|
||||
def get_glow_factor(self) -> float:
|
||||
return self.uniforms["glow_factor"]
|
||||
|
||||
def compute_bounding_box(self) -> np.ndarray:
|
||||
bb = super().compute_bounding_box()
|
||||
radius = self.get_radius()
|
||||
bb[0] += np.full((3,), -radius)
|
||||
bb[2] += np.full((3,), radius)
|
||||
return bb
|
||||
|
||||
def scale(self, scale_factor, scale_radii=True, **kwargs):
|
||||
def scale(
|
||||
self,
|
||||
scale_factor: float | npt.ArrayLike,
|
||||
scale_radii: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
super().scale(scale_factor, **kwargs)
|
||||
if scale_radii:
|
||||
self.set_radii(scale_factor * self.get_radii())
|
||||
return self
|
||||
|
||||
def make_3d(self, gloss=0.5, shadow=0.2):
|
||||
self.set_gloss(gloss)
|
||||
def make_3d(self, reflectiveness: float = 0.5, shadow: float = 0.2):
|
||||
self.set_reflectiveness(reflectiveness)
|
||||
self.set_shadow(shadow)
|
||||
self.apply_depth_test()
|
||||
return self
|
||||
|
||||
def get_shader_data(self):
|
||||
def get_shader_data(self) -> np.ndarray:
|
||||
shader_data = super().get_shader_data()
|
||||
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: np.ndarray = ORIGIN, **kwargs):
|
||||
super().__init__(points=[center], **kwargs)
|
||||
|
||||
|
||||
class GlowDots(DotCloud):
|
||||
CONFIG = {
|
||||
"glow_factor": 2,
|
||||
"radius": DEFAULT_GLOW_DOT_RADIUS,
|
||||
"color": YELLOW,
|
||||
}
|
||||
|
||||
|
||||
class GlowDot(GlowDots, TrueDot):
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from manimlib.constants import *
|
||||
@@ -21,30 +22,36 @@ class ImageMobject(Mobject):
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, filename, **kwargs):
|
||||
path = get_full_raster_image_path(filename)
|
||||
self.image = Image.open(path)
|
||||
self.texture_paths = {"Texture": path}
|
||||
def __init__(self, filename: str, **kwargs):
|
||||
self.set_image_path(get_full_raster_image_path(filename))
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_data(self):
|
||||
def set_image_path(self, path: str) -> None:
|
||||
self.path = path
|
||||
self.image = Image.open(path)
|
||||
self.texture_paths = {"Texture": path}
|
||||
|
||||
def init_data(self) -> None:
|
||||
self.data = {
|
||||
"points": np.array([UL, DL, UR, DR]),
|
||||
"im_coords": np.array([(0, 0), (0, 1), (1, 0), (1, 1)]),
|
||||
"opacity": np.array([[self.opacity]], dtype=np.float32),
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
size = self.image.size
|
||||
self.set_width(2 * size[0] / size[1], stretch=True)
|
||||
self.set_height(self.height)
|
||||
|
||||
def set_opacity(self, opacity, recurse=True):
|
||||
def set_opacity(self, opacity: float, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
|
||||
return self
|
||||
|
||||
def point_to_rgb(self, point):
|
||||
def set_color(self, color, opacity=None, recurse=None):
|
||||
return self
|
||||
|
||||
def point_to_rgb(self, point: np.ndarray) -> np.ndarray:
|
||||
x0, y0 = self.get_corner(UL)[:2]
|
||||
x1, y1 = self.get_corner(DR)[:2]
|
||||
x_alpha = inverse_interpolate(x0, x1, point[0])
|
||||
@@ -60,7 +67,7 @@ class ImageMobject(Mobject):
|
||||
))
|
||||
return np.array(rgb) / 255
|
||||
|
||||
def get_shader_data(self):
|
||||
def get_shader_data(self) -> np.ndarray:
|
||||
shader_data = super().get_shader_data()
|
||||
self.read_data_to_shader(shader_data, "im_coords", "im_coords")
|
||||
self.read_data_to_shader(shader_data, "opacity", "opacity")
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Sequence, Union
|
||||
|
||||
import colour
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.color import color_gradient
|
||||
@@ -6,19 +13,41 @@ from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.iterables import resize_array
|
||||
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
class PMobject(Mobject):
|
||||
CONFIG = {
|
||||
"opacity": 1.0,
|
||||
}
|
||||
|
||||
def resize_points(self, size, resize_func=resize_array):
|
||||
def resize_points(
|
||||
self,
|
||||
size: int,
|
||||
resize_func: Callable[[np.ndarray, int], np.ndarray] = 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)
|
||||
self.data[key] = resize_func(self.data[key], size)
|
||||
return self
|
||||
|
||||
def add_points(self, points, rgbas=None, color=None, opacity=None):
|
||||
def set_points(self, points: npt.ArrayLike):
|
||||
if len(points) == 0:
|
||||
points = np.zeros((0, 3))
|
||||
super().set_points(points)
|
||||
self.resize_points(len(points))
|
||||
return self
|
||||
|
||||
def add_points(
|
||||
self,
|
||||
points: npt.ArrayLike,
|
||||
rgbas: np.ndarray | None = None,
|
||||
color: ManimColor | None = None,
|
||||
opacity: float | None = None
|
||||
):
|
||||
"""
|
||||
points must be a Nx3 numpy array, as must rgbas if it is not None
|
||||
"""
|
||||
@@ -27,37 +56,43 @@ class PMobject(Mobject):
|
||||
if color is not None:
|
||||
if opacity is None:
|
||||
opacity = self.data["rgbas"][-1, 3]
|
||||
new_rgbas = np.repeat(
|
||||
rgbas = np.repeat(
|
||||
[color_to_rgba(color, opacity)],
|
||||
len(points),
|
||||
axis=0
|
||||
)
|
||||
elif rgbas is not None:
|
||||
new_rgbas = rgbas
|
||||
self.data["rgbas"][-len(new_rgbas):] = new_rgbas
|
||||
if rgbas is not None:
|
||||
self.data["rgbas"][-len(rgbas):] = rgbas
|
||||
return self
|
||||
|
||||
def set_color_by_gradient(self, *colors):
|
||||
def add_point(self, point, rgba=None, color=None, opacity=None):
|
||||
rgbas = None if rgba is None else [rgba]
|
||||
self.add_points([point], rgbas, color, opacity)
|
||||
return self
|
||||
|
||||
def set_color_by_gradient(self, *colors: ManimColor):
|
||||
self.data["rgbas"] = np.array(list(map(
|
||||
color_to_rgba,
|
||||
color_gradient(colors, self.get_num_points())
|
||||
)))
|
||||
return self
|
||||
|
||||
def match_colors(self, pmobject):
|
||||
def match_colors(self, pmobject: PMobject):
|
||||
self.data["rgbas"][:] = resize_with_interpolation(
|
||||
pmobject.data["rgbas"], self.get_num_points()
|
||||
)
|
||||
return self
|
||||
|
||||
def filter_out(self, condition):
|
||||
def filter_out(self, condition: Callable[[np.ndarray], bool]):
|
||||
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
|
||||
|
||||
def sort_points(self, function=lambda p: p[0]):
|
||||
def sort_points(self, function: Callable[[np.ndarray]] = lambda p: p[0]):
|
||||
"""
|
||||
function is any map from R^3 to R
|
||||
"""
|
||||
@@ -77,20 +112,22 @@ class PMobject(Mobject):
|
||||
])
|
||||
return self
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
def point_from_proportion(self, alpha: float) -> np.ndarray:
|
||||
index = alpha * (self.get_num_points() - 1)
|
||||
return self.get_points()[int(index)]
|
||||
|
||||
def pointwise_become_partial(self, pmobject, a, b):
|
||||
def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float):
|
||||
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
|
||||
|
||||
|
||||
class PGroup(PMobject):
|
||||
def __init__(self, *pmobs, **kwargs):
|
||||
def __init__(self, *pmobs: PMobject, **kwargs):
|
||||
if not all([isinstance(m, PMobject) for m in pmobs]):
|
||||
raise Exception("All submobjects must be of type PMobject")
|
||||
super().__init__(*pmobs, **kwargs)
|
||||
@@ -101,6 +138,6 @@ class Point(PMobject):
|
||||
"color": BLACK,
|
||||
}
|
||||
|
||||
def __init__(self, location=ORIGIN, **kwargs):
|
||||
def __init__(self, location: np.ndarray = ORIGIN, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.add_points([location])
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Callable
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -9,6 +14,11 @@ from manimlib.utils.images import get_full_raster_image_path
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.space_ops import normalize_along_axis
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.camera.camera import Camera
|
||||
|
||||
|
||||
class Surface(Mobject):
|
||||
CONFIG = {
|
||||
@@ -20,7 +30,8 @@ class Surface(Mobject):
|
||||
"resolution": (101, 101),
|
||||
"color": GREY,
|
||||
"opacity": 1.0,
|
||||
"gloss": 0.3,
|
||||
"reflectiveness": 0.3,
|
||||
"gloss": 0.1,
|
||||
"shadow": 0.4,
|
||||
"prefered_creation_axis": 1,
|
||||
# For du and dv steps. Much smaller and numerical error
|
||||
@@ -41,7 +52,7 @@ class Surface(Mobject):
|
||||
super().__init__(**kwargs)
|
||||
self.compute_triangle_indices()
|
||||
|
||||
def uv_func(self, u, v):
|
||||
def uv_func(self, u: float, v: float) -> tuple[float, float, float]:
|
||||
# To be implemented in subclasses
|
||||
return (u, v, 0.0)
|
||||
|
||||
@@ -84,15 +95,17 @@ class Surface(Mobject):
|
||||
indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right
|
||||
self.triangle_indices = indices
|
||||
|
||||
def get_triangle_indices(self):
|
||||
def get_triangle_indices(self) -> np.ndarray:
|
||||
return self.triangle_indices
|
||||
|
||||
def get_surface_points_and_nudged_points(self):
|
||||
def get_surface_points_and_nudged_points(
|
||||
self
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
points = self.get_points()
|
||||
k = len(points) // 3
|
||||
return points[:k], points[k:2 * k], points[2 * k:]
|
||||
|
||||
def get_unit_normals(self):
|
||||
def get_unit_normals(self) -> np.ndarray:
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
normals = np.cross(
|
||||
(du_points - s_points) / self.epsilon,
|
||||
@@ -100,7 +113,13 @@ class Surface(Mobject):
|
||||
)
|
||||
return normalize_along_axis(normals, 1)
|
||||
|
||||
def pointwise_become_partial(self, smobject, a, b, axis=None):
|
||||
def pointwise_become_partial(
|
||||
self,
|
||||
smobject: "Surface",
|
||||
a: float,
|
||||
b: float,
|
||||
axis: np.ndarray | None = None
|
||||
):
|
||||
assert(isinstance(smobject, Surface))
|
||||
if axis is None:
|
||||
axis = self.prefered_creation_axis
|
||||
@@ -115,7 +134,14 @@ class Surface(Mobject):
|
||||
]))
|
||||
return self
|
||||
|
||||
def get_partial_points_array(self, points, a, b, resolution, axis):
|
||||
def get_partial_points_array(
|
||||
self,
|
||||
points: np.ndarray,
|
||||
a: float,
|
||||
b: float,
|
||||
resolution: npt.ArrayLike,
|
||||
axis: int
|
||||
) -> np.ndarray:
|
||||
if len(points) == 0:
|
||||
return points
|
||||
nu, nv = resolution[:2]
|
||||
@@ -148,7 +174,7 @@ class Surface(Mobject):
|
||||
).reshape(shape)
|
||||
return points.reshape((nu * nv, *resolution[2:]))
|
||||
|
||||
def sort_faces_back_to_front(self, vect=OUT):
|
||||
def sort_faces_back_to_front(self, vect: np.ndarray = OUT):
|
||||
tri_is = self.triangle_indices
|
||||
indices = list(range(len(tri_is) // 3))
|
||||
points = self.get_points()
|
||||
@@ -161,8 +187,13 @@ class Surface(Mobject):
|
||||
tri_is[k::3] = tri_is[k::3][indices]
|
||||
return self
|
||||
|
||||
def always_sort_to_camera(self, camera: Camera):
|
||||
self.add_updater(lambda m: m.sort_faces_back_to_front(
|
||||
camera.get_location() - self.get_center()
|
||||
))
|
||||
|
||||
# For shaders
|
||||
def get_shader_data(self):
|
||||
def get_shader_data(self) -> np.ndarray:
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
shader_data = self.get_resized_shader_data_array(len(s_points))
|
||||
if "points" not in self.locked_data_keys:
|
||||
@@ -172,18 +203,24 @@ class Surface(Mobject):
|
||||
self.fill_in_shader_color_info(shader_data)
|
||||
return shader_data
|
||||
|
||||
def fill_in_shader_color_info(self, shader_data):
|
||||
def fill_in_shader_color_info(self, shader_data: np.ndarray) -> np.ndarray:
|
||||
self.read_data_to_shader(shader_data, "color", "rgbas")
|
||||
return shader_data
|
||||
|
||||
def get_shader_vert_indices(self):
|
||||
def get_shader_vert_indices(self) -> np.ndarray:
|
||||
return self.get_triangle_indices()
|
||||
|
||||
|
||||
class ParametricSurface(Surface):
|
||||
def __init__(self, uv_func, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
uv_func: Callable[[float, float], Iterable[float]],
|
||||
u_range: tuple[float, float] = (0, 1),
|
||||
v_range: tuple[float, float] = (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)
|
||||
@@ -194,7 +231,7 @@ class SGroup(Surface):
|
||||
"resolution": (0, 0),
|
||||
}
|
||||
|
||||
def __init__(self, *parametric_surfaces, **kwargs):
|
||||
def __init__(self, *parametric_surfaces: Surface, **kwargs):
|
||||
super().__init__(uv_func=None, **kwargs)
|
||||
self.add(*parametric_surfaces)
|
||||
|
||||
@@ -214,7 +251,13 @@ class TexturedSurface(Surface):
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, uv_surface, image_file, dark_image_file=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
uv_surface: Surface,
|
||||
image_file: str,
|
||||
dark_image_file: str | None = None,
|
||||
**kwargs
|
||||
):
|
||||
if not isinstance(uv_surface, Surface):
|
||||
raise Exception("uv_surface must be of type Surface")
|
||||
# Set texture information
|
||||
@@ -230,10 +273,10 @@ class TexturedSurface(Surface):
|
||||
|
||||
self.uv_surface = uv_surface
|
||||
self.uv_func = uv_surface.uv_func
|
||||
self.u_range = uv_surface.u_range
|
||||
self.v_range = uv_surface.v_range
|
||||
self.resolution = uv_surface.resolution
|
||||
self.gloss = self.uv_surface.gloss
|
||||
self.u_range: tuple[float, float] = uv_surface.u_range
|
||||
self.v_range: tuple[float, float] = uv_surface.v_range
|
||||
self.resolution: tuple[float, float] = uv_surface.resolution
|
||||
self.gloss: float = self.uv_surface.gloss
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_data(self):
|
||||
@@ -257,12 +300,18 @@ class TexturedSurface(Surface):
|
||||
def init_colors(self):
|
||||
self.data["opacity"] = np.array([self.uv_surface.data["rgbas"][:, 3]])
|
||||
|
||||
def set_opacity(self, opacity, recurse=True):
|
||||
def set_opacity(self, opacity: float, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
|
||||
return self
|
||||
|
||||
def pointwise_become_partial(self, tsmobject, a, b, axis=1):
|
||||
def pointwise_become_partial(
|
||||
self,
|
||||
tsmobject: "TexturedSurface",
|
||||
a: float,
|
||||
b: float,
|
||||
axis: int = 1
|
||||
):
|
||||
super().pointwise_become_partial(tsmobject, a, b, axis)
|
||||
im_coords = self.data["im_coords"]
|
||||
im_coords[:] = tsmobject.data["im_coords"]
|
||||
@@ -274,7 +323,7 @@ class TexturedSurface(Surface):
|
||||
)
|
||||
return self
|
||||
|
||||
def fill_in_shader_color_info(self, shader_data):
|
||||
def fill_in_shader_color_info(self, shader_data: np.ndarray) -> np.ndarray:
|
||||
self.read_data_to_shader(shader_data, "opacity", "opacity")
|
||||
self.read_data_to_shader(shader_data, "im_coords", "im_coords")
|
||||
return shader_data
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import itertools as it
|
||||
import operator as op
|
||||
import moderngl
|
||||
from __future__ import annotations
|
||||
|
||||
import operator as op
|
||||
import itertools as it
|
||||
from functools import reduce, wraps
|
||||
from typing import Iterable, Sequence, Callable, Union
|
||||
|
||||
import colour
|
||||
import moderngl
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -12,6 +17,7 @@ from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points
|
||||
from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points
|
||||
from manimlib.utils.bezier import get_quadratic_approximation_of_cubic
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import inverse_interpolate
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.bezier import partial_quadratic_bezier_points
|
||||
from manimlib.utils.color import rgb_to_hex
|
||||
@@ -28,6 +34,9 @@ from manimlib.utils.space_ops import z_to_vector
|
||||
from manimlib.shader_wrapper import ShaderWrapper
|
||||
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
class VMobject(Mobject):
|
||||
CONFIG = {
|
||||
"fill_color": None,
|
||||
@@ -52,9 +61,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,)),
|
||||
@@ -75,7 +83,6 @@ class VMobject(Mobject):
|
||||
self.needs_new_triangulation = True
|
||||
self.triangulation = np.zeros(0, dtype='i4')
|
||||
super().__init__(**kwargs)
|
||||
self.refresh_unit_normal()
|
||||
|
||||
def get_group_class(self):
|
||||
return VGroup
|
||||
@@ -106,35 +113,83 @@ class VMobject(Mobject):
|
||||
self.set_flat_stroke(self.flat_stroke)
|
||||
return self
|
||||
|
||||
def set_fill(self, color=None, opacity=None, recurse=True):
|
||||
self.set_rgba_array(color, opacity, 'fill_rgba', recurse)
|
||||
def set_rgba_array(
|
||||
self,
|
||||
rgba_array: npt.ArrayLike,
|
||||
name: str = None,
|
||||
recurse: bool = 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_stroke(self, color=None, width=None, opacity=None, background=None, recurse=True):
|
||||
self.set_rgba_array(color, opacity, 'stroke_rgba', recurse)
|
||||
def set_fill(
|
||||
self,
|
||||
color: ManimColor | None = None,
|
||||
opacity: float | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
|
||||
return self
|
||||
|
||||
def set_stroke(
|
||||
self,
|
||||
color: ManimColor | None = None,
|
||||
width: float | npt.ArrayLike | None = None,
|
||||
opacity: float | None = None,
|
||||
background: bool | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
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 set_style(self,
|
||||
fill_color=None,
|
||||
fill_opacity=None,
|
||||
fill_rgba=None,
|
||||
stroke_color=None,
|
||||
stroke_opacity=None,
|
||||
stroke_rgba=None,
|
||||
stroke_width=None,
|
||||
gloss=None,
|
||||
shadow=None,
|
||||
recurse=True):
|
||||
def set_backstroke(
|
||||
self,
|
||||
color: ManimColor = BLACK,
|
||||
width: float | npt.ArrayLike = 3,
|
||||
background: bool = True
|
||||
):
|
||||
self.set_stroke(color, width, background=background)
|
||||
return self
|
||||
|
||||
def align_stroke_width_data_to_points(self, recurse: bool = True) -> None:
|
||||
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: ManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
fill_rgba: npt.ArrayLike | None = None,
|
||||
stroke_color: ManimColor | None = None,
|
||||
stroke_opacity: float | None = None,
|
||||
stroke_rgba: npt.ArrayLike | None = None,
|
||||
stroke_width: float | npt.ArrayLike | None = None,
|
||||
stroke_background: bool = True,
|
||||
reflectiveness: float | None = None,
|
||||
gloss: float | None = None,
|
||||
shadow: float | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
if fill_rgba is not None:
|
||||
self.data['fill_rgba'] = resize_with_interpolation(fill_rgba, len(fill_rgba))
|
||||
else:
|
||||
@@ -146,15 +201,21 @@ 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 reflectiveness is not None:
|
||||
self.set_reflectiveness(reflectiveness, recurse=recurse)
|
||||
if gloss is not None:
|
||||
self.set_gloss(gloss, recurse=recurse)
|
||||
if shadow is not None:
|
||||
@@ -163,14 +224,16 @@ class VMobject(Mobject):
|
||||
|
||||
def get_style(self):
|
||||
return {
|
||||
"fill_rgba": self.data['fill_rgba'],
|
||||
"stroke_rgba": self.data['stroke_rgba'],
|
||||
"stroke_width": self.data['stroke_width'],
|
||||
"fill_rgba": self.data['fill_rgba'].copy(),
|
||||
"stroke_rgba": self.data['stroke_rgba'].copy(),
|
||||
"stroke_width": self.data['stroke_width'].copy(),
|
||||
"stroke_background": self.draw_stroke_behind_fill,
|
||||
"reflectiveness": self.get_reflectiveness(),
|
||||
"gloss": self.get_gloss(),
|
||||
"shadow": self.get_shadow(),
|
||||
}
|
||||
|
||||
def match_style(self, vmobject, recurse=True):
|
||||
def match_style(self, vmobject: VMobject, recurse: bool = True):
|
||||
self.set_style(**vmobject.get_style(), recurse=False)
|
||||
if recurse:
|
||||
# Does its best to match up submobject lists, and
|
||||
@@ -184,101 +247,115 @@ class VMobject(Mobject):
|
||||
sm1.match_style(sm2)
|
||||
return self
|
||||
|
||||
def set_color(self, color, recurse=True):
|
||||
def set_color(self, color: ManimColor, recurse: bool = True):
|
||||
self.set_fill(color, recurse=recurse)
|
||||
self.set_stroke(color, recurse=recurse)
|
||||
return self
|
||||
|
||||
def set_opacity(self, opacity, recurse=True):
|
||||
def set_opacity(self, opacity: float, recurse: bool = True):
|
||||
self.set_fill(opacity=opacity, recurse=recurse)
|
||||
self.set_stroke(opacity=opacity, recurse=recurse)
|
||||
return self
|
||||
|
||||
def fade(self, darkness=0.5, recurse=True):
|
||||
factor = 1.0 - darkness
|
||||
self.set_fill(
|
||||
opacity=factor * self.get_fill_opacity(),
|
||||
recurse=False,
|
||||
)
|
||||
self.set_stroke(
|
||||
opacity=factor * self.get_stroke_opacity(),
|
||||
recurse=False,
|
||||
)
|
||||
super().fade(darkness, recurse)
|
||||
def fade(self, darkness: float = 0.5, recurse: bool = True):
|
||||
mobs = self.get_family() if recurse else [self]
|
||||
for mob in mobs:
|
||||
factor = 1.0 - darkness
|
||||
mob.set_fill(
|
||||
opacity=factor * mob.get_fill_opacity(),
|
||||
recurse=False,
|
||||
)
|
||||
mob.set_stroke(
|
||||
opacity=factor * mob.get_stroke_opacity(),
|
||||
recurse=False,
|
||||
)
|
||||
return self
|
||||
|
||||
def get_fill_colors(self):
|
||||
def get_fill_colors(self) -> list[str]:
|
||||
return [
|
||||
rgb_to_hex(rgba[:3])
|
||||
for rgba in self.data['fill_rgba']
|
||||
]
|
||||
|
||||
def get_fill_opacities(self):
|
||||
def get_fill_opacities(self) -> np.ndarray:
|
||||
return self.data['fill_rgba'][:, 3]
|
||||
|
||||
def get_stroke_colors(self):
|
||||
def get_stroke_colors(self) -> list[str]:
|
||||
return [
|
||||
rgb_to_hex(rgba[:3])
|
||||
for rgba in self.data['stroke_rgba']
|
||||
]
|
||||
|
||||
def get_stroke_opacities(self):
|
||||
def get_stroke_opacities(self) -> np.ndarray:
|
||||
return self.data['stroke_rgba'][:, 3]
|
||||
|
||||
def get_stroke_widths(self):
|
||||
return self.data['stroke_width']
|
||||
def get_stroke_widths(self) -> np.ndarray:
|
||||
return self.data['stroke_width'][:, 0]
|
||||
|
||||
# TODO, it's weird for these to return the first of various lists
|
||||
# rather than the full information
|
||||
def get_fill_color(self):
|
||||
def get_fill_color(self) -> str:
|
||||
"""
|
||||
If there are multiple colors (for gradient)
|
||||
this returns the first one
|
||||
"""
|
||||
return self.get_fill_colors()[0]
|
||||
|
||||
def get_fill_opacity(self):
|
||||
def get_fill_opacity(self) -> float:
|
||||
"""
|
||||
If there are multiple opacities, this returns the
|
||||
first
|
||||
"""
|
||||
return self.get_fill_opacities()[0]
|
||||
|
||||
def get_stroke_color(self):
|
||||
def get_stroke_color(self) -> str:
|
||||
return self.get_stroke_colors()[0]
|
||||
|
||||
def get_stroke_width(self):
|
||||
def get_stroke_width(self) -> float | np.ndarray:
|
||||
return self.get_stroke_widths()[0]
|
||||
|
||||
def get_stroke_opacity(self):
|
||||
def get_stroke_opacity(self) -> float:
|
||||
return self.get_stroke_opacities()[0]
|
||||
|
||||
def get_color(self):
|
||||
if self.has_stroke():
|
||||
return self.get_stroke_color()
|
||||
return self.get_fill_color()
|
||||
def get_color(self) -> str:
|
||||
if self.has_fill():
|
||||
return self.get_fill_color()
|
||||
return self.get_stroke_color()
|
||||
|
||||
def has_stroke(self):
|
||||
return any(self.get_stroke_widths()) and any(self.get_stroke_opacities())
|
||||
def has_stroke(self) -> bool:
|
||||
return self.get_stroke_widths().any() and self.get_stroke_opacities().any()
|
||||
|
||||
def has_fill(self):
|
||||
def has_fill(self) -> bool:
|
||||
return any(self.get_fill_opacities())
|
||||
|
||||
def get_opacity(self):
|
||||
def get_opacity(self) -> float:
|
||||
if self.has_fill():
|
||||
return self.get_fill_opacity()
|
||||
return self.get_stroke_opacity()
|
||||
|
||||
def set_flat_stroke(self, flat_stroke=True, recurse=True):
|
||||
def set_flat_stroke(self, flat_stroke: bool = True, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.flat_stroke = flat_stroke
|
||||
return self
|
||||
|
||||
def get_flat_stroke(self):
|
||||
def get_flat_stroke(self) -> bool:
|
||||
return self.flat_stroke
|
||||
|
||||
def set_joint_type(self, joint_type: str, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.joint_type = joint_type
|
||||
return self
|
||||
|
||||
def get_joint_type(self) -> str:
|
||||
return self.joint_type
|
||||
|
||||
# Points
|
||||
def set_anchors_and_handles(self, anchors1, handles, anchors2):
|
||||
def set_anchors_and_handles(
|
||||
self,
|
||||
anchors1: np.ndarray,
|
||||
handles: np.ndarray,
|
||||
anchors2: np.ndarray
|
||||
):
|
||||
assert(len(anchors1) == len(handles) == len(anchors2))
|
||||
nppc = self.n_points_per_curve
|
||||
new_points = np.zeros((nppc * len(anchors1), self.dim))
|
||||
@@ -288,16 +365,27 @@ class VMobject(Mobject):
|
||||
self.set_points(new_points)
|
||||
return self
|
||||
|
||||
def start_new_path(self, point):
|
||||
def start_new_path(self, point: np.ndarray):
|
||||
assert(self.get_num_points() % self.n_points_per_curve == 0)
|
||||
self.append_points([point])
|
||||
return self
|
||||
|
||||
def add_cubic_bezier_curve(self, anchor1, handle1, handle2, anchor2):
|
||||
def add_cubic_bezier_curve(
|
||||
self,
|
||||
anchor1: npt.ArrayLike,
|
||||
handle1: npt.ArrayLike,
|
||||
handle2: npt.ArrayLike,
|
||||
anchor2: npt.ArrayLike
|
||||
):
|
||||
new_points = get_quadratic_approximation_of_cubic(anchor1, handle1, handle2, anchor2)
|
||||
self.append_points(new_points)
|
||||
|
||||
def add_cubic_bezier_curve_to(self, handle1, handle2, anchor):
|
||||
def add_cubic_bezier_curve_to(
|
||||
self,
|
||||
handle1: npt.ArrayLike,
|
||||
handle2: npt.ArrayLike,
|
||||
anchor: npt.ArrayLike
|
||||
):
|
||||
"""
|
||||
Add cubic bezier curve to the path.
|
||||
"""
|
||||
@@ -310,14 +398,14 @@ class VMobject(Mobject):
|
||||
else:
|
||||
self.append_points(quadratic_approx)
|
||||
|
||||
def add_quadratic_bezier_curve_to(self, handle, anchor):
|
||||
def add_quadratic_bezier_curve_to(self, handle: np.ndarray, anchor: np.ndarray):
|
||||
self.throw_error_if_no_points()
|
||||
if self.has_new_path_started():
|
||||
self.append_points([handle, anchor])
|
||||
else:
|
||||
self.append_points([self.get_last_point(), handle, anchor])
|
||||
|
||||
def add_line_to(self, point):
|
||||
def add_line_to(self, point: np.ndarray):
|
||||
end = self.get_points()[-1]
|
||||
alphas = np.linspace(0, 1, self.n_points_per_curve)
|
||||
if self.long_lines:
|
||||
@@ -339,7 +427,7 @@ class VMobject(Mobject):
|
||||
self.append_points(points)
|
||||
return self
|
||||
|
||||
def add_smooth_curve_to(self, point):
|
||||
def add_smooth_curve_to(self, point: np.ndarray):
|
||||
if self.has_new_path_started():
|
||||
self.add_line_to(point)
|
||||
else:
|
||||
@@ -348,18 +436,21 @@ class VMobject(Mobject):
|
||||
self.add_quadratic_bezier_curve_to(new_handle, point)
|
||||
return self
|
||||
|
||||
def add_smooth_cubic_curve_to(self, handle, point):
|
||||
def add_smooth_cubic_curve_to(self, handle: np.ndarray, point: np.ndarray):
|
||||
self.throw_error_if_no_points()
|
||||
new_handle = self.get_reflection_of_last_handle()
|
||||
if self.get_num_points() == 1:
|
||||
new_handle = self.get_points()[-1]
|
||||
else:
|
||||
new_handle = self.get_reflection_of_last_handle()
|
||||
self.add_cubic_bezier_curve_to(new_handle, handle, point)
|
||||
|
||||
def has_new_path_started(self):
|
||||
def has_new_path_started(self) -> bool:
|
||||
return self.get_num_points() % self.n_points_per_curve == 1
|
||||
|
||||
def get_last_point(self):
|
||||
def get_last_point(self) -> np.ndarray:
|
||||
return self.get_points()[-1]
|
||||
|
||||
def get_reflection_of_last_handle(self):
|
||||
def get_reflection_of_last_handle(self) -> np.ndarray:
|
||||
points = self.get_points()
|
||||
return 2 * points[-1] - points[-2]
|
||||
|
||||
@@ -367,12 +458,16 @@ class VMobject(Mobject):
|
||||
if not self.is_closed():
|
||||
self.add_line_to(self.get_subpaths()[-1][0])
|
||||
|
||||
def is_closed(self):
|
||||
def is_closed(self) -> bool:
|
||||
return self.consider_points_equals(
|
||||
self.get_points()[0], self.get_points()[-1]
|
||||
)
|
||||
|
||||
def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, recurse=True):
|
||||
def subdivide_sharp_curves(
|
||||
self,
|
||||
angle_threshold: float = 30 * DEGREES,
|
||||
recurse: bool = True
|
||||
):
|
||||
vmobs = [vm for vm in self.get_family(recurse) if vm.has_points()]
|
||||
for vmob in vmobs:
|
||||
new_points = []
|
||||
@@ -390,12 +485,12 @@ class VMobject(Mobject):
|
||||
vmob.set_points(np.vstack(new_points))
|
||||
return self
|
||||
|
||||
def add_points_as_corners(self, points):
|
||||
def add_points_as_corners(self, points: Iterable[np.ndarray]):
|
||||
for point in points:
|
||||
self.add_line_to(point)
|
||||
return points
|
||||
|
||||
def set_points_as_corners(self, points):
|
||||
def set_points_as_corners(self, points: Iterable[np.ndarray]):
|
||||
nppc = self.n_points_per_curve
|
||||
points = np.array(points)
|
||||
self.set_anchors_and_handles(*[
|
||||
@@ -404,12 +499,19 @@ class VMobject(Mobject):
|
||||
])
|
||||
return self
|
||||
|
||||
def set_points_smoothly(self, points, true_smooth=False):
|
||||
def set_points_smoothly(
|
||||
self,
|
||||
points: Iterable[np.ndarray],
|
||||
true_smooth: bool = 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):
|
||||
def change_anchor_mode(self, mode: str):
|
||||
assert(mode in ("jagged", "approx_smooth", "true_smooth"))
|
||||
nppc = self.n_points_per_curve
|
||||
for submob in self.family_members_with_points():
|
||||
@@ -454,12 +556,12 @@ class VMobject(Mobject):
|
||||
self.change_anchor_mode("jagged")
|
||||
return self
|
||||
|
||||
def add_subpath(self, points):
|
||||
def add_subpath(self, points: Iterable[np.ndarray]):
|
||||
assert(len(points) % self.n_points_per_curve == 0)
|
||||
self.append_points(points)
|
||||
return self
|
||||
|
||||
def append_vectorized_mobject(self, vectorized_mobject):
|
||||
def append_vectorized_mobject(self, vectorized_mobject: VMobject):
|
||||
new_points = list(vectorized_mobject.get_points())
|
||||
|
||||
if self.has_new_path_started():
|
||||
@@ -470,23 +572,26 @@ class VMobject(Mobject):
|
||||
return self
|
||||
|
||||
#
|
||||
def consider_points_equals(self, p0, p1):
|
||||
def consider_points_equals(self, p0: np.ndarray, p1: np.ndarray) -> bool:
|
||||
return get_norm(p1 - p0) < self.tolerance_for_point_equality
|
||||
|
||||
# Information about the curve
|
||||
def get_bezier_tuples_from_points(self, points):
|
||||
def get_bezier_tuples_from_points(self, points: Sequence[np.ndarray]):
|
||||
nppc = self.n_points_per_curve
|
||||
remainder = len(points) % nppc
|
||||
points = points[:len(points) - remainder]
|
||||
return [
|
||||
return (
|
||||
points[i:i + nppc]
|
||||
for i in range(0, len(points), nppc)
|
||||
]
|
||||
)
|
||||
|
||||
def get_bezier_tuples(self):
|
||||
return self.get_bezier_tuples_from_points(self.get_points())
|
||||
|
||||
def get_subpaths_from_points(self, points):
|
||||
def get_subpaths_from_points(
|
||||
self,
|
||||
points: Sequence[np.ndarray]
|
||||
) -> list[Sequence[np.ndarray]]:
|
||||
nppc = self.n_points_per_curve
|
||||
diffs = points[nppc - 1:-1:nppc] - points[nppc::nppc]
|
||||
splits = (diffs * diffs).sum(1) > self.tolerance_for_point_equality
|
||||
@@ -503,27 +608,50 @@ class VMobject(Mobject):
|
||||
if (i2 - i1) >= nppc
|
||||
]
|
||||
|
||||
def get_subpaths(self):
|
||||
def get_subpaths(self) -> list[Sequence[np.ndarray]]:
|
||||
return self.get_subpaths_from_points(self.get_points())
|
||||
|
||||
def get_nth_curve_points(self, n):
|
||||
def get_nth_curve_points(self, n: int) -> np.ndarray:
|
||||
assert(n < self.get_num_curves())
|
||||
nppc = self.n_points_per_curve
|
||||
return self.get_points()[nppc * n:nppc * (n + 1)]
|
||||
|
||||
def get_nth_curve_function(self, n):
|
||||
def get_nth_curve_function(self, n: int) -> Callable[[float], np.ndarray]:
|
||||
return bezier(self.get_nth_curve_points(n))
|
||||
|
||||
def get_num_curves(self):
|
||||
def get_num_curves(self) -> int:
|
||||
return self.get_num_points() // self.n_points_per_curve
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
def quick_point_from_proportion(self, alpha: float) -> np.ndarray:
|
||||
# Assumes all curves have the same length, so is inaccurate
|
||||
num_curves = self.get_num_curves()
|
||||
n, residue = integer_interpolate(0, num_curves, alpha)
|
||||
curve_func = self.get_nth_curve_function(n)
|
||||
return curve_func(residue)
|
||||
|
||||
def get_anchors_and_handles(self):
|
||||
def point_from_proportion(self, alpha: float) -> np.ndarray:
|
||||
if alpha <= 0:
|
||||
return self.get_start()
|
||||
elif alpha >= 1:
|
||||
return self.get_end()
|
||||
|
||||
partials = [0]
|
||||
for tup in self.get_bezier_tuples():
|
||||
# Approximate length with straight line from start to end
|
||||
arclen = get_norm(tup[0] - tup[-1])
|
||||
partials.append(partials[-1] + arclen)
|
||||
full = partials[-1]
|
||||
if full == 0:
|
||||
return self.get_start()
|
||||
# First index where the partial lenth is more alpha times the full length
|
||||
i = next(
|
||||
(i for i, x in enumerate(partials) if x >= full * alpha),
|
||||
len(partials) # Default
|
||||
)
|
||||
residue = inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha)
|
||||
return self.get_nth_curve_function(i - 1)(residue)
|
||||
|
||||
def get_anchors_and_handles(self) -> list[np.ndarray]:
|
||||
"""
|
||||
returns anchors1, handles, anchors2,
|
||||
where (anchors1[i], handles[i], anchors2[i])
|
||||
@@ -537,14 +665,14 @@ class VMobject(Mobject):
|
||||
for i in range(nppc)
|
||||
]
|
||||
|
||||
def get_start_anchors(self):
|
||||
def get_start_anchors(self) -> np.ndarray:
|
||||
return self.get_points()[0::self.n_points_per_curve]
|
||||
|
||||
def get_end_anchors(self):
|
||||
def get_end_anchors(self) -> np.ndarray:
|
||||
nppc = self.n_points_per_curve
|
||||
return self.get_points()[nppc - 1::nppc]
|
||||
|
||||
def get_anchors(self):
|
||||
def get_anchors(self) -> np.ndarray:
|
||||
points = self.get_points()
|
||||
if len(points) == 1:
|
||||
return points
|
||||
@@ -553,7 +681,7 @@ class VMobject(Mobject):
|
||||
self.get_end_anchors(),
|
||||
))))
|
||||
|
||||
def get_points_without_null_curves(self, atol=1e-9):
|
||||
def get_points_without_null_curves(self, atol: float=1e-9) -> np.ndarray:
|
||||
nppc = self.n_points_per_curve
|
||||
points = self.get_points()
|
||||
distinct_curves = reduce(op.or_, [
|
||||
@@ -562,7 +690,7 @@ class VMobject(Mobject):
|
||||
])
|
||||
return points[distinct_curves.repeat(nppc)]
|
||||
|
||||
def get_arc_length(self, n_sample_points=None):
|
||||
def get_arc_length(self, n_sample_points: int | None = None) -> float:
|
||||
if n_sample_points is None:
|
||||
n_sample_points = 4 * self.get_num_curves() + 1
|
||||
points = np.array([
|
||||
@@ -573,7 +701,7 @@ class VMobject(Mobject):
|
||||
norms = np.array([get_norm(d) for d in diffs])
|
||||
return norms.sum()
|
||||
|
||||
def get_area_vector(self):
|
||||
def get_area_vector(self) -> np.ndarray:
|
||||
# Returns a vector whose length is the area bound by
|
||||
# the polygon formed by the anchor points, pointing
|
||||
# in a direction perpendicular to the polygon according
|
||||
@@ -593,7 +721,7 @@ class VMobject(Mobject):
|
||||
sum((p0[:, 0] + p1[:, 0]) * (p1[:, 1] - p0[:, 1])), # Add up (x1 + x2)*(y2 - y1)
|
||||
])
|
||||
|
||||
def get_unit_normal(self, recompute=False):
|
||||
def get_unit_normal(self, recompute: bool = False) -> np.ndarray:
|
||||
if not recompute:
|
||||
return self.data["unit_normal"][0]
|
||||
|
||||
@@ -603,21 +731,23 @@ class VMobject(Mobject):
|
||||
area_vect = self.get_area_vector()
|
||||
area = get_norm(area_vect)
|
||||
if area > 0:
|
||||
return area_vect / area
|
||||
normal = area_vect / area
|
||||
else:
|
||||
points = self.get_points()
|
||||
return get_unit_normal(
|
||||
normal = get_unit_normal(
|
||||
points[1] - points[0],
|
||||
points[2] - points[1],
|
||||
)
|
||||
self.data["unit_normal"][:] = normal
|
||||
return normal
|
||||
|
||||
def refresh_unit_normal(self):
|
||||
for mob in self.get_family():
|
||||
mob.data["unit_normal"][:] = mob.get_unit_normal(recompute=True)
|
||||
mob.get_unit_normal(recompute=True)
|
||||
return self
|
||||
|
||||
# Alignment
|
||||
def align_points(self, vmobject):
|
||||
def align_points(self, vmobject: VMobject):
|
||||
if self.get_num_points() == len(vmobject.get_points()):
|
||||
return
|
||||
|
||||
@@ -660,7 +790,7 @@ class VMobject(Mobject):
|
||||
vmobject.set_points(np.vstack(new_subpaths2))
|
||||
return self
|
||||
|
||||
def insert_n_curves(self, n, recurse=True):
|
||||
def insert_n_curves(self, n: int, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
if mob.get_num_curves() > 0:
|
||||
new_points = mob.insert_n_curves_to_point_list(n, mob.get_points())
|
||||
@@ -670,12 +800,12 @@ class VMobject(Mobject):
|
||||
mob.set_points(new_points)
|
||||
return self
|
||||
|
||||
def insert_n_curves_to_point_list(self, n, points):
|
||||
def insert_n_curves_to_point_list(self, n: int, points: np.ndarray):
|
||||
nppc = self.n_points_per_curve
|
||||
if len(points) == 1:
|
||||
return np.repeat(points, nppc * n, 0)
|
||||
|
||||
bezier_groups = self.get_bezier_tuples_from_points(points)
|
||||
bezier_groups = list(self.get_bezier_tuples_from_points(points))
|
||||
norms = np.array([
|
||||
get_norm(bg[nppc - 1] - bg[0])
|
||||
for bg in bezier_groups
|
||||
@@ -703,7 +833,13 @@ class VMobject(Mobject):
|
||||
new_points += partial_quadratic_bezier_points(group, a1, a2)
|
||||
return np.vstack(new_points)
|
||||
|
||||
def interpolate(self, mobject1, mobject2, alpha, *args, **kwargs):
|
||||
def interpolate(
|
||||
self,
|
||||
mobject1: VMobject,
|
||||
mobject2: VMobject,
|
||||
alpha: float,
|
||||
*args, **kwargs
|
||||
):
|
||||
super().interpolate(mobject1, mobject2, alpha, *args, **kwargs)
|
||||
if self.has_fill():
|
||||
tri1 = mobject1.get_triangulation()
|
||||
@@ -712,7 +848,7 @@ class VMobject(Mobject):
|
||||
self.refresh_triangulation()
|
||||
return self
|
||||
|
||||
def pointwise_become_partial(self, vmobject, a, b):
|
||||
def pointwise_become_partial(self, vmobject: VMobject, a: float, b: float):
|
||||
assert(isinstance(vmobject, VMobject))
|
||||
if a <= 0 and b >= 1:
|
||||
self.become(vmobject)
|
||||
@@ -754,7 +890,7 @@ class VMobject(Mobject):
|
||||
self.set_points(new_points)
|
||||
return self
|
||||
|
||||
def get_subcurve(self, a, b):
|
||||
def get_subcurve(self, a: float, b: float) -> VMobject:
|
||||
vmob = self.copy()
|
||||
vmob.pointwise_become_partial(self, a, b)
|
||||
return vmob
|
||||
@@ -766,12 +902,12 @@ class VMobject(Mobject):
|
||||
mob.needs_new_triangulation = True
|
||||
return self
|
||||
|
||||
def get_triangulation(self, normal_vector=None):
|
||||
def get_triangulation(self, normal_vector: np.ndarray | None = None):
|
||||
# Figure out how to triangulate the interior to know
|
||||
# how to send the points as to the vertex shader.
|
||||
# First triangles come directly from the points
|
||||
if normal_vector is None:
|
||||
normal_vector = self.get_unit_normal()
|
||||
normal_vector = self.get_unit_normal(recompute=True)
|
||||
|
||||
if not self.needs_new_triangulation:
|
||||
return self.triangulation
|
||||
@@ -827,34 +963,40 @@ class VMobject(Mobject):
|
||||
def triggers_refreshed_triangulation(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
old_points = self.get_points()
|
||||
old_points = self.get_points().copy()
|
||||
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
|
||||
def set_points(self, points):
|
||||
def set_points(self, points: npt.ArrayLike):
|
||||
super().set_points(points)
|
||||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def set_data(self, data):
|
||||
def set_data(self, data: dict):
|
||||
super().set_data(data)
|
||||
return self
|
||||
|
||||
# TODO, how to be smart about tangents here?
|
||||
@triggers_refreshed_triangulation
|
||||
def apply_function(self, function, make_smooth=False, **kwargs):
|
||||
def apply_function(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
make_smooth: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
super().apply_function(function, **kwargs)
|
||||
if self.make_smooth_after_applying_functions or make_smooth:
|
||||
self.make_approximately_smooth()
|
||||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def flip(self, *args, **kwargs):
|
||||
super().flip(*args, **kwargs)
|
||||
def flip(self, axis: np.ndarray = UP, **kwargs):
|
||||
super().flip(axis, **kwargs)
|
||||
self.refresh_unit_normal()
|
||||
self.refresh_triangulation()
|
||||
return self
|
||||
|
||||
# For shaders
|
||||
@@ -878,20 +1020,20 @@ class VMobject(Mobject):
|
||||
wrapper.refresh_id()
|
||||
return self
|
||||
|
||||
def get_fill_shader_wrapper(self):
|
||||
def get_fill_shader_wrapper(self) -> ShaderWrapper:
|
||||
self.fill_shader_wrapper.vert_data = self.get_fill_shader_data()
|
||||
self.fill_shader_wrapper.vert_indices = self.get_fill_shader_vert_indices()
|
||||
self.fill_shader_wrapper.uniforms = self.get_shader_uniforms()
|
||||
self.fill_shader_wrapper.depth_test = self.depth_test
|
||||
return self.fill_shader_wrapper
|
||||
|
||||
def get_stroke_shader_wrapper(self):
|
||||
def get_stroke_shader_wrapper(self) -> ShaderWrapper:
|
||||
self.stroke_shader_wrapper.vert_data = self.get_stroke_shader_data()
|
||||
self.stroke_shader_wrapper.uniforms = self.get_stroke_uniforms()
|
||||
self.stroke_shader_wrapper.depth_test = self.depth_test
|
||||
return self.stroke_shader_wrapper
|
||||
|
||||
def get_shader_wrapper_list(self):
|
||||
def get_shader_wrapper_list(self) -> list[ShaderWrapper]:
|
||||
# Build up data lists
|
||||
fill_shader_wrappers = []
|
||||
stroke_shader_wrappers = []
|
||||
@@ -920,13 +1062,13 @@ class VMobject(Mobject):
|
||||
result.append(wrapper)
|
||||
return result
|
||||
|
||||
def get_stroke_uniforms(self):
|
||||
def get_stroke_uniforms(self) -> dict[str, float]:
|
||||
result = dict(super().get_shader_uniforms())
|
||||
result["joint_type"] = JOINT_TYPE_MAP[self.joint_type]
|
||||
result["flat_stroke"] = float(self.flat_stroke)
|
||||
return result
|
||||
|
||||
def get_stroke_shader_data(self):
|
||||
def get_stroke_shader_data(self) -> np.ndarray:
|
||||
points = self.get_points()
|
||||
if len(self.stroke_data) != len(points):
|
||||
self.stroke_data = resize_array(self.stroke_data, len(points))
|
||||
@@ -945,7 +1087,7 @@ class VMobject(Mobject):
|
||||
|
||||
return self.stroke_data
|
||||
|
||||
def get_fill_shader_data(self):
|
||||
def get_fill_shader_data(self) -> np.ndarray:
|
||||
points = self.get_points()
|
||||
if len(self.fill_data) != len(points):
|
||||
self.fill_data = resize_array(self.fill_data, len(points))
|
||||
@@ -961,17 +1103,21 @@ class VMobject(Mobject):
|
||||
self.get_fill_shader_data()
|
||||
self.get_stroke_shader_data()
|
||||
|
||||
def get_fill_shader_vert_indices(self):
|
||||
def get_fill_shader_vert_indices(self) -> np.ndarray:
|
||||
return self.get_triangulation()
|
||||
|
||||
|
||||
class VGroup(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if not all([isinstance(m, VMobject) for m in vmobjects]):
|
||||
raise Exception("All submobjects must be of type VMobject")
|
||||
super().__init__(**kwargs)
|
||||
self.add(*vmobjects)
|
||||
|
||||
def __add__(self, other: VMobject | VGroup):
|
||||
assert(isinstance(other, VMobject))
|
||||
return self.add(other)
|
||||
|
||||
|
||||
class VectorizedPoint(Point, VMobject):
|
||||
CONFIG = {
|
||||
@@ -982,13 +1128,14 @@ class VectorizedPoint(Point, VMobject):
|
||||
"artificial_height": 0.01,
|
||||
}
|
||||
|
||||
def __init__(self, location=ORIGIN, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
def __init__(self, location: np.ndarray = ORIGIN, **kwargs):
|
||||
Point.__init__(self, **kwargs)
|
||||
VMobject.__init__(self, **kwargs)
|
||||
self.set_points(np.array([location]))
|
||||
|
||||
|
||||
class CurvesAsSubmobjects(VGroup):
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
for tup in vmobject.get_bezier_tuples():
|
||||
part = VMobject()
|
||||
@@ -1004,7 +1151,7 @@ class DashedVMobject(VMobject):
|
||||
"color": WHITE
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
num_dashes = self.num_dashes
|
||||
ps_ratio = self.positive_space_ratio
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import listify
|
||||
|
||||
|
||||
class ValueTracker(Mobject):
|
||||
@@ -14,22 +17,29 @@ class ValueTracker(Mobject):
|
||||
"value_type": np.float64,
|
||||
}
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
def __init__(self, value: float | complex = 0, **kwargs):
|
||||
self.value = value
|
||||
super().__init__(**kwargs)
|
||||
self.set_value(value)
|
||||
|
||||
def init_data(self):
|
||||
def init_data(self) -> None:
|
||||
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]
|
||||
def get_value(self) -> float | complex:
|
||||
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
|
||||
def set_value(self, value: float | complex):
|
||||
self.data["value"][0, :] = value
|
||||
return self
|
||||
|
||||
def increment_value(self, d_value):
|
||||
def increment_value(self, d_value: float | complex) -> None:
|
||||
self.set_value(self.get_value() + d_value)
|
||||
|
||||
|
||||
@@ -40,10 +50,10 @@ class ExponentialValueTracker(ValueTracker):
|
||||
behaves
|
||||
"""
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> float | complex:
|
||||
return np.exp(ValueTracker.get_value(self))
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: float | complex):
|
||||
return ValueTracker.set_value(self, np.log(value))
|
||||
|
||||
|
||||
|
||||
@@ -1,66 +1,47 @@
|
||||
import numpy as np
|
||||
import os
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
from PIL import Image
|
||||
import random
|
||||
from typing import Sequence, TypeVar, Callable, Iterable
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
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
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.coordinate_systems import CoordinateSystem
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
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: T,
|
||||
max_value: T,
|
||||
color_map: str
|
||||
) -> Callable[[npt.ArrayLike], np.ndarray]:
|
||||
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,32 +52,19 @@ 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: T,
|
||||
max_value: T,
|
||||
color_map: str
|
||||
) -> Callable[[T], np.ndarray]:
|
||||
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):
|
||||
def move_along_vector_field(
|
||||
mobject: Mobject,
|
||||
func: Callable[[np.ndarray], np.ndarray]
|
||||
) -> Mobject:
|
||||
mobject.add_updater(
|
||||
lambda m, dt: m.shift(
|
||||
func(m.get_center()) * dt
|
||||
@@ -105,7 +73,10 @@ def move_along_vector_field(mobject, func):
|
||||
return mobject
|
||||
|
||||
|
||||
def move_submobjects_along_vector_field(mobject, func):
|
||||
def move_submobjects_along_vector_field(
|
||||
mobject: Mobject,
|
||||
func: Callable[[np.ndarray], np.ndarray]
|
||||
) -> Mobject:
|
||||
def apply_nudge(mob, dt):
|
||||
for submob in mob:
|
||||
x, y = submob.get_center()[:2]
|
||||
@@ -116,175 +87,232 @@ 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: Mobject,
|
||||
func: Callable[[float, float], Iterable[float]],
|
||||
coordinate_system: CoordinateSystem
|
||||
) -> Mobject:
|
||||
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: CoordinateSystem,
|
||||
step_multiple: float
|
||||
) -> it.product[tuple[np.ndarray, ...]]:
|
||||
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: Callable[[float, float], Sequence[float]],
|
||||
coordinate_system: CoordinateSystem,
|
||||
**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: Iterable[float], **kwargs) -> Arrow:
|
||||
vector_config = merge_dicts_recursively(
|
||||
self.vector_config,
|
||||
kwargs
|
||||
)
|
||||
|
||||
output = np.array(self.func(*coords))
|
||||
norm = get_norm(output)
|
||||
if norm > 0:
|
||||
output *= self.length_func(norm) / norm
|
||||
|
||||
origin = self.coordinate_system.get_origin()
|
||||
_input = self.coordinate_system.c2p(*coords)
|
||||
_output = self.coordinate_system.c2p(*output)
|
||||
|
||||
vect = Arrow(
|
||||
origin, _output, buff=0,
|
||||
**vector_config
|
||||
)
|
||||
vect.shift(_input - origin)
|
||||
vect.set_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: Callable[[float, float], Sequence[float]],
|
||||
coordinate_system: CoordinateSystem,
|
||||
**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: np.ndarray) -> np.ndarray:
|
||||
in_coords = self.coordinate_system.p2c(point)
|
||||
out_coords = self.func(*in_coords)
|
||||
return self.coordinate_system.c2p(*out_coords)
|
||||
|
||||
def draw_lines(self) -> None:
|
||||
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)
|
||||
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)
|
||||
|
||||
self.set_stroke(self.stroke_color, self.stroke_width)
|
||||
def get_start_points(self) -> np.ndarray:
|
||||
cs = self.coordinate_system
|
||||
sample_coords = get_sample_points_from_coordinate_system(
|
||||
cs, self.step_multiple,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
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
|
||||
])
|
||||
|
||||
def init_style(self) -> None:
|
||||
if self.color_by_magnitude:
|
||||
values_to_rgbs = get_vectorized_rgb_gradient_function(
|
||||
*self.magnitude_range, self.color_map,
|
||||
)
|
||||
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)
|
||||
|
||||
# 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
|
||||
if self.taper_stroke_width:
|
||||
width = [0, self.stroke_width, 0]
|
||||
else:
|
||||
width = self.stroke_width
|
||||
self.set_stroke(width=width)
|
||||
|
||||
|
||||
class AnimatedStreamLines(VGroup):
|
||||
CONFIG = {
|
||||
"lag_range": 4,
|
||||
"line_anim_class": VShowPassingFlash,
|
||||
"line_anim_config": {
|
||||
# "run_time": 4,
|
||||
"rate_func": linear,
|
||||
"time_width": 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, stream_lines: StreamLines, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.stream_lines = stream_lines
|
||||
for line in stream_lines:
|
||||
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)
|
||||
|
||||
self.add_updater(lambda m, dt: m.update(dt))
|
||||
|
||||
def update(self, dt: float) -> None:
|
||||
stream_lines = self.stream_lines
|
||||
for line in stream_lines:
|
||||
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,
|
||||
@@ -292,12 +320,12 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
||||
"remover": True
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: 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(
|
||||
VShowPassingFlash(
|
||||
vmobject.deepcopy().set_stroke(width=stroke_width),
|
||||
time_width=time_width,
|
||||
**kwargs
|
||||
@@ -307,35 +335,3 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
||||
np.linspace(max_time_width, 0, self.n_segments)
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
# 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_config": {
|
||||
"run_time": 4,
|
||||
"rate_func": linear,
|
||||
"time_width": 0.3,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, stream_lines, **kwargs):
|
||||
VGroup.__init__(self, **kwargs)
|
||||
self.stream_lines = stream_lines
|
||||
for line in stream_lines:
|
||||
line.anim = self.line_anim_class(line, **self.line_anim_config)
|
||||
line.anim.begin()
|
||||
line.time = -self.lag_range * random.random()
|
||||
self.add(line.anim.mobject)
|
||||
|
||||
self.add_updater(lambda m, dt: m.update(dt))
|
||||
|
||||
def update(self, dt):
|
||||
stream_lines = self.stream_lines
|
||||
for line in stream_lines:
|
||||
line.time += dt
|
||||
adjusted_time = max(line.time, 0) % line.anim.run_time
|
||||
line.anim.update(adjusted_time / line.anim.run_time)
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -277,7 +277,6 @@ class DiscreteGraphScene(Scene):
|
||||
def trace_cycle(self, cycle=None, color="yellow", run_time=2.0):
|
||||
if cycle is None:
|
||||
cycle = self.graph.region_cycles[0]
|
||||
time_per_edge = run_time / len(cycle)
|
||||
next_in_cycle = it.cycle(cycle)
|
||||
next(next_in_cycle) # jump one ahead
|
||||
self.traced_cycle = Mobject(*[
|
||||
@@ -333,7 +332,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):
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
import inspect
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import random
|
||||
import inspect
|
||||
import platform
|
||||
import itertools as it
|
||||
import logging
|
||||
from functools import wraps
|
||||
from typing import Iterable, Callable
|
||||
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
import numpy as np
|
||||
import time
|
||||
import numpy.typing as npt
|
||||
|
||||
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
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL.Image import Image
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
|
||||
class Scene(object):
|
||||
@@ -36,7 +46,9 @@ class Scene(object):
|
||||
"end_at_animation_number": None,
|
||||
"leave_progress_bars": False,
|
||||
"preview": True,
|
||||
"presenter_mode": False,
|
||||
"linger_after_completion": True,
|
||||
"pan_sensitivity": 3,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -45,29 +57,33 @@ 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.camera: Camera = self.camera_class(**self.camera_config)
|
||||
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
|
||||
self.mobjects = []
|
||||
self.num_plays = 0
|
||||
self.time = 0
|
||||
self.skip_time = 0
|
||||
self.original_skipping_status = self.skip_animations
|
||||
self.mobjects: list[Mobject] = [self.camera.frame]
|
||||
self.num_plays: int = 0
|
||||
self.time: float = 0
|
||||
self.skip_time: float = 0
|
||||
self.original_skipping_status: bool = self.skip_animations
|
||||
if self.start_at_animation_number is not None:
|
||||
self.skip_animations = True
|
||||
|
||||
# Items associated with interaction
|
||||
self.mouse_point = Point()
|
||||
self.mouse_drag_point = Point()
|
||||
self.hold_on_wait = self.presenter_mode
|
||||
|
||||
# Much nicer to work with deterministic scenes
|
||||
if self.random_seed is not None:
|
||||
random.seed(self.random_seed)
|
||||
np.random.seed(self.random_seed)
|
||||
|
||||
def run(self):
|
||||
self.virtual_animation_start_time = 0
|
||||
self.real_animation_start_time = time.time()
|
||||
def run(self) -> None:
|
||||
self.virtual_animation_start_time: float = 0
|
||||
self.real_animation_start_time: float = time.time()
|
||||
self.file_writer.begin()
|
||||
|
||||
self.setup()
|
||||
@@ -77,7 +93,7 @@ class Scene(object):
|
||||
pass
|
||||
self.tear_down()
|
||||
|
||||
def setup(self):
|
||||
def setup(self) -> None:
|
||||
"""
|
||||
This is meant to be implement by any scenes which
|
||||
are comonly subclassed, and have some common setup
|
||||
@@ -85,31 +101,33 @@ class Scene(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def construct(self):
|
||||
def construct(self) -> None:
|
||||
# Where all the animation happens
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
|
||||
def tear_down(self):
|
||||
def tear_down(self) -> None:
|
||||
self.stop_skipping()
|
||||
self.file_writer.finish()
|
||||
if self.window and self.linger_after_completion:
|
||||
self.interact()
|
||||
|
||||
def interact(self):
|
||||
def interact(self) -> None:
|
||||
# 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:
|
||||
self.unlock_mobject_data()
|
||||
|
||||
def embed(self):
|
||||
def embed(self, close_scene_on_exit: bool = True) -> None:
|
||||
if not self.preview:
|
||||
# If the scene is just being
|
||||
# written, ignore embed calls
|
||||
@@ -117,33 +135,39 @@ 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()
|
||||
# End scene when exiting an embed
|
||||
if close_scene_on_exit:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
# Only these methods should touch the camera
|
||||
def get_image(self):
|
||||
def get_image(self) -> Image:
|
||||
return self.camera.get_image()
|
||||
|
||||
def show(self):
|
||||
def show(self) -> None:
|
||||
self.update_frame(ignore_skipping=True)
|
||||
self.get_image().show()
|
||||
|
||||
def update_frame(self, dt=0, ignore_skipping=False):
|
||||
def update_frame(self, dt: float = 0, ignore_skipping: bool = False) -> None:
|
||||
self.increment_time(dt)
|
||||
self.update_mobjects(dt)
|
||||
if self.skip_animations and not ignore_skipping:
|
||||
@@ -161,30 +185,37 @@ class Scene(object):
|
||||
if rt < vt:
|
||||
self.update_frame(0)
|
||||
|
||||
def emit_frame(self):
|
||||
def emit_frame(self) -> None:
|
||||
if not self.skip_animations:
|
||||
self.file_writer.write_frame(self.camera)
|
||||
|
||||
# Related to updating
|
||||
def update_mobjects(self, dt):
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
for mobject in self.mobjects:
|
||||
mobject.update(dt)
|
||||
|
||||
def should_update_mobjects(self):
|
||||
def should_update_mobjects(self) -> bool:
|
||||
return self.always_update_mobjects or any([
|
||||
len(mob.get_family_updaters()) > 0
|
||||
for mob in self.mobjects
|
||||
])
|
||||
|
||||
def has_time_based_updaters(self) -> bool:
|
||||
return any([
|
||||
sm.has_time_based_updater()
|
||||
for mob in self.mobjects()
|
||||
for sm in mob.get_family()
|
||||
])
|
||||
|
||||
# Related to time
|
||||
def get_time(self):
|
||||
def get_time(self) -> float:
|
||||
return self.time
|
||||
|
||||
def increment_time(self, dt):
|
||||
def increment_time(self, dt: float) -> None:
|
||||
self.time += dt
|
||||
|
||||
# Related to internal mobject organization
|
||||
def get_top_level_mobjects(self):
|
||||
def get_top_level_mobjects(self) -> list[Mobject]:
|
||||
# Return only those which are not in the family
|
||||
# of another mobject from the scene
|
||||
mobjects = self.get_mobjects()
|
||||
@@ -198,10 +229,10 @@ class Scene(object):
|
||||
return num_families == 1
|
||||
return list(filter(is_top_level, mobjects))
|
||||
|
||||
def get_mobject_family_members(self):
|
||||
def get_mobject_family_members(self) -> list[Mobject]:
|
||||
return extract_mobject_family_members(self.mobjects)
|
||||
|
||||
def add(self, *new_mobjects):
|
||||
def add(self, *new_mobjects: Mobject):
|
||||
"""
|
||||
Mobjects will be displayed, from background to
|
||||
foreground in the order with which they are added.
|
||||
@@ -210,7 +241,7 @@ class Scene(object):
|
||||
self.mobjects += new_mobjects
|
||||
return self
|
||||
|
||||
def add_mobjects_among(self, values):
|
||||
def add_mobjects_among(self, values: Iterable):
|
||||
"""
|
||||
This is meant mostly for quick prototyping,
|
||||
e.g. to add all mobjects defined up to a point,
|
||||
@@ -222,17 +253,17 @@ class Scene(object):
|
||||
))
|
||||
return self
|
||||
|
||||
def remove(self, *mobjects_to_remove):
|
||||
def remove(self, *mobjects_to_remove: Mobject):
|
||||
self.mobjects = restructure_list_to_exclude_certain_family_members(
|
||||
self.mobjects, mobjects_to_remove
|
||||
)
|
||||
return self
|
||||
|
||||
def bring_to_front(self, *mobjects):
|
||||
def bring_to_front(self, *mobjects: Mobject):
|
||||
self.add(*mobjects)
|
||||
return self
|
||||
|
||||
def bring_to_back(self, *mobjects):
|
||||
def bring_to_back(self, *mobjects: Mobject):
|
||||
self.remove(*mobjects)
|
||||
self.mobjects = list(mobjects) + self.mobjects
|
||||
return self
|
||||
@@ -241,71 +272,96 @@ class Scene(object):
|
||||
self.mobjects = []
|
||||
return self
|
||||
|
||||
def get_mobjects(self):
|
||||
def get_mobjects(self) -> list[Mobject]:
|
||||
return list(self.mobjects)
|
||||
|
||||
def get_mobject_copies(self):
|
||||
def get_mobject_copies(self) -> list[Mobject]:
|
||||
return [m.copy() for m in self.mobjects]
|
||||
|
||||
def point_to_mobject(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
search_set: Iterable[Mobject] | None = None,
|
||||
buff: float = 0
|
||||
) -> Mobject | None:
|
||||
"""
|
||||
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):
|
||||
def update_skipping_status(self) -> None:
|
||||
if self.start_at_animation_number is not None:
|
||||
if self.num_plays == self.start_at_animation_number:
|
||||
self.stop_skipping()
|
||||
self.skip_time = self.time
|
||||
if not self.original_skipping_status:
|
||||
self.stop_skipping()
|
||||
if self.end_at_animation_number is not None:
|
||||
if self.num_plays >= self.end_at_animation_number:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
def stop_skipping(self):
|
||||
if self.skip_animations:
|
||||
self.skip_animations = False
|
||||
self.skip_time += self.time
|
||||
def stop_skipping(self) -> None:
|
||||
self.virtual_animation_start_time = self.time
|
||||
self.skip_animations = False
|
||||
|
||||
# Methods associated with running animations
|
||||
def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False):
|
||||
def get_time_progression(
|
||||
self,
|
||||
run_time: float,
|
||||
n_iterations: int | None = None,
|
||||
desc: str = "",
|
||||
override_skip_animations: bool = False
|
||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
if self.skip_animations and not override_skip_animations:
|
||||
times = [run_time]
|
||||
return [run_time]
|
||||
else:
|
||||
step = 1 / self.camera.frame_rate
|
||||
times = np.arange(0, run_time, step)
|
||||
time_progression = ProgressDisplay(
|
||||
|
||||
if self.file_writer.has_progress_display:
|
||||
self.file_writer.set_progress_display_subdescription(desc)
|
||||
return times
|
||||
|
||||
return ProgressDisplay(
|
||||
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,
|
||||
desc=desc,
|
||||
)
|
||||
return time_progression
|
||||
|
||||
def get_run_time(self, animations):
|
||||
def get_run_time(self, animations: Iterable[Animation]) -> float:
|
||||
return np.max([animation.run_time for animation in animations])
|
||||
|
||||
def get_animation_time_progression(self, animations):
|
||||
def get_animation_time_progression(
|
||||
self,
|
||||
animations: Iterable[Animation]
|
||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
run_time = self.get_run_time(animations)
|
||||
time_progression = self.get_time_progression(run_time)
|
||||
time_progression.set_description("".join([
|
||||
f"Animation {self.num_plays}: {animations[0]}",
|
||||
", etc." if len(animations) > 1 else "",
|
||||
]))
|
||||
description = f"{self.num_plays} {animations[0]}"
|
||||
if len(animations) > 1:
|
||||
description += ", etc."
|
||||
time_progression = self.get_time_progression(run_time, desc=description)
|
||||
return time_progression
|
||||
|
||||
def get_wait_time_progression(self, duration, stop_condition):
|
||||
def get_wait_time_progression(
|
||||
self,
|
||||
duration: float,
|
||||
stop_condition: Callable[[], bool] | None = None
|
||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
kw = {"desc": f"{self.num_plays} Waiting"}
|
||||
if stop_condition is not None:
|
||||
time_progression = self.get_time_progression(
|
||||
duration,
|
||||
n_iterations=-1, # So it doesn't show % progress
|
||||
override_skip_animations=True
|
||||
)
|
||||
time_progression.set_description(
|
||||
"Waiting for {}".format(stop_condition.__name__)
|
||||
)
|
||||
else:
|
||||
time_progression = self.get_time_progression(duration)
|
||||
time_progression.set_description(
|
||||
"Waiting {}".format(self.num_plays)
|
||||
)
|
||||
return time_progression
|
||||
kw["n_iterations"] = -1 # So it doesn't show % progress
|
||||
kw["override_skip_animations"] = True
|
||||
return self.get_time_progression(duration, **kw)
|
||||
|
||||
def anims_from_play_args(self, *args, **kwargs):
|
||||
def anims_from_play_args(self, *args, **kwargs) -> list[Animation]:
|
||||
"""
|
||||
Each arg can either be an animation, or a mobject method
|
||||
followed by that methods arguments (and potentially follow
|
||||
@@ -395,7 +451,7 @@ class Scene(object):
|
||||
self.num_plays += 1
|
||||
return wrapper
|
||||
|
||||
def lock_static_mobject_data(self, *animations):
|
||||
def lock_static_mobject_data(self, *animations: Animation) -> None:
|
||||
movers = list(it.chain(*[
|
||||
anim.mobject.get_family()
|
||||
for anim in animations
|
||||
@@ -405,10 +461,15 @@ class Scene(object):
|
||||
continue
|
||||
self.camera.set_mobjects_as_static(mobject)
|
||||
|
||||
def unlock_mobject_data(self):
|
||||
def unlock_mobject_data(self) -> None:
|
||||
self.camera.release_static_mobjects()
|
||||
|
||||
def begin_animations(self, animations):
|
||||
def refresh_locked_data(self):
|
||||
self.unlock_mobject_data()
|
||||
self.lock_static_mobject_data()
|
||||
return self
|
||||
|
||||
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
||||
for animation in animations:
|
||||
animation.begin()
|
||||
# Anything animated that's not already in the
|
||||
@@ -419,7 +480,7 @@ class Scene(object):
|
||||
if animation.mobject not in self.mobjects:
|
||||
self.add(animation.mobject)
|
||||
|
||||
def progress_through_animations(self, animations):
|
||||
def progress_through_animations(self, animations: Iterable[Animation]) -> None:
|
||||
last_t = 0
|
||||
for t in self.get_animation_time_progression(animations):
|
||||
dt = t - last_t
|
||||
@@ -431,7 +492,7 @@ class Scene(object):
|
||||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
|
||||
def finish_animations(self, animations):
|
||||
def finish_animations(self, animations: Iterable[Animation]) -> None:
|
||||
for animation in animations:
|
||||
animation.finish()
|
||||
animation.clean_up_from_scene(self)
|
||||
@@ -441,12 +502,9 @@ class Scene(object):
|
||||
self.update_mobjects(0)
|
||||
|
||||
@handle_play_like_call
|
||||
def play(self, *args, **kwargs):
|
||||
def play(self, *args, **kwargs) -> None:
|
||||
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)
|
||||
@@ -456,10 +514,22 @@ class Scene(object):
|
||||
self.unlock_mobject_data()
|
||||
|
||||
@handle_play_like_call
|
||||
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
|
||||
def wait(
|
||||
self,
|
||||
duration: float = DEFAULT_WAIT_TIME,
|
||||
stop_condition: Callable[[], bool] = None,
|
||||
note: str = None,
|
||||
ignore_presenter_mode: bool = False
|
||||
):
|
||||
if note:
|
||||
log.info(note)
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
if self.should_update_mobjects():
|
||||
self.lock_static_mobject_data()
|
||||
self.lock_static_mobject_data()
|
||||
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
||||
while self.hold_on_wait:
|
||||
self.update_frame(dt=1 / self.camera.frame_rate)
|
||||
self.hold_on_wait = True
|
||||
else:
|
||||
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||
last_t = 0
|
||||
for t in time_progression:
|
||||
@@ -470,18 +540,14 @@ class Scene(object):
|
||||
if stop_condition is not None and stop_condition():
|
||||
time_progression.close()
|
||||
break
|
||||
self.unlock_mobject_data()
|
||||
elif self.skip_animations:
|
||||
# Do nothing
|
||||
return self
|
||||
else:
|
||||
self.update_frame(duration)
|
||||
n_frames = int(duration * self.camera.frame_rate)
|
||||
for n in range(n_frames):
|
||||
self.emit_frame()
|
||||
self.unlock_mobject_data()
|
||||
return self
|
||||
|
||||
def wait_until(self, stop_condition, max_time=60):
|
||||
def wait_until(
|
||||
self,
|
||||
stop_condition: Callable[[], bool],
|
||||
max_time: float = 60
|
||||
):
|
||||
self.wait(max_time, stop_condition=stop_condition)
|
||||
|
||||
def force_skipping(self):
|
||||
@@ -494,14 +560,20 @@ class Scene(object):
|
||||
self.skip_animations = self.original_skipping_status
|
||||
return self
|
||||
|
||||
def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs):
|
||||
def add_sound(
|
||||
self,
|
||||
sound_file: str,
|
||||
time_offset: float = 0,
|
||||
gain: float | None = None,
|
||||
gain_to_background: float | None = None
|
||||
):
|
||||
if self.skip_animations:
|
||||
return
|
||||
time = self.get_time() + time_offset
|
||||
self.file_writer.add_sound(sound_file, time, gain, **kwargs)
|
||||
self.file_writer.add_sound(sound_file, time, gain, gain_to_background)
|
||||
|
||||
# Helpers for interactive development
|
||||
def save_state(self):
|
||||
def save_state(self) -> None:
|
||||
self.saved_state = {
|
||||
"mobjects": self.mobjects,
|
||||
"mobject_states": [
|
||||
@@ -510,7 +582,7 @@ class Scene(object):
|
||||
],
|
||||
}
|
||||
|
||||
def restore(self):
|
||||
def restore(self) -> None:
|
||||
if not hasattr(self, "saved_state"):
|
||||
raise Exception("Trying to restore scene without having saved")
|
||||
mobjects = self.saved_state["mobjects"]
|
||||
@@ -521,7 +593,11 @@ class Scene(object):
|
||||
|
||||
# Event handling
|
||||
|
||||
def on_mouse_motion(self, point, d_point):
|
||||
def on_mouse_motion(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
d_point: np.ndarray
|
||||
) -> None:
|
||||
self.mouse_point.move_to(point)
|
||||
|
||||
event_data = {"point": point, "d_point": d_point}
|
||||
@@ -531,8 +607,8 @@ class Scene(object):
|
||||
|
||||
frame = self.camera.frame
|
||||
if self.window.is_key_pressed(ord("d")):
|
||||
frame.increment_theta(-d_point[0])
|
||||
frame.increment_phi(d_point[1])
|
||||
frame.increment_theta(-self.pan_sensitivity * d_point[0])
|
||||
frame.increment_phi(self.pan_sensitivity * d_point[1])
|
||||
elif self.window.is_key_pressed(ord("s")):
|
||||
shift = -d_point
|
||||
shift[0] *= frame.get_width() / 2
|
||||
@@ -541,7 +617,13 @@ class Scene(object):
|
||||
shift = np.dot(np.transpose(transform), shift)
|
||||
frame.shift(shift)
|
||||
|
||||
def on_mouse_drag(self, point, d_point, buttons, modifiers):
|
||||
def on_mouse_drag(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
d_point: np.ndarray,
|
||||
buttons: int,
|
||||
modifiers: int
|
||||
) -> None:
|
||||
self.mouse_drag_point.move_to(point)
|
||||
|
||||
event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers}
|
||||
@@ -549,19 +631,33 @@ class Scene(object):
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_mouse_press(self, point, button, mods):
|
||||
def on_mouse_press(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
button: int,
|
||||
mods: int
|
||||
) -> None:
|
||||
event_data = {"point": point, "button": button, "mods": mods}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MousePressEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_mouse_release(self, point, button, mods):
|
||||
def on_mouse_release(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
button: int,
|
||||
mods: int
|
||||
) -> None:
|
||||
event_data = {"point": point, "button": button, "mods": mods}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseReleaseEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_mouse_scroll(self, point, offset):
|
||||
def on_mouse_scroll(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
offset: np.ndarray
|
||||
) -> None:
|
||||
event_data = {"point": point, "offset": offset}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
@@ -570,23 +666,31 @@ 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)
|
||||
frame.shift(-20.0 * shift)
|
||||
|
||||
def on_key_release(self, symbol, modifiers):
|
||||
def on_key_release(
|
||||
self,
|
||||
symbol: int,
|
||||
modifiers: int
|
||||
) -> None:
|
||||
event_data = {"symbol": symbol, "modifiers": modifiers}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.KeyReleaseEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
def on_key_press(
|
||||
self,
|
||||
symbol: int,
|
||||
modifiers: int
|
||||
) -> None:
|
||||
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}
|
||||
@@ -598,17 +702,21 @@ class Scene(object):
|
||||
self.camera.frame.to_default_state()
|
||||
elif char == "q":
|
||||
self.quit_interaction = True
|
||||
elif char == " " or symbol == 65363: # Space or right arrow
|
||||
self.hold_on_wait = False
|
||||
elif char == "e" and modifiers == 3: # ctrl + shift + e
|
||||
self.embed(close_scene_on_exit=False)
|
||||
|
||||
def on_resize(self, width: int, height: int):
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
self.camera.reset_pixel_shape(width, height)
|
||||
|
||||
def on_show(self):
|
||||
def on_show(self) -> None:
|
||||
pass
|
||||
|
||||
def on_hide(self):
|
||||
def on_hide(self) -> None:
|
||||
pass
|
||||
|
||||
def on_close(self):
|
||||
def on_close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
import shutil
|
||||
import subprocess as sp
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import platform
|
||||
import subprocess as sp
|
||||
|
||||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
|
||||
from manimlib.constants import FFMPEG_BIN
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
@@ -12,6 +16,14 @@ 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
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.camera.camera import Camera
|
||||
from PIL.Image import Image
|
||||
|
||||
|
||||
class SceneFileWriter(object):
|
||||
@@ -34,17 +46,20 @@ class SceneFileWriter(object):
|
||||
"open_file_upon_completion": False,
|
||||
"show_file_location_upon_completion": False,
|
||||
"quiet": False,
|
||||
"total_frames": 0,
|
||||
"progress_description_len": 60,
|
||||
}
|
||||
|
||||
def __init__(self, scene, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.scene = scene
|
||||
self.writing_process = None
|
||||
self.scene: Scene = scene
|
||||
self.writing_process: sp.Popen | None = None
|
||||
self.has_progress_display: bool = False
|
||||
self.init_output_directories()
|
||||
self.init_audio()
|
||||
|
||||
# Output directories and files
|
||||
def init_output_directories(self):
|
||||
def init_output_directories(self) -> None:
|
||||
out_dir = self.output_directory
|
||||
if self.mirror_module_path:
|
||||
module_dir = self.get_default_module_directory()
|
||||
@@ -64,19 +79,23 @@ class SceneFileWriter(object):
|
||||
movie_dir, "partial_movie_files", scene_name,
|
||||
))
|
||||
|
||||
def get_default_module_directory(self):
|
||||
def get_default_module_directory(self) -> str:
|
||||
path, _ = os.path.splitext(self.input_file_path)
|
||||
if path.startswith("_"):
|
||||
path = path[1:]
|
||||
return path
|
||||
|
||||
def get_default_scene_name(self):
|
||||
if self.file_name is None:
|
||||
return self.scene.__class__.__name__
|
||||
else:
|
||||
return self.file_name
|
||||
def get_default_scene_name(self) -> str:
|
||||
name = str(self.scene)
|
||||
saan = self.scene.start_at_animation_number
|
||||
eaan = self.scene.end_at_animation_number
|
||||
if saan is not None:
|
||||
name += f"_{saan}"
|
||||
if eaan is not None:
|
||||
name += f"_{eaan}"
|
||||
return name
|
||||
|
||||
def get_resolution_directory(self):
|
||||
def get_resolution_directory(self) -> str:
|
||||
pixel_height = self.scene.camera.pixel_height
|
||||
frame_rate = self.scene.camera.frame_rate
|
||||
return "{}p{}".format(
|
||||
@@ -84,10 +103,10 @@ class SceneFileWriter(object):
|
||||
)
|
||||
|
||||
# Directory getters
|
||||
def get_image_file_path(self):
|
||||
def get_image_file_path(self) -> str:
|
||||
return self.image_file_path
|
||||
|
||||
def get_next_partial_movie_path(self):
|
||||
def get_next_partial_movie_path(self) -> str:
|
||||
result = os.path.join(
|
||||
self.partial_movie_directory,
|
||||
"{:05}{}".format(
|
||||
@@ -97,19 +116,22 @@ class SceneFileWriter(object):
|
||||
)
|
||||
return result
|
||||
|
||||
def get_movie_file_path(self):
|
||||
def get_movie_file_path(self) -> str:
|
||||
return self.movie_file_path
|
||||
|
||||
# Sound
|
||||
def init_audio(self):
|
||||
self.includes_sound = False
|
||||
def init_audio(self) -> None:
|
||||
self.includes_sound: bool = False
|
||||
|
||||
def create_audio_segment(self):
|
||||
def create_audio_segment(self) -> None:
|
||||
self.audio_segment = AudioSegment.silent()
|
||||
|
||||
def add_audio_segment(self, new_segment,
|
||||
time=None,
|
||||
gain_to_background=None):
|
||||
def add_audio_segment(
|
||||
self,
|
||||
new_segment: AudioSegment,
|
||||
time: float | None = None,
|
||||
gain_to_background: float | None = None
|
||||
) -> None:
|
||||
if not self.includes_sound:
|
||||
self.includes_sound = True
|
||||
self.create_audio_segment()
|
||||
@@ -133,27 +155,33 @@ class SceneFileWriter(object):
|
||||
gain_during_overlay=gain_to_background,
|
||||
)
|
||||
|
||||
def add_sound(self, sound_file, time=None, gain=None, **kwargs):
|
||||
def add_sound(
|
||||
self,
|
||||
sound_file: str,
|
||||
time: float | None = None,
|
||||
gain: float | None = None,
|
||||
gain_to_background: float | None = None
|
||||
) -> None:
|
||||
file_path = get_full_sound_file_path(sound_file)
|
||||
new_segment = AudioSegment.from_file(file_path)
|
||||
if gain:
|
||||
new_segment = new_segment.apply_gain(gain)
|
||||
self.add_audio_segment(new_segment, time, **kwargs)
|
||||
self.add_audio_segment(new_segment, time, gain_to_background)
|
||||
|
||||
# Writers
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
if not self.break_into_partial_movies and self.write_to_movie:
|
||||
self.open_movie_pipe(self.get_movie_file_path())
|
||||
|
||||
def begin_animation(self):
|
||||
def begin_animation(self) -> None:
|
||||
if self.break_into_partial_movies and self.write_to_movie:
|
||||
self.open_movie_pipe(self.get_next_partial_movie_path())
|
||||
|
||||
def end_animation(self):
|
||||
def end_animation(self) -> None:
|
||||
if self.break_into_partial_movies and self.write_to_movie:
|
||||
self.close_movie_pipe()
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
if self.write_to_movie:
|
||||
if self.break_into_partial_movies:
|
||||
self.combine_movie_files()
|
||||
@@ -168,7 +196,7 @@ class SceneFileWriter(object):
|
||||
if self.should_open_file():
|
||||
self.open_file()
|
||||
|
||||
def open_movie_pipe(self, file_path):
|
||||
def open_movie_pipe(self, file_path: str) -> None:
|
||||
stem, ext = os.path.splitext(file_path)
|
||||
self.final_file_path = file_path
|
||||
self.temp_file_path = stem + "_temp" + ext
|
||||
@@ -183,7 +211,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',
|
||||
@@ -204,18 +232,42 @@ class SceneFileWriter(object):
|
||||
command += [self.temp_file_path]
|
||||
self.writing_process = sp.Popen(command, stdin=sp.PIPE)
|
||||
|
||||
def write_frame(self, camera):
|
||||
if self.total_frames > 0:
|
||||
self.progress_display = ProgressDisplay(
|
||||
range(self.total_frames),
|
||||
# bar_format="{l_bar}{bar}|{n_fmt}/{total_fmt}",
|
||||
leave=False,
|
||||
ascii=True if platform.system() == 'Windows' else None,
|
||||
dynamic_ncols=True,
|
||||
)
|
||||
self.has_progress_display = True
|
||||
|
||||
def set_progress_display_subdescription(self, sub_desc: str) -> None:
|
||||
desc_len = self.progress_description_len
|
||||
file = os.path.split(self.get_movie_file_path())[1]
|
||||
full_desc = f"Rendering {file} ({sub_desc})"
|
||||
if len(full_desc) > desc_len:
|
||||
full_desc = full_desc[:desc_len - 4] + "...)"
|
||||
else:
|
||||
full_desc += " " * (desc_len - len(full_desc))
|
||||
self.progress_display.set_description(full_desc)
|
||||
|
||||
def write_frame(self, camera: Camera) -> None:
|
||||
if self.write_to_movie:
|
||||
raw_bytes = camera.get_raw_fbo_data()
|
||||
self.writing_process.stdin.write(raw_bytes)
|
||||
if self.has_progress_display:
|
||||
self.progress_display.update()
|
||||
|
||||
def close_movie_pipe(self):
|
||||
def close_movie_pipe(self) -> None:
|
||||
self.writing_process.stdin.close()
|
||||
self.writing_process.wait()
|
||||
self.writing_process.terminate()
|
||||
if self.has_progress_display:
|
||||
self.progress_display.close()
|
||||
shutil.move(self.temp_file_path, self.final_file_path)
|
||||
|
||||
def combine_movie_files(self):
|
||||
def combine_movie_files(self) -> None:
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.movie_file_extension,
|
||||
@@ -231,7 +283,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
|
||||
@@ -263,7 +315,7 @@ class SceneFileWriter(object):
|
||||
combine_process = sp.Popen(commands)
|
||||
combine_process.wait()
|
||||
|
||||
def add_sound_to_video(self):
|
||||
def add_sound_to_video(self) -> None:
|
||||
movie_file_path = self.get_movie_file_path()
|
||||
stem, ext = os.path.splitext(movie_file_path)
|
||||
sound_file_path = stem + ".wav"
|
||||
@@ -275,7 +327,7 @@ class SceneFileWriter(object):
|
||||
)
|
||||
temp_file_path = stem + "_temp" + ext
|
||||
commands = [
|
||||
"ffmpeg",
|
||||
FFMPEG_BIN,
|
||||
"-i", movie_file_path,
|
||||
"-i", sound_file_path,
|
||||
'-y', # overwrite output file if it exists
|
||||
@@ -294,21 +346,22 @@ class SceneFileWriter(object):
|
||||
shutil.move(temp_file_path, movie_file_path)
|
||||
os.remove(sound_file_path)
|
||||
|
||||
def save_final_image(self, image):
|
||||
def save_final_image(self, image: Image) -> None:
|
||||
file_path = self.get_image_file_path()
|
||||
image.save(file_path)
|
||||
self.print_file_ready_message(file_path)
|
||||
|
||||
def print_file_ready_message(self, file_path):
|
||||
print(f"\nFile ready at {file_path}\n")
|
||||
def print_file_ready_message(self, file_path: str) -> None:
|
||||
if not self.quiet:
|
||||
log.info(f"File ready at {file_path}")
|
||||
|
||||
def should_open_file(self):
|
||||
def should_open_file(self) -> bool:
|
||||
return any([
|
||||
self.show_file_location_upon_completion,
|
||||
self.open_file_upon_completion,
|
||||
])
|
||||
|
||||
def open_file(self):
|
||||
def open_file(self) -> None:
|
||||
if self.quiet:
|
||||
curr_stdout = sys.stdout
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
@@ -287,9 +287,6 @@ class LinearTransformationScene(VectorScene):
|
||||
},
|
||||
"background_plane_kwargs": {
|
||||
"color": GREY,
|
||||
"axis_config": {
|
||||
"stroke_color": GREY_B,
|
||||
},
|
||||
"axis_config": {
|
||||
"color": GREY,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import copy
|
||||
from typing import Iterable
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
import copy
|
||||
|
||||
from manimlib.utils.directories import get_shader_dir
|
||||
from manimlib.utils.file_ops import find_file
|
||||
@@ -15,15 +19,16 @@ from manimlib.utils.file_ops import find_file
|
||||
|
||||
|
||||
class ShaderWrapper(object):
|
||||
def __init__(self,
|
||||
vert_data=None,
|
||||
vert_indices=None,
|
||||
shader_folder=None,
|
||||
uniforms=None, # A dictionary mapping names of uniform variables
|
||||
texture_paths=None, # A dictionary mapping names to filepaths for textures.
|
||||
depth_test=False,
|
||||
render_primitive=moderngl.TRIANGLE_STRIP,
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
vert_data: np.ndarray | None = None,
|
||||
vert_indices: np.ndarray | None = None,
|
||||
shader_folder: str | None = None,
|
||||
uniforms: dict[str, float] | None = None, # A dictionary mapping names of uniform variables
|
||||
texture_paths: dict[str, str] | None = None, # A dictionary mapping names to filepaths for textures.
|
||||
depth_test: bool = False,
|
||||
render_primitive: int = moderngl.TRIANGLE_STRIP,
|
||||
):
|
||||
self.vert_data = vert_data
|
||||
self.vert_indices = vert_indices
|
||||
self.vert_attributes = vert_data.dtype.names
|
||||
@@ -46,20 +51,20 @@ class ShaderWrapper(object):
|
||||
result.texture_paths = dict(self.texture_paths)
|
||||
return result
|
||||
|
||||
def is_valid(self):
|
||||
def is_valid(self) -> bool:
|
||||
return all([
|
||||
self.vert_data is not None,
|
||||
self.program_code["vertex_shader"] is not None,
|
||||
self.program_code["fragment_shader"] is not None,
|
||||
])
|
||||
|
||||
def get_id(self):
|
||||
def get_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
def get_program_id(self):
|
||||
def get_program_id(self) -> int:
|
||||
return self.program_id
|
||||
|
||||
def create_id(self):
|
||||
def create_id(self) -> str:
|
||||
# A unique id for a shader
|
||||
return "|".join(map(str, [
|
||||
self.program_id,
|
||||
@@ -69,32 +74,32 @@ class ShaderWrapper(object):
|
||||
self.render_primitive,
|
||||
]))
|
||||
|
||||
def refresh_id(self):
|
||||
def refresh_id(self) -> None:
|
||||
self.program_id = self.create_program_id()
|
||||
self.id = self.create_id()
|
||||
|
||||
def create_program_id(self):
|
||||
def create_program_id(self) -> int:
|
||||
return hash("".join((
|
||||
self.program_code[f"{name}_shader"] or ""
|
||||
for name in ("vertex", "geometry", "fragment")
|
||||
)))
|
||||
|
||||
def init_program_code(self):
|
||||
def get_code(name):
|
||||
def init_program_code(self) -> None:
|
||||
def get_code(name: str) -> str | None:
|
||||
return get_shader_code_from_file(
|
||||
os.path.join(self.shader_folder, f"{name}.glsl")
|
||||
)
|
||||
|
||||
self.program_code = {
|
||||
self.program_code: dict[str, str | None] = {
|
||||
"vertex_shader": get_code("vert"),
|
||||
"geometry_shader": get_code("geom"),
|
||||
"fragment_shader": get_code("frag"),
|
||||
}
|
||||
|
||||
def get_program_code(self):
|
||||
def get_program_code(self) -> dict[str, str | None]:
|
||||
return self.program_code
|
||||
|
||||
def replace_code(self, old, new):
|
||||
def replace_code(self, old: str, new: str) -> None:
|
||||
code_map = self.program_code
|
||||
for (name, code) in code_map.items():
|
||||
if code_map[name] is None:
|
||||
@@ -102,7 +107,7 @@ class ShaderWrapper(object):
|
||||
code_map[name] = re.sub(old, new, code_map[name])
|
||||
self.refresh_id()
|
||||
|
||||
def combine_with(self, *shader_wrappers):
|
||||
def combine_with(self, *shader_wrappers: ShaderWrapper):
|
||||
# Assume they are of the same type
|
||||
if len(shader_wrappers) == 0:
|
||||
return
|
||||
@@ -122,10 +127,10 @@ class ShaderWrapper(object):
|
||||
|
||||
|
||||
# For caching
|
||||
filename_to_code_map = {}
|
||||
filename_to_code_map: dict[str, str] = {}
|
||||
|
||||
|
||||
def get_shader_code_from_file(filename):
|
||||
def get_shader_code_from_file(filename: str) -> str | None:
|
||||
if not filename:
|
||||
return None
|
||||
if filename in filename_to_code_map:
|
||||
@@ -157,7 +162,7 @@ def get_shader_code_from_file(filename):
|
||||
return result
|
||||
|
||||
|
||||
def get_colormap_code(rgb_list):
|
||||
def get_colormap_code(rgb_list: Iterable[float]) -> str:
|
||||
data = ",".join(
|
||||
"vec3({}, {}, {})".format(*rgb)
|
||||
for rgb in rgb_list
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
uniform vec2 frame_shape;
|
||||
uniform float anti_alias_width;
|
||||
uniform vec3 camera_center;
|
||||
uniform vec3 camera_offset;
|
||||
uniform mat3 camera_rotation;
|
||||
uniform float is_fixed_in_frame;
|
||||
uniform float focal_distance;
|
||||
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;
|
||||
}
|
||||
@@ -13,39 +13,56 @@ vec4 add_light(vec4 color,
|
||||
vec3 point,
|
||||
vec3 unit_normal,
|
||||
vec3 light_coords,
|
||||
vec3 cam_coords,
|
||||
float reflectiveness,
|
||||
float gloss,
|
||||
float shadow){
|
||||
if(gloss == 0.0 && shadow == 0.0) return color;
|
||||
if(reflectiveness == 0.0 && 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;
|
||||
vec4 result = color;
|
||||
// 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;
|
||||
// cam_coords = vec3(0, 0, focal_distance);
|
||||
vec3 to_camera = normalize(cam_coords - point);
|
||||
vec3 to_light = normalize(light_coords - point);
|
||||
|
||||
// Note, this effectively treats surfaces as two-sided
|
||||
// if(dot(to_camera, unit_normal) < 0) unit_normal *= -1;
|
||||
|
||||
float light_to_normal = dot(to_light, unit_normal);
|
||||
// When unit normal points towards light, brighten
|
||||
float bright_factor = max(light_to_normal, 0) * reflectiveness;
|
||||
// For glossy surface, add extra shine if light beam go towards camera
|
||||
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
|
||||
);
|
||||
float light_to_cam = dot(light_reflection, to_camera);
|
||||
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));
|
||||
bright_factor += shine;
|
||||
|
||||
result.rgb = mix(result.rgb, vec3(1.0), bright_factor);
|
||||
if (light_to_normal < 0){
|
||||
// Darken
|
||||
result.rgb = mix(result.rgb, vec3(0.0), -light_to_normal * shadow);
|
||||
}
|
||||
// float darkening = mix(1, max(light_to_normal, 0), shadow);
|
||||
// return vec4(
|
||||
// darkening * mix(color.rgb, vec3(1.0), shine),
|
||||
// color.a
|
||||
// );
|
||||
return result;
|
||||
}
|
||||
|
||||
vec4 finalize_color(vec4 color,
|
||||
vec3 point,
|
||||
vec3 unit_normal,
|
||||
vec3 light_coords,
|
||||
vec3 cam_coords,
|
||||
float reflectiveness,
|
||||
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
|
||||
return add_light(color, point, unit_normal, light_coords, gloss, shadow);
|
||||
return add_light(
|
||||
color, point, unit_normal, light_coords, cam_coords,
|
||||
reflectiveness, gloss, shadow
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user