mirror of
https://github.com/3b1b/manim.git
synced 2026-01-13 00:18:05 -05:00
Compare commits
1981 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f427fc67df | ||
|
|
3d9a0cd25e | ||
|
|
33dbf04985 | ||
|
|
744e695340 | ||
|
|
00b34f2020 | ||
|
|
bafea89ac9 | ||
|
|
eeb4fdf270 | ||
|
|
e2e785d6c9 | ||
|
|
c6c1a49ede | ||
|
|
6d753a297a | ||
|
|
f9fc543b07 | ||
|
|
bac0c0c9b9 | ||
|
|
9ae5b4dee3 | ||
|
|
0b350e248b | ||
|
|
7148d6bced | ||
|
|
b470a47da7 | ||
|
|
13fdc9629d | ||
|
|
fce92347fa | ||
|
|
185f642826 | ||
|
|
4a6a125739 | ||
|
|
8246d0da5d | ||
|
|
1794e4d0ba | ||
|
|
4d7f6093b4 | ||
|
|
37a05094ea | ||
|
|
76afc42e9a | ||
|
|
5fcb668f07 | ||
|
|
2d7b9d579a | ||
|
|
9ac16ab722 | ||
|
|
8744c878f4 | ||
|
|
9fcdd0de5f | ||
|
|
9f785a5fba | ||
|
|
a03accff9c | ||
|
|
7d3758c44c | ||
|
|
f9a44c9975 | ||
|
|
d5c36de3c5 | ||
|
|
c9b6ee57a8 | ||
|
|
2c43d293a5 | ||
|
|
3d3f8258f4 | ||
|
|
17f37ff02a | ||
|
|
2359ed9aa4 | ||
|
|
32d36a09f6 | ||
|
|
8cf95ec9a4 | ||
|
|
24697377db | ||
|
|
d21fbd02bc | ||
|
|
284c1d8f2c | ||
|
|
ae93d8fcc6 | ||
|
|
1d67768a13 | ||
|
|
07bb34793e | ||
|
|
cd744024ea | ||
|
|
667cfaf160 | ||
|
|
c61e0bcee5 | ||
|
|
d1080aa6fd | ||
|
|
f9fa8ac846 | ||
|
|
bcc4235e2f | ||
|
|
c51a84a6ee | ||
|
|
6b38011078 | ||
|
|
858d8c122b | ||
|
|
4b483b75ce | ||
|
|
4cc2e5ed17 | ||
|
|
d4c5c4736a | ||
|
|
178cca0ca5 | ||
|
|
c02259a39e | ||
|
|
1276724891 | ||
|
|
9e77b0dcdd | ||
|
|
1a14a6bd0d | ||
|
|
950ac31b9b | ||
|
|
8706ba1589 | ||
|
|
dd508b8cfc | ||
|
|
88bae476ce | ||
|
|
7a69807ce6 | ||
|
|
6d0b23f914 | ||
|
|
bf81d94362 | ||
|
|
5b315d5c70 | ||
|
|
cb3e115a6c | ||
|
|
40b5c7c1c1 | ||
|
|
636fb3a45b | ||
|
|
ea3f77e3f1 | ||
|
|
0692afdfec | ||
|
|
14c6fdc1d9 | ||
|
|
89bf0b1297 | ||
|
|
2e8a282cc7 | ||
|
|
5fa99b7723 | ||
|
|
df1e067480 | ||
|
|
0ef12ad7e4 | ||
|
|
09c27a654f | ||
|
|
90dfb02cc6 | ||
|
|
e270f5c3d3 | ||
|
|
fadd045fc1 | ||
|
|
dd0aa14442 | ||
|
|
d357e21c1d | ||
|
|
dd251ab8c2 | ||
|
|
2e49c60148 | ||
|
|
33c7f6d063 | ||
|
|
53b6c34ebe | ||
|
|
49c2b5cfe0 | ||
|
|
09fb8d324e | ||
|
|
6196daa5ec | ||
|
|
e05cae6775 | ||
|
|
94f6f0aa96 | ||
|
|
0e83c9c0d9 | ||
|
|
5a70d67b98 | ||
|
|
66862db9b2 | ||
|
|
5d3f730824 | ||
|
|
3cd3e8cedc | ||
|
|
08acfa6f1f | ||
|
|
75527563de | ||
|
|
c96734ace0 | ||
|
|
71e440be93 | ||
|
|
8098149006 | ||
|
|
4251ff436a | ||
|
|
85f8456228 | ||
|
|
e0031c63bc | ||
|
|
361d9d0652 | ||
|
|
1d14bae092 | ||
|
|
8dfd4c1c4e | ||
|
|
96a4a4b76f | ||
|
|
0496402c55 | ||
|
|
fc32f162a0 | ||
|
|
3b9ef57b22 | ||
|
|
b593cde317 | ||
|
|
34ad61d013 | ||
|
|
cfb7d2fa47 | ||
|
|
43821ab2ba | ||
|
|
89ddfadf6b | ||
|
|
0c385e820f | ||
|
|
ac01b144e8 | ||
|
|
129e512b0c | ||
|
|
88370d4d5d | ||
|
|
671a31b298 | ||
|
|
f8280a12be | ||
|
|
d78fe93743 | ||
|
|
8239f1bf35 | ||
|
|
1fa17030a2 | ||
|
|
530cb4f104 | ||
|
|
85638d88dc | ||
|
|
fbce0b132c | ||
|
|
dd51b696e5 | ||
|
|
9cd6a87ff8 | ||
|
|
54c8a9014b | ||
|
|
e19ceaaff0 | ||
|
|
5b88d2347c | ||
|
|
c6b9826f84 | ||
|
|
90ab2f64bb | ||
|
|
ed2f9f3305 | ||
|
|
1d0deb8a33 | ||
|
|
753a042dbe | ||
|
|
55b12c902c | ||
|
|
e80b9d0e47 | ||
|
|
1248abd922 | ||
|
|
314ca89a45 | ||
|
|
0ad5a0e76e | ||
|
|
64ae1364ca | ||
|
|
af923a2327 | ||
|
|
97b6e39abb | ||
|
|
b84376d6fd | ||
|
|
9475fcd19e | ||
|
|
003c4d8626 | ||
|
|
693a859caf | ||
|
|
52948f846e | ||
|
|
1738876f43 | ||
|
|
dc731f8bf2 | ||
|
|
e5cf0558d8 | ||
|
|
1139b545f9 | ||
|
|
0b65e4c7b6 | ||
|
|
371fca147b | ||
|
|
e1816c2ac5 | ||
|
|
199395b6e3 | ||
|
|
837bb14c03 | ||
|
|
eca370f5ce | ||
|
|
5505fc1d54 | ||
|
|
04295ec177 | ||
|
|
0c7c9dee93 | ||
|
|
1a65498f97 | ||
|
|
a34c4482f6 | ||
|
|
e3e87f6110 | ||
|
|
aaa28a2712 | ||
|
|
aa18373eb7 | ||
|
|
d499544366 | ||
|
|
2dd0256af6 | ||
|
|
d4080b8978 | ||
|
|
23c9e67fa4 | ||
|
|
cce4ffbb60 | ||
|
|
99493fc4f7 | ||
|
|
81b17dd63e | ||
|
|
15e5d8a07a | ||
|
|
154a473a12 | ||
|
|
29cb6f76fe | ||
|
|
09e9e65ba4 | ||
|
|
f69b189f2c | ||
|
|
641c03a95b | ||
|
|
f737823bac | ||
|
|
95bb67c47f | ||
|
|
512fb89726 | ||
|
|
cf37f34e1f | ||
|
|
bddd9c35ea | ||
|
|
f0bf50eb7f | ||
|
|
fea7096cbe | ||
|
|
ccb9977a67 | ||
|
|
1f8ad5be16 | ||
|
|
217eb6b486 | ||
|
|
0804109301 | ||
|
|
62a4ea5165 | ||
|
|
3e7244b90b | ||
|
|
95fca885c9 | ||
|
|
8eac976c8d | ||
|
|
9eda000a97 | ||
|
|
bcf610d1ad | ||
|
|
df9acfb4d5 | ||
|
|
b6e5b0f34a | ||
|
|
6d23df0497 | ||
|
|
05a89d754e | ||
|
|
2178ec2b85 | ||
|
|
1f55832a6a | ||
|
|
aebf2220a6 | ||
|
|
08f7cb8d3e | ||
|
|
c8326d1cce | ||
|
|
133cec9725 | ||
|
|
9812503597 | ||
|
|
9011c864fd | ||
|
|
6b88947151 | ||
|
|
30e6c357ed | ||
|
|
a5137a05f1 | ||
|
|
21f9df8ccd | ||
|
|
8f1299929f | ||
|
|
e7c540f415 | ||
|
|
76fdd02db0 | ||
|
|
a713868f3d | ||
|
|
2836acc3c7 | ||
|
|
f378d33d01 | ||
|
|
054261d86f | ||
|
|
04d77f2bec | ||
|
|
1e996dcd27 | ||
|
|
e85a1ce1b7 | ||
|
|
22a6b47ec9 | ||
|
|
55a798676a | ||
|
|
5cf5e497e7 | ||
|
|
7519ce15da | ||
|
|
f86245517d | ||
|
|
36ea70d990 | ||
|
|
cd5c436ce4 | ||
|
|
827f4db5e2 | ||
|
|
441ac77eae | ||
|
|
51de1fb650 | ||
|
|
8785eb1844 | ||
|
|
a823901b98 | ||
|
|
513de19657 | ||
|
|
fcc5dc00f9 | ||
|
|
566fc87a60 | ||
|
|
499803159c | ||
|
|
487c714d9b | ||
|
|
e939e1de09 | ||
|
|
c26ebfc10f | ||
|
|
5d6a1f30c4 | ||
|
|
a08523d746 | ||
|
|
bcafcbf490 | ||
|
|
e4007f6915 | ||
|
|
ada66ee8fb | ||
|
|
bd2947be28 | ||
|
|
9a7bfdd1c9 | ||
|
|
c8d5e91422 | ||
|
|
09bed1f8f4 | ||
|
|
eda7f81fb9 | ||
|
|
d5575cf1ef | ||
|
|
11df256369 | ||
|
|
cfe70ca869 | ||
|
|
bda894959b | ||
|
|
d870bb29de | ||
|
|
442206faad | ||
|
|
ceac4fbe1a | ||
|
|
22ee13a884 | ||
|
|
eea8416d57 | ||
|
|
b3386ad7a7 | ||
|
|
c83d03aeb7 | ||
|
|
0a89725090 | ||
|
|
0cb7a8f691 | ||
|
|
43f1704f69 | ||
|
|
941513d68c | ||
|
|
9a5386b022 | ||
|
|
67bedc6d1f | ||
|
|
79ec791fc2 | ||
|
|
748780378b | ||
|
|
dfc5f152dd | ||
|
|
d0cb5b4eea | ||
|
|
35ce4c6704 | ||
|
|
7ddbd13e38 | ||
|
|
304856e6e0 | ||
|
|
82582d08bd | ||
|
|
a8784692e8 | ||
|
|
0a313eb119 | ||
|
|
5eb5a11499 | ||
|
|
0a585b123c | ||
|
|
910f28f52e | ||
|
|
7474ae17b0 | ||
|
|
b8931e7b9c | ||
|
|
0414f8786c | ||
|
|
e0191d81d9 | ||
|
|
0ac9ee1fbf | ||
|
|
b9645ad196 | ||
|
|
87ca6e56aa | ||
|
|
e61957a4e0 | ||
|
|
a8ef9629eb | ||
|
|
e796a0c6d6 | ||
|
|
5ff80ffc6c | ||
|
|
f12b143d16 | ||
|
|
08e33faab8 | ||
|
|
0b2c59ac6b | ||
|
|
6223623b40 | ||
|
|
7217c9fca5 | ||
|
|
b288d5301e | ||
|
|
058914fdd2 | ||
|
|
c064b11e2a | ||
|
|
b7337f0781 | ||
|
|
195264f079 | ||
|
|
09d147c8ef | ||
|
|
39bcead679 | ||
|
|
28eba26bee | ||
|
|
3b5d63d2fa | ||
|
|
4cb16dfc0b | ||
|
|
a12fa0c03d | ||
|
|
4174f314b4 | ||
|
|
f2bca0045f | ||
|
|
24b160f9f9 | ||
|
|
f9b9cf69fd | ||
|
|
0efa96e399 | ||
|
|
12d39ef37c | ||
|
|
ccc84f4ab1 | ||
|
|
bb42b66201 | ||
|
|
c20ce8d633 | ||
|
|
0267740bde | ||
|
|
d8edccdab4 | ||
|
|
108db87087 | ||
|
|
21c0bcb8b6 | ||
|
|
902a4f264e | ||
|
|
3f15715ff1 | ||
|
|
174f318602 | ||
|
|
430a88cf13 | ||
|
|
4a6e6ca646 | ||
|
|
e2b0c2b9bf | ||
|
|
04347e7876 | ||
|
|
df0ae6fdc9 | ||
|
|
59235d3eed | ||
|
|
0fa74a7921 | ||
|
|
65d5947966 | ||
|
|
a5ba721f96 | ||
|
|
c7acbe5de6 | ||
|
|
644084d9a7 | ||
|
|
af8e5236d2 | ||
|
|
a4858918dd | ||
|
|
31b6affabb | ||
|
|
cff3bdf8d4 | ||
|
|
9f54b85c4e | ||
|
|
c345d76de0 | ||
|
|
aad2bded14 | ||
|
|
1ff758dea8 | ||
|
|
bf43a648a4 | ||
|
|
099aaaee43 | ||
|
|
70862a068f | ||
|
|
f677a02036 | ||
|
|
bc91e91634 | ||
|
|
0a43a3ff9a | ||
|
|
e130625b9b | ||
|
|
0dcf630222 | ||
|
|
61a2b4d0da | ||
|
|
edb438e5e2 | ||
|
|
ec88673e92 | ||
|
|
44ec9933b7 | ||
|
|
4ff61ed561 | ||
|
|
4223bb6320 | ||
|
|
b45c71d3c2 | ||
|
|
2b6ec2d95f | ||
|
|
a5926195ee | ||
|
|
557819ad03 | ||
|
|
f363eaa2fd | ||
|
|
c61c18486c | ||
|
|
26249c34bb | ||
|
|
b3bbc31ea9 | ||
|
|
71814a118b | ||
|
|
d644e3b184 | ||
|
|
78ddfe29c0 | ||
|
|
8f78e2e127 | ||
|
|
5decf810e7 | ||
|
|
aea747b6d3 | ||
|
|
361817b506 | ||
|
|
a3469c236e | ||
|
|
c6a6503544 | ||
|
|
3ea8393e9a | ||
|
|
5aeb457bb1 | ||
|
|
bbc89d13e9 | ||
|
|
a105216a47 | ||
|
|
c1efd14904 | ||
|
|
a7765dcac3 | ||
|
|
57d4732ef1 | ||
|
|
d1314e5a3c | ||
|
|
79c89ad34d | ||
|
|
0eae42977a | ||
|
|
a07ccf4aca | ||
|
|
4feb831a11 | ||
|
|
63e98eee94 | ||
|
|
ab28804ae5 | ||
|
|
88c7e9d2c9 | ||
|
|
3c374c3e92 | ||
|
|
c970f776bb | ||
|
|
772a328302 | ||
|
|
f5d1a9c449 | ||
|
|
920f2407e0 | ||
|
|
7565e936fa | ||
|
|
1d6aa47933 | ||
|
|
0509e824c6 | ||
|
|
ec42326618 | ||
|
|
223d671eea | ||
|
|
7e6a37d499 | ||
|
|
6b3834739c | ||
|
|
b26feb7045 | ||
|
|
7db69e32aa | ||
|
|
fa99eafe2b | ||
|
|
8235607b2a | ||
|
|
4729e44e05 | ||
|
|
27f397e0a6 | ||
|
|
226d649ee6 | ||
|
|
d3ba101ee5 | ||
|
|
83cd5d6246 | ||
|
|
70b839e188 | ||
|
|
fd35433a62 | ||
|
|
4b14c11e4b | ||
|
|
e124aecd6b | ||
|
|
3c778ba678 | ||
|
|
a6b46c641b | ||
|
|
2380ffd616 | ||
|
|
1372cf101c | ||
|
|
4d67361800 | ||
|
|
a5f2ac689f | ||
|
|
2e9c89502d | ||
|
|
9432a73a9f | ||
|
|
ffbe5c8114 | ||
|
|
7edc4b64ad | ||
|
|
e784c42f0d | ||
|
|
4a89376fdd | ||
|
|
712fa30174 | ||
|
|
5632fee9a3 | ||
|
|
7b577e9fc1 | ||
|
|
ed3ac74d67 | ||
|
|
4ce8a3ba9d | ||
|
|
d44e248277 | ||
|
|
578427543c | ||
|
|
c531e56a2f | ||
|
|
45f8ca7643 | ||
|
|
2966f358a3 | ||
|
|
8417369da1 | ||
|
|
f3571cf2cb | ||
|
|
e4c824e672 | ||
|
|
31b2bcd9e6 | ||
|
|
100b108ad1 | ||
|
|
7009f0f53e | ||
|
|
dfa96c2047 | ||
|
|
ebe689dede | ||
|
|
4aef0d1bf5 | ||
|
|
661814deea | ||
|
|
45d9049405 | ||
|
|
711438f625 | ||
|
|
cde709fcfa | ||
|
|
1c72059725 | ||
|
|
d3dee240c3 | ||
|
|
60b762ca43 | ||
|
|
8179ba88d0 | ||
|
|
855ef9be8d | ||
|
|
2c110790d2 | ||
|
|
41ece958fd | ||
|
|
88672a21ff | ||
|
|
9ba684d35f | ||
|
|
f8fedffa4c | ||
|
|
fa017b94d9 | ||
|
|
dcf3eb8416 | ||
|
|
8a4d7b4e8c | ||
|
|
246a010799 | ||
|
|
17cd597904 | ||
|
|
2cdb85cae9 | ||
|
|
0d046a7eab | ||
|
|
cbc32468bf | ||
|
|
f4778b57ef | ||
|
|
916ab94efd | ||
|
|
a8b1791ff5 | ||
|
|
39e5d24858 | ||
|
|
295a0f76cc | ||
|
|
2b00a9cf80 | ||
|
|
b53ab02675 | ||
|
|
5f41e238ba | ||
|
|
690eb24562 | ||
|
|
60a4f0e167 | ||
|
|
0a642133ad | ||
|
|
87e4a71ca3 | ||
|
|
7278095921 | ||
|
|
f0a61beaf5 | ||
|
|
0b5e9d4a8b | ||
|
|
65e7943ff7 | ||
|
|
fa798a2018 | ||
|
|
13d4ab1eb0 | ||
|
|
c8cf83eedf | ||
|
|
eafd09549d | ||
|
|
f2ad9a70f7 | ||
|
|
4be7f611ec | ||
|
|
d21b05ae0d | ||
|
|
fc522e5278 | ||
|
|
ddf2f7d9bd | ||
|
|
2337be2318 | ||
|
|
7954ba14ef | ||
|
|
c65b7242e4 | ||
|
|
7ff45b4637 | ||
|
|
4f42ebeb4f | ||
|
|
21d20541b5 | ||
|
|
0609c1bfa8 | ||
|
|
162fd4a92b | ||
|
|
cb02066f22 | ||
|
|
3e64111952 | ||
|
|
6f8ea7433d | ||
|
|
bae3b98c0b | ||
|
|
63f6e9d84f | ||
|
|
f01b990c2e | ||
|
|
fa1080d59a | ||
|
|
ce7422f8af | ||
|
|
16f5890fd3 | ||
|
|
5d9a7f49e6 | ||
|
|
f33b8d1d2f | ||
|
|
71ab276e05 | ||
|
|
3b2904b4c7 | ||
|
|
de8e9e5ec1 | ||
|
|
6b24860bbf | ||
|
|
0d415036a9 | ||
|
|
80fb1a98a9 | ||
|
|
d1e2a7a157 | ||
|
|
b644bb51de | ||
|
|
392019fc6e | ||
|
|
6d0b586597 | ||
|
|
b216b8f7e3 | ||
|
|
1eb819363d | ||
|
|
a79d4a862f | ||
|
|
3f2d15986a | ||
|
|
c372ef4aaa | ||
|
|
3a05352f73 | ||
|
|
dcb58c1f4f | ||
|
|
576a26493e | ||
|
|
d8428585f8 | ||
|
|
557cb66c52 | ||
|
|
01c51dbc6d | ||
|
|
ad409999dc | ||
|
|
b39fbb62f4 | ||
|
|
3e3e4de5e9 | ||
|
|
ded06c1f88 | ||
|
|
f2c07afe74 | ||
|
|
169e7a302b | ||
|
|
0ce972991b | ||
|
|
41f0239e9d | ||
|
|
1844f7fd64 | ||
|
|
66b78d01a9 | ||
|
|
d1b1df64a5 | ||
|
|
4e90a77fcd | ||
|
|
7d1330fa68 | ||
|
|
c918e84784 | ||
|
|
e3b95276fa | ||
|
|
3bf9e40aba | ||
|
|
fab917ccee | ||
|
|
b8fe7b0172 | ||
|
|
a54d1eddfc | ||
|
|
e1bb360e0b | ||
|
|
12dc124d72 | ||
|
|
bc107787cc | ||
|
|
b25f022859 | ||
|
|
7c561d3757 | ||
|
|
ac3db9b636 | ||
|
|
772ea792d0 | ||
|
|
ee08c552bf | ||
|
|
c4777015fc | ||
|
|
d10745a379 | ||
|
|
88959df7a8 | ||
|
|
3d0fe27c55 | ||
|
|
4629e08769 | ||
|
|
009f9dd18b | ||
|
|
7f940fbee4 | ||
|
|
fbcbbc9a58 | ||
|
|
1dcc678b2f | ||
|
|
ad2e7144b4 | ||
|
|
e36719a21b | ||
|
|
c4d698a169 | ||
|
|
0e60b124eb | ||
|
|
d263fa23fa | ||
|
|
6f2cbc4d1f | ||
|
|
4e674e571c | ||
|
|
be602930c3 | ||
|
|
9cadfa1818 | ||
|
|
b9d37a9f7e | ||
|
|
d0c6d4d386 | ||
|
|
3c0d682efc | ||
|
|
63dbe3b23f | ||
|
|
bd89056c8e | ||
|
|
b9d6dcd67d | ||
|
|
e5eed7c36a | ||
|
|
acb4b1c6b3 | ||
|
|
a1b9eae301 | ||
|
|
7476740980 | ||
|
|
c3823e722d | ||
|
|
0cf9a35367 | ||
|
|
594b9258da | ||
|
|
4ec2e8b0c5 | ||
|
|
260815c675 | ||
|
|
ab6a7df4af | ||
|
|
17cef427f1 | ||
|
|
b499caaa45 | ||
|
|
9c03a40d68 | ||
|
|
47672d3b1e | ||
|
|
eeadbe4542 | ||
|
|
8adf2a6e07 | ||
|
|
6eafdc63cc | ||
|
|
ebf2ee5849 | ||
|
|
f4a6f99b54 | ||
|
|
8820af65ec | ||
|
|
f83c441210 | ||
|
|
f293ccdff4 | ||
|
|
40bcb7e0f3 | ||
|
|
979589a156 | ||
|
|
9ef14c7260 | ||
|
|
c062592684 | ||
|
|
c8b65d5621 | ||
|
|
e76c64ad52 | ||
|
|
5527c0706d | ||
|
|
280090a7c9 | ||
|
|
b351c9f1c8 | ||
|
|
04733ac32e | ||
|
|
0c9afb65d9 | ||
|
|
b1fb3e1d54 | ||
|
|
7c087838a3 | ||
|
|
bc38165d44 | ||
|
|
c8d1ee5c88 | ||
|
|
f858a439dd | ||
|
|
d3a4d81a63 | ||
|
|
fca5770b9f | ||
|
|
d9c85aac46 | ||
|
|
93e65fa3e7 | ||
|
|
3b0c958189 | ||
|
|
f42b3bfa3e | ||
|
|
077f264890 | ||
|
|
7e78e76966 | ||
|
|
1e46847a69 | ||
|
|
4c327cd5d2 | ||
|
|
3e308d881f | ||
|
|
8e8229b9b8 | ||
|
|
9dc6cce09f | ||
|
|
e8302e6025 | ||
|
|
015a7487e7 | ||
|
|
468fdf9003 | ||
|
|
031adda503 | ||
|
|
0452012d54 | ||
|
|
576d8c996b | ||
|
|
74a11bb05c | ||
|
|
6a42ef846a | ||
|
|
da6875ca55 | ||
|
|
d8c21ff7aa | ||
|
|
af585ca3a1 | ||
|
|
3779577d9f | ||
|
|
b58224f6c8 | ||
|
|
50343e9629 | ||
|
|
a4d9b101de | ||
|
|
60aae748a7 | ||
|
|
92e4d43ca3 | ||
|
|
424db4b3e4 | ||
|
|
2d0bdfbdb6 | ||
|
|
9e5fca6750 | ||
|
|
2cbad30f45 | ||
|
|
5952f9ea74 | ||
|
|
f2d71e6521 | ||
|
|
0645912765 | ||
|
|
57deab6617 | ||
|
|
f8cfcfbc64 | ||
|
|
76ee97adfa | ||
|
|
e7734ca84c | ||
|
|
5ff44f5850 | ||
|
|
a58327657c | ||
|
|
55da5d5d03 | ||
|
|
c469c6b009 | ||
|
|
9017df847d | ||
|
|
33116f8af1 | ||
|
|
0de914fd01 | ||
|
|
e950286fa4 | ||
|
|
5490b3be19 | ||
|
|
215c21babf | ||
|
|
7e00660e47 | ||
|
|
daaaba0a67 | ||
|
|
9628adc957 | ||
|
|
af69cf9c7d | ||
|
|
71bd3edb09 | ||
|
|
277c471c90 | ||
|
|
b85c3bd478 | ||
|
|
285953b44d | ||
|
|
15d8ebb572 | ||
|
|
abdcb64461 | ||
|
|
e58aea9e2f | ||
|
|
2705ba3afa | ||
|
|
901d40ba11 | ||
|
|
0d9bb71d3c | ||
|
|
0fe5922253 | ||
|
|
09900456f7 | ||
|
|
91f69be3e0 | ||
|
|
da9610b9f9 | ||
|
|
b3dec3fd51 | ||
|
|
68255b1c9a | ||
|
|
28c4921a1a | ||
|
|
0d36f17f9c | ||
|
|
c08e111911 | ||
|
|
368f48f8dd | ||
|
|
474a6c27e3 | ||
|
|
31b937a7f1 | ||
|
|
c6db74c996 | ||
|
|
fbde9e8bba | ||
|
|
8d72340501 | ||
|
|
144e512952 | ||
|
|
dec5089777 | ||
|
|
5deef1c249 | ||
|
|
a1449def95 | ||
|
|
2a3f927566 | ||
|
|
c7ef4eefbc | ||
|
|
ab03a0cfba | ||
|
|
05a80f53a7 | ||
|
|
60a27f52f1 | ||
|
|
1a62314719 | ||
|
|
8a18967ea4 | ||
|
|
fc379dab18 | ||
|
|
f296dd8df5 | ||
|
|
047128a663 | ||
|
|
38abef8871 | ||
|
|
8ecfc2b2cf | ||
|
|
ce5d0b61f9 | ||
|
|
71ef39ea5b | ||
|
|
3a01eb31bd | ||
|
|
d5b1a1725d | ||
|
|
1f6363821b | ||
|
|
35c19fe8a7 | ||
|
|
e57ca4e1ee | ||
|
|
86fb69c5bb | ||
|
|
1c432dd6dc | ||
|
|
3a0916fe3a | ||
|
|
40ae481979 | ||
|
|
97e4c25453 | ||
|
|
1707958e0f | ||
|
|
746b52cda5 | ||
|
|
86fb1d82f5 | ||
|
|
de7545e5fa | ||
|
|
b21e470e69 | ||
|
|
79039bde61 | ||
|
|
2863672740 | ||
|
|
422c9cebd2 | ||
|
|
6388647860 | ||
|
|
eab8edd51d | ||
|
|
8f6c14ad5f | ||
|
|
7a59cc2f03 | ||
|
|
0f89349bb8 | ||
|
|
ab57b0acf0 | ||
|
|
1bd6a77151 | ||
|
|
bf2a609246 | ||
|
|
e9333a908c | ||
|
|
5803a00598 | ||
|
|
9ee9e1946a | ||
|
|
3a175c1a4c | ||
|
|
258bc2256a | ||
|
|
adfef48418 | ||
|
|
acdc2654d3 | ||
|
|
9696827213 | ||
|
|
6d4782506a | ||
|
|
28c875c2c3 | ||
|
|
164c9ba754 | ||
|
|
8ef71bb930 | ||
|
|
a8da171adb | ||
|
|
65afed1bd1 | ||
|
|
14cda7e908 | ||
|
|
37f0bf8c11 | ||
|
|
3f5df432ce | ||
|
|
a33b24310e | ||
|
|
c6c23a1fe7 | ||
|
|
f5cb2bfa52 | ||
|
|
05dd399270 | ||
|
|
72590a8fef | ||
|
|
a1595a9e2f | ||
|
|
a68bc1271b | ||
|
|
cb36fda6d7 | ||
|
|
88590e5a05 | ||
|
|
a601384211 | ||
|
|
f96a697ee3 | ||
|
|
3c8e3792e7 | ||
|
|
2beb55727f | ||
|
|
7609b1db78 | ||
|
|
018b07212f | ||
|
|
d2af6a5f4b | ||
|
|
4dfabc1c28 | ||
|
|
424707d035 | ||
|
|
2c737ed540 | ||
|
|
c94d8fd3b0 | ||
|
|
3299741359 | ||
|
|
10047773f7 | ||
|
|
16d773f1b3 | ||
|
|
80729c0cb8 | ||
|
|
8c1e5f3b42 | ||
|
|
3339aad29e | ||
|
|
a9a3ca08cd | ||
|
|
bc5c78de83 | ||
|
|
0ea91f22b2 | ||
|
|
7deaf4cb11 | ||
|
|
93dd9f687b | ||
|
|
4cb9c9c2fc | ||
|
|
6cf8c8d2e8 | ||
|
|
272925fa19 | ||
|
|
0e2d21bed3 | ||
|
|
346d252451 | ||
|
|
088a2f65a3 | ||
|
|
307487e087 | ||
|
|
98eccab977 | ||
|
|
f0df5c759d | ||
|
|
e9c70dbfd9 | ||
|
|
72da9786a3 | ||
|
|
516fe9155e | ||
|
|
b93e284695 | ||
|
|
88ed1a2fdb | ||
|
|
945aa9713f | ||
|
|
aa6c321a0a | ||
|
|
87afdac6a4 | ||
|
|
6e56c31d67 | ||
|
|
4774d2bc3b | ||
|
|
d01658bc5b | ||
|
|
97789fff35 | ||
|
|
b1f0270316 | ||
|
|
b99b88fd25 | ||
|
|
1dda706335 | ||
|
|
8a6deb4068 | ||
|
|
e2421a650c | ||
|
|
b0cca9e4b6 | ||
|
|
c13495deeb | ||
|
|
03080a10a7 | ||
|
|
8d729eef5a | ||
|
|
e8b75941e0 | ||
|
|
1d4fcf020b | ||
|
|
8ce5dc7e84 | ||
|
|
b934ee5f50 | ||
|
|
ba1b43df1a | ||
|
|
847c27ad23 | ||
|
|
8b786311af | ||
|
|
39cda62b66 | ||
|
|
24864a3d61 | ||
|
|
9e02796c9a | ||
|
|
8e1fdd5a79 | ||
|
|
0d66981ac7 | ||
|
|
2e2e8dfee2 | ||
|
|
917481cb23 | ||
|
|
8fee4d1a66 | ||
|
|
f2d4313bcf | ||
|
|
d08a16a5fb | ||
|
|
66d12a1687 | ||
|
|
9249433144 | ||
|
|
23b4e3e03b | ||
|
|
6c262f63b1 | ||
|
|
b7ea24f9ea | ||
|
|
7df6efb55f | ||
|
|
fe7dc3c459 | ||
|
|
2290f810ca | ||
|
|
debc68a3b4 | ||
|
|
909e515a2f | ||
|
|
1c2ec03f7d | ||
|
|
6839de9a31 | ||
|
|
c873d073e2 | ||
|
|
1eae7c06ba | ||
|
|
dbeef42600 | ||
|
|
93f3c6535f | ||
|
|
763967281f | ||
|
|
1367e31439 | ||
|
|
e6abff4299 | ||
|
|
148898f983 | ||
|
|
981fe009e5 | ||
|
|
22d2819ecf | ||
|
|
781e0a9805 | ||
|
|
3820e098c0 | ||
|
|
8e2cf04b71 | ||
|
|
cd3c5031fa | ||
|
|
6c2544098b | ||
|
|
b667d89e9b | ||
|
|
fa525b494c | ||
|
|
13c41be17f | ||
|
|
e20efda3df | ||
|
|
8b3aa8f5c6 | ||
|
|
d39fea0d4d | ||
|
|
96b0ec9094 | ||
|
|
40436d6370 | ||
|
|
44e5f15ae9 | ||
|
|
72e5bde274 | ||
|
|
1a663943c9 | ||
|
|
874906bedf | ||
|
|
0b72bc5d08 | ||
|
|
c7e32e847d | ||
|
|
5e1a02d2ce | ||
|
|
9ed8dd5439 | ||
|
|
b7831ef3f1 | ||
|
|
c2587de691 | ||
|
|
c563ec2036 | ||
|
|
870e88f8c9 | ||
|
|
7fe84d9263 | ||
|
|
1b3bc7a27c | ||
|
|
b16f0981f6 | ||
|
|
abbe131e8d | ||
|
|
4de0d098ea | ||
|
|
ed2dbfd9b9 | ||
|
|
8d277af47c | ||
|
|
8ac0aa484b | ||
|
|
1f613953d6 | ||
|
|
c3cd64f68c | ||
|
|
6e6a30c95a | ||
|
|
20222bc7e9 | ||
|
|
f15ac81131 | ||
|
|
7050c7e7b0 | ||
|
|
161bf7377d | ||
|
|
bd2d45ebc6 | ||
|
|
dd0e91015c | ||
|
|
1a15756330 | ||
|
|
8ef93b0f9d | ||
|
|
a46e580fa4 | ||
|
|
3b40ccc987 | ||
|
|
ae50748717 | ||
|
|
c23f020d9a | ||
|
|
db45d9e646 | ||
|
|
f5480d02ff | ||
|
|
3f8c861973 | ||
|
|
3a09acd28c | ||
|
|
bdcfbc39ec | ||
|
|
74b42a6eb5 | ||
|
|
5a95bfa70f | ||
|
|
eba86be35b | ||
|
|
2ca8848007 | ||
|
|
afbc624ac4 | ||
|
|
77a3984683 | ||
|
|
29f51a7c6a | ||
|
|
ba9f61b50b | ||
|
|
f63331eb24 | ||
|
|
6f9f83fb1b | ||
|
|
3f2fd5b142 | ||
|
|
90ac1fc0bf | ||
|
|
3ba5237f9b | ||
|
|
408890e0d9 | ||
|
|
9704f063c9 | ||
|
|
d267c00761 | ||
|
|
ca5e119425 | ||
|
|
7e45558c55 | ||
|
|
e55434925e | ||
|
|
d868f685dc | ||
|
|
e37b667c8b | ||
|
|
2dafcb3e63 | ||
|
|
801f449ca0 | ||
|
|
f4c50f61b8 | ||
|
|
2815f60616 | ||
|
|
286b8fb6c3 | ||
|
|
f2e91ef66f | ||
|
|
da15eb4ad2 | ||
|
|
ea943de557 | ||
|
|
8e1b23ee98 | ||
|
|
de38b56d0d | ||
|
|
51a5086093 | ||
|
|
bf726667a8 | ||
|
|
4582f5d331 | ||
|
|
5b8fb1828f | ||
|
|
702bb2776c | ||
|
|
c9ba32b568 | ||
|
|
19a7721661 | ||
|
|
c8238f6b39 | ||
|
|
d19b244ee1 | ||
|
|
a004c88e02 | ||
|
|
fa81d9f6ea | ||
|
|
3555936c4d | ||
|
|
dae51abc17 | ||
|
|
1c1325ff8d | ||
|
|
28d5baeeff | ||
|
|
51efe0d18e | ||
|
|
12c3af9647 | ||
|
|
42909a94ac | ||
|
|
071e7f1a74 | ||
|
|
2aa2eedbbd | ||
|
|
e534206eb6 | ||
|
|
394d87effb | ||
|
|
e832bb775f | ||
|
|
31cc2671e5 | ||
|
|
937b894826 | ||
|
|
599f74c749 | ||
|
|
91f976f7e9 | ||
|
|
cbfe82579f | ||
|
|
ae99c8cd2e | ||
|
|
da2b13aee9 | ||
|
|
a88b56bb04 | ||
|
|
73a894c136 | ||
|
|
c820cb4775 | ||
|
|
69ac946e63 | ||
|
|
42d612f253 | ||
|
|
440138aac5 | ||
|
|
108bb3da44 | ||
|
|
10d4db64c8 | ||
|
|
e1abae1d96 | ||
|
|
c8a77a352f | ||
|
|
d2800b6c96 | ||
|
|
c9a2971433 | ||
|
|
a12b5cca67 | ||
|
|
aa8fba5b02 | ||
|
|
2c6e8692ce | ||
|
|
a44fc2e6d5 | ||
|
|
190b9e4603 | ||
|
|
f39fd92e9e | ||
|
|
f0edc6628b | ||
|
|
9f1ab09749 | ||
|
|
a2f3758a7a | ||
|
|
de4a56849e | ||
|
|
923066db2b | ||
|
|
de5198196b | ||
|
|
a47b95044d | ||
|
|
db8b0e7bce | ||
|
|
dcdf74a715 | ||
|
|
9778c3e085 | ||
|
|
644ea41443 | ||
|
|
9464f83d18 | ||
|
|
a07701e295 | ||
|
|
24afb2a24f | ||
|
|
c6d3a9646c | ||
|
|
64578df603 | ||
|
|
e676bd957b | ||
|
|
9320dad45f | ||
|
|
ab30f085b4 | ||
|
|
f3e91db581 | ||
|
|
04fd50491e | ||
|
|
7d90a82317 | ||
|
|
1708fbd672 | ||
|
|
1a8e923ef2 | ||
|
|
a8fb05a44a | ||
|
|
c0b3c246de | ||
|
|
ae42f6244e | ||
|
|
131e1c2eeb | ||
|
|
1f04ba92fb | ||
|
|
ed26fdfab8 | ||
|
|
bf84b1933b | ||
|
|
5a56a2a5ec | ||
|
|
f04d0ad350 | ||
|
|
8c435d6181 | ||
|
|
31715ec98a | ||
|
|
a17a57825e | ||
|
|
61437b2a7f | ||
|
|
333db992ed | ||
|
|
8a08b62f7c | ||
|
|
5d7e923ac6 | ||
|
|
6a18a05a3b | ||
|
|
5fac213fee | ||
|
|
557e57d95b | ||
|
|
d3a40eb1ac | ||
|
|
032a8fd030 | ||
|
|
387de61119 | ||
|
|
5e459d57c6 | ||
|
|
d9dc956137 | ||
|
|
7847ff1a9d | ||
|
|
f2370afea0 | ||
|
|
dba70ceded | ||
|
|
da125c1072 | ||
|
|
c0fba529d9 | ||
|
|
86b756ab1f | ||
|
|
55bc8464b9 | ||
|
|
21908a48de | ||
|
|
40b9e22b6e | ||
|
|
2808710d60 | ||
|
|
c4e1db7f9d | ||
|
|
2a7b787ef6 | ||
|
|
681fa513a7 | ||
|
|
b967c04c2c | ||
|
|
0205a37209 | ||
|
|
eccaa8681e | ||
|
|
9d9e000c63 | ||
|
|
886fd193f0 | ||
|
|
bfaf81c6b3 | ||
|
|
470e7bee1e | ||
|
|
e5b17aad69 | ||
|
|
22420b7724 | ||
|
|
e189df81b1 | ||
|
|
0c3367f27b | ||
|
|
131ecce7c4 | ||
|
|
7212a98d65 | ||
|
|
802bd58aa7 | ||
|
|
116d6fe244 | ||
|
|
6ec3d9f4a5 | ||
|
|
ab1227a908 | ||
|
|
27db1c5987 | ||
|
|
8cd59f852d | ||
|
|
8175c2d408 | ||
|
|
b31ad49850 | ||
|
|
70113d5a48 | ||
|
|
4b652be492 | ||
|
|
05796654f4 | ||
|
|
2b90f0b244 | ||
|
|
dcb166e21b | ||
|
|
02a0ffe04e | ||
|
|
98e358f87d | ||
|
|
494e04405c | ||
|
|
480cc6759f | ||
|
|
b4544052d9 | ||
|
|
f75df1e26e | ||
|
|
c67ae08b62 | ||
|
|
d87db65344 | ||
|
|
d1c765353d | ||
|
|
fc86bf7f9e | ||
|
|
ccf7a503c1 | ||
|
|
ccbb5534fa | ||
|
|
80c0e88133 | ||
|
|
5870274adb | ||
|
|
08c02b21aa | ||
|
|
a6e5f25912 | ||
|
|
47d0dca087 | ||
|
|
16390283cf | ||
|
|
89d9e260eb | ||
|
|
ae2a253fb1 | ||
|
|
0136cef1d9 | ||
|
|
2bf1f5eb40 | ||
|
|
21051ce289 | ||
|
|
9e7cd1399d | ||
|
|
d5fdc75164 | ||
|
|
cae13aa1f0 | ||
|
|
9b2495abb1 | ||
|
|
f5455bb554 | ||
|
|
0d433b075e | ||
|
|
0c1abebd95 | ||
|
|
1cf89abf53 | ||
|
|
c30b102458 | ||
|
|
bf5d587204 | ||
|
|
abf1dd3d8b | ||
|
|
333fd2676d | ||
|
|
a31c4ae3c2 | ||
|
|
4c1f1f8749 | ||
|
|
3aa9eb6372 | ||
|
|
4335e85659 | ||
|
|
96bc95ef38 | ||
|
|
cec43dfe51 | ||
|
|
50960eefd4 | ||
|
|
66f0a57c6b | ||
|
|
dddaef0e6c | ||
|
|
7895a2cfee | ||
|
|
7dde368eeb | ||
|
|
8e3378f798 | ||
|
|
632819dd6d | ||
|
|
fdccfd51fc | ||
|
|
5d87f3f954 | ||
|
|
3b4a233bb1 | ||
|
|
4db01fd221 | ||
|
|
5c33c7e4a8 | ||
|
|
7df12c68dc | ||
|
|
0f9adbf91c | ||
|
|
04d3e6a47c | ||
|
|
124c83d94e | ||
|
|
d6d75d8f9a | ||
|
|
2dbb9367c4 | ||
|
|
4bc7e3a8f2 | ||
|
|
ba68505c18 | ||
|
|
71815fd7de | ||
|
|
cba101995f | ||
|
|
4e014d7a8f | ||
|
|
13fc8daba9 | ||
|
|
a54a81744d | ||
|
|
3165a28cd0 | ||
|
|
25ac5f3507 | ||
|
|
ef09d6fce2 | ||
|
|
3738f0a48e | ||
|
|
9018357d20 | ||
|
|
4330f78ed6 | ||
|
|
1feae23566 | ||
|
|
aedde4dffc | ||
|
|
53f19b6620 | ||
|
|
42f2461acb | ||
|
|
4c39c1abd6 | ||
|
|
596aea3bf5 | ||
|
|
7b0b31e8d9 | ||
|
|
d6b308ed47 | ||
|
|
cc9a4501ad | ||
|
|
8226396382 | ||
|
|
e76b673e63 | ||
|
|
a6ee54488b | ||
|
|
4f37486655 | ||
|
|
a92a506224 | ||
|
|
8fc243e398 | ||
|
|
1180932026 | ||
|
|
e59b3d2ac0 | ||
|
|
926f3515bf | ||
|
|
c7ba775845 | ||
|
|
05f02f5154 | ||
|
|
34d8ab81f9 | ||
|
|
44dc22e5e4 | ||
|
|
4d65ceabf7 | ||
|
|
b96a65d576 | ||
|
|
c8c96fe645 | ||
|
|
6204011fe4 | ||
|
|
083de38e4c | ||
|
|
7dc1fe21bd | ||
|
|
a6e21b2ccd | ||
|
|
97be203b57 | ||
|
|
61155f5c72 | ||
|
|
966e2c9790 | ||
|
|
e73ae78987 | ||
|
|
816f6eb297 | ||
|
|
e54e04a5ce | ||
|
|
49d4472e7e | ||
|
|
6044e475a4 | ||
|
|
8c44834554 | ||
|
|
795f6e2490 | ||
|
|
6d50be55d3 | ||
|
|
f2859a9a8c | ||
|
|
33a92d3ab3 | ||
|
|
934a73ddb8 | ||
|
|
2b67aa0e01 | ||
|
|
3bf5ce5776 | ||
|
|
db0770a4fd | ||
|
|
a2bdf54025 | ||
|
|
22c67df2ad | ||
|
|
fcff44a66b | ||
|
|
62c9e2b58f | ||
|
|
db52d0a73f | ||
|
|
580d57a45c | ||
|
|
453b863738 | ||
|
|
7f203d1611 | ||
|
|
dd2fb6ae74 | ||
|
|
40bf1fd6a9 | ||
|
|
3878b8c077 | ||
|
|
ef04b9eb01 | ||
|
|
baf2690d77 | ||
|
|
c36d178856 | ||
|
|
96d391d9fd | ||
|
|
8c5d4db411 | ||
|
|
b7d473ff43 | ||
|
|
fca7c0609a | ||
|
|
a4d47f64b0 | ||
|
|
5af4b9cc65 | ||
|
|
ff090c016f | ||
|
|
33682b7199 | ||
|
|
805236337e | ||
|
|
958c34c705 | ||
|
|
10d53c82e1 | ||
|
|
6277e28373 | ||
|
|
4b73140435 | ||
|
|
ef941b4040 | ||
|
|
81e6ab5b1d | ||
|
|
e357885da0 | ||
|
|
6d5b980d4a | ||
|
|
0d525baf29 | ||
|
|
3293f72adc | ||
|
|
e0725c111e | ||
|
|
6176bcd45a | ||
|
|
9c106eb873 | ||
|
|
1a485ddd19 | ||
|
|
d4417d3d07 | ||
|
|
dd662b0d12 | ||
|
|
6beea2a7eb | ||
|
|
623aef41f8 | ||
|
|
9f71f87278 | ||
|
|
67912e26d3 | ||
|
|
a26fe605b3 | ||
|
|
5c7caee902 | ||
|
|
c605ac1c83 | ||
|
|
d8deec8f81 | ||
|
|
75a98a8936 | ||
|
|
073a62bf03 | ||
|
|
78fd6d3f35 | ||
|
|
e0e7e24351 | ||
|
|
c2c8149627 | ||
|
|
a53867d8a1 | ||
|
|
b2fd22c539 | ||
|
|
5c0232a5e0 | ||
|
|
ab470c3ee5 | ||
|
|
71c9144952 | ||
|
|
8d05431b7b | ||
|
|
96d9e41a35 | ||
|
|
2c20a1509e | ||
|
|
ef64b90ed3 | ||
|
|
f158d3e751 | ||
|
|
4632228cac | ||
|
|
d79e1d6ed8 | ||
|
|
6615a912bd | ||
|
|
99fa3ee620 | ||
|
|
c330dfddae | ||
|
|
ba93bd0cbf | ||
|
|
b1f363d5a9 | ||
|
|
e5c13ba9d7 | ||
|
|
3c04ffc513 | ||
|
|
4f6c387a03 | ||
|
|
38c4fd8770 | ||
|
|
3e4b6a7fb0 | ||
|
|
af471161ea | ||
|
|
e728196814 | ||
|
|
99dbf6b8c3 | ||
|
|
07feb33cbb | ||
|
|
44b7d33784 | ||
|
|
0433cd727c | ||
|
|
c96cdf43a1 | ||
|
|
903e140719 | ||
|
|
f8b39f2ff1 | ||
|
|
6f0020950f | ||
|
|
7ac78f3dbb | ||
|
|
f53fad1a96 | ||
|
|
c96a698713 | ||
|
|
f6ff226cd4 | ||
|
|
4dec67f9fe | ||
|
|
0dab04080e | ||
|
|
fd20ead11b | ||
|
|
3f024175d4 | ||
|
|
26ff1a9716 | ||
|
|
d4a29df99c | ||
|
|
75979eb7d1 | ||
|
|
365bb12dce | ||
|
|
0115037c82 | ||
|
|
38db5ca9b9 | ||
|
|
24fd6d890e | ||
|
|
810f2c67ab | ||
|
|
bd537afe72 | ||
|
|
3e8738de2a | ||
|
|
b817e6f15f | ||
|
|
5b697b3782 | ||
|
|
0e558db122 | ||
|
|
1f0427d685 | ||
|
|
534770180d | ||
|
|
97f28b34f3 | ||
|
|
8db20cc460 | ||
|
|
5625f63ca2 | ||
|
|
cef6506920 | ||
|
|
dec11a4b17 | ||
|
|
43fd5e1aea | ||
|
|
c00af3c1bf | ||
|
|
ef0999cc09 | ||
|
|
a4272d11a2 | ||
|
|
a6db0877de | ||
|
|
2bab16133d | ||
|
|
d798f5ebf0 | ||
|
|
7510c9808e | ||
|
|
1a0eff05fa | ||
|
|
123f7e5a30 | ||
|
|
15f03dae7b | ||
|
|
c6fc8dcf45 | ||
|
|
b21f5bad00 | ||
|
|
afab37c2d2 | ||
|
|
a1cbff46b8 | ||
|
|
ea4a47aeef | ||
|
|
9ad370a04b | ||
|
|
e4aebaf791 | ||
|
|
0ad2f18ca6 | ||
|
|
89770158de | ||
|
|
271e2f0865 | ||
|
|
ae52f19a4a | ||
|
|
c2cf261c81 | ||
|
|
880aaf913f | ||
|
|
72aaed57d5 | ||
|
|
f64cae1db4 | ||
|
|
9e077b29db | ||
|
|
a04d4c0d79 | ||
|
|
fa7ee22c46 | ||
|
|
7e46c87fc5 | ||
|
|
9039fe69e4 | ||
|
|
451f1df830 | ||
|
|
a7d7ed0793 | ||
|
|
9d65ef3cae | ||
|
|
2a645b27f8 | ||
|
|
c244f8738f | ||
|
|
3be43119cb | ||
|
|
37786edc99 | ||
|
|
25388b4ad3 | ||
|
|
00403fe5b4 | ||
|
|
02143001a4 | ||
|
|
a05820b7c7 | ||
|
|
5b5b3a7d20 | ||
|
|
57875875c1 | ||
|
|
a78e2b6ad2 | ||
|
|
da90c5e297 | ||
|
|
4c894077d3 | ||
|
|
c56968fd09 | ||
|
|
c8d01e7a43 | ||
|
|
3b5181d1a3 | ||
|
|
588c3fce02 | ||
|
|
a3215d0354 | ||
|
|
c55374245d | ||
|
|
c2766c9837 | ||
|
|
5a309d41b7 | ||
|
|
10c0f4b694 | ||
|
|
2a89e84538 | ||
|
|
aac320aa98 | ||
|
|
7aa5c83a14 | ||
|
|
d4b6bf40e5 | ||
|
|
002129e7a2 | ||
|
|
1aaa3b4ad5 | ||
|
|
a39c65cb5c | ||
|
|
6c4e028eab | ||
|
|
bab1f964bb | ||
|
|
3a65eb4d2c | ||
|
|
98c53151ad | ||
|
|
b373e33a22 | ||
|
|
a715a5bc3f | ||
|
|
97a5861ccf | ||
|
|
e8c220a3f2 | ||
|
|
133ac8bb26 | ||
|
|
a817364a0e | ||
|
|
33b4e617a6 | ||
|
|
c57f1f997a | ||
|
|
0b994db0ec | ||
|
|
69b0b0727e | ||
|
|
013bf8b639 | ||
|
|
2f8fe689d9 | ||
|
|
a0a17be6ea | ||
|
|
4e8b80fe86 | ||
|
|
53994f0650 | ||
|
|
ca1ba67a85 | ||
|
|
4aa7d439f1 | ||
|
|
0aa451396d | ||
|
|
a6744a19d3 | ||
|
|
02c3243f98 | ||
|
|
81615d9f4b | ||
|
|
7dcf5eff8e | ||
|
|
a901704b31 | ||
|
|
c8442c404e | ||
|
|
7474a98752 | ||
|
|
5571c7d576 | ||
|
|
adf886dced | ||
|
|
f6858778c4 | ||
|
|
98a969242a | ||
|
|
44a7cfa12e | ||
|
|
187de0163f | ||
|
|
958002152e | ||
|
|
a7bf10c570 | ||
|
|
6f470679f7 | ||
|
|
155cde26da | ||
|
|
a801aefcea | ||
|
|
5281a83e9d | ||
|
|
7de03e2541 | ||
|
|
8cc7616271 | ||
|
|
15a446977f | ||
|
|
12d4b48508 | ||
|
|
a4ffe9b4e5 | ||
|
|
b6dd640fe7 | ||
|
|
83393abb22 | ||
|
|
90e8a397b8 | ||
|
|
6683c9dbca | ||
|
|
004b7427f5 | ||
|
|
d8e4c1d698 | ||
|
|
4e7f06dea8 | ||
|
|
e55dd01081 | ||
|
|
b94e9f3a24 | ||
|
|
764dec20eb | ||
|
|
8adf99b8a7 | ||
|
|
a2606c7e37 | ||
|
|
84fa3de435 | ||
|
|
f878537814 | ||
|
|
8e6265d35e | ||
|
|
834c806e83 | ||
|
|
471dcdbaf1 | ||
|
|
ee668dc741 | ||
|
|
d28ba53f22 | ||
|
|
faa37844e7 | ||
|
|
80d34547db | ||
|
|
ce77f38bf1 | ||
|
|
1bd3d61b08 | ||
|
|
c3c1d3ec35 | ||
|
|
e89f193c56 | ||
|
|
8dfa6415dc | ||
|
|
fb50e4eb55 | ||
|
|
abe52a61d9 | ||
|
|
88f2ae6d0d | ||
|
|
d2e570eb19 | ||
|
|
d48957c312 | ||
|
|
0c75d79080 | ||
|
|
603a773847 | ||
|
|
2f691355db | ||
|
|
a613099b1d | ||
|
|
fa1d938af1 | ||
|
|
455d6604be | ||
|
|
4a575afdde | ||
|
|
ae115d9992 | ||
|
|
8882030136 | ||
|
|
8719107b18 | ||
|
|
53c43ee8ea | ||
|
|
3c0abb0b40 | ||
|
|
eadf611f1e | ||
|
|
4dfe8aff86 | ||
|
|
8bd01d60e4 | ||
|
|
c2a75e15cc | ||
|
|
19c757ec90 | ||
|
|
7ffc7b33f7 | ||
|
|
28e4240475 | ||
|
|
f434eb93e2 | ||
|
|
69bb4f026c | ||
|
|
0406557b5c | ||
|
|
de7d9ce8c9 | ||
|
|
eff9e6f732 | ||
|
|
cc81cc5cf5 | ||
|
|
af7c58dbe8 | ||
|
|
7c4bb9cbbd | ||
|
|
650d49c031 | ||
|
|
3c7a38660a | ||
|
|
606ee5e4f1 | ||
|
|
844d139ed4 | ||
|
|
093af347aa | ||
|
|
93265c7341 | ||
|
|
0d845d5bba | ||
|
|
dfa019fcde | ||
|
|
77309a634b | ||
|
|
1b1ba606ed | ||
|
|
a73bd5d4fe | ||
|
|
97ac8c9953 | ||
|
|
bc939fdd5b | ||
|
|
3faa21cadd | ||
|
|
bf530db2ed | ||
|
|
f0447d7739 | ||
|
|
59eba943e5 | ||
|
|
22a3bef670 | ||
|
|
cbffbfa019 | ||
|
|
07a8274cb1 | ||
|
|
83b4aa6b88 | ||
|
|
117a34dc67 | ||
|
|
ca523c8a5e | ||
|
|
ad58a9e6c1 | ||
|
|
9386461d27 | ||
|
|
1596356385 | ||
|
|
f0984487ea | ||
|
|
3108b49f55 | ||
|
|
edca4a93fa | ||
|
|
cfded00f13 | ||
|
|
26de7c9ce5 | ||
|
|
49723f54cb | ||
|
|
cece830349 | ||
|
|
cd240f2a80 | ||
|
|
6decb0c32a | ||
|
|
fb3cf308df | ||
|
|
dd5d239971 | ||
|
|
25de729bb3 | ||
|
|
5f56778cdf | ||
|
|
2e26d66454 | ||
|
|
f741217c34 | ||
|
|
11d19b6d57 | ||
|
|
97643d788d | ||
|
|
cef7c383a5 | ||
|
|
0060a4860c | ||
|
|
4fdaeb1547 | ||
|
|
48689c8c7b | ||
|
|
cd866573b5 | ||
|
|
584e259b44 | ||
|
|
cdadaf8a8c | ||
|
|
7cf0e0ba10 | ||
|
|
b509f62010 | ||
|
|
642602155d | ||
|
|
41b811a5e7 | ||
|
|
93fc81ac9d | ||
|
|
d662971559 | ||
|
|
c4d452248a | ||
|
|
181038e2f3 | ||
|
|
511a3aab3d | ||
|
|
1cb7401141 | ||
|
|
a9a151d4ef | ||
|
|
22c5e79f5f | ||
|
|
a87d3b5f59 | ||
|
|
ab8f78f40f | ||
|
|
a6fcfa3b40 | ||
|
|
fddb0b29e1 | ||
|
|
57b7af3bf1 | ||
|
|
a09c440281 | ||
|
|
c019210015 | ||
|
|
75e1cff579 | ||
|
|
feab79c260 | ||
|
|
308aadcec5 | ||
|
|
03cb42ba15 | ||
|
|
4a8e8e5447 | ||
|
|
602fbd1a9f | ||
|
|
33ffd4863a | ||
|
|
6a664ece78 | ||
|
|
354030464e | ||
|
|
a791a82111 | ||
|
|
7f94a401a8 | ||
|
|
ed5a435852 | ||
|
|
a4b38fd420 | ||
|
|
c1b222c233 | ||
|
|
5d59f945ca | ||
|
|
ac08963fef | ||
|
|
e83ad785ca | ||
|
|
52259af5df | ||
|
|
7c233123a1 | ||
|
|
c311204993 | ||
|
|
2b104a46fd | ||
|
|
d75439a60e | ||
|
|
1b589e336f | ||
|
|
ec9ed32d78 | ||
|
|
0e45b41fea | ||
|
|
c498b88750 | ||
|
|
2dcc989bb4 | ||
|
|
065900c6ac | ||
|
|
69db53d612 | ||
|
|
b920e7be7b | ||
|
|
3584926036 | ||
|
|
4c1210b3ab | ||
|
|
aaea3f40f6 | ||
|
|
d6bf9f00a1 | ||
|
|
42d1f48c60 | ||
|
|
01f0dd30d0 | ||
|
|
40b432a29b | ||
|
|
d43b5c9bdc | ||
|
|
f2b4245c13 | ||
|
|
e49e4b8373 | ||
|
|
66f695a1ed | ||
|
|
cc8922155d | ||
|
|
6310e2fb64 | ||
|
|
db884b0a67 | ||
|
|
efe051b8e1 | ||
|
|
205116b8ce | ||
|
|
bd2dce0830 | ||
|
|
3ae0a4e81b | ||
|
|
c3c5717dde | ||
|
|
b8efdea6ec | ||
|
|
97edc2d6cf | ||
|
|
30e33b1baa | ||
|
|
304cf88451 | ||
|
|
d9475a6860 | ||
|
|
902c2c002d | ||
|
|
587bc4d0bd | ||
|
|
d733687834 | ||
|
|
0fd8491c51 | ||
|
|
753ef3b74a | ||
|
|
2ba9243067 | ||
|
|
669182944d | ||
|
|
e085c2e214 | ||
|
|
f70e91348c | ||
|
|
754316bf58 | ||
|
|
04bca6cafb | ||
|
|
62289045cc | ||
|
|
3961005fd7 | ||
|
|
7b342a2759 | ||
|
|
59506b89cc | ||
|
|
71c14969df | ||
|
|
b2e0aee93e | ||
|
|
cf466006fa | ||
|
|
4d8698a0e8 | ||
|
|
b9751e9d06 | ||
|
|
bb7fa2c8aa | ||
|
|
e0f5686d66 | ||
|
|
581228b08f | ||
|
|
2737d9a736 | ||
|
|
c96bdc243e | ||
|
|
5927f6a1cd | ||
|
|
1b2460f02a | ||
|
|
37075590b5 | ||
|
|
bf5cec7dba | ||
|
|
f8c8a399c9 | ||
|
|
f226aa7314 | ||
|
|
b4b72d1b68 | ||
|
|
78a7078772 | ||
|
|
4caa033323 | ||
|
|
3a60ab144b | ||
|
|
f53f202dcd | ||
|
|
9d5e2b32fa | ||
|
|
fe3e10acd2 | ||
|
|
c04615c4e9 | ||
|
|
6474e25fcd | ||
|
|
996d71c49e | ||
|
|
d24b8ff48f | ||
|
|
485a4ca33a | ||
|
|
cc563bf5e2 | ||
|
|
19881f3e2d | ||
|
|
a0507c5277 | ||
|
|
1b009a4b03 | ||
|
|
c3afc84bfe | ||
|
|
e579f4c955 | ||
|
|
4f2e3456e2 | ||
|
|
47636686cb | ||
|
|
eae7dbbe6e | ||
|
|
a3579eab41 | ||
|
|
5a34ca1fba | ||
|
|
68e2909af1 | ||
|
|
777b6d3778 | ||
|
|
97400a5cf2 | ||
|
|
cb768c26a0 | ||
|
|
fdeab8ca95 | ||
|
|
b09e6916dc | ||
|
|
a0c46ef3bf | ||
|
|
4839037503 | ||
|
|
f636199d9a | ||
|
|
50f5d20cc3 | ||
|
|
2dd2fb500e | ||
|
|
c1716895c0 | ||
|
|
135f68de35 | ||
|
|
8852921b3d | ||
|
|
cbb7e69f68 | ||
|
|
0e0244128c | ||
|
|
e9298c5faf | ||
|
|
4f5173b633 | ||
|
|
58127e7511 | ||
|
|
b387bc0c95 | ||
|
|
0406ef70bb | ||
|
|
654da85cf6 | ||
|
|
bc18894040 | ||
|
|
ac4620483c | ||
|
|
4690edec3e | ||
|
|
a1e77b0ce2 | ||
|
|
dbefc3b256 | ||
|
|
14dfd776dc | ||
|
|
0a810bb4f1 | ||
|
|
09952756ce | ||
|
|
020bd87271 | ||
|
|
50565fcd7a | ||
|
|
5a91c73b23 | ||
|
|
5e49f20294 | ||
|
|
29816fa74c | ||
|
|
95f56f5e80 | ||
|
|
6a01e36b36 | ||
|
|
4c324767bd | ||
|
|
eec6b01a72 | ||
|
|
0c1e5b337b | ||
|
|
bda7f98d2e | ||
|
|
9d74e8bce3 | ||
|
|
bff9f74b04 | ||
|
|
845ee83f71 | ||
|
|
42444d090e | ||
|
|
b11ce7ff7c | ||
|
|
296ab84b46 | ||
|
|
55684af27d | ||
|
|
93790cde64 | ||
|
|
fbebaf0c75 | ||
|
|
9ef9961d0e | ||
|
|
0cf3199578 | ||
|
|
f307c2a298 | ||
|
|
859680d5ab | ||
|
|
dc4b9bc93c | ||
|
|
705f1a528b | ||
|
|
e8ac25903e | ||
|
|
773520bcd9 | ||
|
|
d26b8a826c | ||
|
|
12bfe88f40 | ||
|
|
31cbf2d905 | ||
|
|
36d62ae1a3 | ||
|
|
e23f667c3d | ||
|
|
2277679111 | ||
|
|
9d7db7aacd | ||
|
|
e8430b38b2 | ||
|
|
1f32a9e674 | ||
|
|
d31f3df5af | ||
|
|
e9bf13882e | ||
|
|
3550108ff7 | ||
|
|
d7bdcab161 | ||
|
|
3b847da9ea | ||
|
|
217c1d7bb0 | ||
|
|
557707ea75 | ||
|
|
13c731e166 | ||
|
|
d349c9283d | ||
|
|
18963fb9fe | ||
|
|
a69c9887f9 | ||
|
|
93f8d3f1ca | ||
|
|
e4ccbdfba9 | ||
|
|
fc97bfb647 | ||
|
|
f9d8a76767 | ||
|
|
55a91a2354 | ||
|
|
50ffcbc5c7 | ||
|
|
b764791258 | ||
|
|
7f616987a3 | ||
|
|
648855dae0 | ||
|
|
974d9d5ab0 | ||
|
|
3c3264d7d6 | ||
|
|
39673a80d7 | ||
|
|
84c56b3624 | ||
|
|
dc816c9f8d | ||
|
|
d5ab9a91c4 | ||
|
|
106f2a3837 | ||
|
|
724a500cc6 | ||
|
|
461500637e | ||
|
|
fc4f649570 | ||
|
|
e74cb85182 | ||
|
|
df2d465140 | ||
|
|
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 |
13
.github/workflows/publish.yml
vendored
13
.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
|
||||
|
||||
@@ -20,11 +25,13 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine build
|
||||
|
||||
- name: Build and publish
|
||||
|
||||
- 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 -m build
|
||||
twine upload dist/*
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -91,6 +91,7 @@ ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
pyrightconfig.json
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 3Blue1Brown LLC
|
||||
Copyright (c) 2020-2023 3Blue1Brown LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
16
README.md
16
README.md
@@ -12,14 +12,16 @@
|
||||
|
||||
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 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.
|
||||
Note, there are two versions of manim. This repository began as a personal project by the author of [3Blue1Brown](https://www.3blue1brown.com/) for the purpose of animating those videos, with video-specific code available [here](https://github.com/3b1b/videos). In 2020 a group of developers forked it into what is now the [community edition](https://github.com/ManimCommunity/manim/), with a goal of being more stable, better tested, quicker to respond to community contributions, and all around friendlier to get started with. See [this page](https://docs.manim.community/en/stable/faq/installation.html#different-versions) for more details.
|
||||
|
||||
## Installation
|
||||
> **WARNING:** These instructions are for ManimGL _only_. Trying to use these instructions to install [ManimCommunity/manim](https://github.com/ManimCommunity/manim) or instructions there to install this version will cause problems. You should first decide which version you wish to install, then only follow the instructions for your desired version.
|
||||
>
|
||||
> [!Warning]
|
||||
> **WARNING:** These instructions are for ManimGL _only_. Trying to use these instructions to install [Manim Community/manim](https://github.com/ManimCommunity/manim) or instructions there to install this version will cause problems. You should first decide which version you wish to install, then only follow the instructions for your desired version.
|
||||
|
||||
> [!Note]
|
||||
> **Note**: To install manim directly through pip, please pay attention to the name of the installed package. This repository is ManimGL of 3b1b. The package name is `manimgl` instead of `manim` or `manimlib`. Please use `pip install manimgl` to install the version in this repository.
|
||||
|
||||
Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
|
||||
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).
|
||||
@@ -91,7 +93,9 @@ manimgl example_scenes.py OpeningManimExample
|
||||
```
|
||||
This should pop up a window playing a simple scene.
|
||||
|
||||
Some useful flags include:
|
||||
Look through the [example scenes](https://3b1b.github.io/manim/getting_started/example_scenes.html) to see examples of the library's syntax, animation types and object types. In the [3b1b/videos](https://github.com/3b1b/videos) repo, you can see all the code for 3blue1brown videos, though code from older videos may not be compatible with the most recent version of manim. The readme of that repo also outlines some details for how to set up a more interactive workflow, as shown in [this manim demo video](https://www.youtube.com/watch?v=rbu7Zu5X1zI) for example.
|
||||
|
||||
When running in the CLI, some useful flags include:
|
||||
* `-w` to write the scene to a file
|
||||
* `-o` to write the scene to a file and open the result
|
||||
* `-s` to skip to the end and just show the final frame.
|
||||
@@ -101,8 +105,6 @@ Some useful flags include:
|
||||
|
||||
Take a look at custom_config.yml for further configuration. To add your customization, you can either edit this file, or add another file by the same name "custom_config.yml" to whatever directory you are running manim from. For example [this is the one](https://github.com/3b1b/videos/blob/master/custom_config.yml) for 3blue1brown videos. There you can specify where videos should be output to, where manim should look for image files and sounds you want to read in, and other defaults regarding style and video quality.
|
||||
|
||||
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**](https://manim.org.cn): [docs.manim.org.cn](https://docs.manim.org.cn/) (in Chinese).
|
||||
|
||||
|
||||
@@ -1,69 +1,304 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
Breaking Changes
|
||||
^^^^^^^^^^^^^^^^
|
||||
- Added ``InteractiveScene`` (`#1794 <https://github.com/3b1b/manim/pull/1794>`__)
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Fixed ``ImageMobject`` by overriding ``set_color`` method (`#1791 <https://github.com/3b1b/manim/pull/1791>`__)
|
||||
- Fixed bug with trying to close window during embed (`#1796 <https://github.com/3b1b/manim/commit/e0f5686d667152582f052021cd62bd2ef8c6b470>`__)
|
||||
- Fixed animating ``Mobject.restore`` bug (`#1796 <https://github.com/3b1b/manim/commit/62289045cc8e102121cfe4d7739f3c89102046fb>`__)
|
||||
- Fixed ``InteractiveScene.refresh_selection_highlight`` (`#1802 <https://github.com/3b1b/manim/commit/205116b8cec964b5619416f6e8acf0d8ac7df828>`__)
|
||||
- Fixed ``VMobject.match_style`` (`#1821 <https://github.com/3b1b/manim/commit/0060a4860c9d6b073a60cd839269c213446bba7b>`__)
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
- Added specific euler angle getters (`#1794 <https://github.com/3b1b/manim/commit/df2d465140e25fee265f602608aebbbaa2898c7e>`__)
|
||||
- Added start angle option to ``Circle`` (`#1794 <https://github.com/3b1b/manim/commit/217c1d7bb02f23a61722bf7275c40802be808563>`__)
|
||||
- Added ``Mobject.is_touching`` (`#1794 <https://github.com/3b1b/manim/commit/c1716895c0d9f36e23487322a18963991100bb95>`__)
|
||||
- Added ``Mobject.get_highlight`` (`#1794 <https://github.com/3b1b/manim/commit/29816fa74c7aa6ca060b63ab4165c89987e58d8b>`__)
|
||||
- Allowed for saving and loading mobjects from file (`#1794 <https://github.com/3b1b/manim/commit/50f5d20cc379947d7253d841c060dd7c55fa7787>`__)
|
||||
- Added ``Mobject.get_all_corners`` (`#1794 <https://github.com/3b1b/manim/commit/f636199d9a5d1e87ab861bcb6aebae6c9d96a133>`__)
|
||||
- Added ``Scene.id_to_mobject`` and ``Scene.ids_to_group`` (`#1794 <https://github.com/3b1b/manim/commit/cb768c26a0bc63e02c3035b4af31ba5cbc2e9dda>`__)
|
||||
- Added ``Scene.save_mobject`` and ``Scene.load_mobject`` to allow for saving and loading mobjects from file at the Scene level (`#1794 <https://github.com/3b1b/manim/commit/777b6d37783f8592df8a8abc3d62af972bc5a0c6>`__)
|
||||
- Added ``InteractiveScene`` (`#1794 <https://github.com/3b1b/manim/commit/c3afc84bfeb3a76ea8ede4ec4d9f36df0d4d9a28>`__)
|
||||
- Added ``VHighlight`` (`#1794 <https://github.com/3b1b/manim/commit/9d5e2b32fa9215219d11a601829126cea40410d1>`__)
|
||||
- Allowed for sweeping selection (`#1796 <https://github.com/3b1b/manim/commit/4caa03332367631d2fff15afd7e56b15fe8701ee>`__)
|
||||
- Allowed stretched-resizing (`#1796 <https://github.com/3b1b/manim/commit/b4b72d1b68d0993b96a6af76c4bb6816f77f0f12>`__)
|
||||
- Added cursor location label (`#1796 <https://github.com/3b1b/manim/commit/b9751e9d06068f27a327b419c52fd3c9d68db2e6>`__)
|
||||
- Added ``Mobject.deserialize`` (`#1796 <https://github.com/3b1b/manim/commit/4d8698a0e88333f6481c08d1b84b6e44f9dc4543>`__)
|
||||
- Added undo and redo stacks for scene (`#1796 <https://github.com/3b1b/manim/commit/cf466006faa00fc12dc22f5732dc21ccedaa5a63>`__)
|
||||
- Added ``Mobject.looks_identical`` (`#1802 <https://github.com/3b1b/manim/commit/c3c5717dde543b172b928b516d80a29bbd12651f>`__)
|
||||
- Added equality for ``ShaderWrapper`` (`#1802 <https://github.com/3b1b/manim/commit/3ae0a4e81b7790194bcf27142a1deb29fa548b9d>`__)
|
||||
- Added ``Mobject.get_ancestors`` (`#1802 <https://github.com/3b1b/manim/commit/db884b0a67fcee1ad7009f1869c475015fa886c7>`__)
|
||||
- Added smarter default radius to ``Polygon.round_corners`` (`#1802 <https://github.com/3b1b/manim/commit/4c1210b3ab1bf66b161f3d00cb859d36068c2fbb>`__)
|
||||
- Added checkpoints to ``Scene`` (`#1821 <https://github.com/3b1b/manim/commit/1b589e336f8151f2914ff00e8956baea8a95abc5>`__)
|
||||
- Added ``crosshair`` to ``InteractiveScene`` (`#1821 <https://github.com/3b1b/manim/commit/33ffd4863aaa7ecf950b7044181a8e8e3c643698>`__)
|
||||
- Added ``SceneState`` (`#1821 <https://github.com/3b1b/manim/commit/75e1cff5792065aa1c7fb3eb02e6ee0fa0e8e18d>`__)
|
||||
- Added ``time_span`` option to ``Animation`` (`#1821 <https://github.com/3b1b/manim/commit/a6fcfa3b4053b7f68f7b029eae87dbd207d97ad2>`__)
|
||||
- Added ``Mobject.arrange_to_fit_dim`` (`#1821 <https://github.com/3b1b/manim/commit/a87d3b5f59a64ce5a89ce6e17310bdbf62166157>`__)
|
||||
- Added ``DecimalNumber.get_tex`` (`#1821 <https://github.com/3b1b/manim/commit/48689c8c7bc0029bf5c1b540c11f647e857d419b>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Updated parent updater status when adding updaters (`#1794 <https://github.com/3b1b/manim/commit/3b847da9eaad7391e779c5dbce63ad9257d8c773>`__)
|
||||
- Added case for zero vectors on ``angle_between_vectors`` (`#1794 <https://github.com/3b1b/manim/commit/e8ac25903e19cbb2b2c2037c988baafce4ddcbbc>`__)
|
||||
- Refactored ``Mobject.clear_updaters`` (`#1794 <https://github.com/3b1b/manim/commit/95f56f5e80106443d705c68fa220850ec38daee0>`__)
|
||||
- Changed the way changing-vs-static mobjects are tracked (more details see `#1794 <https://github.com/3b1b/manim/commit/50565fcd7a43ed13dc532f17515208edf97f64d0>`__)
|
||||
- Refactored ``Mobject.is_point_touching`` (`#1794 <https://github.com/3b1b/manim/commit/135f68de35712be266a1a85261d6d44234fc0056>`__)
|
||||
- Refactored ``Mobject.make_movable`` and ``Mobject.set_animating_status`` to recurse over family (`#1794 <https://github.com/3b1b/manim/commit/48390375037f745c9cb82b03d1cb3a1de6c530f3>`__)
|
||||
- Refactored ``AnimationGroup`` (`#1794 <https://github.com/3b1b/manim/commit/fdeab8ca953b46a902b531febcf132739ca194d4>`__)
|
||||
- Refactored ``Scene.save_state`` and ``Scene.restore`` (`#1794 <https://github.com/3b1b/manim/commit/97400a5cf26f33ed507ddeeb9b9a7f1a558d4f17>`__)
|
||||
- Added ``MANIM_COLORS`` (`#1794 <https://github.com/3b1b/manim/commit/5a34ca1fba8b4724eda0caa11b271d74e49f468c>`__)
|
||||
- Changed default transparent background codec to be prores (`#1794 <https://github.com/3b1b/manim/commit/eae7dbbe6eaf4344374713052aae694e69b62c28>`__)
|
||||
- Simplified ``Mobject.copy`` (`#1794 <https://github.com/3b1b/manim/commit/1b009a4b035244bd6a0b48bc4dc945fd3b4236ef>`__)
|
||||
- Refactored ``StringMobject`` and relevant classes (`#1795 <https://github.com/3b1b/manim/pull/1795>`__)
|
||||
- Updates to copying based on pickle serializing (`#1796 <https://github.com/3b1b/manim/commit/fe3e10acd29a3dd6f8b485c0e36ead819f2d937b>`)
|
||||
- Removed ``refresh_shader_wrapper_id`` from ``Mobject.become`` (`#1796 <https://github.com/3b1b/manim/commit/1b2460f02a694314897437b9b8755443ed290cc1>`__)
|
||||
- Refactored ``Scene.embed`` to play nicely with gui interactions (`#1796 <https://github.com/3b1b/manim/commit/c96bdc243e57c17bb75bf12d73ab5bf119cf1464>`__)
|
||||
- Made ``BlankScene`` inherit from ``InteractiveScene`` (`#1796 <https://github.com/3b1b/manim/commit/2737d9a736885a594dd101ffe07bb82e00069333>`__)
|
||||
- Updated behavior of -e flag to take in (optional) strings as inputs (`#1796 <https://github.com/3b1b/manim/commit/bb7fa2c8aa68d7c7992517cfde3c7d0e804e13e8>`__)
|
||||
- Refactor -e flag (`#1796 <https://github.com/3b1b/manim/commit/71c14969dffc8762a43f9646a0c3dc024a51b8df>`__)
|
||||
- Reverted to original copying scheme (`#1796 <https://github.com/3b1b/manim/commit/59506b89cc73fff3b3736245dd72e61dcebf9a2c>`__)
|
||||
- Renamed ``Mobject.is_movable`` to ``Mobject.interaction_allowed`` (`#1796 <https://github.com/3b1b/manim/commit/3961005fd708333a3e77856d10e78451faa04075>`__)
|
||||
- Refreshed static mobjects on undo's and redo's (`#1796 <https://github.com/3b1b/manim/commit/04bca6cafbb1482b8f25cfb34ce83316d8a095c9>`__)
|
||||
- Factored out event handling (`#1796 <https://github.com/3b1b/manim/commit/754316bf586be5a59839f8bac6fb9fcc47da0efb>`__)
|
||||
- Removed ``Mobject.interaction_allowed``, in favor of using ``_is_animating`` for multiple purposes (`#1796 <https://github.com/3b1b/manim/commit/f70e91348c8241bcb96470e7881dd92d9d3386d3>`__)
|
||||
- Moved Command + z and Command + shift + z behavior to Scene (`#1797 <https://github.com/3b1b/manim/commit/0fd8491c515ad23ca308099abe0f39fc38e2dd0e>`__)
|
||||
- Slight copy refactor (`#1797 <https://github.com/3b1b/manim/commit/902c2c002d6ca03c8080b2bd02ca36f2b8a748b6>`__)
|
||||
- When scene saves state, have it only copy mobjects which have changed (`#1802 <https://github.com/3b1b/manim/commit/bd2dce08300e5b110c6668bd6763f3918fcdc65e>`__)
|
||||
- Cleaned up ``Scene.remove`` function (`#1802 <https://github.com/3b1b/manim/commit/6310e2fb6414b01b3fe4be1d4d98525e34356b5e>`__)
|
||||
- Speed-ups to ``Mobject.copy`` (`#1802 <https://github.com/3b1b/manim/commit/e49e4b8373c13c7a888193aaf61955470acbe5d6>`__)
|
||||
- Slight speed-up to ``InteractiveScene.gather_selection`` (`#1802 <https://github.com/3b1b/manim/commit/f2b4245c134da577a2854732ec0331768d93ffbe>`__)
|
||||
- Only leave wait notes in presenter mode (`#1802 <https://github.com/3b1b/manim/commit/42d1f48c60d11caa043d5458e64bfceb31ea203f>`__)
|
||||
- Refactored ``remove_list_redundancies`` and ``list_update`` (`#1821 <https://github.com/3b1b/manim/commit/b920e7be7b85bc0bb0577e2f71c4320bb97b42d4>`__)
|
||||
- Match updaters in ``Mobject.become`` (`#1821 <https://github.com/3b1b/manim/commit/0e45b41fea5f22d136f62f4af2e0d892e61a12ce>`__)
|
||||
- Don't show animation progress bar by default (`#1821 <https://github.com/3b1b/manim/commit/52259af5df619d3f44fbaff4c43402b93d01be2f>`__)
|
||||
- Handle quitting during scene more gracefully (`#1821 <https://github.com/3b1b/manim/commit/e83ad785caaa1a1456e07b23f207469d335bbc0d>`__)
|
||||
- Made ``selection_highlight`` refresh with an updater (`#1821 <https://github.com/3b1b/manim/commit/ac08963feff24a1dd2e57f604b44ea0a18ab01f3>`__)
|
||||
- Refactored ``anims_from_play_args`` to ``prepare_animations`` which deprecating old style ``self.play(mob.method, ...)`` (`#1821 <https://github.com/3b1b/manim/commit/feab79c260498fd7757a304e24c617a4e51ba1df>`__)
|
||||
- Made presenter mode hold before first play call (`#1821 <https://github.com/3b1b/manim/commit/a9a151d4eff80cc37b9db0fe7117727aac45ba09>`__)
|
||||
- Update frame on all play calls when skipping animations, so as to provide a rapid preview during scene loading (`#1821 <https://github.com/3b1b/manim/commit/41b811a5e7c03f528d41555217106e62b287ca3b>`__)
|
||||
- Renamed frame_rate to fps (`#1821 <https://github.com/3b1b/manim/commit/6decb0c32aec21c09007f9a2b91aaa8e642ca848>`__)
|
||||
- Let default text alignment be decided in default_config (`#1821 <https://github.com/3b1b/manim/commit/83b4aa6b88b6c3defb19f204189681f5afbb219e>`__)
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
- Added dependency on ``pyperclip`` (`#1794 <https://github.com/3b1b/manim/commit/e579f4c955844fba415b976c313f64d1bb0376d0>`__)
|
||||
|
||||
|
||||
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
|
||||
^^^^^^^^^^
|
||||
|
||||
- `#1653 <https://github.com/3b1b/manim/pull/1653>`__: Fixed ``Mobject.stretch_to_fit_depth``
|
||||
- `#1655 <https://github.com/3b1b/manim/pull/1655>`__: Fixed the bug of rotating camera
|
||||
- `c73d507 <https://github.com/3b1b/manim/pull/1688/commits/c73d507c76af5c8602d4118bc7538ba04c03ebae>`__: Fixed ``SurfaceMesh`` to be evenly spaced
|
||||
- `82bd02d <https://github.com/3b1b/manim/pull/1688/commits/82bd02d21fbd89b71baa21e077e143f440df9014>`__: Fixed ``angle_between_vectors`` add ``rotation_between_vectors``
|
||||
- `a717314 <https://github.com/3b1b/manim/pull/1688/commits/a7173142bf93fd309def0cc10f3c56f5e6972332>`__: Fixed ``VMobject.fade``
|
||||
- `fbc329d <https://github.com/3b1b/manim/pull/1688/commits/fbc329d7ce3b11821d47adf6052d932f7eff724a>`__: Fixed ``angle_between_vectors``
|
||||
- `bcd0990 <https://github.com/3b1b/manim/pull/1688/commits/bcd09906bea5eaaa5352e7bee8f3153f434cf606>`__: Fixed bug in ``ShowSubmobjectsOneByOne``
|
||||
- `7023548 <https://github.com/3b1b/manim/pull/1691/commits/7023548ec62c4adb2f371aab6a8c7f62deb7c33c>`__: Fixed bug in ``TransformMatchingParts``
|
||||
- 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
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `e10f850 <https://github.com/3b1b/manim/commit/e10f850d0d9f971931cc85d44befe67dc842af6d>`__: Added CLI flag ``--log-level`` to specify log level
|
||||
- `#1667 <https://github.com/3b1b/manim/pull/1667>`__: Added operations (``+`` and ``*``) for ``Mobject``
|
||||
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added 4 boolean operations for ``VMobject`` in ``manimlib/mobject/boolean_ops.py``
|
||||
- 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)``
|
||||
- `81c3ae3 <https://github.com/3b1b/manim/pull/1688/commits/81c3ae30372e288dc772633dbd17def6e603753e>`__: Added reflectiveness
|
||||
- `2c7689e <https://github.com/3b1b/manim/pull/1688/commits/2c7689ed9e81229ce87c648f97f26267956c0bc9>`__: Enabled ``glow_factor`` on ``DotCloud``
|
||||
- `d065e19 <https://github.com/3b1b/manim/pull/1688/commits/d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772>`__: Added option ``-e`` to insert embed line from the command line
|
||||
- `0e78027 <https://github.com/3b1b/manim/pull/1688/commits/0e78027186a976f7e5fa8d586f586bf6e6baab8d>`__: Improved ``point_from_proportion`` to account for arc length
|
||||
- `781a993 <https://github.com/3b1b/manim/pull/1688/commits/781a9934fda6ba11f22ba32e8ccddcb3ba78592e>`__: Added shortcut ``set_backstroke`` for setting black background stroke
|
||||
- `0b898a5 <https://github.com/3b1b/manim/pull/1688/commits/0b898a5594203668ed9cad38b490ab49ba233bd4>`__: Added ``Suface.always_sort_to_camera``
|
||||
- `e899604 <https://github.com/3b1b/manim/pull/1688/commits/e899604a2d05f78202fcb3b9824ec34647237eae>`__: Added getter methods for specific euler angles
|
||||
- `407c53f <https://github.com/3b1b/manim/pull/1688/commits/407c53f97c061bfd8a53beacd88af4c786f9e9ee>`__: Hade ``rotation_between_vectors`` handle identical/similar vectors
|
||||
- `49743da <https://github.com/3b1b/manim/pull/1688/commits/49743daf3244bfa11a427040bdde8e2bb79589e8>`__: Added ``Mobject.insert_submobject`` method
|
||||
- `9dd1f47 <https://github.com/3b1b/manim/pull/1688/commits/9dd1f47dabca1580d6102e34e44574b0cba556e7>`__: Created single progress display for full scene render
|
||||
- `264f7b1 <https://github.com/3b1b/manim/pull/1691/commits/264f7b11726e9e736f0fe472f66e38539f74e848>`__: Added ``Circle.get_radius``
|
||||
- `83841ae <https://github.com/3b1b/manim/pull/1691/commits/83841ae41568a9c9dff44cd163106c19a74ac281>`__: Added ``Dodecahedron``
|
||||
- `a1d5147 <https://github.com/3b1b/manim/pull/1691/commits/a1d51474ea1ce3b7aa3efbe4c5e221be70ee2f5b>`__: Added ``GlowDot``
|
||||
- `#1678 <https://github.com/3b1b/manim/pull/1678>`__: Added ``MTex`` , see `#1678 <https://github.com/3b1b/manim/pull/1678>`__ for details
|
||||
- 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
|
||||
^^^^^^^^
|
||||
|
||||
- `#1662 <https://github.com/3b1b/manim/pull/1662>`__: Refactored support for command ``A`` in path of SVG
|
||||
- `#1662 <https://github.com/3b1b/manim/pull/1662>`__: Refactored ``SingleStringTex.balance_braces``
|
||||
- `8b454fb <https://github.com/3b1b/manim/pull/1688/commits/8b454fbe9335a7011e947093230b07a74ba9c653>`__: Slight tweaks to how saturation_factor works on newton-fractal
|
||||
- `317a5d6 <https://github.com/3b1b/manim/pull/1688/commits/317a5d6226475b6b54a78db7116c373ef84ea923>`__: Made it possible to set full screen preview as a default
|
||||
- `e764da3 <https://github.com/3b1b/manim/pull/1688/commits/e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc>`__: Used ``quick_point_from_proportion`` for graph points
|
||||
- `d2182b9 <https://github.com/3b1b/manim/pull/1688/commits/d2182b9112300558b6c074cefd685f97c10b3898>`__: Made sure ``Line.set_length`` returns self
|
||||
- `eea3c6b <https://github.com/3b1b/manim/pull/1688/commits/eea3c6b29438f9e9325329c4355e76b9f635e97a>`__: Better align ``SurfaceMesh`` to the corresponding surface polygons
|
||||
- `ee1594a <https://github.com/3b1b/manim/pull/1688/commits/ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3>`__: Match ``fix_in_frame`` status for ``FlashAround`` mobject
|
||||
- `ba23fbe <https://github.com/3b1b/manim/pull/1688/commits/ba23fbe71e4a038201cd7df1d200514ed1c13bc2>`__: Made sure ``Mobject.is_fixed_in_frame`` stays updated with uniforms
|
||||
- `98b0d26 <https://github.com/3b1b/manim/pull/1691/commits/98b0d266d2475926a606331923cca3dc1dea97ad>`__: Made sure ``skip_animations`` and ``start_at_animation_number`` play well together
|
||||
- `f8e6e7d <https://github.com/3b1b/manim/pull/1691/commits/f8e6e7df3ceb6f3d845ced4b690a85b35e0b8d00>`__: Updated progress display for full scene render
|
||||
- `8f1dfab <https://github.com/3b1b/manim/pull/1691/commits/8f1dfabff04a8456f5c4df75b0f97d50b2755003>`__: ``VectorizedPoint`` should call ``__init__`` for both super classes
|
||||
- `758f329 <https://github.com/3b1b/manim/pull/1691/commits/758f329a06a0c198b27a48c577575d94554305bf>`__: Used array copy when checking need for refreshing triangulation
|
||||
- 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
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added dependency on python packages `skia-pathops <https://github.com/fonttools/skia-pathops>`__
|
||||
- Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__ (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
|
||||
|
||||
v1.2.0
|
||||
------
|
||||
@@ -71,57 +306,57 @@ v1.2.0
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- `#1592 <https://github.com/3b1b/manim/pull/1592>`__: Fixed ``put_start_and_end_on`` in 3D
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Fixed ``DecimalNumber``'s scaling issue
|
||||
- `56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__: Fixed bug with common range array used for all coordinate systems
|
||||
- `8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__: Fixed ``CoordinateSystem`` init bug
|
||||
- `0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__: Fixed bug for single-valued ``ValueTracker``
|
||||
- `54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__: Fixed bug with SVG rectangles
|
||||
- `d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__: Fixed ``DotCloud.set_radii``
|
||||
- `b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__: Temporarily fixed bug for ``PMobject`` array resizing
|
||||
- `5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__: Fixed ``match_style``
|
||||
- `719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__: Fixed negative ``path_arc`` case
|
||||
- `c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__: Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis``
|
||||
- `7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__: Fixed ``ComplexPlane`` -i display bug
|
||||
- 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
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `#1598 <https://github.com/3b1b/manim/pull/1598>`__: Supported the elliptical arc command ``A`` for ``SVGMobject``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Added ``FlashyFadeIn``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Save triangulation
|
||||
- `#1625 <https://github.com/3b1b/manim/pull/1625>`__: Added new ``Code`` mobject
|
||||
- `#1637 <https://github.com/3b1b/manim/pull/1637>`__: Add warnings and use rich to display log
|
||||
- `bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__: Added ``VCube``
|
||||
- `6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__: Supported ``ValueTracker`` to track vectors
|
||||
- `3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__: Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject``
|
||||
- `a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__: Added ``TracgTail``
|
||||
- `acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__: Added ``Scene.point_to_mobject``
|
||||
- `f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__: Added poly_fractal shader
|
||||
- `b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__: Added kwargs to ``TipableVMobject.set_length``
|
||||
- `17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__: Added ``Mobject.replicate``
|
||||
- `33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__: Added mandelbrot_fractal shader
|
||||
- `f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__: Saved state before each embed
|
||||
- `e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__: Allowed releasing of Textures
|
||||
- `14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__: Consolidated and renamed newton_fractal shader
|
||||
- `6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__: Hade ``ImageMoject`` remember the filepath to the Image
|
||||
- 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
|
||||
^^^^^^^^
|
||||
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Changed back to simpler ``Mobject.scale`` implementation
|
||||
- `b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__: Simplified ``Square``
|
||||
- `40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__: Removed unused parameter ``triangulation_locked``
|
||||
- `8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__: Reimplemented ``Arrow``
|
||||
- `d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__: Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default
|
||||
- `7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__: Refactored to call ``_handle_scale_side_effects`` after scaling takes place
|
||||
- `7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__: Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end``
|
||||
- `0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__: Made sure framerate is 30 for previewed scenes
|
||||
- `c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__: Pushed ``pixel_coords_to_space_coords`` to ``Window``
|
||||
- `d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__: Refactored to pass tuples and not arrays to uniforms
|
||||
- `9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__: Refactored to copy uniform arrays in ``Mobject.copy``
|
||||
- `ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__: Added ``bounding_box`` as exceptional key to point_cloud mobject
|
||||
- `329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__: Made sure stroke width is always a float
|
||||
- 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
|
||||
@@ -147,7 +382,7 @@ Fixed bugs
|
||||
- Rewrote ``earclip_triangulation`` to fix triangulation
|
||||
- Allowed sound_file_name to be taken in without extensions
|
||||
|
||||
New Features
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Added :class:`~manimlib.animation.indication.VShowPassingFlash`
|
||||
|
||||
@@ -8,38 +8,35 @@ they are only used inside manim.
|
||||
Frame and pixel shape
|
||||
---------------------
|
||||
|
||||
These values will be determined based on the ``camera`` configuration in default_config.yml or custom_config.yml
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ASPECT_RATIO = 16.0 / 9.0
|
||||
FRAME_HEIGHT = 8.0
|
||||
FRAME_WIDTH = FRAME_HEIGHT * ASPECT_RATIO
|
||||
FRAME_Y_RADIUS = FRAME_HEIGHT / 2
|
||||
FRAME_X_RADIUS = FRAME_WIDTH / 2
|
||||
ASPECT_RATIO
|
||||
FRAME_HEIGHT
|
||||
FRAME_WIDTH
|
||||
FRAME_Y_RADIUS
|
||||
FRAME_X_RADIUS
|
||||
|
||||
DEFAULT_PIXEL_HEIGHT = 1080
|
||||
DEFAULT_PIXEL_WIDTH = 1920
|
||||
DEFAULT_FRAME_RATE = 30
|
||||
DEFAULT_PIXEL_HEIGHT
|
||||
DEFAULT_PIXEL_WIDTH
|
||||
DEFAULT_FPS
|
||||
|
||||
Buffs
|
||||
-----
|
||||
|
||||
.. code-block:: python
|
||||
These values will be determined based on the ``size`` configuration in default_config.yml or custom_config.yml
|
||||
|
||||
SMALL_BUFF = 0.1
|
||||
MED_SMALL_BUFF = 0.25
|
||||
MED_LARGE_BUFF = 0.5
|
||||
LARGE_BUFF = 1
|
||||
|
||||
DEFAULT_MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF # Distance between object and edge
|
||||
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF # Distance between objects
|
||||
|
||||
Run times
|
||||
---------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0
|
||||
DEFAULT_WAIT_TIME = 1.0
|
||||
SMALL_BUFF
|
||||
MED_SMALL_BUFF
|
||||
MED_LARGE_BUFF
|
||||
LARGE_BUFF
|
||||
|
||||
DEFAULT_MOBJECT_TO_EDGE_BUFF
|
||||
DEFAULT_MOBJECT_TO_MOBJECT_BUFF
|
||||
|
||||
Coordinates
|
||||
-----------
|
||||
@@ -77,30 +74,23 @@ Mathematical constant
|
||||
|
||||
PI = np.pi
|
||||
TAU = 2 * PI
|
||||
DEGREES = TAU / 360
|
||||
DEG = TAU / 360
|
||||
|
||||
Text
|
||||
----
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
START_X = 30
|
||||
START_Y = 20
|
||||
NORMAL = "NORMAL"
|
||||
ITALIC = "ITALIC"
|
||||
OBLIQUE = "OBLIQUE"
|
||||
BOLD = "BOLD"
|
||||
|
||||
Stroke width
|
||||
------------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
DEFAULT_STROKE_WIDTH = 4
|
||||
|
||||
Colours
|
||||
-------
|
||||
|
||||
Color constants are determined based on the ``color`` configuration in default_config.yml or custom_config.yml
|
||||
|
||||
Here are the preview of default colours. (Modified from
|
||||
`elteoremadebeethoven <https://elteoremadebeethoven.github.io/manim_3feb_docs.github.io/html/_static/colors/colors.html>`_)
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ custom_config
|
||||
running file under the ``output`` path, and save the output (``images/``
|
||||
or ``videos/``) in it.
|
||||
|
||||
- ``base``
|
||||
The root directory that will hold files, such as video files manim renders,
|
||||
or image resources that it pulls from
|
||||
|
||||
- ``output``
|
||||
Output file path, the videos will be saved in the ``videos/`` folder under it,
|
||||
and the pictures will be saved in the ``images/`` folder under it.
|
||||
@@ -66,79 +70,107 @@ custom_config
|
||||
The directory for storing sound files to be used in ``Scene.add_sound()`` (
|
||||
including ``.wav`` and ``.mp3``).
|
||||
|
||||
- ``temporary_storage``
|
||||
- ``cache``
|
||||
The directory for storing temporarily generated cache files, including
|
||||
``Tex`` cache, ``Text`` cache and storage of object points.
|
||||
|
||||
|
||||
``window``
|
||||
----------
|
||||
|
||||
- ``position_string``
|
||||
The relative position of the playback window on the display (two characters,
|
||||
the first character means upper(U) / middle(O) / lower(D), the second character
|
||||
means left(L) / middle(O) / right(R)).
|
||||
|
||||
- ``monitor_index``
|
||||
If using multiple monitors, which one should the window show up in?
|
||||
|
||||
- ``full_screen``
|
||||
Should the preview window be full screen. If not, it defaults to half the screen
|
||||
|
||||
- ``position``
|
||||
This is an option to more manually set the default window position, in pixel
|
||||
coordinates, e.g. (500, 300)
|
||||
|
||||
- ``size``
|
||||
Option to more manually set the default window size, in pixel coordinates,
|
||||
e.g. (1920, 1080)
|
||||
|
||||
|
||||
``camera``
|
||||
----------
|
||||
|
||||
- ``resolution``
|
||||
Resolution to render at, e.g. (1920, 1080)
|
||||
|
||||
- ``background_color``
|
||||
Default background color of scenes
|
||||
|
||||
- ``fps``
|
||||
Framerate
|
||||
|
||||
- ``background_opacity``
|
||||
Opacity of the background
|
||||
|
||||
|
||||
``file_writer``
|
||||
---------------
|
||||
Configuration specifying how files are written, e.g. what ffmpeg parameters to use
|
||||
|
||||
|
||||
``scene``
|
||||
-------
|
||||
Some default configuration for the Scene class
|
||||
|
||||
|
||||
``text``
|
||||
-------
|
||||
|
||||
- ``font``
|
||||
Default font of Text
|
||||
|
||||
- ``text_alignment``
|
||||
Default text alignment for LaTeX
|
||||
|
||||
``tex``
|
||||
-------
|
||||
|
||||
- ``executable``
|
||||
The executable program used to compile LaTeX (``latex`` or ``xelatex -no-pdf``
|
||||
is recommended)
|
||||
- ``template``
|
||||
Which configuration from the manimlib/tex_template.yml file should be used
|
||||
to determine the latex compiler to use, and what preamble to include for
|
||||
rendering tex.
|
||||
|
||||
- ``template_file``
|
||||
LaTeX template used, in ``manimlib/tex_templates``
|
||||
|
||||
- ``intermediate_filetype``
|
||||
The type of intermediate vector file generated after compilation (``dvi`` if
|
||||
``latex`` is used, ``xdv`` if ``xelatex`` is used)
|
||||
|
||||
- ``text_to_replace``
|
||||
The text to be replaced in the template (needn't to change)
|
||||
``sizes``
|
||||
---------
|
||||
|
||||
Valuess for various constants used in manimm to specify distances, like the height
|
||||
of the frame, the value of SMALL_BUFF, LARGE_BUFF, etc.
|
||||
|
||||
|
||||
``colors``
|
||||
----------
|
||||
|
||||
Color pallete to use, determining values of color constants like RED, BLUE_E, TEAL, etc.
|
||||
|
||||
``loglevel``
|
||||
------------
|
||||
|
||||
Can be DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
|
||||
|
||||
``universal_import_line``
|
||||
-------------------------
|
||||
|
||||
Import line that need to execute when entering interactive mode directly.
|
||||
|
||||
``style``
|
||||
---------
|
||||
|
||||
- ``font``
|
||||
Default font of Text
|
||||
``ignore_manimlib_modules_on_reload``
|
||||
-------------------------------------
|
||||
|
||||
- ``background_color``
|
||||
Default background color
|
||||
|
||||
``window_position``
|
||||
-------------------
|
||||
|
||||
The relative position of the playback window on the display (two characters,
|
||||
the first character means upper(U) / middle(O) / lower(D), the second character
|
||||
means left(L) / middle(O) / right(R)).
|
||||
|
||||
``window_monitor``
|
||||
------------------
|
||||
|
||||
The number of the monitor you want the preview window to pop up on. (default is 0)
|
||||
|
||||
``break_into_partial_movies``
|
||||
-----------------------------
|
||||
|
||||
If this is set to ``True``, then many small files will be written corresponding
|
||||
to each ``Scene.play`` and ``Scene.wait`` call, and these files will then be combined
|
||||
to form the full scene.
|
||||
|
||||
Sometimes video-editing is made easier when working with the broken up scene, which
|
||||
effectively has cuts at all the places you might want.
|
||||
|
||||
``camera_qualities``
|
||||
--------------------
|
||||
|
||||
Export quality
|
||||
|
||||
- ``low``
|
||||
Low quality (default is 480p15)
|
||||
|
||||
- ``medium``
|
||||
Medium quality (default is 720p30)
|
||||
|
||||
- ``high``
|
||||
High quality (default is 1080p30)
|
||||
|
||||
- ``ultra_high``
|
||||
Ultra high quality (default is 4K60)
|
||||
|
||||
- ``default_quality``
|
||||
Default quality (one of the above four)
|
||||
When calling ``reload`` during the interactive mode, imported modules are
|
||||
by default reloaded, in case the user writing a scene which pulls from various
|
||||
other files they have written. By default, modules withinn the manim library will
|
||||
be ignored, but one developing manim may want to set this to be False so that
|
||||
edits to the library are reloaded as well.
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
CONFIG dictionary
|
||||
=================
|
||||
|
||||
What's CONFIG
|
||||
-------------
|
||||
|
||||
``CONFIG`` dictionary is a feature of manim, which facilitates the inheritance
|
||||
and modification of parameters between parent and child classes.
|
||||
|
||||
| ``CONFIG`` dictionary 's processing is in ``manimlib/utils/config_ops.py``
|
||||
| It can convert the key-value pairs in the ``CONFIG`` dictionary into class attributes and values
|
||||
|
||||
Generally, the first line of the ``.__init__()`` method in some basic class (``Mobject``, ``Animation``,
|
||||
etc.) will call this function ``digest_config(self, kwargs)`` to convert both
|
||||
the ``CONFIG`` dictionary and ``kwargs`` into attributes. Then it can be accessed
|
||||
directly through ``self.``, which simplifies the handling of inheritance between classes.
|
||||
|
||||
**An example**:
|
||||
|
||||
There are many class inheritance relationships in ``manimlib/mobject/geometry.py``
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Line 279
|
||||
class Circle(Arc):
|
||||
CONFIG = {
|
||||
"color": RED,
|
||||
"close_new_points": True,
|
||||
"anchors_span_full_range": False
|
||||
}
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Line 304
|
||||
class Dot(Circle):
|
||||
CONFIG = {
|
||||
"radius": DEFAULT_DOT_RADIUS,
|
||||
"stroke_width": 0,
|
||||
"fill_opacity": 1.0,
|
||||
"color": WHITE
|
||||
}
|
||||
|
||||
The ``Circle`` class uses the key-value pair ``"color": RED`` in the ``CONFIG``
|
||||
dictionary to add the attribute ``self.color``.
|
||||
|
||||
At the same time, the ``Dot`` class also contains the key ``color`` in the
|
||||
``CONFIG`` dictionary, but the value is different. At this time, the priority will
|
||||
modify the attribute ``self.color`` to ``WHITE``.
|
||||
|
||||
CONFIG nesting
|
||||
--------------
|
||||
|
||||
The ``CONFIG`` dictionary supports nesting, that is, the value of the key is also
|
||||
a dictionary, for example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Camera(object):
|
||||
CONFIG = {
|
||||
# configs
|
||||
}
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Scene(object):
|
||||
CONFIG = {
|
||||
"window_config": {},
|
||||
"camera_class": Camera,
|
||||
"camera_config": {},
|
||||
"file_writer_config": {},
|
||||
# other configs
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
# some lines
|
||||
self.camera = self.camera_class(**self.camera_config)
|
||||
|
||||
The ``CONFIG`` dictionary of the ``Camera`` class contains many key-value pairs,
|
||||
and this class needs to be instantiated in the ``Scene`` class. For more convenient
|
||||
control, there is a special key-value pair in the Scene class ``"camera_config": {}``,
|
||||
Its value is a dictionary, passed in as ``kwargs`` when initializing the ``Camera`` class
|
||||
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``.
|
||||
@@ -32,10 +32,11 @@ Some useful flags
|
||||
All supported flags
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
========================================================== ====== =================================================================================================================================================================================================
|
||||
========================================================== ====== =====================================================================================================================================================================================================
|
||||
flag abbr function
|
||||
========================================================== ====== =================================================================================================================================================================================================
|
||||
========================================================== ====== =====================================================================================================================================================================================================
|
||||
``--help`` ``-h`` Show the help message and exit
|
||||
``--version`` ``-v`` Display the version of manimgl
|
||||
``--write_file`` ``-w`` Render the scene as a movie file
|
||||
``--skip_animations`` ``-s`` Skip to the last frame
|
||||
``--low_quality`` ``-l`` Render at a low quality (for faster rendering)
|
||||
@@ -43,8 +44,9 @@ 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
|
||||
``--gif`` ``-i`` Save the video as gif
|
||||
``--transparent`` ``-t`` Render to a movie file with an alpha channel
|
||||
``--quiet`` ``-q``
|
||||
``--write_all`` ``-a`` Write all the scenes from a file
|
||||
@@ -53,14 +55,16 @@ flag abbr function
|
||||
``--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 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
|
||||
``--embed [EMBED]`` ``-e`` Creates a new file where the line ``self.embed`` is inserted into the Scenes construct method. If a string is passed in, the line will be inserted below the last line of code including that string.
|
||||
``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080"
|
||||
``--frame_rate FRAME_RATE`` Frame rate, as an integer
|
||||
``--fps FPS`` 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
|
||||
========================================================== ====== =================================================================================================================================================================================================
|
||||
``--log-level LOG_LEVEL`` Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
``--autoreload`` Automatically reload Python modules to pick up code changes across different files
|
||||
========================================================== ====== =====================================================================================================================================================================================================
|
||||
|
||||
custom_config
|
||||
--------------
|
||||
|
||||
@@ -23,7 +23,7 @@ InteractiveDevlopment
|
||||
self.play(ShowCreation(square))
|
||||
self.wait()
|
||||
|
||||
# This opens an iPython termnial where you can keep writing
|
||||
# This opens an iPython terminal where you can keep writing
|
||||
# lines as if they were part of this construct method.
|
||||
# In particular, 'square', 'circle' and 'self' will all be
|
||||
# part of the local namespace in that terminal.
|
||||
@@ -34,7 +34,7 @@ InteractiveDevlopment
|
||||
self.play(ReplacementTransform(square, circle))
|
||||
self.wait()
|
||||
self.play(circle.animate.stretch(4, 0))
|
||||
self.play(Rotate(circle, 90 * DEGREES))
|
||||
self.play(Rotate(circle, 90 * DEG))
|
||||
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
|
||||
|
||||
text = Text("""
|
||||
@@ -70,7 +70,7 @@ AnimatingMethods
|
||||
|
||||
class AnimatingMethods(Scene):
|
||||
def construct(self):
|
||||
grid = Tex(r"\pi").get_grid(10, 10, height=4)
|
||||
grid = OldTex(r"\pi").get_grid(10, 10, height=4)
|
||||
self.add(grid)
|
||||
|
||||
# You can animate the application of mobject methods with the
|
||||
@@ -192,16 +192,16 @@ TexTransformExample
|
||||
# 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"),
|
||||
OldTex("A^2", "+", "B^2", "=", "C^2"),
|
||||
# Likewise here
|
||||
Tex("A^2", "=", "C^2", "-", "B^2"),
|
||||
OldTex("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 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])
|
||||
OldTex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
|
||||
# OldTex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
|
||||
OldTex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
|
||||
)
|
||||
lines.arrange(DOWN, buff=LARGE_BUFF)
|
||||
for line in lines:
|
||||
@@ -221,7 +221,7 @@ TexTransformExample
|
||||
self.play(
|
||||
TransformMatchingTex(
|
||||
lines[0].copy(), lines[1],
|
||||
path_arc=90 * DEGREES,
|
||||
path_arc=90 * DEG,
|
||||
),
|
||||
**play_kw
|
||||
)
|
||||
@@ -260,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=["A", *to_isolate])
|
||||
new_line2 = OldTex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
|
||||
new_line2.replace(lines[2])
|
||||
new_line2.match_style(lines[2])
|
||||
|
||||
@@ -599,8 +599,8 @@ SurfaceExample
|
||||
# Set perspective
|
||||
frame = self.camera.frame
|
||||
frame.set_euler_angles(
|
||||
theta=-30 * DEGREES,
|
||||
phi=70 * DEGREES,
|
||||
theta=-30 * DEG,
|
||||
phi=70 * DEG,
|
||||
)
|
||||
|
||||
surface = surfaces[0]
|
||||
@@ -624,8 +624,8 @@ SurfaceExample
|
||||
self.play(
|
||||
Transform(surface, surfaces[2]),
|
||||
# Move camera frame during the transition
|
||||
frame.animate.increment_phi(-10 * DEGREES),
|
||||
frame.animate.increment_theta(-20 * DEGREES),
|
||||
frame.animate.increment_phi(-10 * DEG),
|
||||
frame.animate.increment_theta(-20 * DEG),
|
||||
run_time=3
|
||||
)
|
||||
# Add ambient rotation
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
|
||||
Manim runs on Python 3.7 or higher.
|
||||
|
||||
System requirements are:
|
||||
|
||||
@@ -11,6 +11,32 @@ System requirements are:
|
||||
- `Pango <https://pango.org>`__ (only for Linux)
|
||||
|
||||
|
||||
Install FFmpeg
|
||||
--------------
|
||||
|
||||
|
||||
|
||||
Install FFmpeg Windows
|
||||
------------------------
|
||||
.. code-block:: cmd
|
||||
|
||||
choco install ffmpeg
|
||||
|
||||
|
||||
# Install FFmepeg Linux
|
||||
----------------------------
|
||||
.. code-block:: sh
|
||||
|
||||
$ sudo apt update
|
||||
$ sudo apt install ffmpeg
|
||||
$ ffmpeg -version
|
||||
|
||||
# Install FFmpeg MacOS
|
||||
----------------------------
|
||||
- Download This ZIP file `https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z`(if the link is not working download this zip file from there original website)
|
||||
|
||||
|
||||
|
||||
Directly
|
||||
--------
|
||||
|
||||
|
||||
@@ -96,14 +96,13 @@ Below is the directory structure of manim:
|
||||
└── utils/ # Some useful utility functions
|
||||
├── bezier.py # For bezier curve
|
||||
├── color.py # For color
|
||||
├── config_ops.py # Process CONFIG
|
||||
├── dict_ops.py # Functions related to dictionary processing
|
||||
├── 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
|
||||
├── init_config.py # Configuration guide
|
||||
├── iterables.py # Functions related to list/dictionary processing
|
||||
├── paths.py # Curve path
|
||||
├── rate_functions.py # Some defined rate_functions
|
||||
|
||||
@@ -26,12 +26,12 @@ class OpeningManimExample(Scene):
|
||||
matrix = [[1, 1], [0, 1]]
|
||||
linear_transform_words = VGroup(
|
||||
Text("This is what the matrix"),
|
||||
IntegerMatrix(matrix, include_background_rectangle=True),
|
||||
IntegerMatrix(matrix),
|
||||
Text("looks like")
|
||||
)
|
||||
linear_transform_words.arrange(RIGHT)
|
||||
linear_transform_words.to_edge(UP)
|
||||
linear_transform_words.set_stroke(BLACK, 10, background=True)
|
||||
linear_transform_words.set_backstroke(width=5)
|
||||
|
||||
self.play(
|
||||
ShowCreation(grid),
|
||||
@@ -52,7 +52,7 @@ class OpeningManimExample(Scene):
|
||||
this is the map $z \\rightarrow z^2$
|
||||
""")
|
||||
complex_map_words.to_corner(UR)
|
||||
complex_map_words.set_stroke(BLACK, 5, background=True)
|
||||
complex_map_words.set_backstroke(width=5)
|
||||
|
||||
self.play(
|
||||
FadeOut(grid),
|
||||
@@ -70,17 +70,13 @@ class OpeningManimExample(Scene):
|
||||
|
||||
class AnimatingMethods(Scene):
|
||||
def construct(self):
|
||||
grid = Tex(r"\pi").get_grid(10, 10, height=4)
|
||||
grid = Tex(R"\pi").get_grid(10, 10, height=4)
|
||||
self.add(grid)
|
||||
|
||||
# You can animate the application of mobject methods with the
|
||||
# ".animate" syntax:
|
||||
self.play(grid.animate.shift(LEFT))
|
||||
|
||||
# Alternatively, you can use the older syntax by passing the
|
||||
# method and then the arguments to the scene's "play" function:
|
||||
self.play(grid.shift, LEFT)
|
||||
|
||||
# Both of those will interpolate between the mobject's initial
|
||||
# state and whatever happens when you apply that method.
|
||||
# For this example, calling grid.shift(LEFT) would shift the
|
||||
@@ -159,140 +155,145 @@ class TextExample(Scene):
|
||||
|
||||
class TexTransformExample(Scene):
|
||||
def construct(self):
|
||||
to_isolate = ["B", "C", "=", "(", ")"]
|
||||
# Tex to color map
|
||||
t2c = {
|
||||
"A": BLUE,
|
||||
"B": TEAL,
|
||||
"C": GREEN,
|
||||
}
|
||||
# Configuration to pass along to each Tex mobject
|
||||
kw = dict(font_size=72, t2c=t2c)
|
||||
lines = VGroup(
|
||||
# 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 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])
|
||||
Tex("A^2 + B^2 = C^2", **kw),
|
||||
Tex("A^2 = C^2 - B^2", **kw),
|
||||
Tex("A^2 = (C + B)(C - B)", **kw),
|
||||
Tex(R"A = \sqrt{(C + B)(C - B)}", **kw),
|
||||
)
|
||||
lines.arrange(DOWN, buff=LARGE_BUFF)
|
||||
for line in lines:
|
||||
line.set_color_by_tex_to_color_map({
|
||||
"A": BLUE,
|
||||
"B": TEAL,
|
||||
"C": GREEN,
|
||||
})
|
||||
|
||||
play_kw = {"run_time": 2}
|
||||
self.add(lines[0])
|
||||
# The animation TransformMatchingTex will line up parts
|
||||
# of the source and target which have matching tex strings.
|
||||
# Here, giving it a little path_arc makes each part sort of
|
||||
# rotate into their final positions, which feels appropriate
|
||||
# for the idea of rearranging an equation
|
||||
# The animation TransformMatchingStrings will line up parts
|
||||
# of the source and target which have matching substring strings.
|
||||
# Here, giving it a little path_arc makes each part rotate into
|
||||
# their final positions, which feels appropriate for the idea of
|
||||
# rearranging an equation
|
||||
self.play(
|
||||
TransformMatchingTex(
|
||||
TransformMatchingStrings(
|
||||
lines[0].copy(), lines[1],
|
||||
path_arc=90 * DEGREES,
|
||||
# matched_keys specifies which substring should
|
||||
# line up. If it's not specified, the animation
|
||||
# will align the longest matching substrings.
|
||||
# In this case, the substring "^2 = C^2" would
|
||||
# trip it up
|
||||
matched_keys=["A^2", "B^2", "C^2"],
|
||||
# When you want a substring from the source
|
||||
# to go to a non-equal substring from the target,
|
||||
# use the key map.
|
||||
key_map={"+": "-"},
|
||||
path_arc=90 * DEG,
|
||||
),
|
||||
**play_kw
|
||||
)
|
||||
self.wait()
|
||||
|
||||
# Now, we could try this again on the next line...
|
||||
self.play(
|
||||
TransformMatchingTex(lines[1].copy(), lines[2]),
|
||||
**play_kw
|
||||
)
|
||||
self.play(TransformMatchingStrings(
|
||||
lines[1].copy(), lines[2],
|
||||
matched_keys=["A^2"]
|
||||
))
|
||||
self.wait()
|
||||
# ...and this looks nice enough, but since there's no tex
|
||||
# in lines[2] which matches "C^2" or "B^2", those terms fade
|
||||
# out to nothing while the C and B terms fade in from nothing.
|
||||
# If, however, we want the C^2 to go to C, and B^2 to go to B,
|
||||
# we can specify that with a key map.
|
||||
self.play(FadeOut(lines[2]))
|
||||
self.play(
|
||||
TransformMatchingTex(
|
||||
lines[1].copy(), lines[2],
|
||||
key_map={
|
||||
"C^2": "C",
|
||||
"B^2": "B",
|
||||
}
|
||||
TransformMatchingStrings(
|
||||
lines[2].copy(), lines[3],
|
||||
key_map={"2": R"\sqrt"},
|
||||
path_arc=-30 * DEG,
|
||||
),
|
||||
**play_kw
|
||||
)
|
||||
self.wait()
|
||||
self.wait(2)
|
||||
self.play(LaggedStartMap(FadeOut, lines, shift=2 * RIGHT))
|
||||
|
||||
# And to finish off, a simple TransformMatchingShapes would work
|
||||
# just fine. But perhaps we want that exponent on A^2 to transform into
|
||||
# the square root symbol. At the moment, lines[2] treats the expression
|
||||
# A^2 as a unit, so we might create a new version of the same line which
|
||||
# separates out just the A. This way, when TransformMatchingTex lines up
|
||||
# all matching parts, the only mismatch will be between the "^2" from
|
||||
# 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=["A", *to_isolate])
|
||||
new_line2.replace(lines[2])
|
||||
new_line2.match_style(lines[2])
|
||||
|
||||
self.play(
|
||||
TransformMatchingTex(
|
||||
new_line2, lines[3],
|
||||
transform_mismatches=True,
|
||||
),
|
||||
**play_kw
|
||||
)
|
||||
self.wait(3)
|
||||
self.play(FadeOut(lines, RIGHT))
|
||||
|
||||
# Alternatively, if you don't want to think about breaking up
|
||||
# the tex strings deliberately, you can TransformMatchingShapes,
|
||||
# which will try to line up all pieces of a source mobject with
|
||||
# those of a target, regardless of the submobject hierarchy in
|
||||
# each one, according to whether those pieces have the same
|
||||
# shape (as best it can).
|
||||
# TransformMatchingShapes will try to line up all pieces of a
|
||||
# source mobject with those of a target, regardless of the
|
||||
# what Mobject type they are.
|
||||
source = Text("the morse code", height=1)
|
||||
target = Text("here come dots", height=1)
|
||||
saved_source = source.copy()
|
||||
|
||||
self.play(Write(source))
|
||||
self.wait()
|
||||
kw = {"run_time": 3, "path_arc": PI / 2}
|
||||
kw = dict(run_time=3, path_arc=PI / 2)
|
||||
self.play(TransformMatchingShapes(source, target, **kw))
|
||||
self.wait()
|
||||
self.play(TransformMatchingShapes(target, source, **kw))
|
||||
self.play(TransformMatchingShapes(target, saved_source, **kw))
|
||||
self.wait()
|
||||
|
||||
|
||||
class TexIndexing(Scene):
|
||||
def construct(self):
|
||||
# You can index into Tex mobject (or other StringMobjects) by substrings
|
||||
equation = Tex(R"e^{\pi i} = -1", font_size=144)
|
||||
|
||||
self.add(equation)
|
||||
self.play(FlashAround(equation["e"]))
|
||||
self.wait()
|
||||
self.play(Indicate(equation[R"\pi"]))
|
||||
self.wait()
|
||||
self.play(TransformFromCopy(
|
||||
equation[R"e^{\pi i}"].copy().set_opacity(0.5),
|
||||
equation["-1"],
|
||||
path_arc=-PI / 2,
|
||||
run_time=3
|
||||
))
|
||||
self.play(FadeOut(equation))
|
||||
|
||||
# Or regular expressions
|
||||
equation = Tex("A^2 + B^2 = C^2", font_size=144)
|
||||
|
||||
self.play(Write(equation))
|
||||
for part in equation[re.compile(r"\w\^2")]:
|
||||
self.play(FlashAround(part))
|
||||
self.wait()
|
||||
self.play(FadeOut(equation))
|
||||
|
||||
# Indexing by substrings like this may not work when
|
||||
# the order in which Latex draws symbols does not match
|
||||
# the order in which they show up in the string.
|
||||
# For example, here the infinity is drawn before the sigma
|
||||
# so we don't get the desired behavior.
|
||||
equation = Tex(R"\sum_{n = 1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}", font_size=72)
|
||||
self.play(FadeIn(equation))
|
||||
self.play(equation[R"\infty"].animate.set_color(RED)) # Doesn't hit the infinity
|
||||
self.wait()
|
||||
self.play(FadeOut(equation))
|
||||
|
||||
# However you can always fix this by explicitly passing in
|
||||
# a string you might want to isolate later. Also, using
|
||||
# \over instead of \frac helps to avoid the issue for fractions
|
||||
equation = Tex(
|
||||
R"\sum_{n = 1}^\infty {1 \over n^2} = {\pi^2 \over 6}",
|
||||
# Explicitly mark "\infty" as a substring you might want to access
|
||||
isolate=[R"\infty"],
|
||||
font_size=72
|
||||
)
|
||||
self.play(FadeIn(equation))
|
||||
self.play(equation[R"\infty"].animate.set_color(RED)) # Got it!
|
||||
self.wait()
|
||||
self.play(FadeOut(equation))
|
||||
|
||||
|
||||
class UpdatersExample(Scene):
|
||||
def construct(self):
|
||||
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)
|
||||
|
||||
text, number = label = VGroup(
|
||||
Text("Width = "),
|
||||
DecimalNumber(
|
||||
0,
|
||||
show_ellipsis=True,
|
||||
num_decimal_places=2,
|
||||
include_sign=True,
|
||||
)
|
||||
)
|
||||
label.arrange(RIGHT)
|
||||
label = TexText("Width = 0.00")
|
||||
number = label.make_number_changeable("0.00")
|
||||
|
||||
# This ensures that the method deicmal.next_to(square)
|
||||
# is called on every frame
|
||||
always(label.next_to, brace, UP)
|
||||
label.always.next_to(brace, UP)
|
||||
# You could also write the following equivalent line
|
||||
# label.add_updater(lambda m: m.next_to(brace, UP))
|
||||
|
||||
@@ -301,7 +302,7 @@ class UpdatersExample(Scene):
|
||||
# should be functions returning arguments to that method.
|
||||
# The following line ensures thst decimal.set_value(square.get_y())
|
||||
# is called every frame
|
||||
f_always(number.set_value, square.get_width)
|
||||
number.f_always.set_value(square.get_width)
|
||||
# You could also write the following equivalent line
|
||||
# number.add_updater(lambda m: m.set_value(square.get_width()))
|
||||
|
||||
@@ -350,15 +351,16 @@ class CoordinateSystemExample(Scene):
|
||||
width=10,
|
||||
# Axes is made of two NumberLine mobjects. You can specify
|
||||
# their configuration with axis_config
|
||||
axis_config={
|
||||
"stroke_color": GREY_A,
|
||||
"stroke_width": 2,
|
||||
},
|
||||
axis_config=dict(
|
||||
stroke_color=GREY_A,
|
||||
stroke_width=2,
|
||||
numbers_to_exclude=[0],
|
||||
),
|
||||
# Alternatively, you can specify configuration for just one
|
||||
# of them, like this.
|
||||
y_axis_config={
|
||||
"include_tip": False,
|
||||
}
|
||||
y_axis_config=dict(
|
||||
big_tick_numbers=[-2, 2],
|
||||
)
|
||||
)
|
||||
# Keyword arguments of add_coordinate_labels can be used to
|
||||
# configure the DecimalNumber mobjects which it creates and
|
||||
@@ -417,7 +419,7 @@ class CoordinateSystemExample(Scene):
|
||||
|
||||
class GraphExample(Scene):
|
||||
def construct(self):
|
||||
axes = Axes((-3, 10), (-1, 8))
|
||||
axes = Axes((-3, 10), (-1, 8), height=6)
|
||||
axes.add_coordinate_labels()
|
||||
|
||||
self.play(Write(axes, lag_ratio=0.01, run_time=1))
|
||||
@@ -486,21 +488,82 @@ class GraphExample(Scene):
|
||||
# with the intent of having other mobjects update based
|
||||
# on the parameter
|
||||
x_tracker = ValueTracker(2)
|
||||
f_always(
|
||||
dot.move_to,
|
||||
lambda: axes.i2gp(x_tracker.get_value(), parabola)
|
||||
)
|
||||
dot.add_updater(lambda d: d.move_to(axes.i2gp(x_tracker.get_value(), parabola)))
|
||||
|
||||
self.play(x_tracker.animate.set_value(4), run_time=3)
|
||||
self.play(x_tracker.animate.set_value(-2), run_time=3)
|
||||
self.wait()
|
||||
|
||||
|
||||
class SurfaceExample(Scene):
|
||||
CONFIG = {
|
||||
"camera_class": ThreeDCamera,
|
||||
}
|
||||
class TexAndNumbersExample(Scene):
|
||||
def construct(self):
|
||||
axes = Axes((-3, 3), (-3, 3), unit_size=1)
|
||||
axes.to_edge(DOWN)
|
||||
axes.add_coordinate_labels(font_size=16)
|
||||
circle = Circle(radius=2)
|
||||
circle.set_stroke(YELLOW, 3)
|
||||
circle.move_to(axes.get_origin())
|
||||
self.add(axes, circle)
|
||||
|
||||
# When numbers show up in tex, they can be readily
|
||||
# replaced with DecimalMobjects so that methods like
|
||||
# get_value and set_value can be called on them, and
|
||||
# animations like ChangeDecimalToValue can be called
|
||||
# on them.
|
||||
tex = Tex("x^2 + y^2 = 4.00")
|
||||
tex.next_to(axes, UP, buff=0.5)
|
||||
value = tex.make_number_changeable("4.00")
|
||||
|
||||
|
||||
# This will tie the right hand side of our equation to
|
||||
# the square of the radius of the circle
|
||||
value.add_updater(lambda v: v.set_value(circle.get_radius()**2))
|
||||
self.add(tex)
|
||||
|
||||
text = Text("""
|
||||
You can manipulate numbers
|
||||
in Tex mobjects
|
||||
""", font_size=30)
|
||||
text.next_to(tex, RIGHT, buff=1.5)
|
||||
arrow = Arrow(text, tex)
|
||||
self.add(text, arrow)
|
||||
|
||||
self.play(
|
||||
circle.animate.set_height(2.0),
|
||||
run_time=4,
|
||||
rate_func=there_and_back,
|
||||
)
|
||||
|
||||
# By default, tex.make_number_changeable replaces the first occurance
|
||||
# of the number,but by passing replace_all=True it replaces all and
|
||||
# returns a group of the results
|
||||
exponents = tex.make_number_changeable("2", replace_all=True)
|
||||
self.play(
|
||||
LaggedStartMap(
|
||||
FlashAround, exponents,
|
||||
lag_ratio=0.2, buff=0.1, color=RED
|
||||
),
|
||||
exponents.animate.set_color(RED)
|
||||
)
|
||||
|
||||
def func(x, y):
|
||||
# Switch from manim coords to axes coords
|
||||
xa, ya = axes.point_to_coords(np.array([x, y, 0]))
|
||||
return xa**4 + ya**4 - 4
|
||||
|
||||
new_curve = ImplicitFunction(func)
|
||||
new_curve.match_style(circle)
|
||||
circle.rotate(angle_of_vector(new_curve.get_start())) # Align
|
||||
value.clear_updaters()
|
||||
|
||||
self.play(
|
||||
*(ChangeDecimalToValue(exp, 4) for exp in exponents),
|
||||
ReplacementTransform(circle.copy(), new_curve),
|
||||
circle.animate.set_stroke(width=1, opacity=0.5),
|
||||
)
|
||||
|
||||
|
||||
class SurfaceExample(ThreeDScene):
|
||||
def construct(self):
|
||||
surface_text = Text("For 3d scenes, try using surfaces")
|
||||
surface_text.fix_in_frame()
|
||||
@@ -532,13 +595,6 @@ class SurfaceExample(Scene):
|
||||
mob.mesh = SurfaceMesh(mob)
|
||||
mob.mesh.set_stroke(BLUE, 1, opacity=0.5)
|
||||
|
||||
# Set perspective
|
||||
frame = self.camera.frame
|
||||
frame.set_euler_angles(
|
||||
theta=-30 * DEGREES,
|
||||
phi=70 * DEGREES,
|
||||
)
|
||||
|
||||
surface = surfaces[0]
|
||||
|
||||
self.play(
|
||||
@@ -560,12 +616,12 @@ class SurfaceExample(Scene):
|
||||
self.play(
|
||||
Transform(surface, surfaces[2]),
|
||||
# Move camera frame during the transition
|
||||
frame.animate.increment_phi(-10 * DEGREES),
|
||||
frame.animate.increment_theta(-20 * DEGREES),
|
||||
self.frame.animate.increment_phi(-10 * DEG),
|
||||
self.frame.animate.increment_theta(-20 * DEG),
|
||||
run_time=3
|
||||
)
|
||||
# Add ambient rotation
|
||||
frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
|
||||
self.frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
|
||||
|
||||
# Play around with where the light is
|
||||
light_text = Text("You can move around the light source")
|
||||
@@ -574,12 +630,14 @@ class SurfaceExample(Scene):
|
||||
|
||||
self.play(FadeTransform(surface_text, light_text))
|
||||
light = self.camera.light_source
|
||||
self.add(light)
|
||||
light_dot = GlowDot(color=WHITE, radius=0.5)
|
||||
light_dot.always.move_to(light)
|
||||
self.add(light, light_dot)
|
||||
light.save_state()
|
||||
self.play(light.animate.move_to(3 * IN), run_time=5)
|
||||
self.play(light.animate.shift(10 * OUT), run_time=5)
|
||||
|
||||
drag_text = Text("Try moving the mouse while pressing d or s")
|
||||
drag_text = Text("Try moving the mouse while pressing d or f")
|
||||
drag_text.move_to(light_text)
|
||||
drag_text.fix_in_frame()
|
||||
|
||||
@@ -597,7 +655,7 @@ class InteractiveDevelopment(Scene):
|
||||
self.play(ShowCreation(square))
|
||||
self.wait()
|
||||
|
||||
# This opens an iPython termnial where you can keep writing
|
||||
# This opens an iPython terminal where you can keep writing
|
||||
# lines as if they were part of this construct method.
|
||||
# In particular, 'square', 'circle' and 'self' will all be
|
||||
# part of the local namespace in that terminal.
|
||||
@@ -608,7 +666,7 @@ class InteractiveDevelopment(Scene):
|
||||
self.play(ReplacementTransform(square, circle))
|
||||
self.wait()
|
||||
self.play(circle.animate.stretch(4, 0))
|
||||
self.play(Rotate(circle, 90 * DEGREES))
|
||||
self.play(Rotate(circle, 90 * DEG))
|
||||
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
|
||||
|
||||
text = Text("""
|
||||
@@ -634,6 +692,8 @@ class InteractiveDevelopment(Scene):
|
||||
|
||||
|
||||
class ControlsExample(Scene):
|
||||
drag_to_pan = False
|
||||
|
||||
def setup(self):
|
||||
self.textbox = Textbox()
|
||||
self.checkbox = Checkbox()
|
||||
@@ -650,7 +710,7 @@ class ControlsExample(Scene):
|
||||
|
||||
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():
|
||||
|
||||
@@ -2,8 +2,15 @@ import pkg_resources
|
||||
|
||||
__version__ = pkg_resources.get_distribution("manimgl").version
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import *
|
||||
|
||||
from manimlib.constants import *
|
||||
|
||||
from manimlib.window import *
|
||||
|
||||
from manimlib.animation.animation import *
|
||||
from manimlib.animation.composition import *
|
||||
from manimlib.animation.creation import *
|
||||
@@ -20,52 +27,52 @@ from manimlib.animation.update import *
|
||||
|
||||
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.coordinate_systems import *
|
||||
from manimlib.mobject.frame import *
|
||||
from manimlib.mobject.functions import *
|
||||
from manimlib.mobject.geometry import *
|
||||
from manimlib.mobject.interactive import *
|
||||
from manimlib.mobject.matrix import *
|
||||
from manimlib.mobject.mobject import *
|
||||
from manimlib.mobject.mobject_update_utils import *
|
||||
from manimlib.mobject.number_line import *
|
||||
from manimlib.mobject.numbers import *
|
||||
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.mtex_mobject import *
|
||||
from manimlib.mobject.svg.string_mobject import *
|
||||
from manimlib.mobject.svg.svg_mobject import *
|
||||
from manimlib.mobject.svg.special_tex import *
|
||||
from manimlib.mobject.svg.tex_mobject import *
|
||||
from manimlib.mobject.svg.text_mobject import *
|
||||
from manimlib.mobject.three_dimensions import *
|
||||
from manimlib.mobject.types.dot_cloud import *
|
||||
from manimlib.mobject.types.image_mobject import *
|
||||
from manimlib.mobject.types.point_cloud_mobject import *
|
||||
from manimlib.mobject.types.surface import *
|
||||
from manimlib.mobject.types.vectorized_mobject import *
|
||||
from manimlib.mobject.types.dot_cloud import *
|
||||
from manimlib.mobject.mobject_update_utils import *
|
||||
from manimlib.mobject.value_tracker import *
|
||||
from manimlib.mobject.vector_field import *
|
||||
|
||||
from manimlib.scene.interactive_scene import *
|
||||
from manimlib.scene.scene import *
|
||||
from manimlib.scene.three_d_scene import *
|
||||
|
||||
from manimlib.utils.bezier import *
|
||||
from manimlib.utils.cache import *
|
||||
from manimlib.utils.color import *
|
||||
from manimlib.utils.config_ops import *
|
||||
from manimlib.utils.customization import *
|
||||
from manimlib.utils.dict_ops import *
|
||||
from manimlib.utils.debug import *
|
||||
from manimlib.utils.directories import *
|
||||
from manimlib.utils.file_ops import *
|
||||
from manimlib.utils.images import *
|
||||
from manimlib.utils.iterables import *
|
||||
from manimlib.utils.file_ops import *
|
||||
from manimlib.utils.paths import *
|
||||
from manimlib.utils.rate_functions import *
|
||||
from manimlib.utils.simple_functions import *
|
||||
from manimlib.utils.shaders import *
|
||||
from manimlib.utils.sounds import *
|
||||
from manimlib.utils.space_ops import *
|
||||
from manimlib.utils.strings import *
|
||||
from manimlib.utils.tex import *
|
||||
|
||||
@@ -1,28 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
import manimlib.config
|
||||
import manimlib.logger
|
||||
import manimlib.extract_scene
|
||||
import manimlib.utils.init_config
|
||||
from addict import Dict
|
||||
|
||||
from manimlib import __version__
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.config import parse_cli
|
||||
import manimlib.extract_scene
|
||||
from manimlib.utils.cache import clear_cache
|
||||
from manimlib.window import Window
|
||||
|
||||
|
||||
from IPython.terminal.embed import KillEmbedded
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
|
||||
|
||||
def run_scenes():
|
||||
"""
|
||||
Runs the scenes in a loop and detects when a scene reload is requested.
|
||||
"""
|
||||
# Create a new dict to be able to upate without
|
||||
# altering global configuration
|
||||
scene_config = Dict(manim_config.scene)
|
||||
run_config = manim_config.run
|
||||
|
||||
if run_config.show_in_window:
|
||||
# Create a reusable window
|
||||
window = Window(**manim_config.window)
|
||||
scene_config.update(window=window)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Blocking call since a scene may init an IPython shell()
|
||||
scenes = manimlib.extract_scene.main(scene_config, run_config)
|
||||
for scene in scenes:
|
||||
scene.run()
|
||||
return
|
||||
except KillEmbedded:
|
||||
# Requested via the `exit_raise` IPython runline magic
|
||||
# by means of the reload_scene() command
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for ManimGL.
|
||||
"""
|
||||
print(f"ManimGL \033[32mv{__version__}\033[0m")
|
||||
|
||||
args = manimlib.config.parse_cli()
|
||||
args = parse_cli()
|
||||
if args.version and args.file is None:
|
||||
return
|
||||
if args.log_level:
|
||||
manimlib.logger.log.setLevel(args.log_level)
|
||||
if args.clear_cache:
|
||||
clear_cache()
|
||||
|
||||
if args.config:
|
||||
manimlib.utils.init_config.init_customization()
|
||||
else:
|
||||
config = manimlib.config.get_configuration(args)
|
||||
scenes = manimlib.extract_scene.main(config)
|
||||
|
||||
for scene in scenes:
|
||||
scene.run()
|
||||
run_scenes()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,135 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
DEFAULT_ANIMATION_RUN_TIME = 1.0
|
||||
DEFAULT_ANIMATION_LAG_RATIO = 0
|
||||
|
||||
|
||||
class Animation(object):
|
||||
CONFIG = {
|
||||
"run_time": DEFAULT_ANIMATION_RUN_TIME,
|
||||
"rate_func": smooth,
|
||||
"name": None,
|
||||
# Does this animation add or remove a mobject form the screen
|
||||
"remover": False,
|
||||
# What to enter into the update function upon completion
|
||||
"final_alpha_value": 1,
|
||||
# If 0, the animation is applied to all submobjects
|
||||
# at the same time
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
run_time: float = DEFAULT_ANIMATION_RUN_TIME,
|
||||
# Tuple of times, between which the animation will run
|
||||
time_span: tuple[float, float] | None = None,
|
||||
# If 0, the animation is applied to all submobjects at the same time
|
||||
# If 1, it is applied to each successively.
|
||||
# If 0 < lag_ratio < 1, its applied to each
|
||||
# with lagged start times
|
||||
"lag_ratio": DEFAULT_ANIMATION_LAG_RATIO,
|
||||
"suspend_mobject_updating": True,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
assert(isinstance(mobject, Mobject))
|
||||
digest_config(self, kwargs)
|
||||
# If 0 < lag_ratio < 1, its applied to each with lagged start times
|
||||
lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO,
|
||||
rate_func: Callable[[float], float] = smooth,
|
||||
name: str = "",
|
||||
# Does this animation add or remove a mobject form the screen
|
||||
remover: bool = False,
|
||||
# What to enter into the update function upon completion
|
||||
final_alpha_value: float = 1.0,
|
||||
# If set to True, the mobject itself will have its internal updaters called,
|
||||
# but the start or target mobjects would not be suspended. To completely suspend
|
||||
# updating, call mobject.suspend_updating() before the animation
|
||||
suspend_mobject_updating: bool = False,
|
||||
):
|
||||
self.mobject = mobject
|
||||
self.run_time = run_time
|
||||
self.time_span = time_span
|
||||
self.rate_func = rate_func
|
||||
self.name = name or self.__class__.__name__ + str(self.mobject)
|
||||
self.remover = remover
|
||||
self.final_alpha_value = final_alpha_value
|
||||
self.lag_ratio = lag_ratio
|
||||
self.suspend_mobject_updating = suspend_mobject_updating
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return self.name
|
||||
return self.__class__.__name__ + str(self.mobject)
|
||||
assert isinstance(mobject, Mobject)
|
||||
|
||||
def begin(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
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
|
||||
# this method
|
||||
if self.time_span is not None:
|
||||
start, end = self.time_span
|
||||
self.run_time = max(end, self.run_time)
|
||||
self.mobject.set_animating_status(True)
|
||||
self.starting_mobject = self.create_starting_mobject()
|
||||
if self.suspend_mobject_updating:
|
||||
# All calls to self.mobject's internal updaters
|
||||
# during the animation, either from this Animation
|
||||
# or from the surrounding scene, should do nothing.
|
||||
# It is, however, okay and desirable to call
|
||||
# the internal updaters of self.starting_mobject,
|
||||
# or any others among self.get_all_mobjects()
|
||||
self.mobject_was_updating = not self.mobject.updating_suspended
|
||||
self.mobject.suspend_updating()
|
||||
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.set_animating_status(False)
|
||||
if self.suspend_mobject_updating and self.mobject_was_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
|
||||
(always?) self.mobject will have its updating
|
||||
suspended during the animation, this will do
|
||||
nothing to self.mobject.
|
||||
Transforms) target_mobject.
|
||||
"""
|
||||
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
|
||||
return list(filter(
|
||||
# updating of self.mobject.
|
||||
items = list(filter(
|
||||
lambda m: m is not self.mobject,
|
||||
self.get_all_mobjects()
|
||||
))
|
||||
items = remove_list_redundancies(items)
|
||||
return items
|
||||
|
||||
def copy(self):
|
||||
return deepcopy(self)
|
||||
|
||||
def update_config(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
def update_rate_info(
|
||||
self,
|
||||
run_time: float | None = None,
|
||||
rate_func: Callable[[float], float] | None = None,
|
||||
lag_ratio: float | None = None,
|
||||
):
|
||||
self.run_time = run_time or self.run_time
|
||||
self.rate_func = rate_func or self.rate_func
|
||||
self.lag_ratio = lag_ratio or self.lag_ratio
|
||||
return self
|
||||
|
||||
# Methods for interpolation, the mean of an Animation
|
||||
def interpolate(self, alpha):
|
||||
alpha = clip(alpha, 0, 1)
|
||||
self.interpolate_mobject(self.rate_func(alpha))
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
self.interpolate_mobject(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 time_spanned_alpha(self, alpha: float) -> float:
|
||||
if self.time_span is not None:
|
||||
start, end = self.time_span
|
||||
return clip(alpha * self.run_time - start, 0, end - start) / (end - start)
|
||||
return 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))
|
||||
sub_alpha = self.get_sub_alpha(self.time_spanned_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
|
||||
@@ -137,32 +175,35 @@ class Animation(object):
|
||||
full_length = (num_submobjects - 1) * lag_ratio + 1
|
||||
value = alpha * full_length
|
||||
lower = index * lag_ratio
|
||||
return clip((value - lower), 0, 1)
|
||||
raw_sub_alpha = clip((value - lower), 0, 1)
|
||||
return self.rate_func(raw_sub_alpha)
|
||||
|
||||
# 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:
|
||||
if self.time_span:
|
||||
return max(self.run_time, self.time_span[1])
|
||||
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,74 +1,93 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.animation import Animation, prepare_animation
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
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, Union, Iterable
|
||||
AnimationType = Union[Animation, _AnimationBuilder]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Optional
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
DEFAULT_LAGGED_START_LAG_RATIO = 0.05
|
||||
|
||||
|
||||
class AnimationGroup(Animation):
|
||||
CONFIG = {
|
||||
# If None, this defaults to the sum of all
|
||||
# internal animations
|
||||
"run_time": None,
|
||||
"rate_func": linear,
|
||||
# If 0, all animations are played at once.
|
||||
# If 1, all are played successively.
|
||||
# If >0 and <1, they start at lagged times
|
||||
# from one and other.
|
||||
"lag_ratio": 0,
|
||||
"group": None,
|
||||
}
|
||||
|
||||
def __init__(self, *animations, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
*args: AnimationType | Iterable[AnimationType],
|
||||
run_time: float = -1, # If negative, default to sum of inputed animation runtimes
|
||||
lag_ratio: float = 0.0,
|
||||
group: Optional[Mobject] = None,
|
||||
group_type: Optional[type] = None,
|
||||
**kwargs
|
||||
):
|
||||
animations = args[0] if isinstance(args[0], Iterable) else args
|
||||
self.animations = [prepare_animation(anim) for anim in animations]
|
||||
if self.group is None:
|
||||
self.group = Group(*remove_list_redundancies(
|
||||
[anim.mobject for anim in animations]
|
||||
))
|
||||
self.init_run_time()
|
||||
Animation.__init__(self, self.group, **kwargs)
|
||||
self.build_animations_with_timings(lag_ratio)
|
||||
self.max_end_time = max((awt[2] for awt in self.anims_with_timings), default=0)
|
||||
self.run_time = self.max_end_time if run_time < 0 else run_time
|
||||
self.lag_ratio = lag_ratio
|
||||
mobs = remove_list_redundancies([a.mobject for a in self.animations])
|
||||
if group is not None:
|
||||
self.group = group
|
||||
if group_type is not None:
|
||||
self.group = group_type(*mobs)
|
||||
elif all(isinstance(anim.mobject, VMobject) for anim in animations):
|
||||
self.group = VGroup(*mobs)
|
||||
else:
|
||||
self.group = Group(*mobs)
|
||||
|
||||
def get_all_mobjects(self):
|
||||
super().__init__(
|
||||
self.group,
|
||||
run_time=self.run_time,
|
||||
lag_ratio=lag_ratio,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_all_mobjects(self) -> Mobject:
|
||||
return self.group
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.group.set_animating_status(True)
|
||||
for anim in self.animations:
|
||||
anim.begin()
|
||||
# self.init_run_time()
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
self.group.set_animating_status(False)
|
||||
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):
|
||||
self.build_animations_with_timings()
|
||||
if self.anims_with_timings:
|
||||
self.max_end_time = np.max([
|
||||
awt[2] for awt in self.anims_with_timings
|
||||
])
|
||||
else:
|
||||
self.max_end_time = 0
|
||||
if self.run_time is None:
|
||||
def calculate_max_end_time(self) -> None:
|
||||
self.max_end_time = max(
|
||||
(awt[2] for awt in self.anims_with_timings),
|
||||
default=0,
|
||||
)
|
||||
if self.run_time < 0:
|
||||
self.run_time = self.max_end_time
|
||||
|
||||
def build_animations_with_timings(self):
|
||||
def build_animations_with_timings(self, lag_ratio: float) -> None:
|
||||
"""
|
||||
Creates a list of triplets of the form
|
||||
(anim, start_time, end_time)
|
||||
@@ -81,13 +100,12 @@ class AnimationGroup(Animation):
|
||||
self.anims_with_timings.append(
|
||||
(anim, start_time, end_time)
|
||||
)
|
||||
# Start time of next animation is based on
|
||||
# the lag_ratio
|
||||
# Start time of next animation is based on the lag_ratio
|
||||
curr_time = interpolate(
|
||||
start_time, end_time, self.lag_ratio
|
||||
start_time, end_time, 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,
|
||||
@@ -99,31 +117,31 @@ class AnimationGroup(Animation):
|
||||
if anim_time == 0:
|
||||
sub_alpha = 0
|
||||
else:
|
||||
sub_alpha = clip(
|
||||
(time - start_time) / anim_time,
|
||||
0, 1
|
||||
)
|
||||
sub_alpha = clip((time - start_time) / anim_time, 0, 1)
|
||||
anim.interpolate(sub_alpha)
|
||||
|
||||
|
||||
class Succession(AnimationGroup):
|
||||
CONFIG = {
|
||||
"lag_ratio": 1,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
*animations: Animation,
|
||||
lag_ratio: float = 1.0,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
assert(len(self.animations) > 0)
|
||||
self.init_run_time()
|
||||
def begin(self) -> None:
|
||||
assert len(self.animations) > 0
|
||||
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
|
||||
)
|
||||
@@ -136,28 +154,29 @@ class Succession(AnimationGroup):
|
||||
|
||||
|
||||
class LaggedStart(AnimationGroup):
|
||||
CONFIG = {
|
||||
"lag_ratio": DEFAULT_LAGGED_START_LAG_RATIO,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
*animations,
|
||||
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
|
||||
|
||||
|
||||
class LaggedStartMap(LaggedStart):
|
||||
CONFIG = {
|
||||
"run_time": 2,
|
||||
}
|
||||
|
||||
def __init__(self, AnimationClass, mobject, arg_creator=None, **kwargs):
|
||||
args_list = []
|
||||
for submob in mobject:
|
||||
if arg_creator:
|
||||
args_list.append(arg_creator(submob))
|
||||
else:
|
||||
args_list.append((submob,))
|
||||
def __init__(
|
||||
self,
|
||||
anim_func: Callable[[Mobject], Animation],
|
||||
group: Mobject,
|
||||
run_time: float = 2.0,
|
||||
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
|
||||
**kwargs
|
||||
):
|
||||
anim_kwargs = dict(kwargs)
|
||||
if "lag_ratio" in anim_kwargs:
|
||||
anim_kwargs.pop("lag_ratio")
|
||||
animations = [
|
||||
AnimationClass(*args, **anim_kwargs)
|
||||
for args in args_list
|
||||
]
|
||||
super().__init__(*animations, group=mobject, **kwargs)
|
||||
anim_kwargs.pop("lag_ratio", None)
|
||||
super().__init__(
|
||||
*(anim_func(submob, **anim_kwargs) for submob in group),
|
||||
run_time=run_time,
|
||||
lag_ratio=lag_ratio,
|
||||
group=group
|
||||
)
|
||||
|
||||
@@ -1,122 +1,138 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.composition import Succession
|
||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||
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
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.typing import ManimColor
|
||||
|
||||
|
||||
class ShowPartial(Animation):
|
||||
class ShowPartial(Animation, ABC):
|
||||
"""
|
||||
Abstract class for ShowCreation and ShowPassingFlash
|
||||
"""
|
||||
CONFIG = {
|
||||
"should_match_start": False,
|
||||
}
|
||||
def __init__(self, mobject: Mobject, should_match_start: bool = False, **kwargs):
|
||||
self.should_match_start = should_match_start
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
super().begin()
|
||||
if not self.should_match_start:
|
||||
self.mobject.lock_matching_data(self.mobject, self.starting_mobject)
|
||||
|
||||
def finish(self):
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
|
||||
def interpolate_submobject(self, submob, start_submob, alpha):
|
||||
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")
|
||||
|
||||
|
||||
class ShowCreation(ShowPartial):
|
||||
CONFIG = {
|
||||
"lag_ratio": 1,
|
||||
}
|
||||
def __init__(self, mobject: Mobject, lag_ratio: float = 1.0, **kwargs):
|
||||
super().__init__(mobject, lag_ratio=lag_ratio, **kwargs)
|
||||
|
||||
def get_bounds(self, alpha):
|
||||
def get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
return (0, alpha)
|
||||
|
||||
|
||||
class Uncreate(ShowCreation):
|
||||
CONFIG = {
|
||||
"rate_func": lambda t: smooth(1 - t),
|
||||
"remover": True,
|
||||
"should_match_start": True,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
rate_func: Callable[[float], float] = lambda t: smooth(1 - t),
|
||||
remover: bool = True,
|
||||
should_match_start: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
mobject,
|
||||
rate_func=rate_func,
|
||||
remover=remover,
|
||||
should_match_start=should_match_start,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class DrawBorderThenFill(Animation):
|
||||
CONFIG = {
|
||||
"run_time": 2,
|
||||
"rate_func": double_smooth,
|
||||
"stroke_width": 2,
|
||||
"stroke_color": None,
|
||||
"draw_border_animation_config": {},
|
||||
"fill_animation_config": {},
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
assert(isinstance(vmobject, VMobject))
|
||||
self.sm_to_index = dict([
|
||||
(hash(sm), 0)
|
||||
for sm in vmobject.get_family()
|
||||
])
|
||||
super().__init__(vmobject, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
# Trigger triangulation calculation
|
||||
for submob in self.mobject.get_family():
|
||||
submob.get_triangulation()
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
run_time: float = 2.0,
|
||||
rate_func: Callable[[float], float] = double_smooth,
|
||||
stroke_width: float = 2.0,
|
||||
stroke_color: ManimColor = None,
|
||||
draw_border_animation_config: dict = {},
|
||||
fill_animation_config: dict = {},
|
||||
**kwargs
|
||||
):
|
||||
assert isinstance(vmobject, VMobject)
|
||||
self.sm_to_index = {hash(sm): 0 for sm in vmobject.get_family()}
|
||||
self.stroke_width = stroke_width
|
||||
self.stroke_color = stroke_color
|
||||
self.draw_border_animation_config = draw_border_animation_config
|
||||
self.fill_animation_config = fill_animation_config
|
||||
super().__init__(
|
||||
vmobject,
|
||||
run_time=run_time,
|
||||
rate_func=rate_func,
|
||||
**kwargs
|
||||
)
|
||||
self.mobject = vmobject
|
||||
|
||||
def begin(self) -> None:
|
||||
self.mobject.set_animating_status(True)
|
||||
self.outline = self.get_outline()
|
||||
super().begin()
|
||||
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()
|
||||
self.mobject.refresh_joint_angles()
|
||||
|
||||
def get_outline(self):
|
||||
def get_outline(self) -> VMobject:
|
||||
outline = self.mobject.copy()
|
||||
outline.set_fill(opacity=0)
|
||||
for sm in outline.get_family():
|
||||
for sm in outline.family_members_with_points():
|
||||
sm.set_stroke(
|
||||
color=self.get_stroke_color(sm),
|
||||
width=float(self.stroke_width)
|
||||
color=self.stroke_color or sm.get_stroke_color(),
|
||||
width=self.stroke_width,
|
||||
behind=self.mobject.stroke_behind,
|
||||
)
|
||||
return outline
|
||||
|
||||
def get_stroke_color(self, vmobject):
|
||||
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[Mobject]:
|
||||
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:
|
||||
# First time crossing over
|
||||
submob.set_data(outline.data)
|
||||
submob.unlock_data()
|
||||
if not self.mobject.has_updaters:
|
||||
submob.lock_matching_data(submob, start)
|
||||
submob.needs_new_triangulation = False
|
||||
self.sm_to_index[hash(submob)] = 1
|
||||
|
||||
if index == 0:
|
||||
@@ -126,79 +142,103 @@ class DrawBorderThenFill(Animation):
|
||||
|
||||
|
||||
class Write(DrawBorderThenFill):
|
||||
CONFIG = {
|
||||
# To be figured out in
|
||||
# set_default_config_from_lengths
|
||||
"run_time": None,
|
||||
"lag_ratio": None,
|
||||
"rate_func": linear,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
run_time: float = -1, # If negative, this will be reassigned
|
||||
lag_ratio: float = -1, # If negative, this will be reassigned
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
stroke_color: ManimColor = None,
|
||||
**kwargs
|
||||
):
|
||||
if stroke_color is None:
|
||||
stroke_color = vmobject.get_color()
|
||||
family_size = len(vmobject.family_members_with_points())
|
||||
super().__init__(
|
||||
vmobject,
|
||||
run_time=self.compute_run_time(family_size, run_time),
|
||||
lag_ratio=self.compute_lag_ratio(family_size, lag_ratio),
|
||||
rate_func=rate_func,
|
||||
stroke_color=stroke_color,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.set_default_config_from_length(mobject)
|
||||
super().__init__(mobject, **kwargs)
|
||||
def compute_run_time(self, family_size: int, run_time: float):
|
||||
if run_time < 0:
|
||||
return 1 if family_size < 15 else 2
|
||||
return run_time
|
||||
|
||||
def set_default_config_from_length(self, mobject):
|
||||
length = len(mobject.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)
|
||||
def compute_lag_ratio(self, family_size: int, lag_ratio: float):
|
||||
if lag_ratio < 0:
|
||||
return min(4.0 / (family_size + 1.0), 0.2)
|
||||
return lag_ratio
|
||||
|
||||
|
||||
class ShowIncreasingSubsets(Animation):
|
||||
CONFIG = {
|
||||
"suspend_mobject_updating": False,
|
||||
"int_func": np.round,
|
||||
}
|
||||
|
||||
def __init__(self, group, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
group: Mobject,
|
||||
int_func: Callable[[float], float] = np.round,
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
self.all_submobs = list(group.submobjects)
|
||||
super().__init__(group, **kwargs)
|
||||
self.int_func = int_func
|
||||
super().__init__(
|
||||
group,
|
||||
suspend_mobject_updating=suspend_mobject_updating,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
n_submobs = len(self.all_submobs)
|
||||
alpha = self.rate_func(alpha)
|
||||
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])
|
||||
|
||||
|
||||
class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
|
||||
CONFIG = {
|
||||
"int_func": np.ceil,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
group: Mobject,
|
||||
int_func: Callable[[float], float] = np.ceil,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(group, int_func=int_func, **kwargs)
|
||||
|
||||
def update_submobject_list(self, index):
|
||||
# N = len(self.all_submobs)
|
||||
def update_submobject_list(self, index: int) -> None:
|
||||
index = int(clip(index, 0, len(self.all_submobs) - 1))
|
||||
if index == 0:
|
||||
self.mobject.set_submobjects([])
|
||||
else:
|
||||
self.mobject.set_submobjects([self.all_submobs[index - 1]])
|
||||
|
||||
|
||||
# TODO, this is broken...
|
||||
class AddTextWordByWord(Succession):
|
||||
CONFIG = {
|
||||
# If given a value for run_time, it will
|
||||
# override the time_per_char
|
||||
"run_time": None,
|
||||
"time_per_char": 0.06,
|
||||
}
|
||||
class AddTextWordByWord(ShowIncreasingSubsets):
|
||||
def __init__(
|
||||
self,
|
||||
string_mobject: StringMobject,
|
||||
time_per_word: float = 0.2,
|
||||
run_time: float = -1.0, # If negative, it will be recomputed with time_per_word
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
**kwargs
|
||||
):
|
||||
assert isinstance(string_mobject, StringMobject)
|
||||
grouped_mobject = string_mobject.build_groups()
|
||||
if run_time < 0:
|
||||
run_time = time_per_word * len(grouped_mobject)
|
||||
super().__init__(
|
||||
grouped_mobject,
|
||||
run_time=run_time,
|
||||
rate_func=rate_func,
|
||||
**kwargs
|
||||
)
|
||||
self.string_mobject = string_mobject
|
||||
|
||||
def __init__(self, text_mobject, **kwargs):
|
||||
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)
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
scene.remove(self.mobject)
|
||||
if not self.is_remover():
|
||||
scene.add(self.string_mobject)
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.constants import ORIGIN
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.rate_functions import there_and_back
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
DEFAULT_FADE_LAG_RATIO = 0
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
|
||||
class Fade(Transform):
|
||||
CONFIG = {
|
||||
"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)
|
||||
|
||||
|
||||
class FadeIn(Fade):
|
||||
CONFIG = {
|
||||
"lag_ratio": DEFAULT_FADE_LAG_RATIO,
|
||||
}
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject.copy()
|
||||
|
||||
def create_target(self):
|
||||
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)
|
||||
@@ -39,13 +44,22 @@ class FadeIn(Fade):
|
||||
|
||||
|
||||
class FadeOut(Fade):
|
||||
CONFIG = {
|
||||
"remover": True,
|
||||
# Put it back in original state when done
|
||||
"final_alpha_value": 0,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
shift: Vect3 = ORIGIN,
|
||||
remover: bool = True,
|
||||
final_alpha_value: float = 0.0, # Put it back in original state when done,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
mobject, shift,
|
||||
remover=remover,
|
||||
final_alpha_value=final_alpha_value,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
result = self.mobject.copy()
|
||||
result.set_opacity(0)
|
||||
result.shift(self.shift_vect)
|
||||
@@ -54,7 +68,7 @@ class FadeOut(Fade):
|
||||
|
||||
|
||||
class FadeInFromPoint(FadeIn):
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
def __init__(self, mobject: Mobject, point: Vect3, **kwargs):
|
||||
super().__init__(
|
||||
mobject,
|
||||
shift=mobject.get_center() - point,
|
||||
@@ -64,7 +78,7 @@ class FadeInFromPoint(FadeIn):
|
||||
|
||||
|
||||
class FadeOutToPoint(FadeOut):
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
def __init__(self, mobject: Mobject, point: Vect3, **kwargs):
|
||||
super().__init__(
|
||||
mobject,
|
||||
shift=point - mobject.get_center(),
|
||||
@@ -74,20 +88,22 @@ class FadeOutToPoint(FadeOut):
|
||||
|
||||
|
||||
class FadeTransform(Transform):
|
||||
CONFIG = {
|
||||
"stretch": True,
|
||||
"dim_to_match": 1,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, target_mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
target_mobject: Mobject,
|
||||
stretch: bool = True,
|
||||
dim_to_match: int = 1,
|
||||
**kwargs
|
||||
):
|
||||
self.to_add_on_completion = target_mobject
|
||||
mobject.save_state()
|
||||
super().__init__(
|
||||
Group(mobject, target_mobject.copy()),
|
||||
**kwargs
|
||||
)
|
||||
self.stretch = stretch
|
||||
self.dim_to_match = dim_to_match
|
||||
|
||||
def begin(self):
|
||||
mobject.save_state()
|
||||
super().__init__(mobject.get_group_class()(mobject, target_mobject.copy()), **kwargs)
|
||||
|
||||
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,33 +113,35 @@ 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_uniform(**target.get_uniforms())
|
||||
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()
|
||||
scene.add(self.to_add_on_completion)
|
||||
if not self.remover:
|
||||
scene.add(self.to_add_on_completion)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -132,11 +150,19 @@ class VFadeIn(Animation):
|
||||
"""
|
||||
VFadeIn and VFadeOut only work for VMobjects,
|
||||
"""
|
||||
CONFIG = {
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
def __init__(self, vmobject: VMobject, suspend_mobject_updating: bool = False, **kwargs):
|
||||
super().__init__(
|
||||
vmobject,
|
||||
suspend_mobject_updating=suspend_mobject_updating,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
@@ -146,20 +172,42 @@ class VFadeIn(Animation):
|
||||
|
||||
|
||||
class VFadeOut(VFadeIn):
|
||||
CONFIG = {
|
||||
"remover": True,
|
||||
# Put it back in original state when done
|
||||
"final_alpha_value": 0,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
remover: bool = True,
|
||||
final_alpha_value: float = 0.0,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
vmobject,
|
||||
remover=remover,
|
||||
final_alpha_value=final_alpha_value,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class VFadeInThenOut(VFadeIn):
|
||||
CONFIG = {
|
||||
"rate_func": there_and_back,
|
||||
"remover": True,
|
||||
# Put it back in original state when done
|
||||
"final_alpha_value": 0.5,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
rate_func: Callable[[float], float] = there_and_back,
|
||||
remover: bool = True,
|
||||
final_alpha_value: float = 0.5,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
vmobject,
|
||||
rate_func=rate_func,
|
||||
remover=remover,
|
||||
final_alpha_value=final_alpha_value,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -1,48 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.transform import Transform
|
||||
# from manimlib.utils.paths import counterclockwise_path
|
||||
from manimlib.constants import PI
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.typing import ManimColor
|
||||
|
||||
|
||||
class GrowFromPoint(Transform):
|
||||
CONFIG = {
|
||||
"point_color": None,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
point: np.ndarray,
|
||||
point_color: ManimColor = None,
|
||||
**kwargs
|
||||
):
|
||||
self.point = point
|
||||
self.point_color = point_color
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
return self.mobject
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject.copy()
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
start = super().create_starting_mobject()
|
||||
start.scale(0)
|
||||
start.move_to(self.point)
|
||||
if self.point_color:
|
||||
if self.point_color is not None:
|
||||
start.set_color(self.point_color)
|
||||
return start
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class SpinInFromNothing(GrowFromCenter):
|
||||
CONFIG = {
|
||||
"path_arc": PI,
|
||||
}
|
||||
|
||||
@@ -1,54 +1,67 @@
|
||||
import numpy as np
|
||||
import math
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.movement import Homotopy
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
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.movement import Homotopy
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||
from manimlib.constants import ORIGIN, RIGHT, UP
|
||||
from manimlib.constants import SMALL_BUFF
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.constants import TAU
|
||||
from manimlib.constants import GREY, YELLOW
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.shape_matchers import SurroundingRectangle
|
||||
from manimlib.mobject.shape_matchers import Underline
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
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 manimlib.utils.rate_functions import there_and_back
|
||||
from manimlib.utils.rate_functions import wiggle
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from manimlib.typing import ManimColor
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class FocusOn(Transform):
|
||||
CONFIG = {
|
||||
"opacity": 0.2,
|
||||
"color": GREY,
|
||||
"run_time": 2,
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def __init__(self, focus_point, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
focus_point: np.ndarray | Mobject,
|
||||
opacity: float = 0.2,
|
||||
color: ManimColor = GREY,
|
||||
run_time: float = 2,
|
||||
remover: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
self.focus_point = focus_point
|
||||
self.opacity = opacity
|
||||
self.color = color
|
||||
# Initialize with blank mobject, while create_target
|
||||
# and create_starting_mobject handle the meat
|
||||
super().__init__(VMobject(), **kwargs)
|
||||
super().__init__(VMobject(), run_time=run_time, remover=remover, **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(
|
||||
lambda d: d.move_to(self.focus_point)
|
||||
)
|
||||
little_dot.add_updater(lambda d: d.move_to(self.focus_point))
|
||||
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,
|
||||
@@ -58,13 +71,19 @@ class FocusOn(Transform):
|
||||
|
||||
|
||||
class Indicate(Transform):
|
||||
CONFIG = {
|
||||
"rate_func": there_and_back,
|
||||
"scale_factor": 1.2,
|
||||
"color": YELLOW,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
scale_factor: float = 1.2,
|
||||
color: ManimColor = YELLOW,
|
||||
rate_func: Callable[[float], float] = there_and_back,
|
||||
**kwargs
|
||||
):
|
||||
self.scale_factor = scale_factor
|
||||
self.color = color
|
||||
super().__init__(mobject, rate_func=rate_func, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
target = self.mobject.copy()
|
||||
target.scale(self.scale_factor)
|
||||
target.set_color(self.color)
|
||||
@@ -72,27 +91,34 @@ class Indicate(Transform):
|
||||
|
||||
|
||||
class Flash(AnimationGroup):
|
||||
CONFIG = {
|
||||
"line_length": 0.2,
|
||||
"num_lines": 12,
|
||||
"flash_radius": 0.3,
|
||||
"line_stroke_width": 3,
|
||||
"run_time": 1,
|
||||
}
|
||||
|
||||
def __init__(self, point, color=YELLOW, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
point: np.ndarray | Mobject,
|
||||
color: ManimColor = YELLOW,
|
||||
line_length: float = 0.2,
|
||||
num_lines: int = 12,
|
||||
flash_radius: float = 0.3,
|
||||
line_stroke_width: float = 3.0,
|
||||
run_time: float = 1.0,
|
||||
**kwargs
|
||||
):
|
||||
self.point = point
|
||||
self.color = color
|
||||
digest_config(self, kwargs)
|
||||
self.line_length = line_length
|
||||
self.num_lines = num_lines
|
||||
self.flash_radius = flash_radius
|
||||
self.line_stroke_width = line_stroke_width
|
||||
|
||||
self.lines = self.create_lines()
|
||||
animations = self.create_line_anims()
|
||||
super().__init__(
|
||||
*animations,
|
||||
group=self.lines,
|
||||
run_time=run_time,
|
||||
**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)
|
||||
@@ -106,44 +132,52 @@ 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
|
||||
]
|
||||
|
||||
|
||||
class CircleIndicate(Indicate):
|
||||
CONFIG = {
|
||||
"rate_func": there_and_back,
|
||||
"remover": True,
|
||||
"circle_config": {
|
||||
"color": YELLOW,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
circle = self.get_circle(mobject)
|
||||
super().__init__(circle, **kwargs)
|
||||
|
||||
def get_circle(self, mobject):
|
||||
circle = Circle(**self.circle_config)
|
||||
circle.add_updater(lambda c: c.surround(mobject))
|
||||
return circle
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
super().interpolate_mobject(alpha)
|
||||
self.mobject.set_stroke(opacity=alpha)
|
||||
class CircleIndicate(Transform):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
scale_factor: float = 1.2,
|
||||
rate_func: Callable[[float], float] = there_and_back,
|
||||
stroke_color: ManimColor = YELLOW,
|
||||
stroke_width: float = 3.0,
|
||||
remover: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
circle = Circle(stroke_color=stroke_color, stroke_width=stroke_width)
|
||||
circle.surround(mobject)
|
||||
pre_circle = circle.copy().set_stroke(width=0)
|
||||
pre_circle.scale(1 / scale_factor)
|
||||
super().__init__(
|
||||
pre_circle, circle,
|
||||
rate_func=rate_func,
|
||||
remover=remover,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class ShowPassingFlash(ShowPartial):
|
||||
CONFIG = {
|
||||
"time_width": 0.1,
|
||||
"remover": True,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
time_width: float = 0.1,
|
||||
remover: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
self.time_width = time_width
|
||||
super().__init__(
|
||||
mobject,
|
||||
remover=remover,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
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
|
||||
@@ -151,174 +185,163 @@ 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 __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
time_width: float = 0.3,
|
||||
taper_width: float = 0.05,
|
||||
remover: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
self.time_width = time_width
|
||||
self.taper_width = taper_width
|
||||
super().__init__(vmobject, remover=remover, **kwargs)
|
||||
self.mobject = vmobject
|
||||
|
||||
def begin(self):
|
||||
self.mobject.align_stroke_width_data_to_points()
|
||||
def taper_kernel(self, x):
|
||||
if x < self.taper_width:
|
||||
return x
|
||||
elif x > 1 - self.taper_width:
|
||||
return 1.0 - x
|
||||
return 1.0
|
||||
|
||||
def begin(self) -> None:
|
||||
# Compute an array of stroke widths for each submobject
|
||||
# which tapers out at either end
|
||||
self.submob_to_anchor_widths = dict()
|
||||
self.submob_to_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
|
||||
widths = sm.get_stroke_widths()
|
||||
self.submob_to_widths[hash(sm)] = np.array([
|
||||
width * self.taper_kernel(x)
|
||||
for width, x in zip(widths, np.linspace(0, 1, len(widths)))
|
||||
])
|
||||
super().begin()
|
||||
|
||||
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
|
||||
anchor_widths = self.submob_to_anchor_widths[hash(submobject)]
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submobject: VMobject,
|
||||
starting_sumobject: None,
|
||||
alpha: float
|
||||
) -> None:
|
||||
widths = self.submob_to_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)
|
||||
xs = np.linspace(0, 1, len(widths))
|
||||
zs = (xs - mu) / sigma
|
||||
gaussian = np.exp(-0.5 * zs * zs)
|
||||
gaussian[abs(xs - mu) > 3 * sigma] = 0
|
||||
|
||||
def gauss_kernel(x):
|
||||
if abs(x - mu) > 3 * sigma:
|
||||
return 0
|
||||
z = (x - mu) / sigma
|
||||
return math.exp(-0.5 * z * z)
|
||||
if len(widths * gaussian) !=0:
|
||||
submobject.set_stroke(width=widths * gaussian)
|
||||
|
||||
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):
|
||||
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, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
path = self.get_path(mobject)
|
||||
if mobject.is_fixed_in_frame:
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
time_width: float = 1.0,
|
||||
taper_width: float = 0.0,
|
||||
stroke_width: float = 4.0,
|
||||
color: ManimColor = YELLOW,
|
||||
buff: float = SMALL_BUFF,
|
||||
n_inserted_curves: int = 100,
|
||||
**kwargs
|
||||
):
|
||||
path = self.get_path(mobject, buff)
|
||||
if mobject.is_fixed_in_frame():
|
||||
path.fix_in_frame()
|
||||
path.insert_n_curves(self.n_inserted_curves)
|
||||
path.insert_n_curves(n_inserted_curves)
|
||||
path.set_points(path.get_points_without_null_curves())
|
||||
path.set_stroke(self.color, self.stroke_width)
|
||||
super().__init__(path, **kwargs)
|
||||
path.set_stroke(color, stroke_width)
|
||||
super().__init__(path, time_width=time_width, taper_width=taper_width, **kwargs)
|
||||
|
||||
def get_path(self, mobject):
|
||||
return SurroundingRectangle(mobject, buff=self.buff)
|
||||
def get_path(self, mobject: Mobject, buff: float) -> SurroundingRectangle:
|
||||
return SurroundingRectangle(mobject, buff=buff)
|
||||
|
||||
|
||||
class FlashUnder(FlashAround):
|
||||
def get_path(self, mobject):
|
||||
return Underline(mobject, buff=self.buff)
|
||||
def get_path(self, mobject: Mobject, buff: float) -> Underline:
|
||||
return Underline(mobject, buff=buff, stretch_factor=1.0)
|
||||
|
||||
|
||||
class ShowCreationThenDestruction(ShowPassingFlash):
|
||||
CONFIG = {
|
||||
"time_width": 2.0,
|
||||
"run_time": 1,
|
||||
}
|
||||
def __init__(self, vmobject: VMobject, time_width: float = 2.0, **kwargs):
|
||||
super().__init__(vmobject, time_width=time_width, **kwargs)
|
||||
|
||||
|
||||
class ShowCreationThenFadeOut(Succession):
|
||||
CONFIG = {
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, remover: bool = True, **kwargs):
|
||||
super().__init__(
|
||||
ShowCreation(mobject),
|
||||
FadeOut(mobject),
|
||||
remover=remover,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class AnimationOnSurroundingRectangle(AnimationGroup):
|
||||
CONFIG = {
|
||||
"surrounding_rectangle_config": {},
|
||||
# Function which takes in a rectangle, and spits
|
||||
# out some animation. Could be some animation class,
|
||||
# could be something more
|
||||
"rect_animation": Animation
|
||||
}
|
||||
RectAnimationType: type = Animation
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
if "surrounding_rectangle_config" in kwargs:
|
||||
kwargs.pop("surrounding_rectangle_config")
|
||||
self.mobject_to_surround = mobject
|
||||
|
||||
rect = self.get_rect()
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
stroke_width: float = 2.0,
|
||||
stroke_color: ManimColor = YELLOW,
|
||||
buff: float = SMALL_BUFF,
|
||||
**kwargs
|
||||
):
|
||||
rect = SurroundingRectangle(
|
||||
mobject,
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
buff=buff,
|
||||
)
|
||||
rect.add_updater(lambda r: r.move_to(mobject))
|
||||
|
||||
super().__init__(
|
||||
self.rect_animation(rect, **kwargs),
|
||||
)
|
||||
|
||||
def get_rect(self):
|
||||
return SurroundingRectangle(
|
||||
self.mobject_to_surround,
|
||||
**self.surrounding_rectangle_config
|
||||
)
|
||||
super().__init__(self.RectAnimationType(rect, **kwargs))
|
||||
|
||||
|
||||
class ShowPassingFlashAround(AnimationOnSurroundingRectangle):
|
||||
CONFIG = {
|
||||
"rect_animation": ShowPassingFlash
|
||||
}
|
||||
RectAnimationType = ShowPassingFlash
|
||||
|
||||
|
||||
class ShowCreationThenDestructionAround(AnimationOnSurroundingRectangle):
|
||||
CONFIG = {
|
||||
"rect_animation": ShowCreationThenDestruction
|
||||
}
|
||||
RectAnimationType = ShowCreationThenDestruction
|
||||
|
||||
|
||||
class ShowCreationThenFadeAround(AnimationOnSurroundingRectangle):
|
||||
CONFIG = {
|
||||
"rect_animation": ShowCreationThenFadeOut
|
||||
}
|
||||
RectAnimationType = ShowCreationThenFadeOut
|
||||
|
||||
|
||||
class ApplyWave(Homotopy):
|
||||
CONFIG = {
|
||||
"direction": UP,
|
||||
"amplitude": 0.2,
|
||||
"run_time": 1,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
direction: np.ndarray = UP,
|
||||
amplitude: float = 0.2,
|
||||
run_time: float = 1.0,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
left_x = mobject.get_left()[0]
|
||||
right_x = mobject.get_right()[0]
|
||||
vect = self.amplitude * self.direction
|
||||
vect = amplitude * direction
|
||||
|
||||
def homotopy(x, y, z, t):
|
||||
alpha = (x - left_x) / (right_x - left_x)
|
||||
@@ -330,24 +353,36 @@ class ApplyWave(Homotopy):
|
||||
|
||||
|
||||
class WiggleOutThenIn(Animation):
|
||||
CONFIG = {
|
||||
"scale_value": 1.1,
|
||||
"rotation_angle": 0.01 * TAU,
|
||||
"n_wiggles": 6,
|
||||
"run_time": 2,
|
||||
"scale_about_point": None,
|
||||
"rotate_about_point": None,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
scale_value: float = 1.1,
|
||||
rotation_angle: float = 0.01 * TAU,
|
||||
n_wiggles: int = 6,
|
||||
scale_about_point: np.ndarray | None = None,
|
||||
rotate_about_point: np.ndarray | None = None,
|
||||
run_time: float = 2,
|
||||
**kwargs
|
||||
):
|
||||
self.scale_value = scale_value
|
||||
self.rotation_angle = rotation_angle
|
||||
self.n_wiggles = n_wiggles
|
||||
self.scale_about_point = scale_about_point
|
||||
self.rotate_about_point = rotate_about_point
|
||||
super().__init__(mobject, run_time=run_time, **kwargs)
|
||||
|
||||
def get_scale_about_point(self):
|
||||
if self.scale_about_point is None:
|
||||
return self.mobject.get_center()
|
||||
def get_scale_about_point(self) -> np.ndarray:
|
||||
return self.scale_about_point or self.mobject.get_center()
|
||||
|
||||
def get_rotate_about_point(self):
|
||||
if self.rotate_about_point is None:
|
||||
return self.mobject.get_center()
|
||||
def get_rotate_about_point(self) -> np.ndarray:
|
||||
return self.rotate_about_point or 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)),
|
||||
@@ -360,28 +395,31 @@ class WiggleOutThenIn(Animation):
|
||||
|
||||
|
||||
class TurnInsideOut(Transform):
|
||||
CONFIG = {
|
||||
"path_arc": TAU / 4,
|
||||
}
|
||||
def __init__(self, mobject: Mobject, path_arc: float = 90 * DEG, **kwargs):
|
||||
super().__init__(mobject, path_arc=path_arc, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
return self.mobject.copy().reverse_points()
|
||||
def create_target(self) -> Mobject:
|
||||
result = self.mobject.copy().reverse_points()
|
||||
if isinstance(result, VMobject):
|
||||
result.refresh_triangulation()
|
||||
return result
|
||||
|
||||
|
||||
class FlashyFadeIn(AnimationGroup):
|
||||
CONFIG = {
|
||||
"fade_lag": 0,
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, stroke_width=2, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
def __init__(self,
|
||||
vmobject: VMobject,
|
||||
stroke_width: float = 2.0,
|
||||
fade_lag: float = 0.0,
|
||||
time_width: float = 1.0,
|
||||
**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),
|
||||
FadeIn(vmobject, rate_func=squish_rate_func(rate_func, fade_lag, 1)),
|
||||
VShowPassingFlash(outline, time_width=time_width),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -1,40 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.utils.rate_functions import linear
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
class Homotopy(Animation):
|
||||
CONFIG = {
|
||||
"run_time": 3,
|
||||
"apply_function_kwargs": {},
|
||||
}
|
||||
apply_function_config: dict = dict()
|
||||
|
||||
def __init__(self, homotopy, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
homotopy: Callable[[float, float, float, float], Sequence[float]],
|
||||
mobject: Mobject,
|
||||
run_time: float = 3.0,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Homotopy is a function from
|
||||
(x, y, z, t) to (x', y', z')
|
||||
"""
|
||||
self.homotopy = homotopy
|
||||
super().__init__(mobject, **kwargs)
|
||||
super().__init__(mobject, run_time=run_time, **kwargs)
|
||||
|
||||
def function_at_time_t(self, t):
|
||||
return lambda p: self.homotopy(*p, t)
|
||||
def function_at_time_t(self, t: float) -> Callable[[np.ndarray], Sequence[float]]:
|
||||
def result(p):
|
||||
return self.homotopy(*p, t)
|
||||
return result
|
||||
|
||||
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),
|
||||
**self.apply_function_kwargs
|
||||
**self.apply_function_config
|
||||
)
|
||||
|
||||
|
||||
class SmoothedVectorizedHomotopy(Homotopy):
|
||||
CONFIG = {
|
||||
"apply_function_kwargs": {"make_smooth": True},
|
||||
}
|
||||
apply_function_config: dict = dict(make_smooth=True)
|
||||
|
||||
|
||||
class ComplexHomotopy(Homotopy):
|
||||
def __init__(self, complex_homotopy, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
complex_homotopy: Callable[[complex, float], complex],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Given a function form (z, t) -> w, where z and w
|
||||
are complex numbers and t is time, this animates
|
||||
@@ -43,21 +68,32 @@ class ComplexHomotopy(Homotopy):
|
||||
def homotopy(x, y, z, t):
|
||||
c = complex_homotopy(complex(x, y), t)
|
||||
return (c.real, c.imag, z)
|
||||
|
||||
super().__init__(homotopy, mobject, **kwargs)
|
||||
|
||||
|
||||
class PhaseFlow(Animation):
|
||||
CONFIG = {
|
||||
"virtual_time": 1,
|
||||
"rate_func": linear,
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
mobject: Mobject,
|
||||
virtual_time: float | None = None,
|
||||
suspend_mobject_updating: bool = False,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
run_time: float =3.0,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
super().__init__(mobject, **kwargs)
|
||||
self.virtual_time = virtual_time or run_time
|
||||
super().__init__(
|
||||
mobject,
|
||||
rate_func=rate_func,
|
||||
run_time=run_time,
|
||||
suspend_mobject_updating=suspend_mobject_updating,
|
||||
**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(
|
||||
@@ -67,14 +103,16 @@ class PhaseFlow(Animation):
|
||||
|
||||
|
||||
class MoveAlongPath(Animation):
|
||||
CONFIG = {
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, path, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
path: VMobject,
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
self.path = path
|
||||
super().__init__(mobject, **kwargs)
|
||||
super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
point = self.path.point_from_proportion(alpha)
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
point = self.path.quick_point_from_proportion(self.rate_func(alpha))
|
||||
self.mobject.move_to(point)
|
||||
|
||||
@@ -1,26 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class ChangingDecimal(Animation):
|
||||
CONFIG = {
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, decimal_mob, number_update_func, **kwargs):
|
||||
assert(isinstance(decimal_mob, DecimalNumber))
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
number_update_func: Callable[[float], float],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
assert isinstance(decimal_mob, DecimalNumber)
|
||||
self.number_update_func = number_update_func
|
||||
super().__init__(decimal_mob, **kwargs)
|
||||
super().__init__(
|
||||
decimal_mob,
|
||||
suspend_mobject_updating=suspend_mobject_updating,
|
||||
**kwargs
|
||||
)
|
||||
self.mobject = decimal_mob
|
||||
|
||||
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,10 +50,15 @@ class ChangeDecimalToValue(ChangingDecimal):
|
||||
|
||||
|
||||
class CountInFrom(ChangingDecimal):
|
||||
def __init__(self, decimal_mob, source_number=0, **kwargs):
|
||||
start_number = decimal_mob.number
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
source_number: float | complex = 0,
|
||||
**kwargs
|
||||
):
|
||||
start_number = decimal_mob.get_value()
|
||||
super().__init__(
|
||||
decimal_mob,
|
||||
lambda a: interpolate(source_number, start_number, a),
|
||||
lambda a: interpolate(source_number, start_number, clip(a, 0, 1)),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -1,33 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.constants import OUT
|
||||
from manimlib.constants import PI
|
||||
from manimlib.constants import TAU
|
||||
from manimlib.constants import ORIGIN
|
||||
from manimlib.constants import ORIGIN, OUT
|
||||
from manimlib.constants import PI, TAU
|
||||
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 typing import Callable
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class Rotating(Animation):
|
||||
CONFIG = {
|
||||
# "axis": OUT,
|
||||
# "radians": TAU,
|
||||
"run_time": 5,
|
||||
"rate_func": linear,
|
||||
"about_point": None,
|
||||
"about_edge": None,
|
||||
"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,
|
||||
about_point: np.ndarray | None = None,
|
||||
about_edge: np.ndarray | None = None,
|
||||
run_time: float = 5.0,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
super().__init__(mobject, **kwargs)
|
||||
self.about_point = about_point
|
||||
self.about_edge = about_edge
|
||||
super().__init__(
|
||||
mobject,
|
||||
run_time=run_time,
|
||||
rate_func=rate_func,
|
||||
suspend_mobject_updating=suspend_mobject_updating,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
for sm1, sm2 in self.get_all_families_zipped():
|
||||
sm1.set_points(sm2.get_points())
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
pairs = zip(
|
||||
self.mobject.family_members_with_points(),
|
||||
self.starting_mobject.family_members_with_points(),
|
||||
)
|
||||
for sm1, sm2 in pairs:
|
||||
for key in sm1.pointlike_data_keys:
|
||||
sm1.data[key][:] = sm2.data[key]
|
||||
self.mobject.rotate(
|
||||
alpha * self.angle,
|
||||
self.rate_func(self.time_spanned_alpha(alpha)) * self.angle,
|
||||
axis=self.axis,
|
||||
about_point=self.about_point,
|
||||
about_edge=self.about_edge,
|
||||
@@ -35,11 +56,20 @@ class Rotating(Animation):
|
||||
|
||||
|
||||
class Rotate(Rotating):
|
||||
CONFIG = {
|
||||
"run_time": 1,
|
||||
"rate_func": smooth,
|
||||
"about_edge": ORIGIN,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, angle=PI, axis=OUT, **kwargs):
|
||||
super().__init__(mobject, angle, axis, **kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
angle: float = PI,
|
||||
axis: np.ndarray = OUT,
|
||||
run_time: float = 1,
|
||||
rate_func: Callable[[float], float] = smooth,
|
||||
about_edge: np.ndarray = ORIGIN,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
mobject, angle, axis,
|
||||
run_time=run_time,
|
||||
rate_func=rate_func,
|
||||
about_edge=about_edge,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -1,43 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.composition import LaggedStart
|
||||
from manimlib.animation.transform import Restore
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.constants import BLACK, WHITE
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from manimlib.typing import ManimColor
|
||||
|
||||
|
||||
class Broadcast(LaggedStart):
|
||||
CONFIG = {
|
||||
"small_radius": 0.0,
|
||||
"big_radius": 5,
|
||||
"n_circles": 5,
|
||||
"start_stroke_width": 8,
|
||||
"color": WHITE,
|
||||
"remover": True,
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": 3,
|
||||
"remover": True,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
focal_point: np.ndarray,
|
||||
small_radius: float = 0.0,
|
||||
big_radius: float = 5.0,
|
||||
n_circles: int = 5,
|
||||
start_stroke_width: float = 8.0,
|
||||
color: ManimColor = WHITE,
|
||||
run_time: float = 3.0,
|
||||
lag_ratio: float = 0.2,
|
||||
remover: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
self.focal_point = focal_point
|
||||
self.small_radius = small_radius
|
||||
self.big_radius = big_radius
|
||||
self.n_circles = n_circles
|
||||
self.start_stroke_width = start_stroke_width
|
||||
self.color = color
|
||||
|
||||
def __init__(self, focal_point, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
circles = VGroup()
|
||||
for x in range(self.n_circles):
|
||||
for x in range(n_circles):
|
||||
circle = Circle(
|
||||
radius=self.big_radius,
|
||||
radius=big_radius,
|
||||
stroke_color=BLACK,
|
||||
stroke_width=0,
|
||||
)
|
||||
circle.add_updater(
|
||||
lambda c: c.move_to(focal_point)
|
||||
)
|
||||
circle.add_updater(lambda c: c.move_to(focal_point))
|
||||
circle.save_state()
|
||||
circle.set_width(self.small_radius * 2)
|
||||
circle.set_stroke(self.color, self.start_stroke_width)
|
||||
circle.set_width(small_radius * 2)
|
||||
circle.set_stroke(color, start_stroke_width)
|
||||
circles.add(circle)
|
||||
animations = [
|
||||
Restore(circle)
|
||||
for circle in circles
|
||||
]
|
||||
super().__init__(*animations, **kwargs)
|
||||
super().__init__(
|
||||
*map(Restore, circles),
|
||||
run_time=run_time,
|
||||
lag_ratio=lag_ratio,
|
||||
remover=remover,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -1,34 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.constants import DEFAULT_POINTWISE_FUNCTION_RUN_TIME
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.constants import OUT
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.paths import path_along_arc
|
||||
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:
|
||||
from typing import Callable
|
||||
import numpy.typing as npt
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.typing import ManimColor
|
||||
|
||||
|
||||
class Transform(Animation):
|
||||
CONFIG = {
|
||||
"path_arc": 0,
|
||||
"path_arc_axis": OUT,
|
||||
"path_func": None,
|
||||
"replace_mobject_with_target_in_scene": False,
|
||||
}
|
||||
replace_mobject_with_target_in_scene: bool = False
|
||||
|
||||
def __init__(self, mobject, target_mobject=None, **kwargs):
|
||||
super().__init__(mobject, **kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
target_mobject: Mobject | None = None,
|
||||
path_arc: float = 0.0,
|
||||
path_arc_axis: np.ndarray = OUT,
|
||||
path_func: Callable | None = None,
|
||||
**kwargs
|
||||
):
|
||||
self.target_mobject = target_mobject
|
||||
self.path_arc = path_arc
|
||||
self.path_arc_axis = path_arc_axis
|
||||
self.path_func = path_func
|
||||
super().__init__(mobject, **kwargs)
|
||||
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,43 +51,48 @@ 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 potentially
|
||||
# change the structure of both arguments
|
||||
self.target_copy = self.target_mobject.copy()
|
||||
|
||||
if self.mobject.is_aligned_with(self.target_mobject):
|
||||
self.target_copy = self.target_mobject
|
||||
else:
|
||||
# Use a copy of target_mobject for the align_data_and_family
|
||||
# call so that the actual target_mobject stays
|
||||
# preserved, since calling align_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)
|
||||
super().begin()
|
||||
self.mobject.lock_matching_data(
|
||||
self.starting_mobject,
|
||||
self.target_copy,
|
||||
)
|
||||
if not self.mobject.has_updaters():
|
||||
self.mobject.lock_matching_data(
|
||||
self.starting_mobject,
|
||||
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 +100,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 +108,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,62 +118,48 @@ 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
|
||||
|
||||
|
||||
class ReplacementTransform(Transform):
|
||||
CONFIG = {
|
||||
"replace_mobject_with_target_in_scene": True,
|
||||
}
|
||||
replace_mobject_with_target_in_scene: bool = True
|
||||
|
||||
|
||||
class TransformFromCopy(Transform):
|
||||
"""
|
||||
Performs a reversed Transform
|
||||
"""
|
||||
replace_mobject_with_target_in_scene: bool = True
|
||||
|
||||
def __init__(self, mobject, target_mobject, **kwargs):
|
||||
super().__init__(target_mobject, mobject, **kwargs)
|
||||
|
||||
def interpolate(self, alpha):
|
||||
super().interpolate(1 - alpha)
|
||||
|
||||
|
||||
class ClockwiseTransform(Transform):
|
||||
CONFIG = {
|
||||
"path_arc": -np.pi
|
||||
}
|
||||
|
||||
|
||||
class CounterclockwiseTransform(Transform):
|
||||
CONFIG = {
|
||||
"path_arc": np.pi
|
||||
}
|
||||
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs):
|
||||
super().__init__(mobject.copy(), target_mobject, **kwargs)
|
||||
|
||||
|
||||
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"
|
||||
"without attribute 'target'"
|
||||
"MoveToTarget called on mobject without attribute 'target'"
|
||||
)
|
||||
|
||||
|
||||
class _MethodAnimation(MoveToTarget):
|
||||
def __init__(self, mobject, methods):
|
||||
def __init__(self, mobject: Mobject, methods: list[Callable], **kwargs):
|
||||
self.methods = methods
|
||||
super().__init__(mobject)
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
|
||||
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
|
||||
@@ -170,15 +173,15 @@ 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 "
|
||||
"the method you want to animate"
|
||||
)
|
||||
assert(isinstance(method.__self__, Mobject))
|
||||
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)
|
||||
@@ -193,52 +196,73 @@ class ApplyMethod(Transform):
|
||||
|
||||
|
||||
class ApplyPointwiseFunction(ApplyMethod):
|
||||
CONFIG = {
|
||||
"run_time": DEFAULT_POINTWISE_FUNCTION_RUN_TIME
|
||||
}
|
||||
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
super().__init__(mobject.apply_function, function, **kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
mobject: Mobject,
|
||||
run_time: float = 3.0,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject.apply_function, function, run_time=run_time, **kwargs)
|
||||
|
||||
|
||||
class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction):
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
class ApplyPointwiseFunctionToCenter(Transform):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
super().__init__(mobject.move_to, **kwargs)
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
self.method_args = [
|
||||
self.function(self.mobject.get_center())
|
||||
]
|
||||
super().begin()
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject.copy().move_to(self.function(self.mobject.get_center()))
|
||||
|
||||
|
||||
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):
|
||||
super().__init__(mobject.restore, **kwargs)
|
||||
class Restore(Transform):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
if not hasattr(mobject, "saved_state") or mobject.saved_state is None:
|
||||
raise Exception("Trying to restore without having saved")
|
||||
super().__init__(mobject, mobject.saved_state, **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 +270,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 +283,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 +295,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()
|
||||
@@ -280,54 +314,18 @@ class ApplyComplexFunction(ApplyMethod):
|
||||
|
||||
|
||||
class CyclicReplace(Transform):
|
||||
CONFIG = {
|
||||
"path_arc": 90 * DEGREES,
|
||||
}
|
||||
def __init__(self, *mobjects: Mobject, path_arc=90 * DEG, **kwargs):
|
||||
super().__init__(Group(*mobjects), path_arc=path_arc, **kwargs)
|
||||
|
||||
def __init__(self, *mobjects, **kwargs):
|
||||
self.group = Group(*mobjects)
|
||||
super().__init__(self.group, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
target = self.group.copy()
|
||||
def create_target(self) -> Mobject:
|
||||
group = self.mobject
|
||||
target = group.copy()
|
||||
cycled_targets = [target[-1], *target[:-1]]
|
||||
for m1, m2 in zip(cycled_targets, self.group):
|
||||
for m1, m2 in zip(cycled_targets, group):
|
||||
m1.move_to(m2)
|
||||
return target
|
||||
|
||||
|
||||
class Swap(CyclicReplace):
|
||||
pass # Renaming, more understandable for two entries
|
||||
|
||||
|
||||
# TODO, this may be deprecated...worth reimplementing?
|
||||
class TransformAnimations(Transform):
|
||||
CONFIG = {
|
||||
"rate_func": squish_rate_func(smooth)
|
||||
}
|
||||
|
||||
def __init__(self, start_anim, end_anim, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
if "run_time" in kwargs:
|
||||
self.run_time = kwargs.pop("run_time")
|
||||
else:
|
||||
self.run_time = max(start_anim.run_time, end_anim.run_time)
|
||||
for anim in start_anim, end_anim:
|
||||
anim.set_run_time(self.run_time)
|
||||
|
||||
if start_anim.starting_mobject.get_num_points() != end_anim.starting_mobject.get_num_points():
|
||||
start_anim.starting_mobject.align_data_and_family(end_anim.starting_mobject)
|
||||
for anim in start_anim, end_anim:
|
||||
if hasattr(anim, "target_mobject"):
|
||||
anim.starting_mobject.align_data_and_family(anim.target_mobject)
|
||||
|
||||
Transform.__init__(self, start_anim.mobject,
|
||||
end_anim.mobject, **kwargs)
|
||||
# Rewire starting and ending mobjects
|
||||
start_anim.mobject = self.starting_mobject
|
||||
end_anim.mobject = self.target_mobject
|
||||
|
||||
def interpolate(self, alpha):
|
||||
self.start_anim.interpolate(alpha)
|
||||
self.end_anim.interpolate(alpha)
|
||||
Transform.interpolate(self, alpha)
|
||||
"""Alternate name for CyclicReplace"""
|
||||
pass
|
||||
|
||||
@@ -1,141 +1,191 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
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 Transform
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class TransformMatchingParts(AnimationGroup):
|
||||
CONFIG = {
|
||||
"mobject_type": Mobject,
|
||||
"group_type": Group,
|
||||
"transform_mismatches": False,
|
||||
"fade_transform_mismatches": False,
|
||||
"key_map": dict(),
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
source: Mobject,
|
||||
target: Mobject,
|
||||
matched_pairs: Iterable[tuple[Mobject, Mobject]] = [],
|
||||
match_animation: type = Transform,
|
||||
mismatch_animation: type = Transform,
|
||||
run_time: float = 2,
|
||||
lag_ratio: float = 0,
|
||||
**kwargs,
|
||||
):
|
||||
self.source = source
|
||||
self.target = target
|
||||
self.match_animation = match_animation
|
||||
self.mismatch_animation = mismatch_animation
|
||||
self.anim_config = dict(**kwargs)
|
||||
|
||||
def __init__(self, mobject, target_mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
assert(isinstance(mobject, self.mobject_type))
|
||||
assert(isinstance(target_mobject, self.mobject_type))
|
||||
source_map = self.get_shape_map(mobject)
|
||||
target_map = self.get_shape_map(target_mobject)
|
||||
# We will progressively build up a list of transforms
|
||||
# from pieces in source to those in target. These
|
||||
# two lists keep track of which pieces are accounted
|
||||
# for so far
|
||||
self.source_pieces = source.family_members_with_points()
|
||||
self.target_pieces = target.family_members_with_points()
|
||||
self.anims = []
|
||||
|
||||
# Create two mobjects whose submobjects all match each other
|
||||
# according to whatever keys are used for source_map and
|
||||
# target_map
|
||||
transform_source = self.group_type()
|
||||
transform_target = self.group_type()
|
||||
kwargs["final_alpha_value"] = 0
|
||||
for key in set(source_map).intersection(target_map):
|
||||
transform_source.add(source_map[key])
|
||||
transform_target.add(target_map[key])
|
||||
anims = [Transform(transform_source, transform_target, **kwargs)]
|
||||
# User can manually specify when one part should transform
|
||||
# into another despite not matching by using key_map
|
||||
key_mapped_source = self.group_type()
|
||||
key_mapped_target = self.group_type()
|
||||
for key1, key2 in self.key_map.items():
|
||||
if key1 in source_map and key2 in target_map:
|
||||
key_mapped_source.add(source_map[key1])
|
||||
key_mapped_target.add(target_map[key2])
|
||||
source_map.pop(key1, None)
|
||||
target_map.pop(key2, None)
|
||||
if len(key_mapped_source) > 0:
|
||||
anims.append(FadeTransformPieces(
|
||||
key_mapped_source,
|
||||
key_mapped_target,
|
||||
for pair in matched_pairs:
|
||||
self.add_transform(*pair)
|
||||
|
||||
# Match any pairs with the same shape
|
||||
for pair in self.find_pairs_with_matching_shapes(self.source_pieces, self.target_pieces):
|
||||
self.add_transform(*pair)
|
||||
|
||||
# Finally, account for mismatches
|
||||
for source_piece in self.source_pieces:
|
||||
if any([source_piece in anim.mobject.get_family() for anim in self.anims]):
|
||||
continue
|
||||
self.anims.append(FadeOutToPoint(
|
||||
source_piece, target.get_center(),
|
||||
**self.anim_config
|
||||
))
|
||||
for target_piece in self.target_pieces:
|
||||
if any([target_piece in anim.mobject.get_family() for anim in self.anims]):
|
||||
continue
|
||||
self.anims.append(FadeInFromPoint(
|
||||
target_piece, source.get_center(),
|
||||
**self.anim_config
|
||||
))
|
||||
|
||||
fade_source = self.group_type()
|
||||
fade_target = self.group_type()
|
||||
for key in set(source_map).difference(target_map):
|
||||
fade_source.add(source_map[key])
|
||||
for key in set(target_map).difference(source_map):
|
||||
fade_target.add(target_map[key])
|
||||
super().__init__(
|
||||
*self.anims,
|
||||
run_time=run_time,
|
||||
lag_ratio=lag_ratio,
|
||||
)
|
||||
|
||||
if self.transform_mismatches:
|
||||
anims.append(Transform(fade_source.copy(), fade_target, **kwargs))
|
||||
if self.fade_transform_mismatches:
|
||||
anims.append(FadeTransformPieces(fade_source, fade_target, **kwargs))
|
||||
else:
|
||||
anims.append(FadeOutToPoint(
|
||||
fade_source, target_mobject.get_center(), **kwargs
|
||||
))
|
||||
anims.append(FadeInFromPoint(
|
||||
fade_target.copy(), mobject.get_center(), **kwargs
|
||||
))
|
||||
def add_transform(
|
||||
self,
|
||||
source: Mobject,
|
||||
target: Mobject,
|
||||
):
|
||||
new_source_pieces = source.family_members_with_points()
|
||||
new_target_pieces = target.family_members_with_points()
|
||||
if len(new_source_pieces) == 0 or len(new_target_pieces) == 0:
|
||||
# Don't animate null sorces or null targets
|
||||
return
|
||||
source_is_new = all(char in self.source_pieces for char in new_source_pieces)
|
||||
target_is_new = all(char in self.target_pieces for char in new_target_pieces)
|
||||
if not source_is_new or not target_is_new:
|
||||
return
|
||||
|
||||
super().__init__(*anims)
|
||||
transform_type = self.mismatch_animation
|
||||
if source.has_same_shape_as(target):
|
||||
transform_type = self.match_animation
|
||||
|
||||
self.to_remove = mobject
|
||||
self.to_add = target_mobject
|
||||
self.anims.append(transform_type(source, target, **self.anim_config))
|
||||
for char in new_source_pieces:
|
||||
self.source_pieces.remove(char)
|
||||
for char in new_target_pieces:
|
||||
self.target_pieces.remove(char)
|
||||
|
||||
def get_shape_map(self, mobject):
|
||||
shape_map = {}
|
||||
for sm in self.get_mobject_parts(mobject):
|
||||
key = self.get_mobject_key(sm)
|
||||
if key not in shape_map:
|
||||
shape_map[key] = VGroup()
|
||||
shape_map[key].add(sm)
|
||||
return shape_map
|
||||
def find_pairs_with_matching_shapes(
|
||||
self,
|
||||
chars1: list[Mobject],
|
||||
chars2: list[Mobject]
|
||||
) -> list[tuple[Mobject, Mobject]]:
|
||||
result = []
|
||||
for char1, char2 in it.product(chars1, chars2):
|
||||
if char1.has_same_shape_as(char2):
|
||||
result.append((char1, char2))
|
||||
return result
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
for anim in self.animations:
|
||||
anim.update(0)
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
scene.remove(self.mobject)
|
||||
scene.remove(self.to_remove)
|
||||
scene.add(self.to_add)
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject):
|
||||
# To be implemented in subclass
|
||||
return mobject
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
# To be implemented in subclass
|
||||
return hash(mobject)
|
||||
scene.add(self.target)
|
||||
|
||||
|
||||
class TransformMatchingShapes(TransformMatchingParts):
|
||||
CONFIG = {
|
||||
"mobject_type": VMobject,
|
||||
"group_type": VGroup,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject):
|
||||
return mobject.family_members_with_points()
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
mobject.save_state()
|
||||
mobject.center()
|
||||
mobject.set_height(1)
|
||||
result = hash(np.round(mobject.get_points(), 3).tobytes())
|
||||
mobject.restore()
|
||||
return result
|
||||
"""Alias for TransformMatchingParts"""
|
||||
pass
|
||||
|
||||
|
||||
class TransformMatchingTex(TransformMatchingParts):
|
||||
CONFIG = {
|
||||
"mobject_type": VMobject,
|
||||
"group_type": VGroup,
|
||||
}
|
||||
class TransformMatchingStrings(TransformMatchingParts):
|
||||
def __init__(
|
||||
self,
|
||||
source: StringMobject,
|
||||
target: StringMobject,
|
||||
matched_keys: Iterable[str] = [],
|
||||
key_map: dict[str, str] = dict(),
|
||||
matched_pairs: Iterable[tuple[VMobject, VMobject]] = [],
|
||||
**kwargs,
|
||||
):
|
||||
matched_pairs = [
|
||||
*matched_pairs,
|
||||
*self.matching_blocks(source, target, matched_keys, key_map),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject):
|
||||
return mobject.submobjects
|
||||
super().__init__(
|
||||
source, target,
|
||||
matched_pairs=matched_pairs,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
return mobject.get_tex()
|
||||
def matching_blocks(
|
||||
self,
|
||||
source: StringMobject,
|
||||
target: StringMobject,
|
||||
matched_keys: Iterable[str],
|
||||
key_map: dict[str, str]
|
||||
) -> list[tuple[VMobject, VMobject]]:
|
||||
syms1 = source.get_symbol_substrings()
|
||||
syms2 = target.get_symbol_substrings()
|
||||
counts1 = list(map(source.substr_to_path_count, syms1))
|
||||
counts2 = list(map(target.substr_to_path_count, syms2))
|
||||
|
||||
# Start with user specified matches
|
||||
blocks = [(source[key], target[key]) for key in matched_keys]
|
||||
blocks += [(source[key1], target[key2]) for key1, key2 in key_map.items()]
|
||||
|
||||
# Nullify any intersections with those matches in the two symbol lists
|
||||
for sub_source, sub_target in blocks:
|
||||
for i in range(len(syms1)):
|
||||
if source[i] in sub_source.family_members_with_points():
|
||||
syms1[i] = "Null1"
|
||||
for j in range(len(syms2)):
|
||||
if target[j] in sub_target.family_members_with_points():
|
||||
syms2[j] = "Null2"
|
||||
|
||||
# Group together longest matching substrings
|
||||
while True:
|
||||
matcher = SequenceMatcher(None, syms1, syms2)
|
||||
match = matcher.find_longest_match(0, len(syms1), 0, len(syms2))
|
||||
if match.size == 0:
|
||||
break
|
||||
|
||||
i1 = sum(counts1[:match.a])
|
||||
i2 = sum(counts2[:match.b])
|
||||
size = sum(counts1[match.a:match.a + match.size])
|
||||
|
||||
blocks.append((source[i1:i1 + size], target[i2:i2 + size]))
|
||||
|
||||
for i in range(match.size):
|
||||
syms1[match.a + i] = "Null1"
|
||||
syms2[match.b + i] = "Null2"
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
class TransformMatchingTex(TransformMatchingStrings):
|
||||
"""Alias for TransformMatchingStrings"""
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import operator as op
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class UpdateFromFunc(Animation):
|
||||
"""
|
||||
@@ -9,33 +16,51 @@ class UpdateFromFunc(Animation):
|
||||
to be used when the state of one mobject is dependent
|
||||
on another simultaneously animated mobject
|
||||
"""
|
||||
CONFIG = {
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, update_function, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
update_function: Callable[[Mobject], Mobject | None],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
self.update_function = update_function
|
||||
super().__init__(mobject, **kwargs)
|
||||
super().__init__(
|
||||
mobject,
|
||||
suspend_mobject_updating=suspend_mobject_updating,
|
||||
**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):
|
||||
class UpdateFromAlphaFunc(Animation):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
update_function: Callable[[Mobject, float], Mobject | None],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
self.update_function = update_function
|
||||
super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs)
|
||||
|
||||
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(),
|
||||
tracked_mobject.get_center(),
|
||||
)
|
||||
self.diff = mobject.get_center() - tracked_mobject.get_center()
|
||||
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,285 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import moderngl
|
||||
import math
|
||||
from colour import Color
|
||||
import OpenGL.GL as gl
|
||||
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
import OpenGL.GL as gl
|
||||
from PIL import Image
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.camera.camera_frame import CameraFrame
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.constants import DEFAULT_RESOLUTION
|
||||
from manimlib.constants import FRAME_HEIGHT
|
||||
from manimlib.constants import FRAME_WIDTH
|
||||
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.color import color_to_rgba
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
class CameraFrame(Mobject):
|
||||
CONFIG = {
|
||||
"frame_shape": (FRAME_WIDTH, FRAME_HEIGHT),
|
||||
"center_point": ORIGIN,
|
||||
# Theta, phi, gamma
|
||||
"euler_angles": [0, 0, 0],
|
||||
"focal_distance": 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_points(self):
|
||||
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 to_default_state(self):
|
||||
self.center()
|
||||
self.set_height(FRAME_HEIGHT)
|
||||
self.set_width(FRAME_WIDTH)
|
||||
self.set_euler_angles(0, 0, 0)
|
||||
return self
|
||||
|
||||
def get_euler_angles(self):
|
||||
return self.data["euler_angles"]
|
||||
|
||||
def get_inverse_camera_rotation_matrix(self):
|
||||
return self.inverse_camera_rotation_matrix
|
||||
|
||||
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(clip(Fz[2], -1, 1))
|
||||
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)
|
||||
return self
|
||||
|
||||
def set_euler_angles(self, theta=None, phi=None, gamma=None, units=RADIANS):
|
||||
if theta is not None:
|
||||
self.data["euler_angles"][0] = theta * units
|
||||
if phi is not None:
|
||||
self.data["euler_angles"][1] = phi * units
|
||||
if gamma is not None:
|
||||
self.data["euler_angles"][2] = gamma * units
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def reorient(self, theta_degrees=None, phi_degrees=None, gamma_degrees=None):
|
||||
"""
|
||||
Shortcut for set_euler_angles, defaulting to taking
|
||||
in angles in degrees
|
||||
"""
|
||||
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES)
|
||||
return self
|
||||
|
||||
def set_theta(self, theta):
|
||||
return self.set_euler_angles(theta=theta)
|
||||
|
||||
def set_phi(self, phi):
|
||||
return self.set_euler_angles(phi=phi)
|
||||
|
||||
def set_gamma(self, gamma):
|
||||
return self.set_euler_angles(gamma=gamma)
|
||||
|
||||
def increment_theta(self, dtheta):
|
||||
self.data["euler_angles"][0] += dtheta
|
||||
self.refresh_rotation_matrix()
|
||||
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()
|
||||
return self
|
||||
|
||||
def increment_gamma(self, dgamma):
|
||||
self.data["euler_angles"][2] += dgamma
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def get_theta(self):
|
||||
return self.data["euler_angles"][0]
|
||||
|
||||
def get_phi(self):
|
||||
return self.data["euler_angles"][1]
|
||||
|
||||
def get_gamma(self):
|
||||
return self.data["euler_angles"][2]
|
||||
|
||||
def get_shape(self):
|
||||
return (self.get_width(), self.get_height())
|
||||
|
||||
def get_center(self):
|
||||
# Assumes first point is at the center
|
||||
return self.get_points()[0]
|
||||
|
||||
def get_width(self):
|
||||
points = self.get_points()
|
||||
return points[2, 0] - points[1, 0]
|
||||
|
||||
def get_height(self):
|
||||
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_implied_camera_location(self):
|
||||
theta, phi, gamma = self.get_euler_angles()
|
||||
dist = self.get_focal_distance()
|
||||
x, y, z = self.get_center()
|
||||
return (
|
||||
x + dist * math.sin(theta) * math.sin(phi),
|
||||
y - dist * math.cos(theta) * math.sin(phi),
|
||||
z + dist * math.cos(phi)
|
||||
)
|
||||
|
||||
def interpolate(self, *args, **kwargs):
|
||||
super().interpolate(*args, **kwargs)
|
||||
self.refresh_rotation_matrix()
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
from manimlib.window import Window
|
||||
|
||||
|
||||
class Camera(object):
|
||||
CONFIG = {
|
||||
"background_image": None,
|
||||
"frame_config": {},
|
||||
"pixel_width": DEFAULT_PIXEL_WIDTH,
|
||||
"pixel_height": DEFAULT_PIXEL_HEIGHT,
|
||||
"frame_rate": DEFAULT_FRAME_RATE,
|
||||
# Note: frame height and width will be resized to match
|
||||
# the pixel aspect ratio
|
||||
"background_color": BLACK,
|
||||
"background_opacity": 1,
|
||||
def __init__(
|
||||
self,
|
||||
window: Optional[Window] = None,
|
||||
background_image: Optional[str] = None,
|
||||
frame_config: dict = dict(),
|
||||
# Note: frame height and width will be resized to match this resolution aspect ratio
|
||||
resolution=DEFAULT_RESOLUTION,
|
||||
fps: int = 30,
|
||||
background_color: ManimColor = BLACK,
|
||||
background_opacity: float = 1.0,
|
||||
# Points in vectorized mobjects with norm greater
|
||||
# than this value will be rescaled.
|
||||
"max_allowable_norm": FRAME_WIDTH,
|
||||
"image_mode": "RGBA",
|
||||
"n_channels": 4,
|
||||
"pixel_array_dtype": 'uint8',
|
||||
"light_source_position": [-10, 10, 10],
|
||||
# Measured in pixel widths, used for vector graphics
|
||||
"anti_alias_width": 1.5,
|
||||
max_allowable_norm: float = FRAME_WIDTH,
|
||||
image_mode: str = "RGBA",
|
||||
n_channels: int = 4,
|
||||
pixel_array_dtype: type = np.uint8,
|
||||
light_source_position: Vect3 = np.array([-10, 10, 10]),
|
||||
# Although vector graphics handle antialiasing fine
|
||||
# without multisampling, for 3d scenes one might want
|
||||
# to set samples to be greater than 0.
|
||||
"samples": 0,
|
||||
}
|
||||
samples: int = 0,
|
||||
):
|
||||
self.window = window
|
||||
self.background_image = background_image
|
||||
self.default_pixel_shape = resolution # Rename?
|
||||
self.fps = fps
|
||||
self.max_allowable_norm = max_allowable_norm
|
||||
self.image_mode = image_mode
|
||||
self.n_channels = n_channels
|
||||
self.pixel_array_dtype = pixel_array_dtype
|
||||
self.light_source_position = light_source_position
|
||||
self.samples = samples
|
||||
|
||||
def __init__(self, ctx=None, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
|
||||
self.background_rgba = [
|
||||
*Color(self.background_color).get_rgb(),
|
||||
self.background_opacity
|
||||
]
|
||||
self.init_frame()
|
||||
self.init_context(ctx)
|
||||
self.init_shaders()
|
||||
self.init_textures()
|
||||
self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max
|
||||
self.background_rgba: list[float] = list(color_to_rgba(
|
||||
background_color, background_opacity
|
||||
))
|
||||
self.uniforms = dict()
|
||||
self.init_frame(**frame_config)
|
||||
self.init_context()
|
||||
self.init_fbo()
|
||||
self.init_light_source()
|
||||
self.refresh_perspective_uniforms()
|
||||
self.static_mobject_to_render_group_list = {}
|
||||
|
||||
def init_frame(self):
|
||||
self.frame = CameraFrame(**self.frame_config)
|
||||
def init_frame(self, **config) -> None:
|
||||
self.frame = CameraFrame(**config)
|
||||
|
||||
def init_context(self, ctx=None):
|
||||
if ctx is None:
|
||||
ctx = moderngl.create_standalone_context()
|
||||
fbo = self.get_fbo(ctx, 0)
|
||||
def init_context(self) -> None:
|
||||
if self.window is None:
|
||||
self.ctx: moderngl.Context = moderngl.create_standalone_context()
|
||||
else:
|
||||
fbo = ctx.detect_framebuffer()
|
||||
self.ctx = ctx
|
||||
self.fbo = fbo
|
||||
self.set_ctx_blending()
|
||||
self.ctx: moderngl.Context = self.window.ctx
|
||||
|
||||
# For multisample antialiasing
|
||||
fbo_msaa = self.get_fbo(ctx, self.samples)
|
||||
fbo_msaa.use()
|
||||
self.fbo_msaa = fbo_msaa
|
||||
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
|
||||
self.ctx.enable(moderngl.BLEND)
|
||||
|
||||
def set_ctx_blending(self, enable=True):
|
||||
if enable:
|
||||
self.ctx.enable(moderngl.BLEND)
|
||||
def init_fbo(self) -> None:
|
||||
# This is the buffer used when writing to a video/image file
|
||||
self.fbo_for_files = self.get_fbo(self.samples)
|
||||
|
||||
# This is the frame buffer we'll draw into when emitting frames
|
||||
self.draw_fbo = self.get_fbo(samples=0)
|
||||
|
||||
if self.window is None:
|
||||
self.window_fbo = None
|
||||
self.fbo = self.fbo_for_files
|
||||
else:
|
||||
self.ctx.disable(moderngl.BLEND)
|
||||
self.ctx.blend_func = (
|
||||
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
|
||||
# moderngl.ONE, moderngl.ONE
|
||||
)
|
||||
self.window_fbo = self.ctx.detect_framebuffer()
|
||||
self.fbo = self.window_fbo
|
||||
|
||||
def set_ctx_depth_test(self, enable=True):
|
||||
if enable:
|
||||
self.ctx.enable(moderngl.DEPTH_TEST)
|
||||
else:
|
||||
self.ctx.disable(moderngl.DEPTH_TEST)
|
||||
self.fbo.use()
|
||||
|
||||
def init_light_source(self):
|
||||
def init_light_source(self) -> None:
|
||||
self.light_source = Point(self.light_source_position)
|
||||
|
||||
def use_window_fbo(self, use: bool = True):
|
||||
assert self.window is not None
|
||||
if use:
|
||||
self.fbo = self.window_fbo
|
||||
else:
|
||||
self.fbo = self.fbo_for_files
|
||||
|
||||
# Methods associated with the frame buffer
|
||||
def get_fbo(self, ctx, samples=0):
|
||||
pw = self.pixel_width
|
||||
ph = self.pixel_height
|
||||
return ctx.framebuffer(
|
||||
color_attachments=ctx.texture(
|
||||
(pw, ph),
|
||||
def get_fbo(
|
||||
self,
|
||||
samples: int = 0
|
||||
) -> moderngl.Framebuffer:
|
||||
return self.ctx.framebuffer(
|
||||
color_attachments=self.ctx.texture(
|
||||
self.default_pixel_shape,
|
||||
components=self.n_channels,
|
||||
samples=samples,
|
||||
),
|
||||
depth_attachment=ctx.depth_renderbuffer(
|
||||
(pw, ph),
|
||||
depth_attachment=self.ctx.depth_renderbuffer(
|
||||
self.default_pixel_shape,
|
||||
samples=samples
|
||||
)
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
def clear(self) -> None:
|
||||
self.fbo.clear(*self.background_rgba)
|
||||
self.fbo_msaa.clear(*self.background_rgba)
|
||||
if self.window:
|
||||
self.window.clear(*self.background_rgba)
|
||||
|
||||
def reset_pixel_shape(self, new_width, new_height):
|
||||
self.pixel_width = new_width
|
||||
self.pixel_height = new_height
|
||||
self.refresh_perspective_uniforms()
|
||||
def blit(self, src_fbo, dst_fbo):
|
||||
"""
|
||||
Copy blocks between fbo's using Blit
|
||||
"""
|
||||
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, src_fbo.glo)
|
||||
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst_fbo.glo)
|
||||
gl.glBlitFramebuffer(
|
||||
*src_fbo.viewport,
|
||||
*dst_fbo.viewport,
|
||||
gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
|
||||
)
|
||||
|
||||
def get_raw_fbo_data(self, dtype='f1'):
|
||||
# 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)
|
||||
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.fbo.glo)
|
||||
gl.glBlitFramebuffer(0, 0, pw, ph, 0, 0, pw, ph, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR)
|
||||
return self.fbo.read(
|
||||
viewport=self.fbo.viewport,
|
||||
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
|
||||
self.blit(self.fbo, self.draw_fbo)
|
||||
return self.draw_fbo.read(
|
||||
viewport=self.draw_fbo.viewport,
|
||||
components=self.n_channels,
|
||||
dtype=dtype,
|
||||
)
|
||||
|
||||
def get_image(self, pixel_array=None):
|
||||
def get_image(self) -> Image.Image:
|
||||
return Image.frombytes(
|
||||
'RGBA',
|
||||
self.get_pixel_shape(),
|
||||
@@ -287,15 +154,16 @@ 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])
|
||||
arr = flat_arr.reshape([*reversed(self.draw_fbo.size), self.n_channels])
|
||||
arr = arr[::-1]
|
||||
# Convert from float
|
||||
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,
|
||||
@@ -305,201 +173,89 @@ class Camera(object):
|
||||
return texture
|
||||
|
||||
# Getting camera attributes
|
||||
def get_pixel_shape(self):
|
||||
return self.fbo.viewport[2:4]
|
||||
# return (self.pixel_width, self.pixel_height)
|
||||
def get_pixel_size(self) -> float:
|
||||
return self.frame.get_width() / self.get_pixel_shape()[0]
|
||||
|
||||
def get_pixel_width(self):
|
||||
def get_pixel_shape(self) -> tuple[int, int]:
|
||||
return self.fbo.size
|
||||
|
||||
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_aspect_ratio(self):
|
||||
pw, ph = self.get_pixel_shape()
|
||||
return pw / ph
|
||||
|
||||
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 get_location(self):
|
||||
def get_location(self) -> tuple[float, float, float]:
|
||||
return self.frame.get_implied_camera_location()
|
||||
|
||||
def resize_frame_shape(self, fixed_dimension=0):
|
||||
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
|
||||
whether frame_height or frame_width
|
||||
remains fixed while the other changes accordingly.
|
||||
"""
|
||||
pixel_height = self.get_pixel_height()
|
||||
pixel_width = self.get_pixel_width()
|
||||
frame_height = self.get_frame_height()
|
||||
frame_width = self.get_frame_width()
|
||||
aspect_ratio = fdiv(pixel_width, pixel_height)
|
||||
if fixed_dimension == 0:
|
||||
aspect_ratio = self.get_aspect_ratio()
|
||||
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)
|
||||
self.frame.set_height(frame_height, stretch=True)
|
||||
self.frame.set_width(frame_width, stretch=True)
|
||||
|
||||
# Rendering
|
||||
def capture(self, *mobjects, **kwargs):
|
||||
self.refresh_perspective_uniforms()
|
||||
def capture(self, *mobjects: Mobject) -> None:
|
||||
self.clear()
|
||||
self.refresh_uniforms()
|
||||
self.fbo.use()
|
||||
for mobject in mobjects:
|
||||
for render_group in self.get_render_group_list(mobject):
|
||||
self.render(render_group)
|
||||
mobject.render(self.ctx, self.uniforms)
|
||||
|
||||
def render(self, render_group):
|
||||
shader_wrapper = render_group["shader_wrapper"]
|
||||
shader_program = render_group["prog"]
|
||||
self.set_shader_uniforms(shader_program, 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)
|
||||
if self.window:
|
||||
self.window.swap_buffers()
|
||||
if self.fbo is not self.window_fbo:
|
||||
self.blit(self.fbo, self.window_fbo)
|
||||
self.window.swap_buffers()
|
||||
|
||||
def get_render_group_list(self, mobject):
|
||||
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):
|
||||
# Data buffers
|
||||
vbo = self.ctx.buffer(shader_wrapper.vert_data.tobytes())
|
||||
if shader_wrapper.vert_indices is None:
|
||||
ibo = None
|
||||
else:
|
||||
vert_index_data = shader_wrapper.vert_indices.astype('i4').tobytes()
|
||||
if vert_index_data:
|
||||
ibo = self.ctx.buffer(vert_index_data)
|
||||
else:
|
||||
ibo = None
|
||||
|
||||
# Program and vertex array
|
||||
shader_program, vert_format = self.get_shader_program(shader_wrapper)
|
||||
vao = self.ctx.vertex_array(
|
||||
program=shader_program,
|
||||
content=[(vbo, vert_format, *shader_wrapper.vert_attributes)],
|
||||
index_buffer=ibo,
|
||||
)
|
||||
return {
|
||||
"vbo": vbo,
|
||||
"ibo": ibo,
|
||||
"vao": vao,
|
||||
"prog": shader_program,
|
||||
"shader_wrapper": shader_wrapper,
|
||||
"single_use": single_use,
|
||||
}
|
||||
|
||||
def release_render_group(self, render_group):
|
||||
for key in ["vbo", "ibo", "vao"]:
|
||||
if render_group[key] is not None:
|
||||
render_group[key].release()
|
||||
|
||||
def set_mobjects_as_static(self, *mobjects):
|
||||
# Creates buffer and array objects holding each mobjects shader data
|
||||
for mob in mobjects:
|
||||
self.static_mobject_to_render_group_list[id(mob)] = [
|
||||
self.get_render_group(sw, single_use=False)
|
||||
for sw in mob.get_shader_wrapper_list()
|
||||
]
|
||||
|
||||
def release_static_mobjects(self):
|
||||
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):
|
||||
# Initialize with the null id going to None
|
||||
self.id_to_shader_program = {"": None}
|
||||
|
||||
def get_shader_program(self, shader_wrapper):
|
||||
sid = shader_wrapper.get_program_id()
|
||||
if sid not in self.id_to_shader_program:
|
||||
# Create shader program for the first time, then cache
|
||||
# in the id_to_shader_program dictionary
|
||||
program = self.ctx.program(**shader_wrapper.get_program_code())
|
||||
vert_format = moderngl.detect_format(program, shader_wrapper.vert_attributes)
|
||||
self.id_to_shader_program[sid] = (program, vert_format)
|
||||
return self.id_to_shader_program[sid]
|
||||
|
||||
def set_shader_uniforms(self, shader, shader_wrapper):
|
||||
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(self.perspective_uniforms.items(), shader_wrapper.uniforms.items()):
|
||||
try:
|
||||
if isinstance(value, np.ndarray):
|
||||
value = tuple(value)
|
||||
shader[name].value = value
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def refresh_perspective_uniforms(self):
|
||||
def refresh_uniforms(self) -> None:
|
||||
frame = self.frame
|
||||
pw, ph = self.get_pixel_shape()
|
||||
fw, fh = frame.get_shape()
|
||||
# TODO, this should probably be a mobject uniform, with
|
||||
# the camera taking care of the conversion factor
|
||||
anti_alias_width = self.anti_alias_width / (ph / fh)
|
||||
# Orient light
|
||||
rotation = frame.get_inverse_camera_rotation_matrix()
|
||||
offset = frame.get_center()
|
||||
light_pos = np.dot(
|
||||
rotation, self.light_source.get_location() + offset
|
||||
view_matrix = frame.get_view_matrix()
|
||||
light_pos = self.light_source.get_location()
|
||||
cam_pos = self.frame.get_implied_camera_location()
|
||||
|
||||
self.uniforms.update(
|
||||
view=tuple(view_matrix.T.flatten()),
|
||||
frame_scale=frame.get_scale(),
|
||||
frame_rescale_factors=(
|
||||
2.0 / FRAME_WIDTH,
|
||||
2.0 / FRAME_HEIGHT,
|
||||
frame.get_scale() / frame.get_focal_distance(),
|
||||
),
|
||||
pixel_size=self.get_pixel_size(),
|
||||
camera_position=tuple(cam_pos),
|
||||
light_position=tuple(light_pos),
|
||||
)
|
||||
cam_pos = self.frame.get_implied_camera_location() # TODO
|
||||
|
||||
self.perspective_uniforms = {
|
||||
"frame_shape": frame.get_shape(),
|
||||
"anti_alias_width": anti_alias_width,
|
||||
"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.n_textures = 0
|
||||
self.path_to_texture = {}
|
||||
|
||||
def get_texture_id(self, path):
|
||||
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[path] = (tid, texture)
|
||||
return self.path_to_texture[path][0]
|
||||
|
||||
def release_texture(self, path):
|
||||
tid_and_texture = self.path_to_texture.pop(path, None)
|
||||
if tid_and_texture:
|
||||
tid_and_texture[1].release()
|
||||
return self
|
||||
|
||||
|
||||
# Mostly just defined so old scenes don't break
|
||||
class ThreeDCamera(Camera):
|
||||
CONFIG = {
|
||||
"samples": 4,
|
||||
"anti_alias_width": 0,
|
||||
}
|
||||
def __init__(self, samples: int = 4, **kwargs):
|
||||
super().__init__(samples=samples, **kwargs)
|
||||
|
||||
266
manimlib/camera/camera_frame.py
Normal file
266
manimlib/camera/camera_frame.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial.transform import Rotation
|
||||
|
||||
from manimlib.constants import DEG, RADIANS
|
||||
from manimlib.constants import FRAME_SHAPE
|
||||
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||
from manimlib.constants import PI
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.space_ops import normalize
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
|
||||
class CameraFrame(Mobject):
|
||||
def __init__(
|
||||
self,
|
||||
frame_shape: tuple[float, float] = FRAME_SHAPE,
|
||||
center_point: Vect3 = ORIGIN,
|
||||
# Field of view in the y direction
|
||||
fovy: float = 45 * DEG,
|
||||
euler_axes: str = "zxz",
|
||||
# This keeps it ordered first in a scene
|
||||
z_index=-1,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(z_index=z_index, **kwargs)
|
||||
|
||||
self.uniforms["orientation"] = Rotation.identity().as_quat()
|
||||
self.uniforms["fovy"] = fovy
|
||||
|
||||
self.default_orientation = Rotation.identity()
|
||||
self.view_matrix = np.identity(4)
|
||||
self.id4x4 = np.identity(4)
|
||||
self.camera_location = OUT # This will be updated by set_points
|
||||
self.euler_axes = euler_axes
|
||||
|
||||
self.set_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP]))
|
||||
self.set_width(frame_shape[0], stretch=True)
|
||||
self.set_height(frame_shape[1], stretch=True)
|
||||
self.move_to(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 make_orientation_default(self):
|
||||
self.default_orientation = self.get_orientation()
|
||||
return self
|
||||
|
||||
def to_default_state(self):
|
||||
self.set_shape(*FRAME_SHAPE)
|
||||
self.center()
|
||||
self.set_orientation(self.default_orientation)
|
||||
return self
|
||||
|
||||
def get_euler_angles(self) -> np.ndarray:
|
||||
orientation = self.get_orientation()
|
||||
if np.isclose(orientation.as_quat(), [0, 0, 0, 1]).all():
|
||||
return np.zeros(3)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', UserWarning) # Ignore UserWarnings
|
||||
angles = orientation.as_euler(self.euler_axes)[::-1]
|
||||
# Handle Gimble lock case
|
||||
if self.euler_axes == "zxz":
|
||||
if np.isclose(angles[1], 0, atol=1e-2):
|
||||
angles[0] = angles[0] + angles[2]
|
||||
angles[2] = 0
|
||||
if np.isclose(angles[1], PI, atol=1e-2):
|
||||
angles[0] = angles[0] - angles[2]
|
||||
angles[2] = 0
|
||||
return angles
|
||||
|
||||
def get_theta(self):
|
||||
return self.get_euler_angles()[0]
|
||||
|
||||
def get_phi(self):
|
||||
return self.get_euler_angles()[1]
|
||||
|
||||
def get_gamma(self):
|
||||
return self.get_euler_angles()[2]
|
||||
|
||||
def get_scale(self):
|
||||
return self.get_height() / FRAME_SHAPE[1]
|
||||
|
||||
def get_inverse_camera_rotation_matrix(self):
|
||||
return self.get_orientation().as_matrix().T
|
||||
|
||||
def get_view_matrix(self, refresh=False):
|
||||
"""
|
||||
Returns a 4x4 for the affine transformation mapping a point
|
||||
into the camera's internal coordinate system
|
||||
"""
|
||||
if self._data_has_changed:
|
||||
shift = self.id4x4.copy()
|
||||
rotation = self.id4x4.copy()
|
||||
|
||||
scale = self.get_scale()
|
||||
shift[:3, 3] = -self.get_center()
|
||||
rotation[:3, :3] = self.get_inverse_camera_rotation_matrix()
|
||||
np.dot(rotation, shift, out=self.view_matrix)
|
||||
if scale > 0:
|
||||
self.view_matrix[:3, :4] /= scale
|
||||
|
||||
return self.view_matrix
|
||||
|
||||
def get_inv_view_matrix(self):
|
||||
return np.linalg.inv(self.get_view_matrix())
|
||||
|
||||
@Mobject.affects_data
|
||||
def interpolate(self, *args, **kwargs):
|
||||
super().interpolate(*args, **kwargs)
|
||||
|
||||
@Mobject.affects_data
|
||||
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: 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
|
||||
if all(eulers == 0):
|
||||
rot = Rotation.identity()
|
||||
else:
|
||||
rot = Rotation.from_euler(self.euler_axes, eulers[::-1])
|
||||
self.set_orientation(rot)
|
||||
return self
|
||||
|
||||
def increment_euler_angles(
|
||||
self,
|
||||
dtheta: float = 0,
|
||||
dphi: float = 0,
|
||||
dgamma: float = 0,
|
||||
units: float = RADIANS
|
||||
):
|
||||
angles = self.get_euler_angles()
|
||||
new_angles = angles + np.array([dtheta, dphi, dgamma]) * units
|
||||
|
||||
# Limit range for phi
|
||||
if self.euler_axes == "zxz":
|
||||
new_angles[1] = clip(new_angles[1], 0, PI)
|
||||
elif self.euler_axes == "zxy":
|
||||
new_angles[1] = clip(new_angles[1], -PI / 2, PI / 2)
|
||||
|
||||
new_rot = Rotation.from_euler(self.euler_axes, new_angles[::-1])
|
||||
self.set_orientation(new_rot)
|
||||
return self
|
||||
|
||||
def set_euler_axes(self, seq: str):
|
||||
self.euler_axes = seq
|
||||
|
||||
def reorient(
|
||||
self,
|
||||
theta_degrees: float | None = None,
|
||||
phi_degrees: float | None = None,
|
||||
gamma_degrees: float | None = None,
|
||||
center: Vect3 | tuple[float, float, float] | None = None,
|
||||
height: 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=DEG)
|
||||
if center is not None:
|
||||
self.move_to(np.array(center))
|
||||
if height is not None:
|
||||
self.set_height(height)
|
||||
return self
|
||||
|
||||
def set_theta(self, theta: float):
|
||||
return self.set_euler_angles(theta=theta)
|
||||
|
||||
def set_phi(self, phi: float):
|
||||
return self.set_euler_angles(phi=phi)
|
||||
|
||||
def set_gamma(self, gamma: float):
|
||||
return self.set_euler_angles(gamma=gamma)
|
||||
|
||||
def increment_theta(self, dtheta: float, units=RADIANS):
|
||||
self.increment_euler_angles(dtheta=dtheta, units=units)
|
||||
return self
|
||||
|
||||
def increment_phi(self, dphi: float, units=RADIANS):
|
||||
self.increment_euler_angles(dphi=dphi, units=units)
|
||||
return self
|
||||
|
||||
def increment_gamma(self, dgamma: float, units=RADIANS):
|
||||
self.increment_euler_angles(dgamma=dgamma, units=units)
|
||||
return self
|
||||
|
||||
def add_ambient_rotation(self, angular_speed=1 * DEG):
|
||||
self.add_updater(lambda m, dt: m.increment_theta(angular_speed * dt))
|
||||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def set_focal_distance(self, focal_distance: float):
|
||||
self.uniforms["fovy"] = 2 * math.atan(0.5 * self.get_height() / focal_distance)
|
||||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def set_field_of_view(self, field_of_view: float):
|
||||
self.uniforms["fovy"] = field_of_view
|
||||
return self
|
||||
|
||||
def get_shape(self):
|
||||
return (self.get_width(), self.get_height())
|
||||
|
||||
def get_aspect_ratio(self):
|
||||
width, height = self.get_shape()
|
||||
return width / height
|
||||
|
||||
def get_center(self) -> np.ndarray:
|
||||
# Assumes first point is at the center
|
||||
return self.get_points()[0]
|
||||
|
||||
def get_width(self) -> float:
|
||||
points = self.get_points()
|
||||
return points[2, 0] - points[1, 0]
|
||||
|
||||
def get_height(self) -> float:
|
||||
points = self.get_points()
|
||||
return points[4, 1] - points[3, 1]
|
||||
|
||||
def get_focal_distance(self) -> float:
|
||||
return 0.5 * self.get_height() / math.tan(0.5 * self.uniforms["fovy"])
|
||||
|
||||
def get_field_of_view(self) -> float:
|
||||
return self.uniforms["fovy"]
|
||||
|
||||
def get_implied_camera_location(self) -> np.ndarray:
|
||||
if self._data_has_changed:
|
||||
to_camera = self.get_inverse_camera_rotation_matrix()[2]
|
||||
dist = self.get_focal_distance()
|
||||
self.camera_location = self.get_center() + dist * to_camera
|
||||
return self.camera_location
|
||||
|
||||
def to_fixed_frame_point(self, point: Vect3, relative: bool = False):
|
||||
view = self.get_view_matrix()
|
||||
point4d = [*point, 0 if relative else 1]
|
||||
return np.dot(point4d, view.T)[:3]
|
||||
|
||||
def from_fixed_frame_point(self, point: Vect3, relative: bool = False):
|
||||
inv_view = self.get_inv_view_matrix()
|
||||
point4d = [*point, 0 if relative else 1]
|
||||
return np.dot(point4d, inv_view.T)[:3]
|
||||
@@ -1,19 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import colour
|
||||
import inspect
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from contextlib import contextmanager
|
||||
from screeninfo import get_monitors
|
||||
from pathlib import Path
|
||||
from ast import literal_eval
|
||||
from addict import Dict
|
||||
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.init_config import init_customization
|
||||
from manimlib.logger import log
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
from typing import Optional
|
||||
|
||||
|
||||
__config_file__ = "custom_config.yml"
|
||||
def initialize_manim_config() -> Dict:
|
||||
"""
|
||||
Return default configuration for various classes in manim, such as
|
||||
Scene, Window, Camera, and SceneFileWriter, as well as configuration
|
||||
determining how the scene is run (e.g. written to file or previewed in window).
|
||||
|
||||
The result is initially on the contents of default_config.yml in the manimlib directory,
|
||||
which can be further updated by a custom configuration file custom_config.yml.
|
||||
It is further updated based on command line argument.
|
||||
"""
|
||||
args = parse_cli()
|
||||
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
config = Dict(merge_dicts_recursively(
|
||||
load_yaml(global_defaults_file),
|
||||
load_yaml("custom_config.yml"), # From current working directory
|
||||
load_yaml(args.config_file) if args.config_file else dict(),
|
||||
))
|
||||
|
||||
log.setLevel(args.log_level or config["log_level"])
|
||||
|
||||
update_directory_config(config)
|
||||
update_window_config(config, args)
|
||||
update_camera_config(config, args)
|
||||
update_file_writer_config(config, args)
|
||||
update_scene_config(config, args)
|
||||
update_run_config(config, args)
|
||||
update_embed_config(config, args)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def parse_cli():
|
||||
@@ -23,7 +58,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",
|
||||
@@ -43,12 +78,12 @@ def parse_cli():
|
||||
parser.add_argument(
|
||||
"-l", "--low_quality",
|
||||
action="store_true",
|
||||
help="Render at a low quality (for faster rendering)",
|
||||
help="Render at 480p",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m", "--medium_quality",
|
||||
action="store_true",
|
||||
help="Render at a medium quality",
|
||||
help="Render at 720p",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hd",
|
||||
@@ -66,9 +101,10 @@ def parse_cli():
|
||||
help="Show window in full screen",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-g", "--save_pngs",
|
||||
"-p", "--presenter_mode",
|
||||
action="store_true",
|
||||
help="Save each frame as a png",
|
||||
help="Scene will stay paused during wait calls until " + \
|
||||
"space bar or right arrow is hit, like a slide show"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i", "--gif",
|
||||
@@ -80,6 +116,14 @@ def parse_cli():
|
||||
action="store_true",
|
||||
help="Render to a movie file with an alpha channel",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vcodec",
|
||||
help="Video codec to use with ffmpeg",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pix_fmt",
|
||||
help="Pixel format to use for the output of ffmpeg, defaults to `yuv420p`",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q", "--quiet",
|
||||
action="store_true",
|
||||
@@ -101,9 +145,10 @@ def parse_cli():
|
||||
help="Show the output file in finder",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
"--subdivide",
|
||||
action="store_true",
|
||||
help="Guide for automatic configuration",
|
||||
help="Divide the output animation into individual movie files " +
|
||||
"for each animation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file_name",
|
||||
@@ -111,23 +156,23 @@ def parse_cli():
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--start_at_animation_number",
|
||||
help="Start rendering not from the first animation, but"
|
||||
"from another, specified by its index. If you pass"
|
||||
"in two comma separated values, e.g. \"3,6\", it will end"
|
||||
help="Start rendering not from the first animation, but " + \
|
||||
"from another, specified by its index. If you pass " + \
|
||||
"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."
|
||||
"-e", "--embed",
|
||||
metavar="LINE_NUMBER",
|
||||
help="Adds a breakpoint at the inputted file dropping into an " + \
|
||||
"interactive iPython session at that point of the code."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--resolution",
|
||||
help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frame_rate",
|
||||
"--fps",
|
||||
help="Frame rate, as an integer",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -139,6 +184,17 @@ def parse_cli():
|
||||
action="store_true",
|
||||
help="Leave progress bars displayed in terminal",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show_animation_progress",
|
||||
action="store_true",
|
||||
help="Show progress bar for each animation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prerun",
|
||||
action="store_true",
|
||||
help="Calculate total framecount, to display in a progress bar, by doing " + \
|
||||
"an initial run of the scene which skips animations."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--video_dir",
|
||||
help="Directory to write video",
|
||||
@@ -156,228 +212,184 @@ def parse_cli():
|
||||
"--log-level",
|
||||
help="Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clear-cache",
|
||||
action="store_true",
|
||||
help="Erase the cache used for Tex and Text Mobjects"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--autoreload",
|
||||
action="store_true",
|
||||
help="Automatically reload Python modules to pick up code changes " +
|
||||
"across different files",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
args.write_file = any([args.write_file, args.open, args.finder])
|
||||
return args
|
||||
except argparse.ArgumentError as err:
|
||||
log.error(str(err))
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def update_directory_config(config: Dict):
|
||||
dir_config = config.directories
|
||||
base = dir_config.base
|
||||
for key, subdir in dir_config.subdirs.items():
|
||||
dir_config[key] = os.path.join(base, subdir)
|
||||
|
||||
|
||||
def update_window_config(config: Dict, args: Namespace):
|
||||
window_config = config.window
|
||||
for key in "position", "size":
|
||||
if window_config.get(key):
|
||||
window_config[key] = literal_eval(window_config[key])
|
||||
if args.full_screen:
|
||||
window_config.full_screen = True
|
||||
|
||||
|
||||
def update_camera_config(config: Dict, args: Namespace):
|
||||
camera_config = config.camera
|
||||
arg_resolution = get_resolution_from_args(args, config.resolution_options)
|
||||
camera_config.resolution = arg_resolution or literal_eval(camera_config.resolution)
|
||||
if args.fps:
|
||||
camera_config.fps = args.fps
|
||||
if args.color:
|
||||
try:
|
||||
camera_config.background_color = colour.Color(args.color)
|
||||
except Exception:
|
||||
log.error("Please use a valid color")
|
||||
log.error(err)
|
||||
sys.exit(2)
|
||||
if args.transparent:
|
||||
camera_config.background_opacity = 0.0
|
||||
|
||||
|
||||
def update_file_writer_config(config: Dict, args: Namespace):
|
||||
file_writer_config = config.file_writer
|
||||
file_writer_config.update(
|
||||
write_to_movie=(not args.skip_animations and args.write_file),
|
||||
subdivide_output=args.subdivide,
|
||||
save_last_frame=(args.skip_animations and args.write_file),
|
||||
png_mode=("RGBA" if args.transparent else "RGB"),
|
||||
movie_file_extension=(get_file_ext(args)),
|
||||
output_directory=get_output_directory(args, config),
|
||||
file_name=args.file_name,
|
||||
open_file_upon_completion=args.open,
|
||||
show_file_location_upon_completion=args.finder,
|
||||
quiet=args.quiet,
|
||||
)
|
||||
|
||||
if args.vcodec:
|
||||
file_writer_config.video_codec = args.vcodec
|
||||
elif args.transparent:
|
||||
file_writer_config.video_codec = 'prores_ks'
|
||||
file_writer_config.pixel_format = ''
|
||||
elif args.gif:
|
||||
file_writer_config.video_codec = ''
|
||||
|
||||
if args.pix_fmt:
|
||||
file_writer_config.pixel_format = args.pix_fmt
|
||||
|
||||
|
||||
def update_scene_config(config: Dict, args: Namespace):
|
||||
scene_config = config.scene
|
||||
start, end = get_animations_numbers(args)
|
||||
scene_config.update(
|
||||
# Note, Scene.__init__ makes use of both manimlib.camera and
|
||||
# manimlib.file_writer below, so the arguments here are just for
|
||||
# any future specifications beyond what the global configuration holds
|
||||
camera_config=dict(),
|
||||
file_writer_config=dict(),
|
||||
skip_animations=args.skip_animations,
|
||||
start_at_animation_number=start,
|
||||
end_at_animation_number=end,
|
||||
presenter_mode=args.presenter_mode,
|
||||
)
|
||||
if args.leave_progress_bars:
|
||||
scene_config.leave_progress_bars = True
|
||||
if args.show_animation_progress:
|
||||
scene_config.show_animation_progress = True
|
||||
|
||||
|
||||
def update_run_config(config: Dict, args: Namespace):
|
||||
config.run = Dict(
|
||||
file_name=args.file,
|
||||
embed_line=(int(args.embed) if args.embed is not None else None),
|
||||
is_reload=False,
|
||||
prerun=args.prerun,
|
||||
scene_names=args.scene_names,
|
||||
quiet=args.quiet or args.write_all,
|
||||
write_all=args.write_all,
|
||||
show_in_window=not args.write_file
|
||||
)
|
||||
|
||||
|
||||
def update_embed_config(config: Dict, args: Namespace):
|
||||
if args.autoreload:
|
||||
config.embed.autoreload = True
|
||||
|
||||
|
||||
# Helpers for the functions above
|
||||
|
||||
|
||||
def load_yaml(file_path: str):
|
||||
try:
|
||||
with open(file_path, "r") as file:
|
||||
return yaml.safe_load(file) or {}
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
|
||||
def get_manim_dir():
|
||||
manimlib_module = importlib.import_module("manimlib")
|
||||
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
|
||||
return os.path.abspath(os.path.join(manimlib_dir, ".."))
|
||||
|
||||
|
||||
def get_module(file_name):
|
||||
if file_name is None:
|
||||
return None
|
||||
module_name = file_name.replace(os.sep, ".").replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
def get_resolution_from_args(args: Optional[Namespace], resolution_options: dict) -> Optional[tuple[int, int]]:
|
||||
if args.resolution:
|
||||
return tuple(map(int, args.resolution.split("x")))
|
||||
if args.low_quality:
|
||||
return literal_eval(resolution_options["low"])
|
||||
if args.medium_quality:
|
||||
return literal_eval(resolution_options["med"])
|
||||
if args.hd:
|
||||
return literal_eval(resolution_options["high"])
|
||||
if args.uhd:
|
||||
return literal_eval(resolution_options["4k"])
|
||||
return None
|
||||
|
||||
|
||||
@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():
|
||||
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(__config_file__):
|
||||
with open(__config_file__, "r") as file:
|
||||
local_defaults = yaml.safe_load(file)
|
||||
if local_defaults:
|
||||
config = merge_dicts_recursively(
|
||||
config,
|
||||
local_defaults,
|
||||
)
|
||||
else:
|
||||
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):
|
||||
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(__config_file__)):
|
||||
log.info("There is no configuration file detected. Switch to the config file initializer:")
|
||||
init_customization()
|
||||
|
||||
elif not os.path.exists(__config_file__):
|
||||
log.info(f"Using the default configuration file, which you can modify in `{global_defaults_file}`")
|
||||
log.info(
|
||||
"If you want to create a local configuration file, you can create a file named"
|
||||
f" `{__config_file__}`, or run `manimgl --config`"
|
||||
)
|
||||
|
||||
custom_config = get_custom_config()
|
||||
check_temporary_storage(custom_config)
|
||||
|
||||
write_file = any([args.write_file, args.open, args.finder])
|
||||
def get_file_ext(args: Namespace) -> str:
|
||||
if args.transparent:
|
||||
file_ext = ".mov"
|
||||
elif args.gif:
|
||||
file_ext = ".gif"
|
||||
else:
|
||||
file_ext = ".mp4"
|
||||
return file_ext
|
||||
|
||||
file_writer_config = {
|
||||
"write_to_movie": not args.skip_animations and write_file,
|
||||
"break_into_partial_movies": custom_config["break_into_partial_movies"],
|
||||
"save_last_frame": args.skip_animations and write_file,
|
||||
"save_pngs": args.save_pngs,
|
||||
# If -t is passed in (for transparent), this will be RGBA
|
||||
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||
"movie_file_extension": file_ext,
|
||||
"mirror_module_path": custom_config["directories"]["mirror_module_path"],
|
||||
"output_directory": args.video_dir or custom_config["directories"]["output"],
|
||||
"file_name": args.file_name,
|
||||
"input_file_path": args.file or "",
|
||||
"open_file_upon_completion": args.open,
|
||||
"show_file_location_upon_completion": args.finder,
|
||||
"quiet": args.quiet,
|
||||
}
|
||||
|
||||
if args.embed is None:
|
||||
module = get_module(args.file)
|
||||
def get_animations_numbers(args: Namespace) -> tuple[int | None, int | None]:
|
||||
stan = args.start_at_animation_number
|
||||
if stan is None:
|
||||
return (None, None)
|
||||
elif "," in stan:
|
||||
return tuple(map(int, stan.split(",")))
|
||||
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,
|
||||
"end_at_animation_number": None,
|
||||
"preview": not write_file,
|
||||
"leave_progress_bars": args.leave_progress_bars,
|
||||
}
|
||||
|
||||
# Camera configuration
|
||||
config["camera_config"] = get_camera_configuration(args, custom_config)
|
||||
|
||||
# Default to making window half the screen size
|
||||
# but make it full screen if -f is passed in
|
||||
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 or custom_config["full_screen"]):
|
||||
window_width //= 2
|
||||
window_height = window_width * 9 // 16
|
||||
config["window_config"] = {
|
||||
"size": (window_width, window_height),
|
||||
}
|
||||
|
||||
# Arguments related to skipping
|
||||
stan = config["start_at_animation_number"]
|
||||
if stan is not None:
|
||||
if "," in stan:
|
||||
start, end = stan.split(",")
|
||||
config["start_at_animation_number"] = int(start)
|
||||
config["end_at_animation_number"] = int(end)
|
||||
else:
|
||||
config["start_at_animation_number"] = int(stan)
|
||||
|
||||
return config
|
||||
return int(stan), None
|
||||
|
||||
|
||||
def get_camera_configuration(args, custom_config):
|
||||
camera_config = {}
|
||||
camera_qualities = get_custom_config()["camera_qualities"]
|
||||
if args.low_quality:
|
||||
quality = camera_qualities["low"]
|
||||
elif args.medium_quality:
|
||||
quality = camera_qualities["medium"]
|
||||
elif args.hd:
|
||||
quality = camera_qualities["high"]
|
||||
elif args.uhd:
|
||||
quality = camera_qualities["ultra_high"]
|
||||
else:
|
||||
quality = camera_qualities[camera_qualities["default_quality"]]
|
||||
def get_output_directory(args: Namespace, config: Dict) -> str:
|
||||
dir_config = config.directories
|
||||
out_dir = args.video_dir or dir_config.output
|
||||
if dir_config.mirror_module_path and args.file:
|
||||
file_path = Path(args.file).absolute()
|
||||
rel_path = file_path.relative_to(dir_config.removed_mirror_prefix)
|
||||
rel_path = Path(str(rel_path).lstrip("_"))
|
||||
out_dir = Path(out_dir, rel_path).with_suffix("")
|
||||
return out_dir
|
||||
|
||||
if args.resolution:
|
||||
quality["resolution"] = args.resolution
|
||||
if args.frame_rate:
|
||||
quality["frame_rate"] = int(args.frame_rate)
|
||||
|
||||
width_str, height_str = quality["resolution"].split("x")
|
||||
width = int(width_str)
|
||||
height = int(height_str)
|
||||
|
||||
camera_config.update({
|
||||
"pixel_width": width,
|
||||
"pixel_height": height,
|
||||
"frame_rate": quality["frame_rate"],
|
||||
})
|
||||
|
||||
try:
|
||||
bg_color = args.color or custom_config["style"]["background_color"]
|
||||
camera_config["background_color"] = colour.Color(bg_color)
|
||||
except ValueError as err:
|
||||
log.error("Please use a valid color")
|
||||
log.error(err)
|
||||
sys.exit(2)
|
||||
|
||||
# If rendering a transparent image/move, make sure the
|
||||
# scene has a background opacity of 0
|
||||
if args.transparent:
|
||||
camera_config["background_opacity"] = 0
|
||||
|
||||
return camera_config
|
||||
# Create global configuration
|
||||
manim_config: Dict = initialize_manim_config()
|
||||
|
||||
@@ -1,143 +1,147 @@
|
||||
from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from typing import List
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
|
||||
# See manimlib/default_config.yml
|
||||
from manimlib.config import manim_config
|
||||
|
||||
|
||||
DEFAULT_RESOLUTION: tuple[int, int] = manim_config.camera.resolution
|
||||
DEFAULT_PIXEL_WIDTH: int = DEFAULT_RESOLUTION[0]
|
||||
DEFAULT_PIXEL_HEIGHT: int = DEFAULT_RESOLUTION[1]
|
||||
|
||||
# Sizes relevant to default camera frame
|
||||
ASPECT_RATIO = 16.0 / 9.0
|
||||
FRAME_HEIGHT = 8.0
|
||||
FRAME_WIDTH = FRAME_HEIGHT * ASPECT_RATIO
|
||||
FRAME_Y_RADIUS = FRAME_HEIGHT / 2
|
||||
FRAME_X_RADIUS = FRAME_WIDTH / 2
|
||||
|
||||
DEFAULT_PIXEL_HEIGHT = 1080
|
||||
DEFAULT_PIXEL_WIDTH = 1920
|
||||
DEFAULT_FRAME_RATE = 30
|
||||
|
||||
SMALL_BUFF = 0.1
|
||||
MED_SMALL_BUFF = 0.25
|
||||
MED_LARGE_BUFF = 0.5
|
||||
LARGE_BUFF = 1
|
||||
|
||||
DEFAULT_MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF
|
||||
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF
|
||||
ASPECT_RATIO: float = DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT
|
||||
FRAME_HEIGHT: float = manim_config.sizes.frame_height
|
||||
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
|
||||
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
|
||||
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
|
||||
FRAME_X_RADIUS: float = FRAME_WIDTH / 2
|
||||
|
||||
|
||||
# All in seconds
|
||||
DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0
|
||||
DEFAULT_WAIT_TIME = 1.0
|
||||
# Helpful values for positioning mobjects
|
||||
SMALL_BUFF: float = manim_config.sizes.small_buff
|
||||
MED_SMALL_BUFF: float = manim_config.sizes.med_small_buff
|
||||
MED_LARGE_BUFF: float = manim_config.sizes.med_large_buff
|
||||
LARGE_BUFF: float = manim_config.sizes.large_buff
|
||||
|
||||
DEFAULT_MOBJECT_TO_EDGE_BUFF: float = manim_config.sizes.default_mobject_to_edge_buff
|
||||
DEFAULT_MOBJECT_TO_MOBJECT_BUFF: float = manim_config.sizes.default_mobject_to_mobject_buff
|
||||
|
||||
|
||||
ORIGIN = np.array((0., 0., 0.))
|
||||
UP = np.array((0., 1., 0.))
|
||||
DOWN = np.array((0., -1., 0.))
|
||||
RIGHT = np.array((1., 0., 0.))
|
||||
LEFT = np.array((-1., 0., 0.))
|
||||
IN = np.array((0., 0., -1.))
|
||||
OUT = np.array((0., 0., 1.))
|
||||
X_AXIS = np.array((1., 0., 0.))
|
||||
Y_AXIS = np.array((0., 1., 0.))
|
||||
Z_AXIS = np.array((0., 0., 1.))
|
||||
# Standard vectors
|
||||
ORIGIN: Vect3 = np.array([0., 0., 0.])
|
||||
UP: Vect3 = np.array([0., 1., 0.])
|
||||
DOWN: Vect3 = np.array([0., -1., 0.])
|
||||
RIGHT: Vect3 = np.array([1., 0., 0.])
|
||||
LEFT: Vect3 = np.array([-1., 0., 0.])
|
||||
IN: Vect3 = np.array([0., 0., -1.])
|
||||
OUT: Vect3 = np.array([0., 0., 1.])
|
||||
X_AXIS: Vect3 = np.array([1., 0., 0.])
|
||||
Y_AXIS: Vect3 = np.array([0., 1., 0.])
|
||||
Z_AXIS: Vect3 = np.array([0., 0., 1.])
|
||||
|
||||
NULL_POINTS = np.array([[0., 0., 0.]])
|
||||
|
||||
# Useful abbreviations for diagonals
|
||||
UL = UP + LEFT
|
||||
UR = UP + RIGHT
|
||||
DL = DOWN + LEFT
|
||||
DR = DOWN + RIGHT
|
||||
UL: Vect3 = UP + LEFT
|
||||
UR: Vect3 = UP + RIGHT
|
||||
DL: Vect3 = DOWN + LEFT
|
||||
DR: Vect3 = DOWN + RIGHT
|
||||
|
||||
TOP = FRAME_Y_RADIUS * UP
|
||||
BOTTOM = FRAME_Y_RADIUS * DOWN
|
||||
LEFT_SIDE = FRAME_X_RADIUS * LEFT
|
||||
RIGHT_SIDE = FRAME_X_RADIUS * RIGHT
|
||||
TOP: Vect3 = FRAME_Y_RADIUS * UP
|
||||
BOTTOM: Vect3 = FRAME_Y_RADIUS * DOWN
|
||||
LEFT_SIDE: Vect3 = FRAME_X_RADIUS * LEFT
|
||||
RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT
|
||||
|
||||
PI = np.pi
|
||||
TAU = 2 * PI
|
||||
DEGREES = TAU / 360
|
||||
# Angles
|
||||
PI: float = np.pi
|
||||
TAU: float = 2 * PI
|
||||
DEG: float = TAU / 360
|
||||
DEGREES = DEG # Many older animations use teh full name
|
||||
# Nice to have a constant for readability
|
||||
# when juxtaposed with expressions like 30 * DEGREES
|
||||
RADIANS = 1
|
||||
|
||||
FFMPEG_BIN = "ffmpeg"
|
||||
|
||||
JOINT_TYPE_MAP = {
|
||||
"auto": 0,
|
||||
"round": 1,
|
||||
"bevel": 2,
|
||||
"miter": 3,
|
||||
}
|
||||
# when juxtaposed with expressions like 30 * DEG
|
||||
RADIANS: float = 1
|
||||
|
||||
# Related to Text
|
||||
START_X = 30
|
||||
START_Y = 20
|
||||
NORMAL = "NORMAL"
|
||||
ITALIC = "ITALIC"
|
||||
OBLIQUE = "OBLIQUE"
|
||||
BOLD = "BOLD"
|
||||
NORMAL: str = "NORMAL"
|
||||
ITALIC: str = "ITALIC"
|
||||
OBLIQUE: str = "OBLIQUE"
|
||||
BOLD: str = "BOLD"
|
||||
|
||||
DEFAULT_STROKE_WIDTH = 4
|
||||
DEFAULT_STROKE_WIDTH: float = manim_config.vmobject.default_stroke_width
|
||||
|
||||
# Colors
|
||||
BLUE_E = "#1C758A"
|
||||
BLUE_D = "#29ABCA"
|
||||
BLUE_C = "#58C4DD"
|
||||
BLUE_B = "#9CDCEB"
|
||||
BLUE_A = "#C7E9F1"
|
||||
TEAL_E = "#49A88F"
|
||||
TEAL_D = "#55C1A7"
|
||||
TEAL_C = "#5CD0B3"
|
||||
TEAL_B = "#76DDC0"
|
||||
TEAL_A = "#ACEAD7"
|
||||
GREEN_E = "#699C52"
|
||||
GREEN_D = "#77B05D"
|
||||
GREEN_C = "#83C167"
|
||||
GREEN_B = "#A6CF8C"
|
||||
GREEN_A = "#C9E2AE"
|
||||
YELLOW_E = "#E8C11C"
|
||||
YELLOW_D = "#F4D345"
|
||||
YELLOW_C = "#FFFF00"
|
||||
YELLOW_B = "#FFEA94"
|
||||
YELLOW_A = "#FFF1B6"
|
||||
GOLD_E = "#C78D46"
|
||||
GOLD_D = "#E1A158"
|
||||
GOLD_C = "#F0AC5F"
|
||||
GOLD_B = "#F9B775"
|
||||
GOLD_A = "#F7C797"
|
||||
RED_E = "#CF5044"
|
||||
RED_D = "#E65A4C"
|
||||
RED_C = "#FC6255"
|
||||
RED_B = "#FF8080"
|
||||
RED_A = "#F7A1A3"
|
||||
MAROON_E = "#94424F"
|
||||
MAROON_D = "#A24D61"
|
||||
MAROON_C = "#C55F73"
|
||||
MAROON_B = "#EC92AB"
|
||||
MAROON_A = "#ECABC1"
|
||||
PURPLE_E = "#644172"
|
||||
PURPLE_D = "#715582"
|
||||
PURPLE_C = "#9A72AC"
|
||||
PURPLE_B = "#B189C6"
|
||||
PURPLE_A = "#CAA3E8"
|
||||
GREY_E = "#222222"
|
||||
GREY_D = "#444444"
|
||||
GREY_C = "#888888"
|
||||
GREY_B = "#BBBBBB"
|
||||
GREY_A = "#DDDDDD"
|
||||
WHITE = "#FFFFFF"
|
||||
BLACK = "#000000"
|
||||
GREY_BROWN = "#736357"
|
||||
DARK_BROWN = "#8B4513"
|
||||
LIGHT_BROWN = "#CD853F"
|
||||
PINK = "#D147BD"
|
||||
LIGHT_PINK = "#DC75CD"
|
||||
GREEN_SCREEN = "#00FF00"
|
||||
ORANGE = "#FF862F"
|
||||
BLUE_E: ManimColor = manim_config.colors.blue_e
|
||||
BLUE_D: ManimColor = manim_config.colors.blue_d
|
||||
BLUE_C: ManimColor = manim_config.colors.blue_c
|
||||
BLUE_B: ManimColor = manim_config.colors.blue_b
|
||||
BLUE_A: ManimColor = manim_config.colors.blue_a
|
||||
TEAL_E: ManimColor = manim_config.colors.teal_e
|
||||
TEAL_D: ManimColor = manim_config.colors.teal_d
|
||||
TEAL_C: ManimColor = manim_config.colors.teal_c
|
||||
TEAL_B: ManimColor = manim_config.colors.teal_b
|
||||
TEAL_A: ManimColor = manim_config.colors.teal_a
|
||||
GREEN_E: ManimColor = manim_config.colors.green_e
|
||||
GREEN_D: ManimColor = manim_config.colors.green_d
|
||||
GREEN_C: ManimColor = manim_config.colors.green_c
|
||||
GREEN_B: ManimColor = manim_config.colors.green_b
|
||||
GREEN_A: ManimColor = manim_config.colors.green_a
|
||||
YELLOW_E: ManimColor = manim_config.colors.yellow_e
|
||||
YELLOW_D: ManimColor = manim_config.colors.yellow_d
|
||||
YELLOW_C: ManimColor = manim_config.colors.yellow_c
|
||||
YELLOW_B: ManimColor = manim_config.colors.yellow_b
|
||||
YELLOW_A: ManimColor = manim_config.colors.yellow_a
|
||||
GOLD_E: ManimColor = manim_config.colors.gold_e
|
||||
GOLD_D: ManimColor = manim_config.colors.gold_d
|
||||
GOLD_C: ManimColor = manim_config.colors.gold_c
|
||||
GOLD_B: ManimColor = manim_config.colors.gold_b
|
||||
GOLD_A: ManimColor = manim_config.colors.gold_a
|
||||
RED_E: ManimColor = manim_config.colors.red_e
|
||||
RED_D: ManimColor = manim_config.colors.red_d
|
||||
RED_C: ManimColor = manim_config.colors.red_c
|
||||
RED_B: ManimColor = manim_config.colors.red_b
|
||||
RED_A: ManimColor = manim_config.colors.red_a
|
||||
MAROON_E: ManimColor = manim_config.colors.maroon_e
|
||||
MAROON_D: ManimColor = manim_config.colors.maroon_d
|
||||
MAROON_C: ManimColor = manim_config.colors.maroon_c
|
||||
MAROON_B: ManimColor = manim_config.colors.maroon_b
|
||||
MAROON_A: ManimColor = manim_config.colors.maroon_a
|
||||
PURPLE_E: ManimColor = manim_config.colors.purple_e
|
||||
PURPLE_D: ManimColor = manim_config.colors.purple_d
|
||||
PURPLE_C: ManimColor = manim_config.colors.purple_c
|
||||
PURPLE_B: ManimColor = manim_config.colors.purple_b
|
||||
PURPLE_A: ManimColor = manim_config.colors.purple_a
|
||||
GREY_E: ManimColor = manim_config.colors.grey_e
|
||||
GREY_D: ManimColor = manim_config.colors.grey_d
|
||||
GREY_C: ManimColor = manim_config.colors.grey_c
|
||||
GREY_B: ManimColor = manim_config.colors.grey_b
|
||||
GREY_A: ManimColor = manim_config.colors.grey_a
|
||||
WHITE: ManimColor = manim_config.colors.white
|
||||
BLACK: ManimColor = manim_config.colors.black
|
||||
GREY_BROWN: ManimColor = manim_config.colors.grey_brown
|
||||
DARK_BROWN: ManimColor = manim_config.colors.dark_brown
|
||||
LIGHT_BROWN: ManimColor = manim_config.colors.light_brown
|
||||
PINK: ManimColor = manim_config.colors.pink
|
||||
LIGHT_PINK: ManimColor = manim_config.colors.light_pink
|
||||
GREEN_SCREEN: ManimColor = manim_config.colors.green_screen
|
||||
ORANGE: ManimColor = manim_config.colors.orange
|
||||
|
||||
MANIM_COLORS: List[ManimColor] = list(manim_config.colors.values())
|
||||
|
||||
# Abbreviated names for the "median" colors
|
||||
BLUE = BLUE_C
|
||||
TEAL = TEAL_C
|
||||
GREEN = GREEN_C
|
||||
YELLOW = YELLOW_C
|
||||
GOLD = GOLD_C
|
||||
RED = RED_C
|
||||
MAROON = MAROON_C
|
||||
PURPLE = PURPLE_C
|
||||
GREY = GREY_C
|
||||
BLUE: ManimColor = BLUE_C
|
||||
TEAL: ManimColor = TEAL_C
|
||||
GREEN: ManimColor = GREEN_C
|
||||
YELLOW: ManimColor = YELLOW_C
|
||||
GOLD: ManimColor = GOLD_C
|
||||
RED: ManimColor = RED_C
|
||||
MAROON: ManimColor = MAROON_C
|
||||
PURPLE: ManimColor = PURPLE_C
|
||||
GREY: ManimColor = GREY_C
|
||||
|
||||
COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED]
|
||||
COLORMAP_3B1B: List[ManimColor] = [BLUE_E, GREEN, YELLOW, RED]
|
||||
|
||||
@@ -1,58 +1,175 @@
|
||||
# This file determines the default configuration for how manim is
|
||||
# run, including names for directories it will write to, default
|
||||
# parameters for various classes, style choices, etc. To customize
|
||||
# your own, create a custom_config.yml file in whatever directory
|
||||
# you are running manim. For 3blue1brown, for instance, mind is
|
||||
# here: https://github.com/3b1b/videos/blob/master/custom_config.yml
|
||||
|
||||
# Alternatively, you can create it whereever you like, and on running
|
||||
# manim, pass in `--config_file /path/to/custom/config/file.yml`
|
||||
|
||||
directories:
|
||||
# Set this to true if you want the path to video files
|
||||
# to match the directory structure of the path to the
|
||||
# sourcecode generating that video
|
||||
mirror_module_path: False
|
||||
# Where should manim output video and image files?
|
||||
output: ""
|
||||
# If you want to use images, manim will look to these folders to find them
|
||||
raster_images: ""
|
||||
vector_images: ""
|
||||
# If you want to use sounds, manim will look here to find it.
|
||||
sounds: ""
|
||||
# Manim often generates tex_files or other kinds of serialized data
|
||||
# to keep from having to generate the same thing too many times. By
|
||||
# default, these will be stored at tempfile.gettempdir(), e.g. this might
|
||||
# return whatever is at to the TMPDIR environment variable. If you want to
|
||||
# specify them elsewhere,
|
||||
temporary_storage: ""
|
||||
tex:
|
||||
executable: "latex"
|
||||
template_file: "tex_template.tex"
|
||||
intermediate_filetype: "dvi"
|
||||
text_to_replace: "[tex_expression]"
|
||||
# For ctex, use the following configuration
|
||||
# executable: "xelatex -no-pdf"
|
||||
# template_file: "ctex_template.tex"
|
||||
# intermediate_filetype: "xdv"
|
||||
universal_import_line: "from manimlib import *"
|
||||
style:
|
||||
font: "Consolas"
|
||||
# Manim may write to and read from teh file system, e.g.
|
||||
# to render videos and to look for svg/png assets. This
|
||||
# will specify where those assets live, with a base directory,
|
||||
# and various subdirectory names within it
|
||||
base: ""
|
||||
subdirs:
|
||||
# Where should manim output video and image files?
|
||||
output: "videos"
|
||||
# If you want to use images, manim will look to these folders to find them
|
||||
raster_images: "raster_images"
|
||||
vector_images: "vector_images"
|
||||
# If you want to use sounds, manim will look here to find it.
|
||||
sounds: "sounds"
|
||||
# Place for other forms of data relevant to any projects, like csv's
|
||||
data: "data"
|
||||
# When downloading, say an image, where will it go?
|
||||
downloads: "downloads"
|
||||
# For certain object types, especially Tex and Text, manim will save information
|
||||
# to file to prevent the need to re-compute, e.g. recompiling the latex. By default,
|
||||
# it stores this saved data to whatever directory appdirs.user_cache_dir("manim") returns,
|
||||
# but here a user can specify a different cache location
|
||||
cache: ""
|
||||
window:
|
||||
# The position of window on screen. UR -> Upper Right, and likewise DL -> Down and Left,
|
||||
# UO would be upper middle, etc.
|
||||
position_string: UR
|
||||
# If using multiple monitors, which one should show the window
|
||||
monitor_index: 0
|
||||
# If not full screen, the default to give it half the screen width
|
||||
full_screen: False
|
||||
# Other optional specifications that override the above include:
|
||||
# position: (500, 500) # Specific position, in pixel coordiantes, for upper right corner
|
||||
# size: (1920, 1080) # Specific size, in pixels
|
||||
camera:
|
||||
resolution: (1920, 1080)
|
||||
background_color: "#333333"
|
||||
# Set the position of preview window, you can use directions, e.g. UL/DR/OL/OO/...
|
||||
# also, you can also specify the position(pixel) of the upper left corner of
|
||||
# the window on the monitor, e.g. "960,540"
|
||||
window_position: UR
|
||||
window_monitor: 0
|
||||
full_screen: False
|
||||
# If break_into_partial_movies is set to True, then many small
|
||||
# files will be written corresponding to each Scene.play and
|
||||
# Scene.wait call, and these files will then be combined
|
||||
# to form the full scene. Sometimes video-editing is made
|
||||
# easier when working with the broken up scene, which
|
||||
# effectively has cuts at all the places you might want.
|
||||
break_into_partial_movies: False
|
||||
camera_qualities:
|
||||
low:
|
||||
resolution: "854x480"
|
||||
frame_rate: 15
|
||||
medium:
|
||||
resolution: "1280x720"
|
||||
frame_rate: 30
|
||||
high:
|
||||
resolution: "1920x1080"
|
||||
frame_rate: 30
|
||||
ultra_high:
|
||||
resolution: "3840x2160"
|
||||
frame_rate: 60
|
||||
default_quality: "high"
|
||||
fps: 30
|
||||
background_opacity: 1.0
|
||||
file_writer:
|
||||
# What command to use for ffmpeg
|
||||
ffmpeg_bin: "ffmpeg"
|
||||
# Parameters to pass into ffmpeg
|
||||
video_codec: "libx264"
|
||||
pixel_format: "yuv420p"
|
||||
saturation: 1.0
|
||||
gamma: 1.0
|
||||
# Most of the scene configuration will come from CLI arguments,
|
||||
# but defaults can be set here
|
||||
scene:
|
||||
show_animation_progress: False
|
||||
leave_progress_bars: False
|
||||
# When skipping animations, should a single frame be rendered
|
||||
# at the end of each play call?
|
||||
preview_while_skipping: True
|
||||
# How long does a scene pause on Scene.wait calls
|
||||
default_wait_time: 1.0
|
||||
vmobject:
|
||||
default_stroke_width: 4.0
|
||||
tex:
|
||||
# See tex_templates.yml
|
||||
template: "default"
|
||||
text:
|
||||
font: "Consolas"
|
||||
alignment: "LEFT"
|
||||
embed:
|
||||
exception_mode: "Verbose"
|
||||
autoreload: False
|
||||
resolution_options:
|
||||
# When the user passes in -l, -m, --hd or --uhd, these are the corresponding
|
||||
# resolutions
|
||||
low: (854, 480)
|
||||
med: (1280, 720)
|
||||
high: (1920, 1080)
|
||||
4k: (3840, 2160)
|
||||
sizes:
|
||||
# This determines the scale of the manim coordinate system with respect to
|
||||
# the viewing frame
|
||||
frame_height: 8.0
|
||||
# These determine the constants SMALL_BUFF, MED_SMALL_BUFF, etc., useful
|
||||
# for nudging things around and having default spacing values
|
||||
small_buff: 0.1
|
||||
med_small_buff: 0.25
|
||||
med_large_buff: 0.5
|
||||
large_buff: 1.0
|
||||
# Default buffers used in Mobject.next_to or Mobject.to_edge
|
||||
default_mobject_to_edge_buff: 0.5
|
||||
default_mobject_to_mobject_buff: 0.25
|
||||
key_bindings:
|
||||
pan_3d: 'd'
|
||||
pan: 'f'
|
||||
reset: 'r'
|
||||
quit: 'q' # Together with command
|
||||
select: 's'
|
||||
unselect: 'u'
|
||||
grab: 'g'
|
||||
x_grab: 'h'
|
||||
y_grab: 'v'
|
||||
resize: 't'
|
||||
color: 'c'
|
||||
information: 'i'
|
||||
cursor: 'k'
|
||||
colors:
|
||||
blue_e: "#1C758A"
|
||||
blue_d: "#29ABCA"
|
||||
blue_c: "#58C4DD"
|
||||
blue_b: "#9CDCEB"
|
||||
blue_a: "#C7E9F1"
|
||||
teal_e: "#49A88F"
|
||||
teal_d: "#55C1A7"
|
||||
teal_c: "#5CD0B3"
|
||||
teal_b: "#76DDC0"
|
||||
teal_a: "#ACEAD7"
|
||||
green_e: "#699C52"
|
||||
green_d: "#77B05D"
|
||||
green_c: "#83C167"
|
||||
green_b: "#A6CF8C"
|
||||
green_a: "#C9E2AE"
|
||||
yellow_e: "#E8C11C"
|
||||
yellow_d: "#F4D345"
|
||||
yellow_c: "#FFFF00"
|
||||
yellow_b: "#FFEA94"
|
||||
yellow_a: "#FFF1B6"
|
||||
gold_e: "#C78D46"
|
||||
gold_d: "#E1A158"
|
||||
gold_c: "#F0AC5F"
|
||||
gold_b: "#F9B775"
|
||||
gold_a: "#F7C797"
|
||||
red_e: "#CF5044"
|
||||
red_d: "#E65A4C"
|
||||
red_c: "#FC6255"
|
||||
red_b: "#FF8080"
|
||||
red_a: "#F7A1A3"
|
||||
maroon_e: "#94424F"
|
||||
maroon_d: "#A24D61"
|
||||
maroon_c: "#C55F73"
|
||||
maroon_b: "#EC92AB"
|
||||
maroon_a: "#ECABC1"
|
||||
purple_e: "#644172"
|
||||
purple_d: "#715582"
|
||||
purple_c: "#9A72AC"
|
||||
purple_b: "#B189C6"
|
||||
purple_a: "#CAA3E8"
|
||||
grey_e: "#222222"
|
||||
grey_d: "#444444"
|
||||
grey_c: "#888888"
|
||||
grey_b: "#BBBBBB"
|
||||
grey_a: "#DDDDDD"
|
||||
white: "#FFFFFF"
|
||||
black: "#000000"
|
||||
grey_brown: "#736357"
|
||||
dark_brown: "#8B4513"
|
||||
light_brown: "#CD853F"
|
||||
pink: "#D147BD"
|
||||
light_pink: "#DC75CD"
|
||||
green_screen: "#00FF00"
|
||||
orange: "#FF862F"
|
||||
# Can be DEBUG / INFO / WARNING / ERROR / CRITICAL
|
||||
log_level: "INFO"
|
||||
universal_import_line: "from manimlib import *"
|
||||
ignore_manimlib_modules_on_reload: True
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.event_handler.event_listner import EventListener
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
from manimlib.event_handler.event_listner import EventListner
|
||||
|
||||
|
||||
class EventDispatcher(object):
|
||||
def __init__(self):
|
||||
self.event_listners = {
|
||||
self.event_listners: dict[
|
||||
EventType, list[EventListener]
|
||||
] = {
|
||||
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[EventListener] = []
|
||||
|
||||
def add_listner(self, event_listner):
|
||||
assert(isinstance(event_listner, EventListner))
|
||||
def add_listner(self, event_listner: EventListener):
|
||||
assert isinstance(event_listner, EventListener)
|
||||
self.event_listners[event_listner.event_type].append(event_listner)
|
||||
return self
|
||||
|
||||
def remove_listner(self, event_listner):
|
||||
assert(isinstance(event_listner, EventListner))
|
||||
def remove_listner(self, event_listner: EventListener):
|
||||
assert isinstance(event_listner, EventListener)
|
||||
try:
|
||||
while event_listner in self.event_listners[event_listner.event_type]:
|
||||
self.event_listners[event_listner.event_type].remove(event_listner)
|
||||
@@ -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:
|
||||
@@ -53,7 +56,7 @@ class EventDispatcher(object):
|
||||
|
||||
if event_type == EventType.MouseDragEvent:
|
||||
for listner in self.draggable_object_listners:
|
||||
assert(isinstance(listner, EventListner))
|
||||
assert isinstance(listner, EventListener)
|
||||
propagate_event = listner.callback(listner.mobject, event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return propagate_event
|
||||
@@ -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,21 @@
|
||||
class EventListner(object):
|
||||
def __init__(self, mobject, event_type, event_callback):
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class EventListener(object):
|
||||
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,15 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import sys
|
||||
import copy
|
||||
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.module_loader import ModuleLoader
|
||||
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.logger import log
|
||||
from manimlib.scene.interactive_scene import InteractiveScene
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
Module = importlib.util.types.ModuleType
|
||||
from typing import Optional
|
||||
from addict import Dict
|
||||
|
||||
|
||||
class BlankScene(Scene):
|
||||
class BlankScene(InteractiveScene):
|
||||
def construct(self):
|
||||
exec(get_custom_config()["universal_import_line"])
|
||||
exec(manim_config.universal_import_line)
|
||||
self.embed()
|
||||
|
||||
|
||||
@@ -33,11 +44,7 @@ def prompt_user_for_choice(scene_classes):
|
||||
print(f"{str(idx).zfill(max_digits)}: {name}")
|
||||
name_to_class[name] = scene_class
|
||||
try:
|
||||
user_input = input(
|
||||
"\nThat module has multiple scenes, "
|
||||
"which ones would you like to render?"
|
||||
"\nScene Name or Number: "
|
||||
)
|
||||
user_input = input("\nSelect which scene to render (by name or number): ")
|
||||
return [
|
||||
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str) - 1]
|
||||
for split_str in user_input.replace(" ", "").split(",")
|
||||
@@ -52,70 +59,59 @@ def prompt_user_for_choice(scene_classes):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_scene_config(config):
|
||||
return dict([
|
||||
(key, config[key])
|
||||
for key in [
|
||||
"window_config",
|
||||
"camera_config",
|
||||
"file_writer_config",
|
||||
"skip_animations",
|
||||
"start_at_animation_number",
|
||||
"end_at_animation_number",
|
||||
"leave_progress_bars",
|
||||
"preview",
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
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.
|
||||
errors to be exposed preemptively for long running scenes.
|
||||
"""
|
||||
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"]["save_last_frame"] = False
|
||||
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"])
|
||||
return int(total_time * manim_config.camera.fps)
|
||||
|
||||
|
||||
def get_scenes_to_render(scene_classes, scene_config, config):
|
||||
if config["write_all"]:
|
||||
return [sc(**scene_config) for sc in scene_classes]
|
||||
def scene_from_class(scene_class, scene_config: Dict, run_config: Dict):
|
||||
fw_config = manim_config.file_writer
|
||||
if fw_config.write_to_movie and run_config.prerun:
|
||||
scene_config.file_writer_config.total_frames = compute_total_frames(scene_class, scene_config)
|
||||
return scene_class(**scene_config)
|
||||
|
||||
result = []
|
||||
for scene_name in config["scene_names"]:
|
||||
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 != ""):
|
||||
log.error(f"No scene named {scene_name} found")
|
||||
if result:
|
||||
return result
|
||||
if len(scene_classes) == 1:
|
||||
result = [scene_classes[0]]
|
||||
|
||||
def note_missing_scenes(arg_names, module_names):
|
||||
for name in arg_names:
|
||||
if name not in module_names:
|
||||
log.error(f"No scene named {name} found")
|
||||
|
||||
|
||||
def get_scenes_to_render(all_scene_classes: list, scene_config: Dict, run_config: Dict):
|
||||
if run_config["write_all"] or len(all_scene_classes) == 1:
|
||||
classes_to_run = all_scene_classes
|
||||
else:
|
||||
result = prompt_user_for_choice(scene_classes)
|
||||
return [scene_class(**scene_config) for scene_class in result]
|
||||
name_to_class = {sc.__name__: sc for sc in all_scene_classes}
|
||||
classes_to_run = [name_to_class.get(name) for name in run_config.scene_names]
|
||||
classes_to_run = list(filter(lambda x: x, classes_to_run)) # Remove Nones
|
||||
note_missing_scenes(run_config.scene_names, name_to_class.keys())
|
||||
|
||||
if len(classes_to_run) == 0:
|
||||
classes_to_run = prompt_user_for_choice(all_scene_classes)
|
||||
|
||||
return [
|
||||
scene_from_class(scene_class, scene_config, run_config)
|
||||
for scene_class in classes_to_run
|
||||
]
|
||||
|
||||
|
||||
def get_scene_classes_from_module(module):
|
||||
def get_scene_classes(module: Optional[Module]):
|
||||
if module is None:
|
||||
# If no module was passed in, just play the blank scene
|
||||
return [BlankScene]
|
||||
if hasattr(module, "SCENES_IN_ORDER"):
|
||||
return module.SCENES_IN_ORDER
|
||||
else:
|
||||
@@ -128,13 +124,55 @@ def get_scene_classes_from_module(module):
|
||||
]
|
||||
|
||||
|
||||
def main(config):
|
||||
module = config["module"]
|
||||
scene_config = get_scene_config(config)
|
||||
if module is None:
|
||||
# If no module was passed in, just play the blank scene
|
||||
return [BlankScene(**scene_config)]
|
||||
def get_indent(code_lines: list[str], line_number: int) -> str:
|
||||
"""
|
||||
Find the indent associated with a given line of python code,
|
||||
as a string of spaces
|
||||
"""
|
||||
# Find most recent non-empty line
|
||||
try:
|
||||
line = next(filter(lambda line: line.strip(), code_lines[line_number - 1::-1]))
|
||||
except StopIteration:
|
||||
return ""
|
||||
|
||||
all_scene_classes = get_scene_classes_from_module(module)
|
||||
scenes = get_scenes_to_render(all_scene_classes, scene_config, config)
|
||||
# Either return its leading spaces, or add for if it ends with colon
|
||||
n_spaces = len(line) - len(line.lstrip())
|
||||
if line.endswith(":"):
|
||||
n_spaces += 4
|
||||
return n_spaces * " "
|
||||
|
||||
|
||||
def insert_embed_line_to_module(module: Module, line_number: int):
|
||||
"""
|
||||
This is hacky, but convenient. When user includes the argument "-e", it will try
|
||||
to recreate a file that inserts the line `self.embed()` into the end of the scene's
|
||||
construct method. If there is an argument passed in, it will insert the line after
|
||||
the last line in the sourcefile which includes that string.
|
||||
"""
|
||||
lines = inspect.getsource(module).splitlines()
|
||||
|
||||
# Add the relevant embed line to the code
|
||||
indent = get_indent(lines, line_number)
|
||||
lines.insert(line_number, indent + "self.embed()")
|
||||
new_code = "\n".join(lines)
|
||||
|
||||
# Execute the code, which presumably redefines the user's
|
||||
# scene to include this embed line, within the relevant module.
|
||||
code_object = compile(new_code, module.__name__, 'exec')
|
||||
exec(code_object, module.__dict__)
|
||||
|
||||
|
||||
def get_module(file_name: Optional[str], embed_line: Optional[int], is_reload: bool = False) -> Module:
|
||||
module = ModuleLoader.get_module(file_name, is_reload)
|
||||
if embed_line:
|
||||
insert_embed_line_to_module(module, embed_line)
|
||||
return module
|
||||
|
||||
|
||||
def main(scene_config: Dict, run_config: Dict):
|
||||
module = get_module(run_config.file_name, run_config.embed_line, run_config.is_reload)
|
||||
all_scene_classes = get_scene_classes(module)
|
||||
scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config)
|
||||
if len(scenes) == 0:
|
||||
print("No scenes found to run")
|
||||
return scenes
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
|
||||
from rich.logging import RichHandler
|
||||
|
||||
__all__ = ["log"]
|
||||
@@ -10,4 +11,3 @@ logging.basicConfig(
|
||||
)
|
||||
|
||||
log = logging.getLogger("manimgl")
|
||||
log.setLevel("DEBUG")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pathops
|
||||
|
||||
@@ -5,23 +7,26 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
# Boolean operations between 2D mobjects
|
||||
# Borrowed from from https://github.com/ManimCommunity/manim/
|
||||
# Borrowed from https://github.com/ManimCommunity/manim/
|
||||
|
||||
def _convert_vmobject_to_skia_path(vmobject):
|
||||
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()
|
||||
for submob in vmobject.family_members_with_points():
|
||||
for subpath in submob.get_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_equal(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
return path
|
||||
|
||||
|
||||
def _convert_skia_path_to_vmobject(path, vmobject):
|
||||
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:
|
||||
@@ -41,11 +46,11 @@ def _convert_skia_path_to_vmobject(path, vmobject):
|
||||
vmobject.add_quadratic_bezier_curve_to(*points)
|
||||
else:
|
||||
raise Exception(f"Unsupported: {path_verb}")
|
||||
return vmobject
|
||||
return vmobject.reverse_points()
|
||||
|
||||
|
||||
class Union(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Union.")
|
||||
super().__init__(**kwargs)
|
||||
@@ -59,7 +64,7 @@ class Union(VMobject):
|
||||
|
||||
|
||||
class Difference(VMobject):
|
||||
def __init__(self, subject, clip, **kwargs):
|
||||
def __init__(self, subject: VMobject, clip: VMobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
pathops.difference(
|
||||
@@ -71,7 +76,7 @@ class Difference(VMobject):
|
||||
|
||||
|
||||
class Intersection(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Intersection.")
|
||||
super().__init__(**kwargs)
|
||||
@@ -94,7 +99,7 @@ class Intersection(VMobject):
|
||||
|
||||
|
||||
class Exclusion(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Exclusion.")
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -1,29 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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.constants import BLUE_B, BLUE_D, BLUE_E, GREY_BROWN, 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.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, List, Iterable
|
||||
from manimlib.typing import ManimColor, Vect3, Self
|
||||
|
||||
|
||||
class AnimatedBoundary(VGroup):
|
||||
CONFIG = {
|
||||
"colors": [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN],
|
||||
"max_stroke_width": 3,
|
||||
"cycle_rate": 0.5,
|
||||
"back_and_forth": True,
|
||||
"draw_rate_func": smooth,
|
||||
"fade_rate_func": smooth,
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
colors: List[ManimColor] = [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN],
|
||||
max_stroke_width: float = 3.0,
|
||||
cycle_rate: float = 0.5,
|
||||
back_and_forth: bool = True,
|
||||
draw_rate_func: Callable[[float], float] = smooth,
|
||||
fade_rate_func: Callable[[float], float] = smooth,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.vmobject = vmobject
|
||||
self.boundary_copies = [
|
||||
self.vmobject: VMobject = vmobject
|
||||
self.colors = colors
|
||||
self.max_stroke_width = max_stroke_width
|
||||
self.cycle_rate = cycle_rate
|
||||
self.back_and_forth = back_and_forth
|
||||
self.draw_rate_func = draw_rate_func
|
||||
self.fade_rate_func = fade_rate_func
|
||||
|
||||
self.boundary_copies: list[VMobject] = [
|
||||
vmobject.copy().set_style(
|
||||
stroke_width=0,
|
||||
fill_opacity=0
|
||||
@@ -31,12 +44,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) -> Self:
|
||||
# Not actual time, but something which passes at
|
||||
# an altered rate to make the implementation below
|
||||
# cleaner
|
||||
@@ -66,8 +79,15 @@ class AnimatedBoundary(VGroup):
|
||||
)
|
||||
|
||||
self.total_time += dt
|
||||
return self
|
||||
|
||||
def full_family_become_partial(self, mob1, mob2, a, b):
|
||||
def full_family_become_partial(
|
||||
self,
|
||||
mob1: VMobject,
|
||||
mob2: VMobject,
|
||||
a: float,
|
||||
b: float
|
||||
) -> Self:
|
||||
family1 = mob1.family_members_with_points()
|
||||
family2 = mob2.family_members_with_points()
|
||||
for sm1, sm2 in zip(family1, family2):
|
||||
@@ -76,22 +96,25 @@ class AnimatedBoundary(VGroup):
|
||||
|
||||
|
||||
class TracedPath(VMobject):
|
||||
CONFIG = {
|
||||
"stroke_width": 2,
|
||||
"stroke_color": WHITE,
|
||||
"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[[], Vect3],
|
||||
time_traced: float = np.inf,
|
||||
time_per_anchor: float = 1.0 / 15,
|
||||
stroke_width: float | Iterable[float] = 2.0,
|
||||
stroke_color: ManimColor = WHITE,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.time = 0
|
||||
self.traced_points = []
|
||||
self.time_traced = time_traced
|
||||
self.time_per_anchor = time_per_anchor
|
||||
self.time: float = 0
|
||||
self.traced_points: list[np.ndarray] = []
|
||||
self.add_updater(lambda m, dt: m.update_path(dt))
|
||||
self.set_stroke(stroke_color, stroke_width)
|
||||
|
||||
def update_path(self, dt):
|
||||
def update_path(self, dt: float) -> Self:
|
||||
if dt == 0:
|
||||
return self
|
||||
point = self.traced_point_func().copy()
|
||||
@@ -99,23 +122,15 @@ class TracedPath(VMobject):
|
||||
|
||||
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:
|
||||
# sparseness = max(int(self.time_per_anchor / dt), 1)
|
||||
# points = self.traced_points[::sparseness]
|
||||
# points[-1] = self.traced_points[-1]
|
||||
points = self.traced_points
|
||||
|
||||
if points:
|
||||
@@ -126,16 +141,25 @@ class TracedPath(VMobject):
|
||||
|
||||
|
||||
class TracingTail(TracedPath):
|
||||
CONFIG = {
|
||||
"stroke_width": (0, 3),
|
||||
"stroke_opacity": (0, 1),
|
||||
"stroke_color": WHITE,
|
||||
"time_traced": 1.0,
|
||||
}
|
||||
|
||||
def __init__(self, mobject_or_func, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject_or_func: Mobject | Callable[[], np.ndarray],
|
||||
time_traced: float = 1.0,
|
||||
stroke_width: float | Iterable[float] = (0, 3),
|
||||
stroke_opacity: float | Iterable[float] = (0, 1),
|
||||
stroke_color: ManimColor = WHITE,
|
||||
**kwargs
|
||||
):
|
||||
if isinstance(mobject_or_func, Mobject):
|
||||
func = mobject_or_func.get_center
|
||||
else:
|
||||
func = mobject_or_func
|
||||
super().__init__(func, **kwargs)
|
||||
super().__init__(
|
||||
func,
|
||||
time_traced=time_traced,
|
||||
stroke_width=stroke_width,
|
||||
stroke_opacity=stroke_opacity,
|
||||
stroke_color=stroke_color,
|
||||
**kwargs
|
||||
)
|
||||
self.add_updater(lambda m: m.set_stroke(width=stroke_width, opacity=stroke_opacity))
|
||||
|
||||
@@ -1,142 +1,220 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import numbers
|
||||
|
||||
from manimlib.constants import *
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
|
||||
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
|
||||
from manimlib.constants import DEG, PI
|
||||
from manimlib.constants import DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
|
||||
from manimlib.mobject.functions import ParametricCurve
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import DashedLine
|
||||
from manimlib.mobject.geometry import Line
|
||||
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.dot_cloud import DotCloud
|
||||
from manimlib.mobject.types.surface import ParametricSurface
|
||||
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.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.bezier import inverse_interpolate
|
||||
from manimlib.utils.dict_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 manimlib.utils.space_ops import normalize
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, Sequence, Type, TypeVar, Optional
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier, Self
|
||||
|
||||
T = TypeVar("T", bound=Mobject)
|
||||
|
||||
|
||||
EPSILON = 1e-8
|
||||
DEFAULT_X_RANGE = (-8.0, 8.0, 1.0)
|
||||
DEFAULT_Y_RANGE = (-4.0, 4.0, 1.0)
|
||||
|
||||
|
||||
class CoordinateSystem():
|
||||
def full_range_specifier(range_args):
|
||||
if len(range_args) == 2:
|
||||
return (*range_args, 1)
|
||||
return range_args
|
||||
|
||||
|
||||
class CoordinateSystem(ABC):
|
||||
"""
|
||||
Abstract class for Axes and NumberPlane
|
||||
"""
|
||||
CONFIG = {
|
||||
"dimension": 2,
|
||||
"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,
|
||||
}
|
||||
dimension: int = 2
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.x_range = np.array(self.default_x_range)
|
||||
self.y_range = np.array(self.default_y_range)
|
||||
def __init__(
|
||||
self,
|
||||
x_range: RangeSpecifier = DEFAULT_X_RANGE,
|
||||
y_range: RangeSpecifier = DEFAULT_Y_RANGE,
|
||||
num_sampled_graph_points_per_tick: int = 5,
|
||||
):
|
||||
self.x_range = full_range_specifier(x_range)
|
||||
self.y_range = full_range_specifier(y_range)
|
||||
self.num_sampled_graph_points_per_tick = num_sampled_graph_points_per_tick
|
||||
|
||||
def coords_to_point(self, *coords):
|
||||
@abstractmethod
|
||||
def coords_to_point(self, *coords: float | VectN) -> Vect3 | Vect3Array:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def point_to_coords(self, point):
|
||||
@abstractmethod
|
||||
def point_to_coords(self, point: Vect3 | Vect3Array) -> tuple[float | VectN, ...]:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def c2p(self, *coords):
|
||||
def c2p(self, *coords: float) -> Vect3 | Vect3Array:
|
||||
"""Abbreviation for coords_to_point"""
|
||||
return self.coords_to_point(*coords)
|
||||
|
||||
def p2c(self, point):
|
||||
def p2c(self, point: Vect3) -> tuple[float | VectN, ...]:
|
||||
"""Abbreviation for point_to_coords"""
|
||||
return self.point_to_coords(point)
|
||||
|
||||
def get_origin(self):
|
||||
def get_origin(self) -> Vect3:
|
||||
return self.c2p(*[0] * self.dimension)
|
||||
|
||||
def get_axes(self):
|
||||
@abstractmethod
|
||||
def get_axes(self) -> VGroup:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def get_all_ranges(self):
|
||||
@abstractmethod
|
||||
def get_all_ranges(self) -> list[np.ndarray]:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def get_axis(self, index):
|
||||
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: Vect3 = RIGHT,
|
||||
direction: Vect3 = 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: Vect3 = UP,
|
||||
direction: Vect3 = 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: Vect3,
|
||||
edge: Vect3,
|
||||
direction: Vect3,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
ensure_on_screen: bool = False
|
||||
) -> Tex:
|
||||
label = Tex(label_tex)
|
||||
label.next_to(
|
||||
axis.get_edge_center(edge), direction,
|
||||
buff=buff
|
||||
)
|
||||
label.shift_onto_screen(buff=MED_SMALL_BUFF)
|
||||
if ensure_on_screen:
|
||||
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: Vect3,
|
||||
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: Vect3, **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: Vect3, **kwargs):
|
||||
return self.get_line_from_axis_to_point(1, point, **kwargs)
|
||||
|
||||
# Useful for graphing
|
||||
def get_graph(self, function, x_range=None, **kwargs):
|
||||
t_range = np.array(self.x_range, dtype=float)
|
||||
if x_range is not None:
|
||||
t_range[:len(x_range)] = x_range
|
||||
def get_graph(
|
||||
self,
|
||||
function: Callable[[float], float],
|
||||
x_range: Sequence[float] | None = None,
|
||||
bind: bool = False,
|
||||
**kwargs
|
||||
) -> ParametricCurve:
|
||||
x_range = x_range or self.x_range
|
||||
t_range = np.ones(3)
|
||||
t_range[:len(x_range)] = x_range
|
||||
# For axes, the third coordinate of x_range indicates
|
||||
# tick frequency. But for functions, it indicates a
|
||||
# sample frequency
|
||||
if x_range is None or len(x_range) < 3:
|
||||
t_range[2] /= self.num_sampled_graph_points_per_tick
|
||||
t_range[2] /= self.num_sampled_graph_points_per_tick
|
||||
|
||||
def parametric_function(t: float) -> Vect3:
|
||||
return self.c2p(t, function(t))
|
||||
|
||||
graph = ParametricCurve(
|
||||
lambda t: self.c2p(t, function(t)),
|
||||
t_range=t_range,
|
||||
parametric_function,
|
||||
t_range=tuple(t_range),
|
||||
**kwargs
|
||||
)
|
||||
graph.underlying_function = function
|
||||
graph.x_range = x_range
|
||||
|
||||
if bind:
|
||||
self.bind_graph_to_func(graph, function)
|
||||
|
||||
return graph
|
||||
|
||||
def get_parametric_curve(self, function, **kwargs):
|
||||
def get_parametric_curve(
|
||||
self,
|
||||
function: Callable[[float], Vect3],
|
||||
**kwargs
|
||||
) -> ParametricCurve:
|
||||
dim = self.dimension
|
||||
graph = ParametricCurve(
|
||||
lambda t: self.coords_to_point(*function(t)[:dim]),
|
||||
@@ -145,7 +223,11 @@ 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
|
||||
) -> Vect3 | None:
|
||||
if hasattr(graph, "underlying_function"):
|
||||
return self.coords_to_point(x, graph.underlying_function(x))
|
||||
else:
|
||||
@@ -162,19 +244,50 @@ class CoordinateSystem():
|
||||
else:
|
||||
return None
|
||||
|
||||
def i2gp(self, x, graph):
|
||||
def i2gp(self, x: float, graph: ParametricCurve) -> Vect3 | 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 bind_graph_to_func(
|
||||
self,
|
||||
graph: VMobject,
|
||||
func: Callable[[VectN], VectN],
|
||||
jagged: bool = False,
|
||||
get_discontinuities: Optional[Callable[[], Vect3]] = None
|
||||
) -> VMobject:
|
||||
"""
|
||||
Use for graphing functions which might change over time, or change with
|
||||
conditions
|
||||
"""
|
||||
x_values = np.array([self.x_axis.p2n(p) for p in graph.get_points()])
|
||||
|
||||
def get_graph_points():
|
||||
xs = x_values
|
||||
if get_discontinuities:
|
||||
ds = get_discontinuities()
|
||||
ep = 1e-6
|
||||
added_xs = it.chain(*((d - ep, d + ep) for d in ds))
|
||||
xs[:] = sorted([*x_values, *added_xs])[:len(x_values)]
|
||||
return self.c2p(xs, func(xs))
|
||||
|
||||
graph.add_updater(
|
||||
lambda g: g.set_points_as_corners(get_graph_points())
|
||||
)
|
||||
if not jagged:
|
||||
graph.add_updater(lambda g: g.make_smooth(approx=True))
|
||||
return graph
|
||||
|
||||
def get_graph_label(
|
||||
self,
|
||||
graph: ParametricCurve,
|
||||
label: str | Mobject = "f(x)",
|
||||
x: float | None = None,
|
||||
direction: Vect3 = RIGHT,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
color: ManimColor | None = None
|
||||
) -> Tex | Mobject:
|
||||
if isinstance(label, str):
|
||||
label = Tex(label)
|
||||
if color is None:
|
||||
@@ -194,45 +307,71 @@ class CoordinateSystem():
|
||||
|
||||
point = self.input_to_graph_point(x, graph)
|
||||
angle = self.angle_of_tangent(x, graph)
|
||||
normal = rotate_vector(RIGHT, angle + 90 * DEGREES)
|
||||
normal = rotate_vector(RIGHT, angle + 90 * DEG)
|
||||
if normal[1] < 0:
|
||||
normal *= -1
|
||||
label.next_to(point, normal, buff=buff)
|
||||
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)
|
||||
|
||||
def get_scatterplot(self,
|
||||
x_values: Vect3Array,
|
||||
y_values: Vect3Array,
|
||||
**dot_config):
|
||||
return DotCloud(self.c2p(x_values, y_values), **dot_config)
|
||||
|
||||
# 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),
|
||||
negative_color: ManimColor = RED,
|
||||
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,6 +380,7 @@ class CoordinateSystem():
|
||||
x_range = [*x_range, dx]
|
||||
|
||||
rects = []
|
||||
x_range[1] = x_range[1] + dx
|
||||
xs = np.arange(*x_range)
|
||||
for x0, x1 in zip(xs, xs[1:]):
|
||||
if input_sample_type == "left":
|
||||
@@ -251,11 +391,13 @@ class CoordinateSystem():
|
||||
sample = 0.5 * x0 + 0.5 * x1
|
||||
else:
|
||||
raise Exception("Invalid input sample type")
|
||||
height = get_norm(
|
||||
self.i2gp(sample, graph) - self.c2p(sample, 0)
|
||||
height_vect = self.i2gp(sample, graph) - self.c2p(sample, 0)
|
||||
rect = Rectangle(
|
||||
width=self.x_axis.n2p(x1)[0] - self.x_axis.n2p(x0)[0],
|
||||
height=get_norm(height_vect),
|
||||
)
|
||||
rect = Rectangle(width=x1 - x0, height=height)
|
||||
rect.move_to(self.c2p(x0, 0), DL)
|
||||
rect.positive = height_vect[1] > 0
|
||||
rect.move_to(self.c2p(x0, 0), DL if rect.positive else UL)
|
||||
rects.append(rect)
|
||||
result = VGroup(*rects)
|
||||
result.set_submobject_colors_by_gradient(*colors)
|
||||
@@ -263,47 +405,76 @@ class CoordinateSystem():
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_behind=stroke_background
|
||||
)
|
||||
for rect in result:
|
||||
if not rect.positive:
|
||||
rect.set_fill(negative_color)
|
||||
return result
|
||||
|
||||
def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=1):
|
||||
# TODO
|
||||
pass
|
||||
def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=0.5):
|
||||
if not hasattr(graph, "x_range"):
|
||||
raise Exception("Argument `graph` must have attribute `x_range`")
|
||||
|
||||
alpha_bounds = [
|
||||
inverse_interpolate(*graph.x_range, x)
|
||||
for x in x_range
|
||||
]
|
||||
sub_graph = graph.copy()
|
||||
sub_graph.pointwise_become_partial(graph, *alpha_bounds)
|
||||
sub_graph.add_line_to(self.c2p(x_range[1], 0))
|
||||
sub_graph.add_line_to(self.c2p(x_range[0], 0))
|
||||
sub_graph.add_line_to(sub_graph.get_start())
|
||||
|
||||
sub_graph.set_stroke(width=0)
|
||||
sub_graph.set_fill(fill_color, fill_opacity)
|
||||
|
||||
return sub_graph
|
||||
|
||||
|
||||
class Axes(VGroup, CoordinateSystem):
|
||||
CONFIG = {
|
||||
"axis_config": {
|
||||
"include_tip": True,
|
||||
"numbers_to_exclude": [0],
|
||||
},
|
||||
"x_axis_config": {},
|
||||
"y_axis_config": {
|
||||
"line_to_number_direction": LEFT,
|
||||
},
|
||||
"height": FRAME_HEIGHT - 2,
|
||||
"width": FRAME_WIDTH - 2,
|
||||
}
|
||||
default_axis_config: dict = dict()
|
||||
default_x_axis_config: dict = dict()
|
||||
default_y_axis_config: dict = dict(line_to_number_direction=LEFT)
|
||||
|
||||
def __init__(self,
|
||||
x_range=None,
|
||||
y_range=None,
|
||||
**kwargs):
|
||||
CoordinateSystem.__init__(self, **kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
x_range: RangeSpecifier = DEFAULT_X_RANGE,
|
||||
y_range: RangeSpecifier = DEFAULT_Y_RANGE,
|
||||
axis_config: dict = dict(),
|
||||
x_axis_config: dict = dict(),
|
||||
y_axis_config: dict = dict(),
|
||||
height: float | None = None,
|
||||
width: float | None = None,
|
||||
unit_size: float = 1.0,
|
||||
**kwargs
|
||||
):
|
||||
CoordinateSystem.__init__(self, x_range, y_range, **kwargs)
|
||||
kwargs.pop("num_sampled_graph_points_per_tick", None)
|
||||
VGroup.__init__(self, **kwargs)
|
||||
|
||||
if x_range is not None:
|
||||
self.x_range[:len(x_range)] = x_range
|
||||
if y_range is not None:
|
||||
self.y_range[:len(y_range)] = y_range
|
||||
|
||||
axis_config = dict(**axis_config, unit_size=unit_size)
|
||||
self.x_axis = self.create_axis(
|
||||
self.x_range, self.x_axis_config, self.width,
|
||||
self.x_range,
|
||||
axis_config=merge_dicts_recursively(
|
||||
self.default_axis_config,
|
||||
self.default_x_axis_config,
|
||||
axis_config,
|
||||
x_axis_config
|
||||
),
|
||||
length=width,
|
||||
)
|
||||
self.y_axis = self.create_axis(
|
||||
self.y_range, self.y_axis_config, self.height
|
||||
self.y_range,
|
||||
axis_config=merge_dicts_recursively(
|
||||
self.default_axis_config,
|
||||
self.default_y_axis_config,
|
||||
axis_config,
|
||||
y_axis_config
|
||||
),
|
||||
length=height,
|
||||
)
|
||||
self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN)
|
||||
self.y_axis.rotate(90 * DEG, about_point=ORIGIN)
|
||||
# Add as a separate group in case various other
|
||||
# mobjects are added to self, as for example in
|
||||
# NumberPlane below
|
||||
@@ -311,118 +482,176 @@ class Axes(VGroup, CoordinateSystem):
|
||||
self.add(*self.axes)
|
||||
self.center()
|
||||
|
||||
def create_axis(self, range_terms, axis_config, length):
|
||||
new_config = merge_dicts_recursively(self.axis_config, axis_config)
|
||||
new_config["width"] = length
|
||||
axis = NumberLine(range_terms, **new_config)
|
||||
def create_axis(
|
||||
self,
|
||||
range_terms: RangeSpecifier,
|
||||
axis_config: dict,
|
||||
length: float | None
|
||||
) -> NumberLine:
|
||||
axis = NumberLine(range_terms, width=length, **axis_config)
|
||||
axis.shift(-axis.n2p(0))
|
||||
return axis
|
||||
|
||||
def coords_to_point(self, *coords):
|
||||
def coords_to_point(self, *coords: float | VectN) -> Vect3 | Vect3Array:
|
||||
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: Vect3 | Vect3Array) -> tuple[float | VectN, ...]:
|
||||
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 get_all_ranges(self):
|
||||
def get_all_ranges(self) -> list[Sequence[float]]:
|
||||
return [self.x_range, self.y_range]
|
||||
|
||||
def add_coordinate_labels(self,
|
||||
x_values=None,
|
||||
y_values=None,
|
||||
**kwargs):
|
||||
def add_coordinate_labels(
|
||||
self,
|
||||
x_values: Iterable[float] | None = None,
|
||||
y_values: Iterable[float] | None = None,
|
||||
excluding: Iterable[float] = [0],
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
axes = self.get_axes()
|
||||
self.coordinate_labels = VGroup()
|
||||
for axis, values in zip(axes, [x_values, y_values]):
|
||||
labels = axis.add_numbers(values, **kwargs)
|
||||
labels = axis.add_numbers(values, excluding=excluding, **kwargs)
|
||||
self.coordinate_labels.add(labels)
|
||||
return self.coordinate_labels
|
||||
|
||||
|
||||
class ThreeDAxes(Axes):
|
||||
CONFIG = {
|
||||
"dimension": 3,
|
||||
"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,
|
||||
}
|
||||
dimension: int = 3
|
||||
default_z_axis_config: dict = dict()
|
||||
|
||||
def __init__(self, x_range=None, y_range=None, z_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
x_range: RangeSpecifier = (-6.0, 6.0, 1.0),
|
||||
y_range: RangeSpecifier = (-5.0, 5.0, 1.0),
|
||||
z_range: RangeSpecifier = (-4.0, 4.0, 1.0),
|
||||
z_axis_config: dict = dict(),
|
||||
z_normal: Vect3 = DOWN,
|
||||
depth: 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(
|
||||
self.z_range = full_range_specifier(z_range)
|
||||
self.z_axis = self.create_axis(
|
||||
self.z_range,
|
||||
self.z_axis_config,
|
||||
self.depth,
|
||||
axis_config=merge_dicts_recursively(
|
||||
self.default_axis_config,
|
||||
self.default_z_axis_config,
|
||||
kwargs.get("axis_config", {}),
|
||||
z_axis_config
|
||||
),
|
||||
length=depth,
|
||||
)
|
||||
z_axis.rotate(-PI / 2, UP, about_point=ORIGIN)
|
||||
z_axis.rotate(
|
||||
angle_of_vector(self.z_normal), OUT,
|
||||
self.z_axis.rotate(-PI / 2, UP, about_point=ORIGIN)
|
||||
self.z_axis.rotate(
|
||||
angle_of_vector(z_normal), OUT,
|
||||
about_point=ORIGIN
|
||||
)
|
||||
z_axis.shift(self.x_axis.n2p(0))
|
||||
self.axes.add(z_axis)
|
||||
self.add(z_axis)
|
||||
self.z_axis = z_axis
|
||||
self.z_axis.shift(self.x_axis.n2p(0))
|
||||
self.axes.add(self.z_axis)
|
||||
self.add(self.z_axis)
|
||||
|
||||
for axis in self.axes:
|
||||
axis.insert_n_curves(self.num_axis_pieces - 1)
|
||||
|
||||
def get_all_ranges(self):
|
||||
def get_all_ranges(self) -> list[Sequence[float]]:
|
||||
return [self.x_range, self.y_range, self.z_range]
|
||||
|
||||
def add_axis_labels(self, x_tex="x", y_tex="y", z_tex="z", font_size=24, buff=0.2):
|
||||
x_label, y_label, z_label = labels = VGroup(*(
|
||||
Tex(tex, font_size=font_size)
|
||||
for tex in [x_tex, y_tex, z_tex]
|
||||
))
|
||||
z_label.rotate(PI / 2, RIGHT)
|
||||
for label, axis in zip(labels, self):
|
||||
label.next_to(axis, normalize(np.round(axis.get_vector()), 2), buff=buff)
|
||||
axis.add(label)
|
||||
self.axis_labels = labels
|
||||
|
||||
def get_graph(
|
||||
self,
|
||||
func,
|
||||
color=BLUE_E,
|
||||
opacity=0.9,
|
||||
u_range=None,
|
||||
v_range=None,
|
||||
**kwargs
|
||||
) -> ParametricSurface:
|
||||
xu = self.x_axis.get_unit_size()
|
||||
yu = self.y_axis.get_unit_size()
|
||||
zu = self.z_axis.get_unit_size()
|
||||
x0, y0, z0 = self.get_origin()
|
||||
u_range = u_range or self.x_range[:2]
|
||||
v_range = v_range or self.y_range[:2]
|
||||
return ParametricSurface(
|
||||
lambda u, v: [xu * u + x0, yu * v + y0, zu * func(u, v) + z0],
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
color=color,
|
||||
opacity=opacity,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_parametric_surface(
|
||||
self,
|
||||
func,
|
||||
color=BLUE_E,
|
||||
opacity=0.9,
|
||||
**kwargs
|
||||
) -> ParametricSurface:
|
||||
surface = ParametricSurface(func, color=color, opacity=opacity, **kwargs)
|
||||
axes = [self.x_axis, self.y_axis, self.z_axis]
|
||||
for dim, axis in zip(range(3), axes):
|
||||
surface.stretch(axis.get_unit_size(), dim, about_point=ORIGIN)
|
||||
surface.shift(self.get_origin())
|
||||
return surface
|
||||
|
||||
|
||||
class NumberPlane(Axes):
|
||||
CONFIG = {
|
||||
"axis_config": {
|
||||
"stroke_color": WHITE,
|
||||
"stroke_width": 2,
|
||||
"include_ticks": False,
|
||||
"include_tip": False,
|
||||
"line_to_number_buff": SMALL_BUFF,
|
||||
"line_to_number_direction": DL,
|
||||
},
|
||||
"y_axis_config": {
|
||||
"line_to_number_direction": DL,
|
||||
},
|
||||
"background_line_style": {
|
||||
"stroke_color": BLUE_D,
|
||||
"stroke_width": 2,
|
||||
"stroke_opacity": 1,
|
||||
},
|
||||
"height": None,
|
||||
"width": None,
|
||||
# Defaults to a faded version of line_config
|
||||
"faded_line_style": None,
|
||||
"faded_line_ratio": 4,
|
||||
"make_smooth_after_applying_functions": True,
|
||||
}
|
||||
default_axis_config: dict = dict(
|
||||
stroke_color=WHITE,
|
||||
stroke_width=2,
|
||||
include_ticks=False,
|
||||
include_tip=False,
|
||||
line_to_number_buff=SMALL_BUFF,
|
||||
line_to_number_direction=DL,
|
||||
)
|
||||
default_y_axis_config: dict = dict(
|
||||
line_to_number_direction=DL,
|
||||
)
|
||||
|
||||
def __init__(self, x_range=None, y_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
x_range: RangeSpecifier = (-8.0, 8.0, 1.0),
|
||||
y_range: RangeSpecifier = (-4.0, 4.0, 1.0),
|
||||
background_line_style: dict = dict(
|
||||
stroke_color=BLUE_D,
|
||||
stroke_width=2,
|
||||
stroke_opacity=1,
|
||||
),
|
||||
# Defaults to a faded version of line_config
|
||||
faded_line_style: dict = dict(),
|
||||
faded_line_ratio: int = 4,
|
||||
make_smooth_after_applying_functions: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(x_range, y_range, **kwargs)
|
||||
self.background_line_style = dict(background_line_style)
|
||||
self.faded_line_style = dict(faded_line_style)
|
||||
self.faded_line_ratio = faded_line_ratio
|
||||
self.make_smooth_after_applying_functions = make_smooth_after_applying_functions
|
||||
self.init_background_lines()
|
||||
|
||||
def init_background_lines(self):
|
||||
if self.faded_line_style is None:
|
||||
def init_background_lines(self) -> None:
|
||||
if not self.faded_line_style:
|
||||
style = dict(self.background_line_style)
|
||||
# For anything numerical, like stroke_width
|
||||
# and stroke_opacity, chop it in half
|
||||
@@ -439,7 +668,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()
|
||||
|
||||
@@ -449,7 +678,11 @@ class NumberPlane(Axes):
|
||||
lines2 = VGroup(*x_lines2, *y_lines2)
|
||||
return lines1, lines2
|
||||
|
||||
def get_lines_parallel_to_axis(self, axis1, axis2):
|
||||
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())
|
||||
@@ -460,6 +693,8 @@ class NumberPlane(Axes):
|
||||
lines2 = VGroup()
|
||||
inputs = np.arange(axis2.x_min, axis2.x_max + step, step)
|
||||
for i, x in enumerate(inputs):
|
||||
if abs(x) < 1e-8:
|
||||
continue
|
||||
new_line = line.copy()
|
||||
new_line.shift(axis2.n2p(x) - axis2.n2p(0))
|
||||
if i % (1 + ratio) == 0:
|
||||
@@ -468,20 +703,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) -> Self:
|
||||
for mob in self.family_members_with_points():
|
||||
num_curves = mob.get_num_curves()
|
||||
if num_inserted_curves > num_curves:
|
||||
@@ -491,32 +726,36 @@ class NumberPlane(Axes):
|
||||
|
||||
|
||||
class ComplexPlane(NumberPlane):
|
||||
CONFIG = {
|
||||
"color": BLUE,
|
||||
"line_frequency": 1,
|
||||
}
|
||||
|
||||
def number_to_point(self, number):
|
||||
def number_to_point(self, number: complex | float) -> Vect3:
|
||||
number = complex(number)
|
||||
return self.coords_to_point(number.real, number.imag)
|
||||
|
||||
def n2p(self, number):
|
||||
def n2p(self, number: complex | float) -> Vect3:
|
||||
return self.number_to_point(number)
|
||||
|
||||
def point_to_number(self, point):
|
||||
def point_to_number(self, point: Vect3) -> complex:
|
||||
x, y = self.point_to_coords(point)
|
||||
return complex(x, y)
|
||||
|
||||
def p2n(self, point):
|
||||
def p2n(self, point: Vect3) -> complex:
|
||||
return self.point_to_number(point)
|
||||
|
||||
def get_default_coordinate_values(self, skip_first=True):
|
||||
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, skip_first=True, **kwargs):
|
||||
def add_coordinate_labels(
|
||||
self,
|
||||
numbers: list[complex] | None = None,
|
||||
skip_first: bool = True,
|
||||
font_size: int = 36,
|
||||
**kwargs
|
||||
) -> Self:
|
||||
if numbers is None:
|
||||
numbers = self.get_default_coordinate_values(skip_first)
|
||||
|
||||
@@ -526,14 +765,12 @@ class ComplexPlane(NumberPlane):
|
||||
if abs(z.imag) > abs(z.real):
|
||||
axis = self.get_y_axis()
|
||||
value = z.imag
|
||||
kwargs["unit"] = "i"
|
||||
kwargs["unit_tex"] = "i"
|
||||
else:
|
||||
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])
|
||||
number_mob = axis.get_number_mobject(value, font_size=font_size, **kwargs)
|
||||
# For -i, remove the "1"
|
||||
if z.imag == -1:
|
||||
number_mob.remove(number_mob[1])
|
||||
number_mob[0].next_to(
|
||||
|
||||
@@ -1,50 +1,56 @@
|
||||
from manimlib.constants import *
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import BLACK, GREY_E
|
||||
from manimlib.constants import FRAME_HEIGHT
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import ManimColor
|
||||
|
||||
|
||||
class ScreenRectangle(Rectangle):
|
||||
CONFIG = {
|
||||
"aspect_ratio": 16.0 / 9.0,
|
||||
"height": 4
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
Rectangle.__init__(self, **kwargs)
|
||||
self.set_width(
|
||||
self.aspect_ratio * self.get_height(),
|
||||
stretch=True
|
||||
def __init__(
|
||||
self,
|
||||
aspect_ratio: float = 16.0 / 9.0,
|
||||
height: float = 4,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
width=aspect_ratio * height,
|
||||
height=height,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class FullScreenRectangle(ScreenRectangle):
|
||||
CONFIG = {
|
||||
"height": FRAME_HEIGHT,
|
||||
"fill_color": GREY_E,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
height: float = FRAME_HEIGHT,
|
||||
fill_color: ManimColor = GREY_E,
|
||||
fill_opacity: float = 1,
|
||||
stroke_width: float = 0,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
height=height,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_width=stroke_width,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class FullScreenFadeRectangle(FullScreenRectangle):
|
||||
CONFIG = {
|
||||
"stroke_width": 0,
|
||||
"fill_color": BLACK,
|
||||
"fill_opacity": 0.7,
|
||||
}
|
||||
|
||||
|
||||
class PictureInPictureFrame(Rectangle):
|
||||
CONFIG = {
|
||||
"height": 3,
|
||||
"aspect_ratio": 16.0 / 9.0
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
Rectangle.__init__(
|
||||
self,
|
||||
width=self.aspect_ratio * self.height,
|
||||
height=self.height,
|
||||
**kwargs
|
||||
def __init__(
|
||||
self,
|
||||
stroke_width: float = 0.0,
|
||||
fill_color: ManimColor = BLACK,
|
||||
fill_opacity: float = 0.7,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
stroke_width=stroke_width,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
)
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
from manimlib.constants import *
|
||||
from __future__ import annotations
|
||||
|
||||
from isosurfaces import plot_isoline
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||
from manimlib.constants import YELLOW
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Sequence, Tuple
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
|
||||
|
||||
class ParametricCurve(VMobject):
|
||||
CONFIG = {
|
||||
"t_range": [0, 1, 0.1],
|
||||
"epsilon": 1e-8,
|
||||
def __init__(
|
||||
self,
|
||||
t_func: Callable[[float], Sequence[float] | Vect3],
|
||||
t_range: Tuple[float, float, float] = (0, 1, 0.1),
|
||||
epsilon: float = 1e-8,
|
||||
# TODO, automatically figure out discontinuities
|
||||
"discontinuities": [],
|
||||
"use_smoothing": True,
|
||||
}
|
||||
|
||||
def __init__(self, t_func, t_range=None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
if t_range is not None:
|
||||
self.t_range[:len(t_range)] = t_range
|
||||
# To be backward compatible with all the scenes specifying t_min, t_max, step_size
|
||||
self.t_range = [
|
||||
kwargs.get("t_min", self.t_range[0]),
|
||||
kwargs.get("t_max", self.t_range[1]),
|
||||
kwargs.get("step_size", self.t_range[2]),
|
||||
]
|
||||
discontinuities: Sequence[float] = [],
|
||||
use_smoothing: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
self.t_func = t_func
|
||||
VMobject.__init__(self, **kwargs)
|
||||
self.t_range = t_range
|
||||
self.epsilon = epsilon
|
||||
self.discontinuities = discontinuities
|
||||
self.use_smoothing = use_smoothing
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_point_from_function(self, t):
|
||||
return self.t_func(t)
|
||||
def get_point_from_function(self, t: float) -> Vect3:
|
||||
return np.array(self.t_func(t))
|
||||
|
||||
def init_points(self):
|
||||
t_min, t_max, step = self.t_range
|
||||
@@ -41,30 +48,74 @@ class ParametricCurve(VMobject):
|
||||
self.start_new_path(points[0])
|
||||
self.add_points_as_corners(points[1:])
|
||||
if self.use_smoothing:
|
||||
self.make_approximately_smooth()
|
||||
self.make_smooth(approx=True)
|
||||
if not self.has_points():
|
||||
self.set_points(np.array([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 = {
|
||||
"color": YELLOW,
|
||||
"x_range": [-8, 8, 0.25],
|
||||
}
|
||||
|
||||
def __init__(self, function, x_range=None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[float], float],
|
||||
x_range: Tuple[float, float, float] = (-8, 8, 0.25),
|
||||
color: ManimColor = YELLOW,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
|
||||
if x_range is not None:
|
||||
self.x_range[:len(x_range)] = x_range
|
||||
self.x_range = x_range
|
||||
|
||||
def parametric_function(t):
|
||||
return [t, function(t), 0]
|
||||
|
||||
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):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], float],
|
||||
x_range: Tuple[float, float] = (-FRAME_X_RADIUS, FRAME_X_RADIUS),
|
||||
y_range: Tuple[float, float] = (-FRAME_Y_RADIUS, FRAME_Y_RADIUS),
|
||||
min_depth: int = 5,
|
||||
max_quads: int = 1500,
|
||||
use_smoothing: bool = False,
|
||||
joint_type: str = 'no_joint',
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(joint_type=joint_type, **kwargs)
|
||||
|
||||
p_min, p_max = (
|
||||
np.array([x_range[0], y_range[0]]),
|
||||
np.array([x_range[1], y_range[1]]),
|
||||
)
|
||||
curves = plot_isoline(
|
||||
fn=lambda u: func(u[0], u[1]),
|
||||
pmin=p_min,
|
||||
pmax=p_max,
|
||||
min_depth=min_depth,
|
||||
max_quads=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 use_smoothing:
|
||||
self.make_smooth()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from pyglet.window import key as PygletWindowKeys
|
||||
|
||||
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
||||
from manimlib.constants import LEFT, RIGHT, UP, DOWN, ORIGIN
|
||||
from manimlib.constants import SMALL_BUFF, MED_SMALL_BUFF, MED_LARGE_BUFF
|
||||
from manimlib.constants import BLACK, GREY_A, GREY_C, RED, GREEN, BLUE, WHITE
|
||||
from manimlib.mobject.mobject import Mobject, Group
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.geometry import Dot, Line, Square, Rectangle, RoundedRectangle, Circle
|
||||
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, UP
|
||||
from manimlib.constants import MED_LARGE_BUFF, MED_SMALL_BUFF, SMALL_BUFF
|
||||
from manimlib.constants import BLACK, BLUE, GREEN, GREY_A, GREY_C, RED, WHITE
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import RoundedRectangle
|
||||
from manimlib.mobject.geometry import Square
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.value_tracker import ValueTracker
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import get_norm, get_closest_point_on_line
|
||||
from manimlib.utils.color import rgb_to_color, color_to_rgba, rgb_to_hex
|
||||
from manimlib.utils.color import rgb_to_hex
|
||||
from manimlib.utils.space_ops import get_closest_point_on_line
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from manimlib.typing import ManimColor
|
||||
|
||||
|
||||
# Interactive Mobjects
|
||||
@@ -21,17 +35,16 @@ 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))
|
||||
assert isinstance(mobject, Mobject)
|
||||
self.mobject = mobject
|
||||
self.mobject.add_mouse_drag_listner(self.mob_on_mouse_drag)
|
||||
# To avoid locking it as static 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,15 +56,15 @@ 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))
|
||||
assert isinstance(mobject, Mobject)
|
||||
self.on_click = on_click
|
||||
self.mobject = 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 +72,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 +80,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)
|
||||
@@ -82,86 +95,97 @@ class ControlMobject(ValueTracker):
|
||||
|
||||
|
||||
class EnableDisableButton(ControlMobject):
|
||||
CONFIG = {
|
||||
"value_type": np.dtype(bool),
|
||||
"rect_kwargs": {
|
||||
def __init__(
|
||||
self,
|
||||
value: bool = True,
|
||||
value_type: np.dtype = np.dtype(bool),
|
||||
rect_kwargs: dict = {
|
||||
"width": 0.5,
|
||||
"height": 0.5,
|
||||
"fill_opacity": 1.0
|
||||
},
|
||||
"enable_color": GREEN,
|
||||
"disable_color": RED
|
||||
}
|
||||
enable_color: ManimColor = GREEN,
|
||||
disable_color: ManimColor = RED,
|
||||
**kwargs
|
||||
):
|
||||
self.value = value
|
||||
self.value_type = value_type
|
||||
self.rect_kwargs = rect_kwargs
|
||||
self.enable_color = enable_color
|
||||
self.disable_color = disable_color
|
||||
|
||||
def __init__(self, value=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):
|
||||
assert(isinstance(value, bool))
|
||||
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
|
||||
|
||||
|
||||
class Checkbox(ControlMobject):
|
||||
CONFIG = {
|
||||
"value_type": np.dtype(bool),
|
||||
"rect_kwargs": {
|
||||
def __init__(
|
||||
self,
|
||||
value: bool = True,
|
||||
value_type: np.dtype = np.dtype(bool),
|
||||
rect_kwargs: dict = {
|
||||
"width": 0.5,
|
||||
"height": 0.5,
|
||||
"fill_opacity": 0.0
|
||||
},
|
||||
|
||||
"checkmark_kwargs": {
|
||||
checkmark_kwargs: dict = {
|
||||
"stroke_color": GREEN,
|
||||
"stroke_width": 6,
|
||||
},
|
||||
"cross_kwargs": {
|
||||
cross_kwargs: dict = {
|
||||
"stroke_color": RED,
|
||||
"stroke_width": 6,
|
||||
},
|
||||
"box_content_buff": SMALL_BUFF
|
||||
}
|
||||
box_content_buff: float = SMALL_BUFF,
|
||||
**kwargs
|
||||
):
|
||||
self.value_type = value_type
|
||||
self.rect_kwargs = rect_kwargs
|
||||
self.checkmark_kwargs = checkmark_kwargs
|
||||
self.cross_kwargs = cross_kwargs
|
||||
self.box_content_buff = box_content_buff
|
||||
|
||||
def __init__(self, value=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):
|
||||
assert(isinstance(value, bool))
|
||||
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 +197,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)
|
||||
@@ -187,27 +211,33 @@ class Checkbox(ControlMobject):
|
||||
|
||||
|
||||
class LinearNumberSlider(ControlMobject):
|
||||
CONFIG = {
|
||||
"value_type": np.float64,
|
||||
"min_value": -10.0,
|
||||
"max_value": 10.0,
|
||||
"step": 1.0,
|
||||
|
||||
"rounded_rect_kwargs": {
|
||||
def __init__(
|
||||
self,
|
||||
value: float = 0,
|
||||
value_type: type = np.float64,
|
||||
min_value: float = -10.0,
|
||||
max_value: float = 10.0,
|
||||
step: float = 1.0,
|
||||
rounded_rect_kwargs: dict = {
|
||||
"height": 0.075,
|
||||
"width": 2,
|
||||
"corner_radius": 0.0375
|
||||
},
|
||||
"circle_kwargs": {
|
||||
circle_kwargs: dict = {
|
||||
"radius": 0.1,
|
||||
"stroke_color": GREY_A,
|
||||
"fill_color": GREY_A,
|
||||
"fill_opacity": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
**kwargs
|
||||
):
|
||||
self.value_type = value_type
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
self.step = step
|
||||
self.rounded_rect_kwargs = rounded_rect_kwargs
|
||||
self.circle_kwargs = circle_kwargs
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.bar = RoundedRectangle(**self.rounded_rect_kwargs)
|
||||
self.slider = Circle(**self.circle_kwargs)
|
||||
self.slider_axis = Line(
|
||||
@@ -219,22 +249,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):
|
||||
assert(self.min_value <= value <= self.max_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)
|
||||
@@ -245,24 +275,29 @@ class LinearNumberSlider(ControlMobject):
|
||||
|
||||
|
||||
class ColorSliders(Group):
|
||||
CONFIG = {
|
||||
"sliders_kwargs": {},
|
||||
"rect_kwargs": {
|
||||
def __init__(
|
||||
self,
|
||||
sliders_kwargs: dict = {},
|
||||
rect_kwargs: dict = {
|
||||
"width": 2.0,
|
||||
"height": 0.5,
|
||||
"stroke_opacity": 1.0
|
||||
},
|
||||
"background_grid_kwargs": {
|
||||
background_grid_kwargs: dict = {
|
||||
"colors": [GREY_A, GREY_C],
|
||||
"single_square_len": 0.1
|
||||
},
|
||||
"sliders_buff": MED_LARGE_BUFF,
|
||||
"default_rgb_value": 255,
|
||||
"default_a_value": 1,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
sliders_buff: float = MED_LARGE_BUFF,
|
||||
default_rgb_value: int = 255,
|
||||
default_a_value: int = 1,
|
||||
**kwargs
|
||||
):
|
||||
self.sliders_kwargs = sliders_kwargs
|
||||
self.rect_kwargs = rect_kwargs
|
||||
self.background_grid_kwargs = background_grid_kwargs
|
||||
self.sliders_buff = sliders_buff
|
||||
self.default_rgb_value = default_rgb_value
|
||||
self.default_a_value = default_a_value
|
||||
|
||||
rgb_kwargs = {"value": self.default_rgb_value, "min_value": 0, "max_value": 255, "step": 1}
|
||||
a_kwargs = {"value": self.default_a_value, "min_value": 0, "max_value": 1, "step": 0.04}
|
||||
@@ -282,7 +317,7 @@ class ColorSliders(Group):
|
||||
self.r_slider.slider.set_color(RED)
|
||||
self.g_slider.slider.set_color(GREEN)
|
||||
self.b_slider.slider.set_color(BLUE)
|
||||
self.a_slider.slider.set_color_by_gradient([BLACK, WHITE])
|
||||
self.a_slider.slider.set_color_by_gradient(BLACK, WHITE)
|
||||
|
||||
self.selected_color_box = Rectangle(**self.rect_kwargs)
|
||||
self.selected_color_box.add_updater(
|
||||
@@ -300,7 +335,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"]
|
||||
@@ -316,55 +351,62 @@ class ColorSliders(Group):
|
||||
grid.move_to(self.selected_color_box)
|
||||
|
||||
for idx, square in enumerate(grid):
|
||||
assert(isinstance(square, Square))
|
||||
assert isinstance(square, Square)
|
||||
square.set_stroke(width=0.0, opacity=0.0)
|
||||
square.set_fill(colors[idx % len(colors)], 1.0)
|
||||
|
||||
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)
|
||||
return np.array((r, g, b, 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]
|
||||
|
||||
|
||||
class Textbox(ControlMobject):
|
||||
CONFIG = {
|
||||
"value_type": np.dtype(object),
|
||||
|
||||
"box_kwargs": {
|
||||
def __init__(
|
||||
self,
|
||||
value: str = "",
|
||||
value_type: np.dtype = np.dtype(object),
|
||||
box_kwargs: dict = {
|
||||
"width": 2.0,
|
||||
"height": 1.0,
|
||||
"fill_color": WHITE,
|
||||
"fill_opacity": 1.0,
|
||||
},
|
||||
"text_kwargs": {
|
||||
text_kwargs: dict = {
|
||||
"color": BLUE
|
||||
},
|
||||
"text_buff": MED_SMALL_BUFF,
|
||||
"isInitiallyActive": False,
|
||||
"active_color": BLUE,
|
||||
"deactive_color": RED,
|
||||
}
|
||||
text_buff: float = MED_SMALL_BUFF,
|
||||
isInitiallyActive: bool = False,
|
||||
active_color: ManimColor = BLUE,
|
||||
deactive_color: ManimColor = RED,
|
||||
**kwargs
|
||||
):
|
||||
self.value_type = value_type
|
||||
self.box_kwargs = box_kwargs
|
||||
self.text_kwargs = text_kwargs
|
||||
self.text_buff = text_buff
|
||||
self.isInitiallyActive = isInitiallyActive
|
||||
self.active_color = active_color
|
||||
self.deactive_color = deactive_color
|
||||
|
||||
def __init__(self, value="", **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.isActive = self.isInitiallyActive
|
||||
self.box = Rectangle(**self.box_kwargs)
|
||||
self.box.add_mouse_press_listner(self.box_on_mouse_press)
|
||||
@@ -374,10 +416,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 +431,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)
|
||||
@@ -423,28 +465,31 @@ class Textbox(ControlMobject):
|
||||
|
||||
|
||||
class ControlPanel(Group):
|
||||
CONFIG = {
|
||||
"panel_kwargs": {
|
||||
def __init__(
|
||||
self,
|
||||
*controls: ControlMobject,
|
||||
panel_kwargs: dict = {
|
||||
"width": FRAME_WIDTH / 4,
|
||||
"height": MED_SMALL_BUFF + FRAME_HEIGHT,
|
||||
"fill_color": GREY_C,
|
||||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0.0
|
||||
},
|
||||
"opener_kwargs": {
|
||||
opener_kwargs: dict = {
|
||||
"width": FRAME_WIDTH / 8,
|
||||
"height": 0.5,
|
||||
"fill_color": GREY_C,
|
||||
"fill_opacity": 1.0
|
||||
},
|
||||
"opener_text_kwargs": {
|
||||
opener_text_kwargs: dict = {
|
||||
"text": "Control Panel",
|
||||
"font_size": 20
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, *controls, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
},
|
||||
**kwargs
|
||||
):
|
||||
self.panel_kwargs = panel_kwargs
|
||||
self.opener_kwargs = opener_kwargs
|
||||
self.opener_text_kwargs = opener_text_kwargs
|
||||
|
||||
self.panel = Rectangle(**self.panel_kwargs)
|
||||
self.panel.to_corner(UP + LEFT, buff=0)
|
||||
@@ -472,7 +517,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 +533,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 +555,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,201 +1,293 @@
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import *
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DOWN, LEFT, RIGHT, ORIGIN
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.mobject.numbers import Integer
|
||||
from manimlib.mobject.shape_matchers import BackgroundRectangle
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
VECTOR_LABEL_SCALE_FACTOR = 0.8
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence, Union, Optional
|
||||
from manimlib.typing import ManimColor, Vect3, VectNArray, Self
|
||||
|
||||
def matrix_to_tex_string(matrix):
|
||||
matrix = np.array(matrix).astype("str")
|
||||
if matrix.ndim == 1:
|
||||
matrix = matrix.reshape((matrix.size, 1))
|
||||
n_rows, n_cols = matrix.shape
|
||||
prefix = "\\left[ \\begin{array}{%s}" % ("c" * n_cols)
|
||||
suffix = "\\end{array} \\right]"
|
||||
rows = [
|
||||
" & ".join(row)
|
||||
for row in matrix
|
||||
]
|
||||
return prefix + " \\\\ ".join(rows) + suffix
|
||||
|
||||
|
||||
def matrix_to_mobject(matrix):
|
||||
return Tex(matrix_to_tex_string(matrix))
|
||||
|
||||
|
||||
def vector_coordinate_label(vector_mob, integer_labels=True,
|
||||
n_dim=2, color=WHITE):
|
||||
vect = np.array(vector_mob.get_end())
|
||||
if integer_labels:
|
||||
vect = np.round(vect).astype(int)
|
||||
vect = vect[:n_dim]
|
||||
vect = vect.reshape((n_dim, 1))
|
||||
label = Matrix(vect, add_background_rectangles_to_entries=True)
|
||||
label.scale(VECTOR_LABEL_SCALE_FACTOR)
|
||||
|
||||
shift_dir = np.array(vector_mob.get_end())
|
||||
if shift_dir[0] >= 0: # Pointing right
|
||||
shift_dir -= label.get_left() + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER * LEFT
|
||||
else: # Pointing left
|
||||
shift_dir -= label.get_right() + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER * RIGHT
|
||||
label.shift(shift_dir)
|
||||
label.set_color(color)
|
||||
label.rect = BackgroundRectangle(label)
|
||||
label.add_to_back(label.rect)
|
||||
return label
|
||||
StringMatrixType = Union[Sequence[Sequence[str]], np.ndarray[int, np.dtype[np.str_]]]
|
||||
FloatMatrixType = Union[Sequence[Sequence[float]], VectNArray]
|
||||
VMobjectMatrixType = Sequence[Sequence[VMobject]]
|
||||
GenericMatrixType = Union[FloatMatrixType, StringMatrixType, VMobjectMatrixType]
|
||||
|
||||
|
||||
class Matrix(VMobject):
|
||||
CONFIG = {
|
||||
"v_buff": 0.8,
|
||||
"h_buff": 1.3,
|
||||
"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": DOWN,
|
||||
}
|
||||
|
||||
def __init__(self, matrix, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
matrix: GenericMatrixType,
|
||||
v_buff: float = 0.5,
|
||||
h_buff: float = 0.5,
|
||||
bracket_h_buff: float = 0.2,
|
||||
bracket_v_buff: float = 0.25,
|
||||
height: float | None = None,
|
||||
element_config: dict = dict(),
|
||||
element_alignment_corner: Vect3 = DOWN,
|
||||
ellipses_row: Optional[int] = None,
|
||||
ellipses_col: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Matrix can either include numbers, tex_strings,
|
||||
or mobjects
|
||||
"""
|
||||
VMobject.__init__(self, **kwargs)
|
||||
matrix = self.matrix = np.array(matrix, ndmin=2)
|
||||
mob_matrix = self.matrix_to_mob_matrix(matrix)
|
||||
self.organize_mob_matrix(mob_matrix)
|
||||
# self.elements = VGroup(*mob_matrix.flatten())
|
||||
self.elements = VGroup(*it.chain(*mob_matrix))
|
||||
self.add(self.elements)
|
||||
self.add_brackets()
|
||||
self.center()
|
||||
self.mob_matrix = mob_matrix
|
||||
if self.add_background_rectangles_to_entries:
|
||||
for mob in self.elements:
|
||||
mob.add_background_rectangle()
|
||||
if self.include_background_rectangle:
|
||||
self.add_background_rectangle()
|
||||
super().__init__()
|
||||
|
||||
def matrix_to_mob_matrix(self, matrix):
|
||||
return [
|
||||
self.mob_matrix = self.create_mobject_matrix(
|
||||
matrix, v_buff, h_buff, element_alignment_corner,
|
||||
**element_config
|
||||
)
|
||||
|
||||
# Create helpful groups for the elements
|
||||
n_cols = len(self.mob_matrix[0])
|
||||
self.elements = [elem for row in self.mob_matrix for elem in row]
|
||||
self.columns = VGroup(*(
|
||||
VGroup(*(row[i] for row in self.mob_matrix))
|
||||
for i in range(n_cols)
|
||||
))
|
||||
self.rows = VGroup(*(VGroup(*row) for row in self.mob_matrix))
|
||||
if height is not None:
|
||||
self.rows.set_height(height - 2 * bracket_v_buff)
|
||||
self.brackets = self.create_brackets(self.rows, bracket_v_buff, bracket_h_buff)
|
||||
self.ellipses = []
|
||||
|
||||
# Add elements and brackets
|
||||
self.add(*self.elements)
|
||||
self.add(*self.brackets)
|
||||
self.center()
|
||||
|
||||
# Potentially add ellipses
|
||||
self.swap_entries_for_ellipses(
|
||||
ellipses_row,
|
||||
ellipses_col,
|
||||
)
|
||||
|
||||
def copy(self, deep: bool = False):
|
||||
result = super().copy(deep)
|
||||
self_family = self.get_family()
|
||||
copy_family = result.get_family()
|
||||
for attr in ["elements", "ellipses"]:
|
||||
setattr(result, attr, [
|
||||
copy_family[self_family.index(mob)]
|
||||
for mob in getattr(self, attr)
|
||||
])
|
||||
return result
|
||||
|
||||
def create_mobject_matrix(
|
||||
self,
|
||||
matrix: GenericMatrixType,
|
||||
v_buff: float,
|
||||
h_buff: float,
|
||||
aligned_corner: Vect3,
|
||||
**element_config
|
||||
) -> VMobjectMatrixType:
|
||||
"""
|
||||
Creates and organizes the matrix of mobjects
|
||||
"""
|
||||
mob_matrix = [
|
||||
[
|
||||
self.element_to_mobject(item, **self.element_to_mobject_config)
|
||||
for item in row
|
||||
self.element_to_mobject(element, **element_config)
|
||||
for element in row
|
||||
]
|
||||
for row in matrix
|
||||
]
|
||||
|
||||
def organize_mob_matrix(self, matrix):
|
||||
for i, row in enumerate(matrix):
|
||||
max_width = max(elem.get_width() for row in mob_matrix for elem in row)
|
||||
max_height = max(elem.get_height() for row in mob_matrix for elem in row)
|
||||
x_step = (max_width + h_buff) * RIGHT
|
||||
y_step = (max_height + v_buff) * DOWN
|
||||
for i, row in enumerate(mob_matrix):
|
||||
for j, elem in enumerate(row):
|
||||
mob = matrix[i][j]
|
||||
mob.move_to(
|
||||
i * self.v_buff * DOWN + j * self.h_buff * RIGHT,
|
||||
self.element_alignment_corner
|
||||
)
|
||||
return self
|
||||
elem.move_to(i * y_step + j * x_step, aligned_corner)
|
||||
return mob_matrix
|
||||
|
||||
def add_brackets(self):
|
||||
height = self.matrix.shape[0]
|
||||
bracket_pair = Tex("".join([
|
||||
"\\left[",
|
||||
"\\begin{array}{c}",
|
||||
*height * ["\\quad \\\\"],
|
||||
"\\end{array}"
|
||||
"\\right]",
|
||||
]))[0]
|
||||
bracket_pair.set_height(
|
||||
self.get_height() + 1 * self.bracket_v_buff
|
||||
)
|
||||
l_bracket = bracket_pair[:len(bracket_pair) // 2]
|
||||
r_bracket = bracket_pair[len(bracket_pair) // 2:]
|
||||
l_bracket.next_to(self, LEFT, self.bracket_h_buff)
|
||||
r_bracket.next_to(self, RIGHT, self.bracket_h_buff)
|
||||
self.add(l_bracket, r_bracket)
|
||||
self.brackets = VGroup(l_bracket, r_bracket)
|
||||
return self
|
||||
def element_to_mobject(self, element, **config) -> VMobject:
|
||||
if isinstance(element, VMobject):
|
||||
return element
|
||||
elif isinstance(element, float | complex):
|
||||
return DecimalNumber(element, **config)
|
||||
else:
|
||||
return Tex(str(element), **config)
|
||||
|
||||
def get_columns(self):
|
||||
return VGroup(*[
|
||||
VGroup(*[row[i] for row in self.mob_matrix])
|
||||
for i in range(len(self.mob_matrix[0]))
|
||||
])
|
||||
def create_brackets(self, rows, v_buff: float, h_buff: float) -> VGroup:
|
||||
brackets = Tex("".join((
|
||||
R"\left[\begin{array}{c}",
|
||||
*len(rows) * [R"\quad \\"],
|
||||
R"\end{array}\right]",
|
||||
)))
|
||||
brackets.set_height(rows.get_height() + v_buff)
|
||||
l_bracket = brackets[:len(brackets) // 2]
|
||||
r_bracket = brackets[len(brackets) // 2:]
|
||||
l_bracket.next_to(rows, LEFT, h_buff)
|
||||
r_bracket.next_to(rows, RIGHT, h_buff)
|
||||
return VGroup(l_bracket, r_bracket)
|
||||
|
||||
def get_rows(self):
|
||||
return VGroup(*[
|
||||
VGroup(*row)
|
||||
for row in self.mob_matrix
|
||||
])
|
||||
def get_column(self, index: int):
|
||||
if not 0 <= index < len(self.columns):
|
||||
raise IndexError(f"Index {index} out of bound for matrix with {len(self.columns)} columns")
|
||||
return self.columns[index]
|
||||
|
||||
def set_column_colors(self, *colors):
|
||||
def get_row(self, index: int):
|
||||
if not 0 <= index < len(self.rows):
|
||||
raise IndexError(f"Index {index} out of bound for matrix with {len(self.rows)} rows")
|
||||
return self.rows[index]
|
||||
|
||||
def get_columns(self) -> VGroup:
|
||||
return self.columns
|
||||
|
||||
def get_rows(self) -> VGroup:
|
||||
return self.rows
|
||||
|
||||
def set_column_colors(self, *colors: ManimColor) -> Self:
|
||||
columns = self.get_columns()
|
||||
for color, column in zip(colors, columns):
|
||||
column.set_color(color)
|
||||
return self
|
||||
|
||||
def add_background_to_entries(self):
|
||||
def add_background_to_entries(self) -> Self:
|
||||
for mob in self.get_entries():
|
||||
mob.add_background_rectangle()
|
||||
return self
|
||||
|
||||
def get_mob_matrix(self):
|
||||
def swap_entry_for_dots(self, entry, dots):
|
||||
dots.move_to(entry)
|
||||
entry.become(dots)
|
||||
if entry in self.elements:
|
||||
self.elements.remove(entry)
|
||||
if entry not in self.ellipses:
|
||||
self.ellipses.append(entry)
|
||||
|
||||
def swap_entries_for_ellipses(
|
||||
self,
|
||||
row_index: Optional[int] = None,
|
||||
col_index: Optional[int] = None,
|
||||
height_ratio: float = 0.65,
|
||||
width_ratio: float = 0.4
|
||||
):
|
||||
rows = self.get_rows()
|
||||
cols = self.get_columns()
|
||||
|
||||
avg_row_height = rows.get_height() / len(rows)
|
||||
vdots_height = height_ratio * avg_row_height
|
||||
|
||||
avg_col_width = cols.get_width() / len(cols)
|
||||
hdots_width = width_ratio * avg_col_width
|
||||
|
||||
use_vdots = row_index is not None and -len(rows) <= row_index < len(rows)
|
||||
use_hdots = col_index is not None and -len(cols) <= col_index < len(cols)
|
||||
|
||||
if use_vdots:
|
||||
for column in cols:
|
||||
# Add vdots
|
||||
dots = Tex(R"\vdots")
|
||||
dots.set_height(vdots_height)
|
||||
self.swap_entry_for_dots(column[row_index], dots)
|
||||
if use_hdots:
|
||||
for row in rows:
|
||||
# Add hdots
|
||||
dots = Tex(R"\hdots")
|
||||
dots.set_width(hdots_width)
|
||||
self.swap_entry_for_dots(row[col_index], dots)
|
||||
if use_vdots and use_hdots:
|
||||
rows[row_index][col_index].rotate(-45 * DEG)
|
||||
return self
|
||||
|
||||
def get_mob_matrix(self) -> VMobjectMatrixType:
|
||||
return self.mob_matrix
|
||||
|
||||
def get_entries(self):
|
||||
return self.elements
|
||||
def get_entries(self) -> VGroup:
|
||||
return VGroup(*self.elements)
|
||||
|
||||
def get_brackets(self):
|
||||
return self.brackets
|
||||
def get_brackets(self) -> VGroup:
|
||||
return VGroup(*self.brackets)
|
||||
|
||||
def get_ellipses(self) -> VGroup:
|
||||
return VGroup(*self.ellipses)
|
||||
|
||||
|
||||
class DecimalMatrix(Matrix):
|
||||
CONFIG = {
|
||||
"element_to_mobject": DecimalNumber,
|
||||
"element_to_mobject_config": {"num_decimal_places": 1}
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
matrix: FloatMatrixType,
|
||||
num_decimal_places: int = 2,
|
||||
decimal_config: dict = dict(),
|
||||
**config
|
||||
):
|
||||
self.float_matrix = matrix
|
||||
super().__init__(
|
||||
matrix,
|
||||
element_config=dict(
|
||||
num_decimal_places=num_decimal_places,
|
||||
**decimal_config
|
||||
),
|
||||
**config
|
||||
)
|
||||
|
||||
def element_to_mobject(self, element, **decimal_config) -> DecimalNumber:
|
||||
return DecimalNumber(element, **decimal_config)
|
||||
|
||||
|
||||
class IntegerMatrix(Matrix):
|
||||
CONFIG = {
|
||||
"element_to_mobject": Integer,
|
||||
"element_alignment_corner": UP,
|
||||
}
|
||||
class IntegerMatrix(DecimalMatrix):
|
||||
def __init__(
|
||||
self,
|
||||
matrix: FloatMatrixType,
|
||||
num_decimal_places: int = 0,
|
||||
decimal_config: dict = dict(),
|
||||
**config
|
||||
):
|
||||
super().__init__(matrix, num_decimal_places, decimal_config, **config)
|
||||
|
||||
|
||||
class TexMatrix(Matrix):
|
||||
def __init__(
|
||||
self,
|
||||
matrix: StringMatrixType,
|
||||
tex_config: dict = dict(),
|
||||
**config,
|
||||
):
|
||||
super().__init__(
|
||||
matrix,
|
||||
element_config=tex_config,
|
||||
**config
|
||||
)
|
||||
|
||||
|
||||
class MobjectMatrix(Matrix):
|
||||
CONFIG = {
|
||||
"element_to_mobject": lambda m: m,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
group: VGroup,
|
||||
n_rows: int | None = None,
|
||||
n_cols: int | None = None,
|
||||
height: float = 4.0,
|
||||
element_alignment_corner=ORIGIN,
|
||||
**config,
|
||||
):
|
||||
# Have fallback defaults of n_rows and n_cols
|
||||
n_mobs = len(group)
|
||||
if n_rows is None:
|
||||
n_rows = int(np.sqrt(n_mobs)) if n_cols is None else n_mobs // n_cols
|
||||
if n_cols is None:
|
||||
n_cols = n_mobs // n_rows
|
||||
|
||||
if len(group) < n_rows * n_cols:
|
||||
raise Exception("Input to MobjectMatrix must have at least n_rows * n_cols entries")
|
||||
|
||||
def get_det_text(matrix, determinant=None, background_rect=False, initial_scale_factor=2):
|
||||
parens = Tex("(", ")")
|
||||
parens.scale(initial_scale_factor)
|
||||
parens.stretch_to_fit_height(matrix.get_height())
|
||||
l_paren, r_paren = parens.split()
|
||||
l_paren.next_to(matrix, LEFT, buff=0.1)
|
||||
r_paren.next_to(matrix, RIGHT, buff=0.1)
|
||||
det = TexText("det")
|
||||
det.scale(initial_scale_factor)
|
||||
det.next_to(l_paren, LEFT, buff=0.1)
|
||||
if background_rect:
|
||||
det.add_background_rectangle()
|
||||
det_text = VGroup(det, l_paren, r_paren)
|
||||
if determinant is not None:
|
||||
eq = Tex("=")
|
||||
eq.next_to(r_paren, RIGHT, buff=0.1)
|
||||
result = Tex(str(determinant))
|
||||
result.next_to(eq, RIGHT, buff=0.2)
|
||||
det_text.add(eq, result)
|
||||
return det_text
|
||||
mob_matrix = [
|
||||
[group[n * n_cols + k] for k in range(n_cols)]
|
||||
for n in range(n_rows)
|
||||
]
|
||||
config.update(
|
||||
height=height,
|
||||
element_alignment_corner=element_alignment_corner,
|
||||
)
|
||||
super().__init__(mob_matrix, **config)
|
||||
|
||||
def element_to_mobject(self, element: VMobject, **config) -> VMobject:
|
||||
return element
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
|
||||
def assert_is_mobject_method(method):
|
||||
assert(inspect.ismethod(method))
|
||||
assert inspect.ismethod(method)
|
||||
mobject = method.__self__
|
||||
assert(isinstance(mobject, Mobject))
|
||||
assert isinstance(mobject, Mobject)
|
||||
|
||||
|
||||
def always(method, *args, **kwargs):
|
||||
@@ -41,27 +52,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 * DEG,
|
||||
**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
|
||||
@@ -70,7 +93,7 @@ def turn_animation_into_updater(animation, cycle=False, **kwargs):
|
||||
the updater will be popped uplon completion
|
||||
"""
|
||||
mobject = animation.mobject
|
||||
animation.update_config(**kwargs)
|
||||
animation.update_rate_info(**kwargs)
|
||||
animation.suspend_mobject_updating = False
|
||||
animation.begin()
|
||||
animation.total_time = 0
|
||||
@@ -94,7 +117,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,94 +1,121 @@
|
||||
from manimlib.constants import *
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DOWN, LEFT, RIGHT, UP
|
||||
from manimlib.constants import GREY_B
|
||||
from manimlib.constants import MED_SMALL_BUFF
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.bezier import outer_interpolate
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
from manimlib.utils.simple_functions import fdiv
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable, Optional
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier
|
||||
|
||||
|
||||
class NumberLine(Line):
|
||||
CONFIG = {
|
||||
"color": GREY_B,
|
||||
"stroke_width": 2,
|
||||
# List of 2 or 3 elements, x_min, x_max, step_size
|
||||
"x_range": [-8, 8, 1],
|
||||
def __init__(
|
||||
self,
|
||||
x_range: RangeSpecifier = (-8, 8, 1),
|
||||
color: ManimColor = GREY_B,
|
||||
stroke_width: float = 2.0,
|
||||
# How big is one one unit of this number line in terms of absolute spacial distance
|
||||
"unit_size": 1,
|
||||
"width": None,
|
||||
"include_ticks": True,
|
||||
"tick_size": 0.1,
|
||||
"longer_tick_multiple": 1.5,
|
||||
"tick_offset": 0,
|
||||
unit_size: float = 1.0,
|
||||
width: Optional[float] = None,
|
||||
include_ticks: bool = True,
|
||||
tick_size: float = 0.1,
|
||||
longer_tick_multiple: float = 1.5,
|
||||
tick_offset: float = 0.0,
|
||||
# Change name
|
||||
"numbers_with_elongated_ticks": [],
|
||||
"include_numbers": False,
|
||||
"line_to_number_direction": DOWN,
|
||||
"line_to_number_buff": MED_SMALL_BUFF,
|
||||
"include_tip": False,
|
||||
"tip_config": {
|
||||
"width": 0.25,
|
||||
"length": 0.25,
|
||||
},
|
||||
"decimal_number_config": {
|
||||
"num_decimal_places": 0,
|
||||
"font_size": 36,
|
||||
},
|
||||
"numbers_to_exclude": None
|
||||
}
|
||||
|
||||
def __init__(self, x_range=None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
if x_range is None:
|
||||
x_range = self.x_range
|
||||
if len(x_range) == 2:
|
||||
x_range = [*x_range, 1]
|
||||
|
||||
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)
|
||||
|
||||
super().__init__(self.x_min * RIGHT, self.x_max * RIGHT, **kwargs)
|
||||
if self.width:
|
||||
self.set_width(self.width)
|
||||
self.unit_size = self.get_unit_size()
|
||||
big_tick_spacing: Optional[float] = None,
|
||||
big_tick_numbers: list[float] = [],
|
||||
include_numbers: bool = False,
|
||||
line_to_number_direction: Vect3 = DOWN,
|
||||
line_to_number_buff: float = MED_SMALL_BUFF,
|
||||
include_tip: bool = False,
|
||||
tip_config: dict = dict(
|
||||
width=0.25,
|
||||
length=0.25,
|
||||
),
|
||||
decimal_number_config: dict = dict(
|
||||
num_decimal_places=0,
|
||||
font_size=36,
|
||||
),
|
||||
numbers_to_exclude: list | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.x_range = x_range
|
||||
self.tick_size = tick_size
|
||||
self.longer_tick_multiple = longer_tick_multiple
|
||||
self.tick_offset = tick_offset
|
||||
if big_tick_spacing is not None:
|
||||
self.big_tick_numbers = np.arange(
|
||||
x_range[0],
|
||||
x_range[1] + big_tick_spacing,
|
||||
big_tick_spacing,
|
||||
)
|
||||
else:
|
||||
self.scale(self.unit_size)
|
||||
self.big_tick_numbers = list(big_tick_numbers)
|
||||
self.line_to_number_direction = line_to_number_direction
|
||||
self.line_to_number_buff = line_to_number_buff
|
||||
self.include_tip = include_tip
|
||||
self.tip_config = dict(tip_config)
|
||||
self.decimal_number_config = dict(decimal_number_config)
|
||||
self.numbers_to_exclude = numbers_to_exclude
|
||||
|
||||
self.x_min, self.x_max = x_range[:2]
|
||||
self.x_step = 1 if len(x_range) == 2 else x_range[2]
|
||||
|
||||
super().__init__(
|
||||
self.x_min * RIGHT, self.x_max * RIGHT,
|
||||
color=color,
|
||||
stroke_width=stroke_width,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if width:
|
||||
self.set_width(width)
|
||||
else:
|
||||
self.scale(unit_size)
|
||||
self.center()
|
||||
|
||||
if self.include_tip:
|
||||
if include_tip:
|
||||
self.add_tip()
|
||||
self.tip.set_stroke(
|
||||
self.stroke_color,
|
||||
self.stroke_width,
|
||||
)
|
||||
if self.include_ticks:
|
||||
if include_ticks:
|
||||
self.add_ticks()
|
||||
if self.include_numbers:
|
||||
if 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)
|
||||
result = np.arange(self.x_min, x_max, self.x_step)
|
||||
return result[result <= self.x_max]
|
||||
|
||||
def add_ticks(self):
|
||||
def add_ticks(self) -> None:
|
||||
ticks = VGroup()
|
||||
for x in self.get_tick_range():
|
||||
size = self.tick_size
|
||||
if np.isclose(self.numbers_with_elongated_ticks, x).any():
|
||||
if np.isclose(self.big_tick_numbers, 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)
|
||||
@@ -97,17 +124,18 @@ 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 | VectN) -> Vect3 | Vect3Array:
|
||||
start = self.get_points()[0]
|
||||
end = self.get_points()[-1]
|
||||
alpha = (number - self.x_min) / (self.x_max - self.x_min)
|
||||
return outer_interpolate(start, end, alpha)
|
||||
|
||||
def point_to_number(self, point):
|
||||
points = self.get_points()
|
||||
start = points[0]
|
||||
end = points[-1]
|
||||
def point_to_number(self, point: Vect3 | Vect3Array) -> float | VectN:
|
||||
start = self.get_points()[0]
|
||||
end = self.get_points()[-1]
|
||||
vect = end - start
|
||||
proportion = fdiv(
|
||||
np.dot(point - start, vect),
|
||||
@@ -115,30 +143,37 @@ class NumberLine(Line):
|
||||
)
|
||||
return interpolate(self.x_min, self.x_max, proportion)
|
||||
|
||||
def n2p(self, number):
|
||||
def n2p(self, number: float | VectN) -> Vect3 | Vect3Array:
|
||||
"""Abbreviation for number_to_point"""
|
||||
return self.number_to_point(number)
|
||||
|
||||
def p2n(self, point):
|
||||
def p2n(self, point: Vect3 | Vect3Array) -> float | VectN:
|
||||
"""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: Vect3 | None = None,
|
||||
buff: float | None = None,
|
||||
unit: float = 1.0,
|
||||
unit_tex: str = "",
|
||||
**number_config
|
||||
) -> DecimalNumber:
|
||||
number_config = merge_dicts_recursively(
|
||||
self.decimal_number_config, number_config
|
||||
self.decimal_number_config, number_config,
|
||||
)
|
||||
if direction is None:
|
||||
direction = self.line_to_number_direction
|
||||
if buff is None:
|
||||
buff = self.line_to_number_buff
|
||||
if unit_tex:
|
||||
number_config["unit"] = unit_tex
|
||||
|
||||
num_mob = DecimalNumber(x, **number_config)
|
||||
num_mob = DecimalNumber(x / unit, **number_config)
|
||||
num_mob.next_to(
|
||||
self.number_to_point(x),
|
||||
direction=direction,
|
||||
@@ -147,9 +182,19 @@ class NumberLine(Line):
|
||||
if x < 0 and direction[0] == 0:
|
||||
# Align without the minus sign
|
||||
num_mob.shift(num_mob[0].get_width() * LEFT / 2)
|
||||
if x == unit and unit_tex:
|
||||
center = num_mob.get_center()
|
||||
num_mob.remove(num_mob[0])
|
||||
num_mob.move_to(center)
|
||||
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()
|
||||
|
||||
@@ -169,11 +214,18 @@ class NumberLine(Line):
|
||||
|
||||
|
||||
class UnitInterval(NumberLine):
|
||||
CONFIG = {
|
||||
"x_range": [0, 1, 0.1],
|
||||
"unit_size": 10,
|
||||
"numbers_with_elongated_ticks": [0, 1],
|
||||
"decimal_number_config": {
|
||||
"num_decimal_places": 1,
|
||||
}
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
x_range: RangeSpecifier = (0, 1, 0.1),
|
||||
unit_size: float = 10,
|
||||
big_tick_numbers: list[float] = [0, 1],
|
||||
decimal_number_config: dict = dict(
|
||||
num_decimal_places=1,
|
||||
)
|
||||
):
|
||||
super().__init__(
|
||||
x_range=x_range,
|
||||
unit_size=unit_size,
|
||||
big_tick_numbers=big_tick_numbers,
|
||||
decimal_number_config=decimal_number_config,
|
||||
)
|
||||
|
||||
@@ -1,72 +1,128 @@
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.svg.tex_mobject import SingleStringTex
|
||||
from __future__ import annotations
|
||||
from functools import lru_cache
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DOWN, LEFT, RIGHT, UP
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.paths import straight_path
|
||||
from manimlib.utils.bezier import interpolate
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypeVar, Callable
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.typing import ManimColor, Vect3, Self
|
||||
|
||||
T = TypeVar("T", bound=VMobject)
|
||||
|
||||
|
||||
string_to_mob_map = {}
|
||||
@lru_cache()
|
||||
def char_to_cahced_mob(char: str, **text_config):
|
||||
if "\\" in char:
|
||||
# This is for when the "character" is a LaTeX command
|
||||
# like ^\circ or \dots
|
||||
return Tex(char, **text_config)
|
||||
else:
|
||||
return Text(char, **text_config)
|
||||
|
||||
|
||||
class DecimalNumber(VMobject):
|
||||
CONFIG = {
|
||||
"stroke_width": 0,
|
||||
"fill_opacity": 1.0,
|
||||
"num_decimal_places": 2,
|
||||
"include_sign": False,
|
||||
"group_with_commas": True,
|
||||
"digit_buff_per_font_unit": 0.001,
|
||||
"show_ellipsis": False,
|
||||
"unit": None, # Aligned to bottom unless it starts with "^"
|
||||
"include_background_rectangle": False,
|
||||
"edge_to_fix": LEFT,
|
||||
"font_size": 48,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
number: float | complex = 0,
|
||||
color: ManimColor = WHITE,
|
||||
stroke_width: float = 0,
|
||||
fill_opacity: float = 1.0,
|
||||
fill_border_width: float = 0.5,
|
||||
num_decimal_places: int = 2,
|
||||
include_sign: bool = False,
|
||||
group_with_commas: bool = True,
|
||||
digit_buff_per_font_unit: float = 0.001,
|
||||
show_ellipsis: bool = False,
|
||||
unit: str | None = None, # Aligned to bottom unless it starts with "^"
|
||||
include_background_rectangle: bool = False,
|
||||
edge_to_fix: Vect3 = LEFT,
|
||||
font_size: float = 48,
|
||||
text_config: dict = dict(), # Do not pass in font_size here
|
||||
**kwargs
|
||||
):
|
||||
self.num_decimal_places = num_decimal_places
|
||||
self.include_sign = include_sign
|
||||
self.group_with_commas = group_with_commas
|
||||
self.digit_buff_per_font_unit = digit_buff_per_font_unit
|
||||
self.show_ellipsis = show_ellipsis
|
||||
self.unit = unit
|
||||
self.include_background_rectangle = include_background_rectangle
|
||||
self.edge_to_fix = edge_to_fix
|
||||
self.font_size = font_size
|
||||
self.text_config = dict(text_config)
|
||||
|
||||
super().__init__(
|
||||
color=color,
|
||||
stroke_width=stroke_width,
|
||||
fill_opacity=fill_opacity,
|
||||
fill_border_width=fill_border_width,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def __init__(self, number=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:
|
||||
# Create the submobject list
|
||||
self.number = number
|
||||
self.set_submobjects([])
|
||||
self.num_string = self.get_num_string(number)
|
||||
|
||||
num_string = self.get_num_string(number)
|
||||
self.add(*map(self.string_to_mob, num_string))
|
||||
|
||||
# Add non-numerical bits
|
||||
# Submob_templates will be a list of cached Tex and Text mobjects,
|
||||
# with the intent of calling .copy or .become on them
|
||||
submob_templates = list(map(self.char_to_mob, self.num_string))
|
||||
if self.show_ellipsis:
|
||||
dots = self.string_to_mob("...")
|
||||
dots = self.char_to_mob("...")
|
||||
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
|
||||
self.add(dots)
|
||||
submob_templates.append(dots)
|
||||
if self.unit is not None:
|
||||
self.unit_sign = self.string_to_mob(self.unit, SingleStringTex)
|
||||
self.add(self.unit_sign)
|
||||
submob_templates.append(self.char_to_mob(self.unit))
|
||||
|
||||
self.arrange(
|
||||
buff=self.digit_buff_per_font_unit * self.get_font_size(),
|
||||
aligned_edge=DOWN
|
||||
)
|
||||
# Set internals
|
||||
font_size = self.get_font_size()
|
||||
if len(submob_templates) == len(self.submobjects):
|
||||
for sm, smt in zip(self.submobjects, submob_templates):
|
||||
sm.become(smt)
|
||||
sm.scale(font_size / smt.font_size)
|
||||
else:
|
||||
self.set_submobjects([
|
||||
smt.copy().scale(font_size / smt.font_size)
|
||||
for smt in submob_templates
|
||||
])
|
||||
|
||||
# Handle alignment of parts that should be aligned
|
||||
# to the bottom
|
||||
for i, c in enumerate(num_string):
|
||||
if c == "–" and len(num_string) > i + 1:
|
||||
digit_buff = self.digit_buff_per_font_unit * font_size
|
||||
self.arrange(RIGHT, buff=digit_buff, aligned_edge=DOWN)
|
||||
|
||||
# Handle alignment of special characters
|
||||
for i, c in enumerate(self.num_string):
|
||||
if c == "–" and len(self.num_string) > i + 1:
|
||||
self[i].align_to(self[i + 1], UP)
|
||||
self[i].shift(self[i + 1].get_height() * DOWN / 2)
|
||||
elif c == ",":
|
||||
self[i].shift(self[i].get_height() * DOWN / 2)
|
||||
if self.unit and self.unit.startswith("^"):
|
||||
self.unit_sign.align_to(self, UP)
|
||||
self[-1].align_to(self, UP)
|
||||
|
||||
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:
|
||||
formatter = self.get_formatter()
|
||||
if self.num_decimal_places == 0 and isinstance(number, float):
|
||||
number = int(number)
|
||||
num_string = formatter.format(number)
|
||||
|
||||
rounded_num = np.round(number, self.num_decimal_places)
|
||||
@@ -78,21 +134,24 @@ class DecimalNumber(VMobject):
|
||||
num_string = num_string.replace("-", "–")
|
||||
return num_string
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
self.data["font_size"] = np.array([self.font_size], dtype=float)
|
||||
def char_to_mob(self, char: str) -> Text:
|
||||
return char_to_cahced_mob(char, **self.text_config)
|
||||
|
||||
def get_font_size(self):
|
||||
return self.data["font_size"][0]
|
||||
def interpolate(
|
||||
self,
|
||||
mobject1: Mobject,
|
||||
mobject2: Mobject,
|
||||
alpha: float,
|
||||
path_func: Callable[[np.ndarray, np.ndarray, float], np.ndarray] = straight_path
|
||||
) -> Self:
|
||||
super().interpolate(mobject1, mobject2, alpha, path_func)
|
||||
if hasattr(mobject1, "font_size") and hasattr(mobject2, "font_size"):
|
||||
self.font_size = interpolate(mobject1.font_size, mobject2.font_size, alpha)
|
||||
|
||||
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()
|
||||
mob.scale(self.get_font_size())
|
||||
return mob
|
||||
def get_font_size(self) -> float:
|
||||
return self.font_size
|
||||
|
||||
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
|
||||
@@ -111,46 +170,57 @@ class DecimalNumber(VMobject):
|
||||
]
|
||||
])
|
||||
config.update(kwargs)
|
||||
ndp = config["num_decimal_places"]
|
||||
return "".join([
|
||||
"{",
|
||||
config.get("field_name", ""),
|
||||
":",
|
||||
"+" if config["include_sign"] else "",
|
||||
"," if config["group_with_commas"] else "",
|
||||
".", str(config["num_decimal_places"]), "f",
|
||||
f".{ndp}f" if ndp > 0 else "d",
|
||||
"}",
|
||||
])
|
||||
|
||||
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 get_tex(self):
|
||||
return self.num_string
|
||||
|
||||
def set_value(self, number: float | complex) -> Self:
|
||||
move_to_point = self.get_edge_center(self.edge_to_fix)
|
||||
old_submobjects = list(self.submobjects)
|
||||
style = self.family_members_with_points()[0].get_style()
|
||||
self.set_submobjects_from_number(number)
|
||||
self.move_to(move_to_point, self.edge_to_fix)
|
||||
for sm1, sm2 in zip(self.submobjects, old_submobjects):
|
||||
sm1.match_style(sm2)
|
||||
self.set_style(**style)
|
||||
for submob in self.get_family():
|
||||
submob.uniforms.update(self.uniforms)
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor):
|
||||
self.data["font_size"] *= scale_factor
|
||||
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
|
||||
self.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) -> Self:
|
||||
self.set_value(self.get_value() + delta_t)
|
||||
return self
|
||||
|
||||
|
||||
class Integer(DecimalNumber):
|
||||
CONFIG = {
|
||||
"num_decimal_places": 0,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
number: int = 0,
|
||||
num_decimal_places: int = 0,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(number, num_decimal_places=num_decimal_places, **kwargs)
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> int:
|
||||
return int(np.round(super().get_value()))
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
from manimlib.constants import *
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import BLUE, BLUE_E, GREEN_E, GREY_B, GREY_D, MAROON_B, YELLOW
|
||||
from manimlib.constants import DOWN, LEFT, RIGHT, UP
|
||||
from manimlib.constants import MED_LARGE_BUFF, MED_SMALL_BUFF, SMALL_BUFF
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -9,22 +15,42 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.color import color_gradient
|
||||
from manimlib.utils.iterables import listify
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable
|
||||
from manimlib.typing import ManimColor
|
||||
|
||||
|
||||
EPSILON = 0.0001
|
||||
|
||||
|
||||
class SampleSpace(Rectangle):
|
||||
CONFIG = {
|
||||
"height": 3,
|
||||
"width": 3,
|
||||
"fill_color": GREY_D,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0.5,
|
||||
"stroke_color": GREY_B,
|
||||
##
|
||||
"default_label_scale_val": 1,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
width: float = 3,
|
||||
height: float = 3,
|
||||
fill_color: ManimColor = GREY_D,
|
||||
fill_opacity: float = 1,
|
||||
stroke_width: float = 0.5,
|
||||
stroke_color: ManimColor = GREY_B,
|
||||
default_label_scale_val: float = 1,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
width, height,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
)
|
||||
self.default_label_scale_val = default_label_scale_val
|
||||
|
||||
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 +59,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 +92,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 +147,35 @@ class SampleSpace(Rectangle):
|
||||
}
|
||||
return VGroup(parts.braces, parts.labels)
|
||||
|
||||
def get_side_braces_and_labels(self, labels, direction=LEFT, **kwargs):
|
||||
assert(hasattr(self, "horizontal_parts"))
|
||||
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):
|
||||
assert(hasattr(self, "vertical_parts"))
|
||||
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):
|
||||
assert(hasattr(self, "vertical_parts"))
|
||||
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 +184,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"):
|
||||
@@ -145,58 +193,88 @@ class SampleSpace(Rectangle):
|
||||
|
||||
|
||||
class BarChart(VGroup):
|
||||
CONFIG = {
|
||||
"height": 4,
|
||||
"width": 6,
|
||||
"n_ticks": 4,
|
||||
"tick_width": 0.2,
|
||||
"label_y_axis": True,
|
||||
"y_axis_label_height": 0.25,
|
||||
"max_value": 1,
|
||||
"bar_colors": [BLUE, YELLOW],
|
||||
"bar_fill_opacity": 0.8,
|
||||
"bar_stroke_width": 3,
|
||||
"bar_names": [],
|
||||
"bar_label_scale_val": 0.75,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
values: Iterable[float],
|
||||
height: float = 4,
|
||||
width: float = 6,
|
||||
n_ticks: int = 4,
|
||||
include_x_ticks: bool = False,
|
||||
tick_width: float = 0.2,
|
||||
tick_height: float = 0.15,
|
||||
label_y_axis: bool = True,
|
||||
y_axis_label_height: float = 0.25,
|
||||
max_value: float = 1,
|
||||
bar_colors: list[ManimColor] = [BLUE, YELLOW],
|
||||
bar_fill_opacity: float = 0.8,
|
||||
bar_stroke_width: float = 3,
|
||||
bar_names: list[str] = [],
|
||||
bar_label_scale_val: float = 0.75,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.height = height
|
||||
self.width = width
|
||||
self.n_ticks = n_ticks
|
||||
self.include_x_ticks = include_x_ticks
|
||||
self.tick_width = tick_width
|
||||
self.tick_height = tick_height
|
||||
self.label_y_axis = label_y_axis
|
||||
self.y_axis_label_height = y_axis_label_height
|
||||
self.max_value = max_value
|
||||
self.bar_colors = bar_colors
|
||||
self.bar_fill_opacity = bar_fill_opacity
|
||||
self.bar_stroke_width = bar_stroke_width
|
||||
self.bar_names = bar_names
|
||||
self.bar_label_scale_val = bar_label_scale_val
|
||||
|
||||
def __init__(self, values, **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 +283,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,13 +298,10 @@ 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(
|
||||
(value / self.max_value) * self.height
|
||||
)
|
||||
bar.move_to(bar_bottom, DOWN)
|
||||
|
||||
def copy(self):
|
||||
return self.deepcopy()
|
||||
|
||||
@@ -1,54 +1,88 @@
|
||||
from manimlib.constants import *
|
||||
from __future__ import annotations
|
||||
|
||||
from colour import Color
|
||||
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.constants import BLACK, RED, YELLOW, WHITE
|
||||
from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR
|
||||
from manimlib.constants import SMALL_BUFF
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.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 Sequence
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.typing import ManimColor, Self
|
||||
|
||||
|
||||
class SurroundingRectangle(Rectangle):
|
||||
CONFIG = {
|
||||
"color": YELLOW,
|
||||
"buff": SMALL_BUFF,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
buff: float = SMALL_BUFF,
|
||||
color: ManimColor = YELLOW,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(color=color, **kwargs)
|
||||
self.buff = buff
|
||||
self.surround(mobject)
|
||||
if mobject.is_fixed_in_frame():
|
||||
self.fix_in_frame()
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
kwargs["width"] = mobject.get_width() + 2 * self.buff
|
||||
kwargs["height"] = mobject.get_height() + 2 * self.buff
|
||||
Rectangle.__init__(self, **kwargs)
|
||||
self.move_to(mobject)
|
||||
def surround(self, mobject, buff=None) -> Self:
|
||||
self.mobject = mobject
|
||||
self.buff = buff if buff is not None else self.buff
|
||||
super().surround(mobject, self.buff)
|
||||
return self
|
||||
|
||||
def set_buff(self, buff) -> Self:
|
||||
self.buff = buff
|
||||
self.surround(self.mobject)
|
||||
return self
|
||||
|
||||
|
||||
class BackgroundRectangle(SurroundingRectangle):
|
||||
CONFIG = {
|
||||
"stroke_width": 0,
|
||||
"stroke_opacity": 0,
|
||||
"fill_opacity": 0.75,
|
||||
"buff": 0
|
||||
}
|
||||
|
||||
def __init__(self, mobject, color=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
color: ManimColor = None,
|
||||
stroke_width: float = 0,
|
||||
stroke_opacity: float = 0,
|
||||
fill_opacity: float = 0.75,
|
||||
buff: float = 0,
|
||||
**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
|
||||
color = manim_config.camera.background_color
|
||||
super().__init__(
|
||||
mobject,
|
||||
color=color,
|
||||
stroke_width=stroke_width,
|
||||
stroke_opacity=stroke_opacity,
|
||||
fill_opacity=fill_opacity,
|
||||
buff=buff,
|
||||
**kwargs
|
||||
)
|
||||
self.original_fill_opacity = fill_opacity
|
||||
|
||||
def pointwise_become_partial(self, mobject, a, b):
|
||||
def pointwise_become_partial(self, mobject: Mobject, a: float, b: float) -> Self:
|
||||
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
|
||||
):
|
||||
def set_style(
|
||||
self,
|
||||
stroke_color: ManimColor | None = None,
|
||||
stroke_width: float | None = None,
|
||||
fill_color: ManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
family: bool = True
|
||||
) -> Self:
|
||||
# Unchangeable style, except for fill_opacity
|
||||
VMobject.set_style_data(
|
||||
VMobject.set_style(
|
||||
self,
|
||||
stroke_color=BLACK,
|
||||
stroke_width=0,
|
||||
@@ -57,32 +91,40 @@ 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": [0, 6, 0],
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
stroke_color: ManimColor = RED,
|
||||
stroke_width: float | Sequence[float] = [0, 6, 0],
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
Line(UL, DR),
|
||||
Line(UR, DL),
|
||||
)
|
||||
self.insert_n_curves(2)
|
||||
self.insert_n_curves(20)
|
||||
self.replace(mobject, stretch=True)
|
||||
self.set_stroke(self.stroke_color, width=self.stroke_width)
|
||||
self.set_stroke(stroke_color, width=stroke_width)
|
||||
|
||||
|
||||
class Underline(Line):
|
||||
CONFIG = {
|
||||
"buff": SMALL_BUFF,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
buff: float = SMALL_BUFF,
|
||||
stroke_color=WHITE,
|
||||
stroke_width: float | Sequence[float] = [0, 3, 3, 0],
|
||||
stretch_factor=1.2,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(LEFT, RIGHT, **kwargs)
|
||||
self.match_width(mobject)
|
||||
self.next_to(mobject, DOWN, buff=self.buff)
|
||||
if not isinstance(stroke_width, (float, int)):
|
||||
self.insert_n_curves(len(stroke_width) - 2)
|
||||
self.set_stroke(stroke_color, stroke_width)
|
||||
self.set_width(mobject.get_width() * stretch_factor)
|
||||
self.next_to(mobject, DOWN, buff=buff)
|
||||
|
||||
@@ -1,41 +1,58 @@
|
||||
import numpy as np
|
||||
import math
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import copy
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF, SMALL_BUFF
|
||||
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL
|
||||
from manimlib.constants import PI
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.constants import *
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.animation.growing import GrowFromCenter
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import SingleStringTex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
class Brace(SingleStringTex):
|
||||
CONFIG = {
|
||||
"buff": 0.2,
|
||||
"tex_string": r"\underbrace{\qquad}"
|
||||
}
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
|
||||
class Brace(Tex):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
direction: Vect3 = DOWN,
|
||||
buff: float = 0.2,
|
||||
tex_string: str = R"\underbrace{\qquad}",
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(tex_string, **kwargs)
|
||||
|
||||
def __init__(self, mobject, direction=DOWN, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
angle = -math.atan2(*direction[:2]) + PI
|
||||
mobject.rotate(-angle, about_point=ORIGIN)
|
||||
left = mobject.get_corner(DOWN + LEFT)
|
||||
right = mobject.get_corner(DOWN + RIGHT)
|
||||
left = mobject.get_corner(DL)
|
||||
right = mobject.get_corner(DR)
|
||||
target_width = right[0] - left[0]
|
||||
|
||||
super().__init__(self.tex_string, **kwargs)
|
||||
self.tip_point_index = np.argmin(self.get_all_points()[:, 1])
|
||||
self.set_initial_width(target_width)
|
||||
self.shift(left - self.get_corner(UP + LEFT) + self.buff * DOWN)
|
||||
self.shift(left - self.get_corner(UL) + buff * DOWN)
|
||||
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)]:
|
||||
@@ -48,7 +65,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(),
|
||||
@@ -57,60 +79,69 @@ class Brace(SingleStringTex):
|
||||
)
|
||||
else:
|
||||
mob.move_to(self.get_tip())
|
||||
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFFER)
|
||||
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFF)
|
||||
shift_distance = mob.get_width() / 2.0 + buff
|
||||
mob.shift(self.get_direction() * shift_distance)
|
||||
return self
|
||||
|
||||
def get_text(self, text, **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):
|
||||
tex_mob = Tex(*tex)
|
||||
self.put_at_tip(tex_mob, **kwargs)
|
||||
def get_tex(self, *tex: str, **kwargs) -> Tex:
|
||||
buff = kwargs.pop("buff", SMALL_BUFF)
|
||||
tex_mob = Tex(*tex, **kwargs)
|
||||
self.put_at_tip(tex_mob, buff=buff)
|
||||
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)
|
||||
|
||||
|
||||
class BraceLabel(VMobject):
|
||||
CONFIG = {
|
||||
"label_constructor": Tex,
|
||||
"label_scale": 1,
|
||||
}
|
||||
label_constructor: type = Tex
|
||||
|
||||
def __init__(self, obj, text, brace_direction=DOWN, **kwargs):
|
||||
VMobject.__init__(self, **kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
obj: VMobject | list[VMobject],
|
||||
text: str | Iterable[str],
|
||||
brace_direction: np.ndarray = DOWN,
|
||||
label_scale: float = 1.0,
|
||||
label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF,
|
||||
**kwargs
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.brace_direction = brace_direction
|
||||
self.label_scale = label_scale
|
||||
self.label_buff = label_buff
|
||||
|
||||
if isinstance(obj, list):
|
||||
obj = VMobject(*obj)
|
||||
obj = VGroup(*obj)
|
||||
self.brace = Brace(obj, brace_direction, **kwargs)
|
||||
|
||||
if isinstance(text, tuple) or isinstance(text, list):
|
||||
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.label = self.label_constructor(*listify(text), **kwargs)
|
||||
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: VMobject | list[VMobject], **kwargs):
|
||||
if isinstance(obj, list):
|
||||
obj = VMobject(*obj)
|
||||
self.brace = Brace(obj, self.brace_direction, **kwargs)
|
||||
@@ -118,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)
|
||||
@@ -127,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: VMobject | list[VMobject], *text: str):
|
||||
self.shift_brace(obj)
|
||||
self.change_label(*text)
|
||||
return self
|
||||
@@ -142,6 +173,4 @@ class BraceLabel(VMobject):
|
||||
|
||||
|
||||
class BraceText(BraceLabel):
|
||||
CONFIG = {
|
||||
"label_constructor": TexText
|
||||
}
|
||||
label_constructor: type = TexText
|
||||
|
||||
@@ -1,81 +1,146 @@
|
||||
from manimlib.animation.animation import Animation
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
import random
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.animation.rotation import Rotating
|
||||
from manimlib.constants import *
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.constants import BLUE_A
|
||||
from manimlib.constants import BLUE_B
|
||||
from manimlib.constants import BLUE_C
|
||||
from manimlib.constants import BLUE_D
|
||||
from manimlib.constants import DOWN
|
||||
from manimlib.constants import DOWN
|
||||
from manimlib.constants import FRAME_WIDTH
|
||||
from manimlib.constants import GREEN
|
||||
from manimlib.constants import GREEN_SCREEN
|
||||
from manimlib.constants import GREEN_E
|
||||
from manimlib.constants import GREY
|
||||
from manimlib.constants import GREY_A
|
||||
from manimlib.constants import GREY_B
|
||||
from manimlib.constants import GREY_E
|
||||
from manimlib.constants import LEFT
|
||||
from manimlib.constants import LEFT
|
||||
from manimlib.constants import MED_LARGE_BUFF
|
||||
from manimlib.constants import MED_SMALL_BUFF
|
||||
from manimlib.constants import ORIGIN
|
||||
from manimlib.constants import OUT
|
||||
from manimlib.constants import PI
|
||||
from manimlib.constants import RED
|
||||
from manimlib.constants import RED_E
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.constants import SMALL_BUFF
|
||||
from manimlib.constants import SMALL_BUFF
|
||||
from manimlib.constants import UP
|
||||
from manimlib.constants import UL
|
||||
from manimlib.constants import UR
|
||||
from manimlib.constants import DL
|
||||
from manimlib.constants import DR
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import YELLOW
|
||||
from manimlib.constants import TAU
|
||||
from manimlib.mobject.boolean_ops import Difference
|
||||
from manimlib.mobject.boolean_ops import Union
|
||||
from manimlib.mobject.geometry import Arc
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Polygon
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import Square
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.geometry import AnnularSector
|
||||
from manimlib.mobject.numbers import Integer
|
||||
from manimlib.mobject.shape_matchers import SurroundingRectangle
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.three_dimensions import Cube
|
||||
from manimlib.mobject.svg.special_tex import TexTextFromPresetString
|
||||
from manimlib.mobject.three_dimensions import Prismify
|
||||
from manimlib.mobject.three_dimensions import VCube
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.iterables import adjacent_pairs
|
||||
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 compass_directions
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.space_ops import midpoint
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
class Checkmark(TexText):
|
||||
CONFIG = {
|
||||
"color": GREEN
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("\\ding{51}")
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple, Sequence, Callable
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
|
||||
|
||||
class Exmark(TexText):
|
||||
CONFIG = {
|
||||
"color": RED
|
||||
}
|
||||
class Checkmark(TexTextFromPresetString):
|
||||
tex: str = R"\ding{51}"
|
||||
default_color: ManimColor = GREEN
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("\\ding{55}")
|
||||
|
||||
class Exmark(TexTextFromPresetString):
|
||||
tex: str = R"\ding{55}"
|
||||
default_color: ManimColor = RED
|
||||
|
||||
|
||||
class Lightbulb(SVGMobject):
|
||||
CONFIG = {
|
||||
"height": 1,
|
||||
"stroke_color": YELLOW,
|
||||
"stroke_width": 3,
|
||||
"fill_color": YELLOW,
|
||||
"fill_opacity": 0,
|
||||
}
|
||||
file_name = "lightbulb"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("lightbulb", **kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
height: float = 1.0,
|
||||
color: ManimColor = YELLOW,
|
||||
stroke_width: float = 3.0,
|
||||
fill_opacity: float = 0.0,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
height=height,
|
||||
color=color,
|
||||
stroke_width=stroke_width,
|
||||
fill_opacity=fill_opacity,
|
||||
**kwargs
|
||||
)
|
||||
self.insert_n_curves(25)
|
||||
|
||||
|
||||
class Speedometer(VMobject):
|
||||
CONFIG = {
|
||||
"arc_angle": 4 * np.pi / 3,
|
||||
"num_ticks": 8,
|
||||
"tick_length": 0.2,
|
||||
"needle_width": 0.1,
|
||||
"needle_height": 0.8,
|
||||
"needle_color": YELLOW,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
arc_angle: float = 4 * PI / 3,
|
||||
num_ticks: int = 8,
|
||||
tick_length: float = 0.2,
|
||||
needle_width: float = 0.1,
|
||||
needle_height: float = 0.8,
|
||||
needle_color: ManimColor = YELLOW,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
start_angle = np.pi / 2 + self.arc_angle / 2
|
||||
end_angle = np.pi / 2 - self.arc_angle / 2
|
||||
self.add(Arc(
|
||||
self.arc_angle = arc_angle
|
||||
self.num_ticks = num_ticks
|
||||
self.tick_length = tick_length
|
||||
self.needle_width = needle_width
|
||||
self.needle_height = needle_height
|
||||
self.needle_color = needle_color
|
||||
|
||||
start_angle = PI / 2 + arc_angle / 2
|
||||
end_angle = PI / 2 - arc_angle / 2
|
||||
self.arc = Arc(
|
||||
start_angle=start_angle,
|
||||
angle=-self.arc_angle
|
||||
))
|
||||
tick_angle_range = np.linspace(start_angle, end_angle, self.num_ticks)
|
||||
)
|
||||
self.add(self.arc)
|
||||
tick_angle_range = np.linspace(start_angle, end_angle, num_ticks)
|
||||
for index, angle in enumerate(tick_angle_range):
|
||||
vect = rotate_vector(RIGHT, angle)
|
||||
tick = Line((1 - self.tick_length) * vect, vect)
|
||||
label = Tex(str(10 * index))
|
||||
label.set_height(self.tick_length)
|
||||
label.shift((1 + self.tick_length) * vect)
|
||||
tick = Line((1 - tick_length) * vect, vect)
|
||||
label = Integer(10 * index)
|
||||
label.set_height(tick_length)
|
||||
label.shift((1 + tick_length) * vect)
|
||||
self.add(tick, label)
|
||||
|
||||
needle = Polygon(
|
||||
@@ -84,8 +149,8 @@ class Speedometer(VMobject):
|
||||
fill_opacity=1,
|
||||
fill_color=self.needle_color
|
||||
)
|
||||
needle.stretch_to_fit_width(self.needle_width)
|
||||
needle.stretch_to_fit_height(self.needle_height)
|
||||
needle.stretch_to_fit_width(needle_width)
|
||||
needle.stretch_to_fit_height(needle_height)
|
||||
needle.rotate(start_angle - np.pi / 2, about_point=ORIGIN)
|
||||
self.add(needle)
|
||||
self.needle = needle
|
||||
@@ -107,7 +172,7 @@ class Speedometer(VMobject):
|
||||
)
|
||||
|
||||
def rotate_needle(self, angle):
|
||||
self.needle.rotate(angle, about_point=self.get_center())
|
||||
self.needle.rotate(angle, about_point=self.arc.get_arc_center())
|
||||
return self
|
||||
|
||||
def move_needle_to_velocity(self, velocity):
|
||||
@@ -120,66 +185,67 @@ class Speedometer(VMobject):
|
||||
|
||||
|
||||
class Laptop(VGroup):
|
||||
CONFIG = {
|
||||
"width": 3,
|
||||
"body_dimensions": [4, 3, 0.05],
|
||||
"screen_thickness": 0.01,
|
||||
"keyboard_width_to_body_width": 0.9,
|
||||
"keyboard_height_to_body_height": 0.5,
|
||||
"screen_width_to_screen_plate_width": 0.9,
|
||||
"key_color_kwargs": {
|
||||
"stroke_width": 0,
|
||||
"fill_color": BLACK,
|
||||
"fill_opacity": 1,
|
||||
},
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
"body_color": GREY_B,
|
||||
"shaded_body_color": GREY,
|
||||
"open_angle": np.pi / 4,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
width: float = 3,
|
||||
body_dimensions: Tuple[float, float, float] = (4.0, 3.0, 0.05),
|
||||
screen_thickness: float = 0.01,
|
||||
keyboard_width_to_body_width: float = 0.9,
|
||||
keyboard_height_to_body_height: float = 0.5,
|
||||
screen_width_to_screen_plate_width: float = 0.9,
|
||||
key_color_kwargs: dict = dict(
|
||||
stroke_width=0,
|
||||
fill_color=BLACK,
|
||||
fill_opacity=1,
|
||||
),
|
||||
fill_opacity: float = 1.0,
|
||||
stroke_width: float = 0.0,
|
||||
body_color: ManimColor = GREY_B,
|
||||
shaded_body_color: ManimColor = GREY,
|
||||
open_angle: float = np.pi / 4,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
body = Cube(side_length=1)
|
||||
for dim, scale_factor in enumerate(self.body_dimensions):
|
||||
|
||||
body = VCube(side_length=1)
|
||||
for dim, scale_factor in enumerate(body_dimensions):
|
||||
body.stretch(scale_factor, dim=dim)
|
||||
body.set_width(self.width)
|
||||
body.set_fill(self.shaded_body_color, opacity=1)
|
||||
body.set_width(width)
|
||||
body.set_fill(shaded_body_color, opacity=1)
|
||||
body.sort(lambda p: p[2])
|
||||
body[-1].set_fill(self.body_color)
|
||||
body[-1].set_fill(body_color)
|
||||
screen_plate = body.copy()
|
||||
keyboard = VGroup(*[
|
||||
VGroup(*[
|
||||
Square(**self.key_color_kwargs)
|
||||
Square(**key_color_kwargs)
|
||||
for x in range(12 - y % 2)
|
||||
]).arrange(RIGHT, buff=SMALL_BUFF)
|
||||
for y in range(4)
|
||||
]).arrange(DOWN, buff=MED_SMALL_BUFF)
|
||||
keyboard.stretch_to_fit_width(
|
||||
self.keyboard_width_to_body_width * body.get_width(),
|
||||
keyboard_width_to_body_width * body.get_width(),
|
||||
)
|
||||
keyboard.stretch_to_fit_height(
|
||||
self.keyboard_height_to_body_height * body.get_height(),
|
||||
keyboard_height_to_body_height * body.get_height(),
|
||||
)
|
||||
keyboard.next_to(body, OUT, buff=0.1 * SMALL_BUFF)
|
||||
keyboard.shift(MED_SMALL_BUFF * UP)
|
||||
body.add(keyboard)
|
||||
|
||||
screen_plate.stretch(self.screen_thickness /
|
||||
self.body_dimensions[2], dim=2)
|
||||
screen_plate.stretch(screen_thickness /
|
||||
body_dimensions[2], dim=2)
|
||||
screen = Rectangle(
|
||||
stroke_width=0,
|
||||
fill_color=BLACK,
|
||||
fill_opacity=1,
|
||||
)
|
||||
screen.replace(screen_plate, stretch=True)
|
||||
screen.scale(self.screen_width_to_screen_plate_width)
|
||||
screen.scale(screen_width_to_screen_plate_width)
|
||||
screen.next_to(screen_plate, OUT, buff=0.1 * SMALL_BUFF)
|
||||
screen_plate.add(screen)
|
||||
screen_plate.next_to(body, UP, buff=0)
|
||||
screen_plate.rotate(
|
||||
self.open_angle, RIGHT,
|
||||
open_angle, RIGHT,
|
||||
about_point=screen_plate.get_bottom()
|
||||
)
|
||||
self.screen_plate = screen_plate
|
||||
@@ -194,170 +260,182 @@ class Laptop(VGroup):
|
||||
self.axis = axis
|
||||
|
||||
self.add(body, screen_plate, axis)
|
||||
self.rotate(5 * np.pi / 12, LEFT, about_point=ORIGIN)
|
||||
self.rotate(np.pi / 6, DOWN, about_point=ORIGIN)
|
||||
|
||||
|
||||
class VideoIcon(SVGMobject):
|
||||
CONFIG = {
|
||||
"width": FRAME_WIDTH / 12.,
|
||||
}
|
||||
file_name: str = "video_icon"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(file_name="video_icon", **kwargs)
|
||||
self.center()
|
||||
self.set_width(self.width)
|
||||
self.set_stroke(color=WHITE, width=0)
|
||||
self.set_fill(color=WHITE, opacity=1)
|
||||
def __init__(
|
||||
self,
|
||||
width: float = 1.2,
|
||||
color=BLUE_A,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(color=color, **kwargs)
|
||||
self.set_width(width)
|
||||
|
||||
|
||||
class VideoSeries(VGroup):
|
||||
CONFIG = {
|
||||
"num_videos": 11,
|
||||
"gradient_colors": [BLUE_B, BLUE_D],
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
videos = [VideoIcon() for x in range(self.num_videos)]
|
||||
VGroup.__init__(self, *videos, **kwargs)
|
||||
self.arrange()
|
||||
self.set_width(FRAME_WIDTH - MED_LARGE_BUFF)
|
||||
self.set_color_by_gradient(*self.gradient_colors)
|
||||
def __init__(
|
||||
self,
|
||||
num_videos: int = 11,
|
||||
gradient_colors: Sequence[ManimColor] = [BLUE_B, BLUE_D],
|
||||
width: float = FRAME_WIDTH - MED_LARGE_BUFF,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
*(VideoIcon() for x in range(num_videos)),
|
||||
**kwargs
|
||||
)
|
||||
self.arrange(RIGHT)
|
||||
self.set_width(width)
|
||||
self.set_color_by_gradient(*gradient_colors)
|
||||
|
||||
|
||||
class Clock(VGroup):
|
||||
CONFIG = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
circle = Circle(color=WHITE)
|
||||
def __init__(
|
||||
self,
|
||||
stroke_color: ManimColor = WHITE,
|
||||
stroke_width: float = 3.0,
|
||||
hour_hand_height: float = 0.3,
|
||||
minute_hand_height: float = 0.6,
|
||||
tick_length: float = 0.1,
|
||||
**kwargs,
|
||||
):
|
||||
style = dict(stroke_color=stroke_color, stroke_width=stroke_width)
|
||||
circle = Circle(**style)
|
||||
ticks = []
|
||||
for x in range(12):
|
||||
alpha = x / 12.
|
||||
point = complex_to_R3(
|
||||
np.exp(2 * np.pi * alpha * complex(0, 1))
|
||||
)
|
||||
length = 0.2 if x % 3 == 0 else 0.1
|
||||
ticks.append(
|
||||
Line(point, (1 - length) * point)
|
||||
)
|
||||
self.hour_hand = Line(ORIGIN, 0.3 * UP)
|
||||
self.minute_hand = Line(ORIGIN, 0.6 * UP)
|
||||
# for hand in self.hour_hand, self.minute_hand:
|
||||
# #Balance out where the center is
|
||||
# hand.add(VectorizedPoint(-hand.get_end()))
|
||||
for x, point in enumerate(compass_directions(12, UP)):
|
||||
length = tick_length
|
||||
if x % 3 == 0:
|
||||
length *= 2
|
||||
ticks.append(Line(point, (1 - length) * point, **style))
|
||||
self.hour_hand = Line(ORIGIN, hour_hand_height * UP, **style)
|
||||
self.minute_hand = Line(ORIGIN, minute_hand_height * UP, **style)
|
||||
|
||||
VGroup.__init__(
|
||||
self, circle,
|
||||
self.hour_hand, self.minute_hand,
|
||||
super().__init__(
|
||||
circle, self.hour_hand, self.minute_hand,
|
||||
*ticks
|
||||
)
|
||||
|
||||
|
||||
class ClockPassesTime(Animation):
|
||||
CONFIG = {
|
||||
"run_time": 5,
|
||||
"hours_passed": 12,
|
||||
"rate_func": linear,
|
||||
}
|
||||
|
||||
def __init__(self, clock, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
assert(isinstance(clock, Clock))
|
||||
rot_kwargs = {
|
||||
"axis": OUT,
|
||||
"about_point": clock.get_center()
|
||||
}
|
||||
hour_radians = -self.hours_passed * 2 * np.pi / 12
|
||||
self.hour_rotation = Rotating(
|
||||
clock.hour_hand,
|
||||
angle=hour_radians,
|
||||
**rot_kwargs
|
||||
class ClockPassesTime(AnimationGroup):
|
||||
def __init__(
|
||||
self,
|
||||
clock: Clock,
|
||||
run_time: float = 5.0,
|
||||
hours_passed: float = 12.0,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
**kwargs
|
||||
):
|
||||
rot_kwargs = dict(
|
||||
axis=OUT,
|
||||
about_point=clock.get_center()
|
||||
)
|
||||
self.hour_rotation.begin()
|
||||
self.minute_rotation = Rotating(
|
||||
clock.minute_hand,
|
||||
angle=12 * hour_radians,
|
||||
**rot_kwargs
|
||||
hour_radians = -hours_passed * 2 * PI / 12
|
||||
super().__init__(
|
||||
Rotating(
|
||||
clock.hour_hand,
|
||||
angle=hour_radians,
|
||||
**rot_kwargs
|
||||
),
|
||||
Rotating(
|
||||
clock.minute_hand,
|
||||
angle=12 * hour_radians,
|
||||
**rot_kwargs
|
||||
),
|
||||
**kwargs
|
||||
)
|
||||
self.minute_rotation.begin()
|
||||
Animation.__init__(self, clock, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
for rotation in self.hour_rotation, self.minute_rotation:
|
||||
rotation.interpolate_mobject(alpha)
|
||||
|
||||
|
||||
class Bubble(SVGMobject):
|
||||
CONFIG = {
|
||||
"direction": LEFT,
|
||||
"center_point": ORIGIN,
|
||||
"content_scale_factor": 0.75,
|
||||
"height": 5,
|
||||
"width": 8,
|
||||
"bubble_center_adjustment_factor": 1. / 8,
|
||||
"file_name": None,
|
||||
"fill_color": BLACK,
|
||||
"fill_opacity": 0.8,
|
||||
"stroke_color": WHITE,
|
||||
"stroke_width": 3,
|
||||
}
|
||||
class Bubble(VGroup):
|
||||
file_name: str = "Bubbles_speech.svg"
|
||||
bubble_center_adjustment_factor = 0.125
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
if self.file_name is None:
|
||||
raise Exception("Must invoke Bubble subclass")
|
||||
SVGMobject.__init__(self, self.file_name, **kwargs)
|
||||
self.center()
|
||||
self.stretch_to_fit_height(self.height)
|
||||
self.stretch_to_fit_width(self.width)
|
||||
if self.direction[0] > 0:
|
||||
self.flip()
|
||||
self.direction_was_specified = ("direction" in kwargs)
|
||||
self.content = Mobject()
|
||||
self.refresh_triangulation()
|
||||
def __init__(
|
||||
self,
|
||||
content: str | VMobject | None = None,
|
||||
buff: float = 1.0,
|
||||
filler_shape: Tuple[float, float] = (3.0, 2.0),
|
||||
pin_point: Vect3 | None = None,
|
||||
direction: Vect3 = LEFT,
|
||||
add_content: bool = True,
|
||||
fill_color: ManimColor = BLACK,
|
||||
fill_opacity: float = 0.8,
|
||||
stroke_color: ManimColor = WHITE,
|
||||
stroke_width: float = 3.0,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.direction = direction
|
||||
|
||||
if content is None:
|
||||
content = Rectangle(*filler_shape)
|
||||
content.set_fill(opacity=0)
|
||||
content.set_stroke(width=0)
|
||||
elif isinstance(content, str):
|
||||
content = Text(content)
|
||||
self.content = content
|
||||
|
||||
self.body = self.get_body(content, direction, buff)
|
||||
self.body.set_fill(fill_color, fill_opacity)
|
||||
self.body.set_stroke(stroke_color, stroke_width)
|
||||
self.add(self.body)
|
||||
|
||||
if add_content:
|
||||
self.add(self.content)
|
||||
|
||||
if pin_point is not None:
|
||||
self.pin_to(pin_point)
|
||||
|
||||
def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
|
||||
body = SVGMobject(self.file_name)
|
||||
if direction[0] > 0:
|
||||
body.flip()
|
||||
# Resize
|
||||
width = content.get_width()
|
||||
height = content.get_height()
|
||||
target_width = width + min(buff, height)
|
||||
target_height = 1.35 * (height + buff) # Magic number?
|
||||
body.set_shape(target_width, target_height)
|
||||
body.move_to(content)
|
||||
body.shift(self.bubble_center_adjustment_factor * body.get_height() * DOWN)
|
||||
return body
|
||||
|
||||
def get_tip(self):
|
||||
# TODO, find a better way
|
||||
return self.get_corner(DOWN + self.direction) - 0.6 * self.direction
|
||||
return self.get_corner(DOWN + self.direction)
|
||||
|
||||
def get_bubble_center(self):
|
||||
factor = self.bubble_center_adjustment_factor
|
||||
return self.get_center() + factor * self.get_height() * UP
|
||||
|
||||
def move_tip_to(self, point):
|
||||
mover = VGroup(self)
|
||||
if self.content is not None:
|
||||
mover.add(self.content)
|
||||
mover.shift(point - self.get_tip())
|
||||
self.shift(point - self.get_tip())
|
||||
return self
|
||||
|
||||
def flip(self, axis=UP):
|
||||
Mobject.flip(self, axis=axis)
|
||||
self.refresh_unit_normal()
|
||||
self.refresh_triangulation()
|
||||
def flip(self, axis=UP, only_body=True, **kwargs):
|
||||
super().flip(axis=axis, **kwargs)
|
||||
if only_body:
|
||||
# Flip in place, don't use kwargs
|
||||
self.content.flip(axis=axis)
|
||||
if abs(axis[1]) > 0:
|
||||
self.direction = -np.array(self.direction)
|
||||
return self
|
||||
|
||||
def pin_to(self, mobject):
|
||||
def pin_to(self, mobject, auto_flip=False):
|
||||
mob_center = mobject.get_center()
|
||||
want_to_flip = np.sign(mob_center[0]) != np.sign(self.direction[0])
|
||||
can_flip = not self.direction_was_specified
|
||||
if want_to_flip and can_flip:
|
||||
if want_to_flip and auto_flip:
|
||||
self.flip()
|
||||
boundary_point = mobject.get_bounding_box_point(UP - self.direction)
|
||||
vector_from_center = 1.0 * (boundary_point - mob_center)
|
||||
self.move_tip_to(mob_center + vector_from_center)
|
||||
return self
|
||||
|
||||
def position_mobject_inside(self, mobject):
|
||||
scaled_width = self.content_scale_factor * self.get_width()
|
||||
if mobject.get_width() > scaled_width:
|
||||
mobject.set_width(scaled_width)
|
||||
mobject.shift(
|
||||
self.get_bubble_center() - mobject.get_center()
|
||||
)
|
||||
def position_mobject_inside(self, mobject, buff=MED_LARGE_BUFF):
|
||||
mobject.set_max_width(self.body.get_width() - 2 * buff)
|
||||
mobject.set_max_height(self.body.get_height() / 1.5 - 2 * buff)
|
||||
mobject.shift(self.get_bubble_center() - mobject.get_center())
|
||||
return mobject
|
||||
|
||||
def add_content(self, mobject):
|
||||
@@ -365,65 +443,140 @@ class Bubble(SVGMobject):
|
||||
self.content = mobject
|
||||
return self.content
|
||||
|
||||
def write(self, *text):
|
||||
self.add_content(TexText(*text))
|
||||
def write(self, text):
|
||||
self.add_content(Text(text))
|
||||
return self
|
||||
|
||||
def resize_to_content(self):
|
||||
target_width = self.content.get_width()
|
||||
target_width += max(MED_LARGE_BUFF, 2)
|
||||
target_height = self.content.get_height()
|
||||
target_height += 2.5 * LARGE_BUFF
|
||||
tip_point = self.get_tip()
|
||||
self.stretch_to_fit_width(target_width)
|
||||
self.stretch_to_fit_height(target_height)
|
||||
self.move_tip_to(tip_point)
|
||||
self.position_mobject_inside(self.content)
|
||||
def resize_to_content(self, buff=1.0): # TODO
|
||||
self.body.match_points(self.get_body(
|
||||
self.content, self.direction, buff
|
||||
))
|
||||
|
||||
def clear(self):
|
||||
self.add_content(VMobject())
|
||||
self.remove(self.content)
|
||||
return self
|
||||
|
||||
|
||||
class SpeechBubble(Bubble):
|
||||
CONFIG = {
|
||||
"file_name": "Bubbles_speech.svg",
|
||||
"height": 4
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
content: str | VMobject | None = None,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
filler_shape: Tuple[float, float] = (2.0, 1.0),
|
||||
stem_height_to_bubble_height: float = 0.5,
|
||||
stem_top_x_props: Tuple[float, float] = (0.2, 0.3),
|
||||
**kwargs
|
||||
):
|
||||
self.stem_height_to_bubble_height = stem_height_to_bubble_height
|
||||
self.stem_top_x_props = stem_top_x_props
|
||||
super().__init__(content, buff, filler_shape, **kwargs)
|
||||
|
||||
def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
|
||||
rect = SurroundingRectangle(content, buff=buff)
|
||||
rect.round_corners()
|
||||
lp = rect.get_corner(DL)
|
||||
rp = rect.get_corner(DR)
|
||||
stem_height = self.stem_height_to_bubble_height * rect.get_height()
|
||||
low_prop, high_prop = self.stem_top_x_props
|
||||
triangle = Polygon(
|
||||
interpolate(lp, rp, low_prop),
|
||||
interpolate(lp, rp, high_prop),
|
||||
lp + stem_height * DOWN,
|
||||
)
|
||||
result = Union(rect, triangle)
|
||||
result.insert_n_curves(20)
|
||||
if direction[0] > 0:
|
||||
result.flip()
|
||||
|
||||
class DoubleSpeechBubble(Bubble):
|
||||
CONFIG = {
|
||||
"file_name": "Bubbles_double_speech.svg",
|
||||
"height": 4
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
class ThoughtBubble(Bubble):
|
||||
CONFIG = {
|
||||
"file_name": "Bubbles_thought.svg",
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
content: str | VMobject | None = None,
|
||||
buff: float = SMALL_BUFF,
|
||||
filler_shape: Tuple[float, float] = (2.0, 1.0),
|
||||
bulge_radius: float = 0.35,
|
||||
bulge_overlap: float = 0.25,
|
||||
noise_factor: float = 0.1,
|
||||
circle_radii: list[float] = [0.1, 0.15, 0.2],
|
||||
**kwargs
|
||||
):
|
||||
self.bulge_radius = bulge_radius
|
||||
self.bulge_overlap = bulge_overlap
|
||||
self.noise_factor = noise_factor
|
||||
self.circle_radii = circle_radii
|
||||
super().__init__(content, buff, filler_shape, **kwargs)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
Bubble.__init__(self, **kwargs)
|
||||
self.submobjects.sort(
|
||||
key=lambda m: m.get_bottom()[1]
|
||||
)
|
||||
def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
|
||||
rect = SurroundingRectangle(content, buff)
|
||||
perimeter = rect.get_arc_length()
|
||||
radius = self.bulge_radius
|
||||
step = (1 - self.bulge_overlap) * (2 * radius)
|
||||
nf = self.noise_factor
|
||||
corners = [rect.get_corner(v) for v in [DL, UL, UR, DR]]
|
||||
points = []
|
||||
for c1, c2 in adjacent_pairs(corners):
|
||||
n_alphas = int(get_norm(c1 - c2) / step) + 1
|
||||
for alpha in np.linspace(0, 1, n_alphas):
|
||||
points.append(interpolate(
|
||||
c1, c2, alpha + nf * (step / n_alphas) * (random.random() - 0.5)
|
||||
))
|
||||
|
||||
cloud = Union(rect, *(
|
||||
# Add bulges
|
||||
Circle(radius=radius * (1 + nf * random.random())).move_to(point)
|
||||
for point in points
|
||||
))
|
||||
cloud.set_stroke(WHITE, 2)
|
||||
|
||||
circles = VGroup(Circle(radius=radius) for radius in self.circle_radii)
|
||||
circ_buff = 0.25 * self.circle_radii[0]
|
||||
circles.arrange(UR, buff=circ_buff)
|
||||
circles[1].shift(circ_buff * DR)
|
||||
circles.next_to(cloud, DOWN, 4 * circ_buff, aligned_edge=LEFT)
|
||||
circles.set_stroke(WHITE, 2)
|
||||
|
||||
result = VGroup(*circles, cloud)
|
||||
|
||||
if direction[0] > 0:
|
||||
result.flip()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class OldSpeechBubble(Bubble):
|
||||
file_name: str = "Bubbles_speech.svg"
|
||||
|
||||
|
||||
class DoubleSpeechBubble(Bubble):
|
||||
file_name: str = "Bubbles_double_speech.svg"
|
||||
|
||||
|
||||
class OldThoughtBubble(Bubble):
|
||||
file_name: str = "Bubbles_thought.svg"
|
||||
|
||||
def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
|
||||
body = super().get_body(content, direction, buff)
|
||||
body.sort(lambda p: p[1])
|
||||
return body
|
||||
|
||||
def make_green_screen(self):
|
||||
self.submobjects[-1].set_fill(GREEN_SCREEN, opacity=1)
|
||||
self.body[-1].set_fill(GREEN_SCREEN, opacity=1)
|
||||
return self
|
||||
|
||||
|
||||
class VectorizedEarth(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name": "earth",
|
||||
"height": 1.5,
|
||||
"fill_color": BLACK,
|
||||
}
|
||||
file_name: str = "earth"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
SVGMobject.__init__(self, **kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
height: float = 2.0,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(height=height, **kwargs)
|
||||
self.insert_n_curves(20)
|
||||
circle = Circle(
|
||||
stroke_width=3,
|
||||
stroke_color=GREEN,
|
||||
@@ -432,3 +585,188 @@ class VectorizedEarth(SVGMobject):
|
||||
)
|
||||
circle.replace(self)
|
||||
self.add_to_back(circle)
|
||||
|
||||
|
||||
class Piano(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
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,
|
||||
**kwargs
|
||||
):
|
||||
self.n_white_keys = n_white_keys
|
||||
self.black_pattern = black_pattern
|
||||
self.white_keys_per_octave = white_keys_per_octave
|
||||
self.white_key_dims = white_key_dims
|
||||
self.black_key_dims = black_key_dims
|
||||
self.key_buff = key_buff
|
||||
self.white_key_color = white_key_color
|
||||
self.black_key_color = black_key_color
|
||||
self.total_width = total_width
|
||||
|
||||
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):
|
||||
def __init__(
|
||||
self,
|
||||
shading: Tuple[float, float, float] = (1.0, 0.2, 0.2),
|
||||
stroke_width: float = 0.25,
|
||||
stroke_color: ManimColor = BLACK,
|
||||
key_depth: float = 0.1,
|
||||
black_key_shift: float = 0.05,
|
||||
piano_2d_config: dict = dict(
|
||||
white_key_color=GREY_A,
|
||||
key_buff=0.001
|
||||
),
|
||||
**kwargs
|
||||
):
|
||||
piano_2d = Piano(**piano_2d_config)
|
||||
super().__init__(*(
|
||||
Prismify(key, key_depth)
|
||||
for key in piano_2d
|
||||
))
|
||||
self.set_stroke(stroke_color, stroke_width)
|
||||
self.set_shading(*shading)
|
||||
self.apply_depth_test()
|
||||
|
||||
# Elevate black keys
|
||||
for i, key in enumerate(self):
|
||||
if piano_2d[i] in piano_2d.black_keys:
|
||||
key.shift(black_key_shift * OUT)
|
||||
key.set_color(BLACK)
|
||||
|
||||
|
||||
class DieFace(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
value: int,
|
||||
side_length: float = 1.0,
|
||||
corner_radius: float = 0.15,
|
||||
stroke_color: ManimColor = WHITE,
|
||||
stroke_width: float = 2.0,
|
||||
fill_color: ManimColor = GREY_E,
|
||||
dot_radius: float = 0.08,
|
||||
dot_color: ManimColor = WHITE,
|
||||
dot_coalesce_factor: float = 0.5
|
||||
):
|
||||
dot = Dot(radius=dot_radius, fill_color=dot_color)
|
||||
square = Square(
|
||||
side_length=side_length,
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=1.0,
|
||||
)
|
||||
square.round_corners(corner_radius)
|
||||
|
||||
if not (1 <= value <= 6):
|
||||
raise Exception("DieFace only accepts integer inputs between 1 and 6")
|
||||
|
||||
edge_group = [
|
||||
(ORIGIN,),
|
||||
(UL, DR),
|
||||
(UL, ORIGIN, DR),
|
||||
(UL, UR, DL, DR),
|
||||
(UL, UR, ORIGIN, DL, DR),
|
||||
(UL, UR, LEFT, RIGHT, DL, DR),
|
||||
][value - 1]
|
||||
|
||||
arrangement = VGroup(*(
|
||||
dot.copy().move_to(square.get_bounding_box_point(vect))
|
||||
for vect in edge_group
|
||||
))
|
||||
arrangement.space_out_submobjects(dot_coalesce_factor)
|
||||
|
||||
super().__init__(square, arrangement)
|
||||
self.dots = arrangement
|
||||
self.value = value
|
||||
self.index = value
|
||||
|
||||
|
||||
class Dartboard(VGroup):
|
||||
radius = 3
|
||||
n_sectors = 20
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
n_sectors = self.n_sectors
|
||||
angle = TAU / n_sectors
|
||||
|
||||
segments = VGroup(*[
|
||||
VGroup(*[
|
||||
AnnularSector(
|
||||
inner_radius=in_r,
|
||||
outer_radius=out_r,
|
||||
start_angle=n * angle,
|
||||
angle=angle,
|
||||
fill_color=color,
|
||||
)
|
||||
for n, color in zip(
|
||||
range(n_sectors),
|
||||
it.cycle(colors)
|
||||
)
|
||||
])
|
||||
for colors, in_r, out_r in [
|
||||
([GREY_B, GREY_E], 0, 1),
|
||||
([GREEN_E, RED_E], 0.5, 0.55),
|
||||
([GREEN_E, RED_E], 0.95, 1),
|
||||
]
|
||||
])
|
||||
segments.rotate(-angle / 2)
|
||||
bullseyes = VGroup(*[
|
||||
Circle(radius=r)
|
||||
for r in [0.07, 0.035]
|
||||
])
|
||||
bullseyes.set_fill(opacity=1)
|
||||
bullseyes.set_stroke(width=0)
|
||||
bullseyes[0].set_color(GREEN_E)
|
||||
bullseyes[1].set_color(RED_E)
|
||||
|
||||
self.bullseye = bullseyes[1]
|
||||
self.add(*segments, *bullseyes)
|
||||
self.scale(self.radius)
|
||||
|
||||
@@ -1,521 +0,0 @@
|
||||
import itertools as it
|
||||
import re
|
||||
from types import MethodType
|
||||
|
||||
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.iterables import adjacent_pairs
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
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
|
||||
|
||||
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
|
||||
|
||||
tex_hash_to_mob_map = {}
|
||||
|
||||
|
||||
def _get_neighbouring_pairs(iterable):
|
||||
return list(adjacent_pairs(iterable))[:-1]
|
||||
|
||||
|
||||
class _LabelledTex(SVGMobject):
|
||||
CONFIG = {
|
||||
"height": None,
|
||||
"path_string_config": {
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def color_str_to_label(color_str):
|
||||
if len(color_str) == 4:
|
||||
# "#RGB" => "#RRGGBB"
|
||||
color_str = "#" + "".join([c * 2 for c in color_str[1:]])
|
||||
return int(color_str[1:], 16) - 1
|
||||
|
||||
def get_mobjects_from(self, element):
|
||||
result = super().get_mobjects_from(element)
|
||||
for mob in result:
|
||||
if not hasattr(mob, "glyph_label"):
|
||||
mob.glyph_label = -1
|
||||
try:
|
||||
color_str = element.getAttribute("fill")
|
||||
if color_str:
|
||||
glyph_label = _LabelledTex.color_str_to_label(color_str)
|
||||
for mob in result:
|
||||
mob.glyph_label = glyph_label
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
class _TexSpan(object):
|
||||
def __init__(self, script_type, label):
|
||||
# script_type: 0 for normal, 1 for subscript, 2 for superscript.
|
||||
# Only those spans with `script_type == 0` will be colored.
|
||||
self.script_type = script_type
|
||||
self.label = label
|
||||
self.containing_labels = []
|
||||
|
||||
def __repr__(self):
|
||||
return "_TexSpan(" + ", ".join([
|
||||
attrib_name + "=" + str(getattr(self, attrib_name))
|
||||
for attrib_name in ["script_type", "label", "containing_labels"]
|
||||
]) + ")"
|
||||
|
||||
|
||||
class _TexParser(object):
|
||||
def __init__(self, mtex):
|
||||
self.tex_string = mtex.tex_string
|
||||
strings_to_break_up = remove_list_redundancies([
|
||||
*mtex.isolate, *mtex.tex_to_color_map.keys(), mtex.tex_string
|
||||
])
|
||||
if "" in strings_to_break_up:
|
||||
strings_to_break_up.remove("")
|
||||
unbreakable_commands = mtex.unbreakable_commands
|
||||
|
||||
self.tex_spans_dict = {}
|
||||
self.current_label = 0
|
||||
self.break_up_by_braces()
|
||||
self.break_up_by_scripts()
|
||||
self.break_up_by_additional_strings(strings_to_break_up)
|
||||
self.merge_unbreakable_commands(unbreakable_commands)
|
||||
self.analyse_containing_labels()
|
||||
|
||||
@staticmethod
|
||||
def label_to_color_tuple(n):
|
||||
# Get a unique color different from black,
|
||||
# or the svg file will not include the color information.
|
||||
rgb = n + 1
|
||||
rg, b = divmod(rgb, 256)
|
||||
r, g = divmod(rg, 256)
|
||||
return r, g, b
|
||||
|
||||
@staticmethod
|
||||
def contains(span_0, span_1):
|
||||
return span_0[0] <= span_1[0] and span_1[1] <= span_0[1]
|
||||
|
||||
def add_tex_span(self, span_tuple, script_type=0, label=-1):
|
||||
if script_type == 0:
|
||||
# Should be additionally labelled.
|
||||
label = self.current_label
|
||||
self.current_label += 1
|
||||
|
||||
tex_span = _TexSpan(script_type, label)
|
||||
self.tex_spans_dict[span_tuple] = tex_span
|
||||
|
||||
def break_up_by_braces(self):
|
||||
tex_string = self.tex_string
|
||||
span_tuples = []
|
||||
left_brace_indices = []
|
||||
for match_obj in re.finditer(r"(\\*)(\{|\})", tex_string):
|
||||
# Braces following even numbers of backslashes are counted.
|
||||
if len(match_obj.group(1)) % 2 == 1:
|
||||
continue
|
||||
if match_obj.group(2) == "{":
|
||||
left_brace_index = match_obj.span(2)[0]
|
||||
left_brace_indices.append(left_brace_index)
|
||||
else:
|
||||
left_brace_index = left_brace_indices.pop()
|
||||
right_brace_index = match_obj.span(2)[1]
|
||||
span_tuples.append((left_brace_index, right_brace_index))
|
||||
if left_brace_indices:
|
||||
self.raise_tex_parsing_error()
|
||||
|
||||
self.paired_braces_tuples = span_tuples
|
||||
for span_tuple in span_tuples:
|
||||
self.add_tex_span(span_tuple)
|
||||
|
||||
def break_up_by_scripts(self):
|
||||
tex_string = self.tex_string
|
||||
brace_indices_dict = dict(self.tex_spans_dict.keys())
|
||||
for match_obj in re.finditer(r"((?<!\\)(_|\^)\s*)|(\s+(_|\^)\s*)", tex_string):
|
||||
script_type = 1 if "_" in match_obj.group() else 2
|
||||
token_begin, token_end = match_obj.span()
|
||||
if token_end in brace_indices_dict:
|
||||
content_span = (token_end, brace_indices_dict[token_end])
|
||||
else:
|
||||
content_match_obj = re.match(r"\w|\\[a-zA-Z]+", tex_string[token_end:])
|
||||
if not content_match_obj:
|
||||
self.raise_tex_parsing_error()
|
||||
content_span = tuple([
|
||||
index + token_end for index in content_match_obj.span()
|
||||
])
|
||||
self.add_tex_span(content_span)
|
||||
label = self.tex_spans_dict[content_span].label
|
||||
self.add_tex_span(
|
||||
(token_begin, content_span[1]),
|
||||
script_type=script_type,
|
||||
label=label
|
||||
)
|
||||
|
||||
def break_up_by_additional_strings(self, strings_to_break_up):
|
||||
tex_string = self.tex_string
|
||||
all_span_tuples = []
|
||||
for string in strings_to_break_up:
|
||||
# Only matches non-crossing strings.
|
||||
for match_obj in re.finditer(re.escape(string), tex_string):
|
||||
all_span_tuples.append(match_obj.span())
|
||||
|
||||
script_spans_dict = dict([
|
||||
span_tuple[::-1]
|
||||
for span_tuple, tex_span in self.tex_spans_dict.items()
|
||||
if tex_span.script_type != 0
|
||||
])
|
||||
for span_begin, span_end in all_span_tuples:
|
||||
if span_end in script_spans_dict.values():
|
||||
# Deconstruct spans with subscripts & superscripts.
|
||||
while span_end in script_spans_dict:
|
||||
span_end = script_spans_dict[span_end]
|
||||
if span_begin >= span_end:
|
||||
continue
|
||||
span_tuple = (span_begin, span_end)
|
||||
if span_tuple not in self.tex_spans_dict:
|
||||
self.add_tex_span(span_tuple)
|
||||
|
||||
def merge_unbreakable_commands(self, unbreakable_commands):
|
||||
tex_string = self.tex_string
|
||||
command_merge_spans = []
|
||||
brace_indices_dict = dict(self.paired_braces_tuples)
|
||||
# Braces leading by `unbreakable_commands` shouldn't be marked.
|
||||
for command in unbreakable_commands:
|
||||
for match_obj in re.finditer(re.escape(command), tex_string):
|
||||
merge_begin_index = match_obj.span()[1]
|
||||
merge_end_index = merge_begin_index
|
||||
if merge_end_index not in brace_indices_dict:
|
||||
continue
|
||||
while merge_end_index in brace_indices_dict:
|
||||
merge_end_index = brace_indices_dict[merge_end_index]
|
||||
command_merge_spans.append((merge_begin_index, merge_end_index))
|
||||
|
||||
self.tex_spans_dict = {
|
||||
span_tuple: tex_span
|
||||
for span_tuple, tex_span in self.tex_spans_dict.items()
|
||||
if all([
|
||||
not _TexParser.contains(merge_span, span_tuple)
|
||||
for merge_span in command_merge_spans
|
||||
])
|
||||
}
|
||||
|
||||
def analyse_containing_labels(self):
|
||||
for span_0, tex_span_0 in self.tex_spans_dict.items():
|
||||
if tex_span_0.script_type != 0:
|
||||
continue
|
||||
for span_1, tex_span_1 in self.tex_spans_dict.items():
|
||||
if _TexParser.contains(span_1, span_0):
|
||||
tex_span_1.containing_labels.append(tex_span_0.label)
|
||||
|
||||
def get_labelled_expression(self):
|
||||
tex_string = self.tex_string
|
||||
if not self.tex_spans_dict:
|
||||
return tex_string
|
||||
|
||||
indices_with_labels = sorted([
|
||||
(span_tuple[i], i, span_tuple[1 - i], tex_span.label)
|
||||
for span_tuple, tex_span in self.tex_spans_dict.items()
|
||||
if tex_span.script_type == 0
|
||||
for i in range(2)
|
||||
], key=lambda t: (t[0], -t[1], -t[2]))
|
||||
# Add one more item to ensure all the substrings are joined.
|
||||
indices_with_labels.append((len(tex_string), 0, 0, 0))
|
||||
|
||||
result = tex_string[: indices_with_labels[0][0]]
|
||||
index_with_label_pairs = _get_neighbouring_pairs(indices_with_labels)
|
||||
for index_with_label, next_index_with_label in index_with_label_pairs:
|
||||
index, flag, _, label = index_with_label
|
||||
next_index, *_ = next_index_with_label
|
||||
# Adding one more pair of braces will help maintain the glyghs of tex file...
|
||||
if flag == 0:
|
||||
color_tuple = _TexParser.label_to_color_tuple(label)
|
||||
result += "".join([
|
||||
"{{",
|
||||
"\\color[RGB]",
|
||||
"{",
|
||||
",".join(map(str, color_tuple)),
|
||||
"}"
|
||||
])
|
||||
else:
|
||||
result += "}}"
|
||||
result += tex_string[index : next_index]
|
||||
return result
|
||||
|
||||
def raise_tex_parsing_error(self):
|
||||
raise ValueError(f"Failed to parse tex: \"{self.tex_string}\"")
|
||||
|
||||
|
||||
class MTex(VMobject):
|
||||
CONFIG = {
|
||||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0,
|
||||
"should_center": True,
|
||||
"font_size": 48,
|
||||
"height": None,
|
||||
"organize_left_to_right": False,
|
||||
"alignment": "\\centering",
|
||||
"tex_environment": "align*",
|
||||
"isolate": [],
|
||||
"unbreakable_commands": ["\\begin", "\\end"],
|
||||
"tex_to_color_map": {},
|
||||
}
|
||||
|
||||
def __init__(self, tex_string, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.tex_string = MTex.modify_tex_string(tex_string)
|
||||
|
||||
tex_parser = _TexParser(self)
|
||||
self.tex_spans_dict = tex_parser.tex_spans_dict
|
||||
|
||||
new_tex = tex_parser.get_labelled_expression()
|
||||
full_tex = self.get_tex_file_body(new_tex)
|
||||
hash_val = hash(full_tex)
|
||||
if hash_val not in tex_hash_to_mob_map:
|
||||
with display_during_execution(f"Writing \"{tex_string}\""):
|
||||
filename = tex_to_svg_file(full_tex)
|
||||
svg_mob = _LabelledTex(filename)
|
||||
tex_hash_to_mob_map[hash_val] = svg_mob
|
||||
self.add(*[
|
||||
submob.copy()
|
||||
for submob in tex_hash_to_mob_map[hash_val]
|
||||
])
|
||||
self.build_submobjects()
|
||||
|
||||
self.init_colors()
|
||||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||
|
||||
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()
|
||||
|
||||
@staticmethod
|
||||
def modify_tex_string(tex_string):
|
||||
result = tex_string.strip("\n")
|
||||
# Prevent from passing an empty string.
|
||||
if not result:
|
||||
result = "\\quad"
|
||||
return result
|
||||
|
||||
def get_tex_file_body(self, new_tex):
|
||||
if self.tex_environment:
|
||||
new_tex = "\n".join([
|
||||
f"\\begin{{{self.tex_environment}}}",
|
||||
new_tex,
|
||||
f"\\end{{{self.tex_environment}}}"
|
||||
])
|
||||
if self.alignment:
|
||||
new_tex = "\n".join([self.alignment, new_tex])
|
||||
|
||||
tex_config = get_tex_config()
|
||||
return tex_config["tex_body"].replace(
|
||||
tex_config["text_to_replace"],
|
||||
new_tex
|
||||
)
|
||||
|
||||
def build_submobjects(self):
|
||||
if not self.submobjects:
|
||||
return
|
||||
self.group_submobjects()
|
||||
self.sort_scripts_in_tex_order()
|
||||
self.assign_submob_tex_strings()
|
||||
|
||||
def group_submobjects(self):
|
||||
# Simply pack together adjacent mobjects with the same label.
|
||||
new_submobjects = []
|
||||
def append_new_submobject(glyphs):
|
||||
if glyphs:
|
||||
submobject = VGroup(*glyphs)
|
||||
submobject.submob_label = glyphs[0].glyph_label
|
||||
new_submobjects.append(submobject)
|
||||
|
||||
new_glyphs = []
|
||||
current_glyph_label = -1
|
||||
for submob in self.submobjects:
|
||||
if submob.glyph_label == current_glyph_label:
|
||||
new_glyphs.append(submob)
|
||||
else:
|
||||
append_new_submobject(new_glyphs)
|
||||
new_glyphs = [submob]
|
||||
current_glyph_label = submob.glyph_label
|
||||
append_new_submobject(new_glyphs)
|
||||
self.set_submobjects(new_submobjects)
|
||||
|
||||
def sort_scripts_in_tex_order(self):
|
||||
# LaTeX always puts superscripts before subscripts.
|
||||
# This function sorts the submobjects of scripts in the order of tex given.
|
||||
index_and_span_list = sorted([
|
||||
(index, span_tuple)
|
||||
for span_tuple, tex_span in self.tex_spans_dict.items()
|
||||
if tex_span.script_type != 0
|
||||
for index in span_tuple
|
||||
])
|
||||
index_and_span_pair = _get_neighbouring_pairs(index_and_span_list)
|
||||
for index_and_span_0, index_and_span_1 in index_and_span_pair:
|
||||
index_0, span_tuple_0 = index_and_span_0
|
||||
index_1, span_tuple_1 = index_and_span_1
|
||||
if index_0 != index_1:
|
||||
continue
|
||||
if not all([
|
||||
self.tex_spans_dict[span_tuple_0].script_type == 1,
|
||||
self.tex_spans_dict[span_tuple_1].script_type == 2
|
||||
]):
|
||||
continue
|
||||
submob_slice_0 = self.slice_of_part(
|
||||
self.get_part_by_span_tuples([span_tuple_0])
|
||||
)
|
||||
submob_slice_1 = self.slice_of_part(
|
||||
self.get_part_by_span_tuples([span_tuple_1])
|
||||
)
|
||||
submobs = self.submobjects
|
||||
self.set_submobjects([
|
||||
*submobs[: submob_slice_1.start],
|
||||
*submobs[submob_slice_0],
|
||||
*submobs[submob_slice_1.stop : submob_slice_0.start],
|
||||
*submobs[submob_slice_1],
|
||||
*submobs[submob_slice_0.stop :]
|
||||
])
|
||||
|
||||
def assign_submob_tex_strings(self):
|
||||
# Not sure whether this is the best practice...
|
||||
# Just a temporary hack for supporting `TransformMatchingTex`.
|
||||
tex_string = self.tex_string
|
||||
# Use tex strings including "_", "^".
|
||||
label_dict = {}
|
||||
for span_tuple, tex_span in self.tex_spans_dict.items():
|
||||
if tex_span.script_type != 0:
|
||||
label_dict[tex_span.label] = span_tuple
|
||||
else:
|
||||
if tex_span.label not in label_dict:
|
||||
label_dict[tex_span.label] = span_tuple
|
||||
|
||||
curr_labels = [submob.submob_label for submob in self.submobjects]
|
||||
prev_labels = [curr_labels[-1], *curr_labels[:-1]]
|
||||
next_labels = [*curr_labels[1:], curr_labels[0]]
|
||||
tex_string_spans = []
|
||||
for curr_label, prev_label, next_label in zip(
|
||||
curr_labels, prev_labels, next_labels
|
||||
):
|
||||
curr_span_tuple = label_dict[curr_label]
|
||||
prev_span_tuple = label_dict[prev_label]
|
||||
next_span_tuple = label_dict[next_label]
|
||||
containing_labels = self.tex_spans_dict[curr_span_tuple].containing_labels
|
||||
tex_string_spans.append([
|
||||
prev_span_tuple[1] if prev_label in containing_labels else curr_span_tuple[0],
|
||||
next_span_tuple[0] if next_label in containing_labels else curr_span_tuple[1]
|
||||
])
|
||||
tex_string_spans[0][0] = label_dict[curr_labels[0]][0]
|
||||
tex_string_spans[-1][1] = label_dict[curr_labels[-1]][1]
|
||||
for submob, tex_string_span in zip(self.submobjects, tex_string_spans):
|
||||
submob.tex_string = tex_string[slice(*tex_string_span)]
|
||||
# Support `get_tex()` method here.
|
||||
submob.get_tex = MethodType(lambda inst: inst.tex_string, submob)
|
||||
|
||||
def get_part_by_span_tuples(self, span_tuples):
|
||||
labels = remove_list_redundancies(list(it.chain(*[
|
||||
self.tex_spans_dict[span_tuple].containing_labels
|
||||
for span_tuple in span_tuples
|
||||
])))
|
||||
return VGroup(*filter(
|
||||
lambda submob: submob.submob_label in labels,
|
||||
self.submobjects
|
||||
))
|
||||
|
||||
def find_span_components_of_custom_span(self, custom_span_tuple, partial_result=[]):
|
||||
span_begin, span_end = custom_span_tuple
|
||||
if span_begin == span_end:
|
||||
return partial_result
|
||||
next_begin_choices = sorted([
|
||||
span_tuple[1]
|
||||
for span_tuple in self.tex_spans_dict.keys()
|
||||
if span_tuple[0] == span_begin and span_tuple[1] <= span_end
|
||||
], reverse=True)
|
||||
for next_begin in next_begin_choices:
|
||||
result = self.find_span_components_of_custom_span(
|
||||
(next_begin, span_end), [*partial_result, (span_begin, next_begin)]
|
||||
)
|
||||
if result is not None:
|
||||
return result
|
||||
return None
|
||||
|
||||
def get_part_by_custom_span_tuple(self, custom_span_tuple):
|
||||
span_tuples = self.find_span_components_of_custom_span(custom_span_tuple)
|
||||
if span_tuples is None:
|
||||
tex = self.tex_string[slice(*custom_span_tuple)]
|
||||
raise ValueError(f"Failed to get span of tex: \"{tex}\"")
|
||||
return self.get_part_by_span_tuples(span_tuples)
|
||||
|
||||
def get_parts_by_tex(self, tex):
|
||||
return VGroup(*[
|
||||
self.get_part_by_custom_span_tuple(match_obj.span())
|
||||
for match_obj in re.finditer(re.escape(tex), self.tex_string)
|
||||
])
|
||||
|
||||
def get_part_by_tex(self, tex, index=0):
|
||||
all_parts = self.get_parts_by_tex(tex)
|
||||
return all_parts[index]
|
||||
|
||||
def set_color_by_tex(self, tex, color):
|
||||
self.get_parts_by_tex(tex).set_color(color)
|
||||
return self
|
||||
|
||||
def set_color_by_tex_to_color_map(self, tex_to_color_map):
|
||||
for tex, color in list(tex_to_color_map.items()):
|
||||
self.set_color_by_tex(tex, color)
|
||||
return self
|
||||
|
||||
def indices_of_part(self, part):
|
||||
indices = [
|
||||
i for i, submob in enumerate(self.submobjects)
|
||||
if submob in part
|
||||
]
|
||||
if not indices:
|
||||
raise ValueError("Failed to find part in tex")
|
||||
return indices
|
||||
|
||||
def indices_of_part_by_tex(self, tex, index=0):
|
||||
part = self.get_part_by_tex(tex, index=index)
|
||||
return self.indices_of_part(part)
|
||||
|
||||
def slice_of_part(self, part):
|
||||
indices = self.indices_of_part(part)
|
||||
return slice(indices[0], indices[-1] + 1)
|
||||
|
||||
def slice_of_part_by_tex(self, tex, index=0):
|
||||
part = self.get_part_by_tex(tex, index=index)
|
||||
return self.slice_of_part(part)
|
||||
|
||||
def index_of_part(self, part):
|
||||
return self.indices_of_part(part)[0]
|
||||
|
||||
def index_of_part_by_tex(self, tex, index=0):
|
||||
part = self.get_part_by_tex(tex, index=index)
|
||||
return self.index_of_part(part)
|
||||
|
||||
def get_tex(self):
|
||||
return self.tex_string
|
||||
|
||||
def get_all_isolated_substrings(self):
|
||||
tex_string = self.tex_string
|
||||
return remove_list_redundancies([
|
||||
tex_string[slice(*span_tuple)]
|
||||
for span_tuple in self.tex_spans_dict.keys()
|
||||
])
|
||||
|
||||
def print_tex_strings_of_submobjects(self):
|
||||
# For debugging
|
||||
# Work with `index_labels()`
|
||||
print("\n")
|
||||
print(f"Submobjects of \"{self.get_tex()}\":")
|
||||
for i, submob in enumerate(self.submobjects):
|
||||
print(f"{i}: \"{submob.get_tex()}\"")
|
||||
print("\n")
|
||||
|
||||
|
||||
class MTexText(MTex):
|
||||
CONFIG = {
|
||||
"tex_environment": None,
|
||||
}
|
||||
332
manimlib/mobject/svg/old_tex_mobject.py
Normal file
332
manimlib/mobject/svg/old_tex_mobject.py
Normal file
@@ -0,0 +1,332 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import reduce
|
||||
import operator as op
|
||||
import re
|
||||
|
||||
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.tex_file_writing import latex_to_svg
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable, List, Dict
|
||||
from manimlib.typing import ManimColor
|
||||
|
||||
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
|
||||
|
||||
class SingleStringTex(SVGMobject):
|
||||
height: float | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tex_string: str,
|
||||
height: float | None = None,
|
||||
fill_color: ManimColor = WHITE,
|
||||
fill_opacity: float = 1.0,
|
||||
stroke_width: float = 0,
|
||||
svg_default: dict = dict(fill_color=WHITE),
|
||||
path_string_config: dict = dict(),
|
||||
font_size: int = 48,
|
||||
alignment: str = R"\centering",
|
||||
math_mode: bool = True,
|
||||
organize_left_to_right: bool = False,
|
||||
template: str = "",
|
||||
additional_preamble: str = "",
|
||||
**kwargs
|
||||
):
|
||||
self.tex_string = tex_string
|
||||
self.svg_default = dict(svg_default)
|
||||
self.path_string_config = dict(path_string_config)
|
||||
self.font_size = font_size
|
||||
self.alignment = alignment
|
||||
self.math_mode = math_mode
|
||||
self.organize_left_to_right = organize_left_to_right
|
||||
self.template = template
|
||||
self.additional_preamble = additional_preamble
|
||||
|
||||
super().__init__(
|
||||
height=height,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_width=stroke_width,
|
||||
path_string_config=path_string_config,
|
||||
**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()
|
||||
|
||||
@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,
|
||||
self.template,
|
||||
self.additional_preamble
|
||||
)
|
||||
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
return latex_to_svg(content, self.template, self.additional_preamble)
|
||||
|
||||
def get_tex_file_body(self, tex_string: str) -> str:
|
||||
new_tex = self.get_modified_expression(tex_string)
|
||||
if self.math_mode:
|
||||
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
|
||||
return self.alignment + "\n" + new_tex
|
||||
|
||||
def get_modified_expression(self, tex_string: str) -> str:
|
||||
return self.modify_special_strings(tex_string.strip())
|
||||
|
||||
def modify_special_strings(self, tex: str) -> str:
|
||||
tex = tex.strip()
|
||||
should_add_filler = reduce(op.or_, [
|
||||
# Fraction line needs something to be over
|
||||
tex == "\\over",
|
||||
tex == "\\overline",
|
||||
# Makesure sqrt has overbar
|
||||
tex == "\\sqrt",
|
||||
tex == "\\sqrt{",
|
||||
# Need to add blank subscript or superscript
|
||||
tex.endswith("_"),
|
||||
tex.endswith("^"),
|
||||
tex.endswith("dot"),
|
||||
])
|
||||
if should_add_filler:
|
||||
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"
|
||||
|
||||
if tex == "":
|
||||
tex = "\\quad"
|
||||
|
||||
# To keep files from starting with a line break
|
||||
if tex.startswith("\\\\"):
|
||||
tex = tex.replace("\\\\", "\\quad\\\\")
|
||||
|
||||
tex = self.balance_braces(tex)
|
||||
|
||||
# Handle imbalanced \left and \right
|
||||
num_lefts, num_rights = [
|
||||
len([
|
||||
s for s in tex.split(substr)[1:]
|
||||
if s and s[0] in "(){}[]|.\\"
|
||||
])
|
||||
for substr in ("\\left", "\\right")
|
||||
]
|
||||
if num_lefts != num_rights:
|
||||
tex = tex.replace("\\left", "\\big")
|
||||
tex = tex.replace("\\right", "\\big")
|
||||
|
||||
for context in ["array"]:
|
||||
begin_in = ("\\begin{%s}" % context) in tex
|
||||
end_in = ("\\end{%s}" % context) in tex
|
||||
if begin_in ^ end_in:
|
||||
# Just turn this into a blank string,
|
||||
# which means caller should leave a
|
||||
# stray \\begin{...} with other symbols
|
||||
tex = ""
|
||||
return tex
|
||||
|
||||
def balance_braces(self, tex: str) -> str:
|
||||
"""
|
||||
Makes Tex resiliant to unmatched braces
|
||||
"""
|
||||
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) -> str:
|
||||
return self.tex_string
|
||||
|
||||
def organize_submobjects_left_to_right(self):
|
||||
self.sort(lambda p: p[0])
|
||||
return self
|
||||
|
||||
|
||||
class OldTex(SingleStringTex):
|
||||
def __init__(
|
||||
self,
|
||||
*tex_strings: str,
|
||||
arg_separator: str = "",
|
||||
isolate: List[str] = [],
|
||||
tex_to_color_map: Dict[str, ManimColor] = {},
|
||||
**kwargs
|
||||
):
|
||||
self.tex_strings = self.break_up_tex_strings(
|
||||
tex_strings,
|
||||
substrings_to_isolate=[*isolate, *tex_to_color_map.keys()]
|
||||
)
|
||||
full_string = arg_separator.join(self.tex_strings)
|
||||
|
||||
super().__init__(full_string, **kwargs)
|
||||
self.break_up_by_substrings(self.tex_strings)
|
||||
self.set_color_by_tex_to_color_map(tex_to_color_map)
|
||||
|
||||
if self.organize_left_to_right:
|
||||
self.organize_submobjects_left_to_right()
|
||||
|
||||
def break_up_tex_strings(self, tex_strings: Iterable[str], substrings_to_isolate: List[str] = []) -> Iterable[str]:
|
||||
# Separate out any strings specified in the isolate
|
||||
# or tex_to_color_map lists.
|
||||
if len(substrings_to_isolate) == 0:
|
||||
return tex_strings
|
||||
patterns = (
|
||||
"({})".format(re.escape(ss))
|
||||
for ss in substrings_to_isolate
|
||||
)
|
||||
pattern = "|".join(patterns)
|
||||
pieces = []
|
||||
for s in tex_strings:
|
||||
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, tex_strings: Iterable[str]):
|
||||
"""
|
||||
Reorganize existing submojects one layer
|
||||
deeper based on the structure of tex_strings (as a list
|
||||
of tex_strings)
|
||||
"""
|
||||
if len(list(tex_strings)) == 1:
|
||||
submob = self.copy()
|
||||
self.set_submobjects([submob])
|
||||
return self
|
||||
new_submobjects = []
|
||||
curr_index = 0
|
||||
for tex_string in tex_strings:
|
||||
tex_string = tex_string.strip()
|
||||
if len(tex_string) == 0:
|
||||
continue
|
||||
sub_tex_mob = SingleStringTex(tex_string, math_mode=self.math_mode)
|
||||
num_submobs = len(sub_tex_mob)
|
||||
if num_submobs == 0:
|
||||
continue
|
||||
new_index = curr_index + num_submobs
|
||||
sub_tex_mob.set_submobjects(self.submobjects[curr_index:new_index])
|
||||
new_submobjects.append(sub_tex_mob)
|
||||
curr_index = new_index
|
||||
self.set_submobjects(new_submobjects)
|
||||
return self
|
||||
|
||||
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()
|
||||
tex2 = tex2.lower()
|
||||
if substring:
|
||||
return tex1 in tex2
|
||||
else:
|
||||
return tex1 == tex2
|
||||
|
||||
return VGroup(*filter(
|
||||
lambda m: isinstance(m, SingleStringTex) and test(tex, m.get_tex()),
|
||||
self.submobjects
|
||||
))
|
||||
|
||||
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: 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: 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: SingleStringTex, start: int = 0) -> int:
|
||||
return self.submobjects.index(part, start)
|
||||
|
||||
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: str | None = None,
|
||||
stop_tex: str | None = None,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
if start_tex is None:
|
||||
start_index = 0
|
||||
else:
|
||||
start_index = self.index_of_part_by_tex(start_tex, **kwargs)
|
||||
|
||||
if stop_tex is None:
|
||||
return self[start_index:]
|
||||
else:
|
||||
stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
|
||||
return self[start_index:stop_index]
|
||||
|
||||
def sort_alphabetically(self) -> None:
|
||||
self.submobjects.sort(key=lambda m: m.get_tex())
|
||||
|
||||
def set_bstroke(self, color: ManimColor = BLACK, width: float = 4):
|
||||
self.set_stroke(color, width, background=True)
|
||||
return self
|
||||
|
||||
|
||||
class OldTexText(OldTex):
|
||||
def __init__(
|
||||
self,
|
||||
*tex_strings: str,
|
||||
math_mode: bool = False,
|
||||
arg_separator: str = "",
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
*tex_strings,
|
||||
math_mode=math_mode,
|
||||
arg_separator=arg_separator,
|
||||
**kwargs
|
||||
)
|
||||
79
manimlib/mobject/svg/special_tex.py
Normal file
79
manimlib/mobject/svg/special_tex.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import MED_SMALL_BUFF, WHITE, GREY_C
|
||||
from manimlib.constants import DOWN, LEFT, RIGHT, UP
|
||||
from manimlib.constants import FRAME_WIDTH
|
||||
from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
|
||||
|
||||
class BulletedList(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
*items: str,
|
||||
buff: float = MED_LARGE_BUFF,
|
||||
aligned_edge: Vect3 = LEFT,
|
||||
**kwargs
|
||||
):
|
||||
labelled_content = [R"\item " + item for item in items]
|
||||
tex_string = "\n".join([
|
||||
R"\begin{itemize}",
|
||||
*labelled_content,
|
||||
R"\end{itemize}"
|
||||
])
|
||||
tex_text = TexText(tex_string, isolate=labelled_content, **kwargs)
|
||||
lines = (tex_text.select_part(part) for part in labelled_content)
|
||||
|
||||
super().__init__(*lines)
|
||||
|
||||
self.arrange(DOWN, buff=buff, aligned_edge=aligned_edge)
|
||||
|
||||
def fade_all_but(self, index: int, opacity: float = 0.25) -> None:
|
||||
for i, part in enumerate(self.submobjects):
|
||||
part.set_fill(opacity=(1.0 if i == index else opacity))
|
||||
|
||||
|
||||
class TexTextFromPresetString(TexText):
|
||||
tex: str = ""
|
||||
default_color: ManimColor = WHITE
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
self.tex,
|
||||
color=kwargs.pop("color", self.default_color),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class Title(TexText):
|
||||
def __init__(
|
||||
self,
|
||||
*text_parts: str,
|
||||
font_size: int = 72,
|
||||
include_underline: bool = True,
|
||||
underline_width: float = FRAME_WIDTH - 2,
|
||||
# This will override underline_width
|
||||
match_underline_width_to_text: bool = False,
|
||||
underline_buff: float = SMALL_BUFF,
|
||||
underline_style: dict = dict(stroke_width=2, stroke_color=GREY_C),
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(*text_parts, font_size=font_size, **kwargs)
|
||||
self.to_edge(UP, buff=MED_SMALL_BUFF)
|
||||
if include_underline:
|
||||
underline = Line(LEFT, RIGHT, **underline_style)
|
||||
underline.next_to(self, DOWN, buff=underline_buff)
|
||||
if match_underline_width_to_text:
|
||||
underline.match_width(self)
|
||||
else:
|
||||
underline.set_width(underline_width)
|
||||
self.add(underline)
|
||||
self.underline = underline
|
||||
585
manimlib/mobject/svg/string_mobject.py
Normal file
585
manimlib/mobject/svg/string_mobject.py
Normal file
@@ -0,0 +1,585 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import itertools as it
|
||||
import re
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
from scipy.spatial.distance import cdist
|
||||
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.logger import log
|
||||
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.color import color_to_hex
|
||||
from manimlib.utils.color import hex_to_int
|
||||
from manimlib.utils.color import int_to_hex
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from manimlib.typing import ManimColor, Span, Selector
|
||||
|
||||
|
||||
class StringMobject(SVGMobject, ABC):
|
||||
"""
|
||||
An abstract base class for `Tex` and `MarkupText`
|
||||
|
||||
This class aims to optimize the logic of "slicing submobjects
|
||||
via substrings". This could be much clearer and more user-friendly
|
||||
than slicing through numerical indices explicitly.
|
||||
|
||||
Users are expected to specify substrings in `isolate` parameter
|
||||
if they want to do anything with their corresponding submobjects.
|
||||
`isolate` parameter can be either a string, a `re.Pattern` object,
|
||||
or a 2-tuple containing integers or None, or a collection of the above.
|
||||
Note, substrings specified cannot *partly* overlap with each other.
|
||||
|
||||
Each instance of `StringMobject` may generate 2 svg files.
|
||||
The additional one is generated with some color commands inserted,
|
||||
so that each submobject of the original `SVGMobject` will be labelled
|
||||
by the color of its paired submobject from the additional `SVGMobject`.
|
||||
"""
|
||||
height = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
string: str,
|
||||
fill_color: ManimColor = WHITE,
|
||||
fill_border_width: float = 0.5,
|
||||
stroke_color: ManimColor = WHITE,
|
||||
stroke_width: float = 0,
|
||||
base_color: ManimColor = WHITE,
|
||||
isolate: Selector = (),
|
||||
protect: Selector = (),
|
||||
# When set to true, only the labelled svg is
|
||||
# rendered, and its contents are used directly
|
||||
# for the body of this String Mobject
|
||||
use_labelled_svg: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
self.string = string
|
||||
self.base_color = base_color or WHITE
|
||||
self.isolate = isolate
|
||||
self.protect = protect
|
||||
self.use_labelled_svg = use_labelled_svg
|
||||
|
||||
self.parse()
|
||||
svg_string = self.get_svg_string()
|
||||
super().__init__(svg_string=svg_string, **kwargs)
|
||||
self.set_stroke(stroke_color, stroke_width)
|
||||
self.set_fill(fill_color, border_width=fill_border_width)
|
||||
self.labels = [submob.label for submob in self.submobjects]
|
||||
|
||||
def get_svg_string(self, is_labelled: bool = False) -> str:
|
||||
content = self.get_content(is_labelled or self.use_labelled_svg)
|
||||
return self.get_svg_string_by_content(content)
|
||||
|
||||
@abstractmethod
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
return ""
|
||||
|
||||
def assign_labels_by_color(self, mobjects: list[VMobject]) -> None:
|
||||
"""
|
||||
Assuming each mobject in the list `mobjects` has a fill color
|
||||
meant to represent a numerical label, this assigns those
|
||||
those numerical labels to each mobject as an attribute
|
||||
"""
|
||||
labels_count = len(self.labelled_spans)
|
||||
if labels_count == 1:
|
||||
for mob in mobjects:
|
||||
mob.label = 0
|
||||
return
|
||||
|
||||
unrecognizable_colors = []
|
||||
for mob in mobjects:
|
||||
label = hex_to_int(color_to_hex(mob.get_fill_color()))
|
||||
if label >= labels_count:
|
||||
unrecognizable_colors.append(label)
|
||||
label = 0
|
||||
mob.label = label
|
||||
|
||||
if unrecognizable_colors:
|
||||
log.warning(
|
||||
"Unrecognizable color labels detected (%s). " + \
|
||||
"The result could be unexpected.",
|
||||
", ".join(
|
||||
int_to_hex(color)
|
||||
for color in unrecognizable_colors
|
||||
)
|
||||
)
|
||||
|
||||
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
|
||||
submobs = super().mobjects_from_svg_string(svg_string)
|
||||
|
||||
if self.use_labelled_svg:
|
||||
# This means submobjects are colored according to spans
|
||||
self.assign_labels_by_color(submobs)
|
||||
return submobs
|
||||
|
||||
# Otherwise, submobs are not colored, so generate a new list
|
||||
# of submobject which are and use those for labels
|
||||
unlabelled_submobs = submobs
|
||||
labelled_content = self.get_content(is_labelled=True)
|
||||
labelled_file = self.get_file_path_by_content(labelled_content)
|
||||
labelled_submobs = super().mobjects_from_file(labelled_file)
|
||||
self.labelled_submobs = labelled_submobs
|
||||
self.unlabelled_submobs = unlabelled_submobs
|
||||
|
||||
self.assign_labels_by_color(labelled_submobs)
|
||||
self.rearrange_submobjects_by_positions(labelled_submobs, unlabelled_submobs)
|
||||
for usm, lsm in zip(unlabelled_submobs, labelled_submobs):
|
||||
usm.label = lsm.label
|
||||
|
||||
if len(unlabelled_submobs) != len(labelled_submobs):
|
||||
log.warning(
|
||||
"Cannot align submobjects of the labelled svg " + \
|
||||
"to the original svg. Skip the labelling process."
|
||||
)
|
||||
for usm in unlabelled_submobs:
|
||||
usm.label = 0
|
||||
return unlabelled_submobs
|
||||
|
||||
return unlabelled_submobs
|
||||
|
||||
def rearrange_submobjects_by_positions(
|
||||
self, labelled_submobs: list[VMobject], unlabelled_submobs: list[VMobject],
|
||||
) -> None:
|
||||
"""
|
||||
Rearrange `labeleled_submobjects` so that each submobject
|
||||
is labelled by the nearest one of `unlabelled_submobs`.
|
||||
The correctness cannot be ensured, since the svg may
|
||||
change significantly after inserting color commands.
|
||||
"""
|
||||
if len(labelled_submobs) == 0:
|
||||
return
|
||||
|
||||
labelled_svg = VGroup(*labelled_submobs)
|
||||
labelled_svg.replace(VGroup(*unlabelled_submobs))
|
||||
distance_matrix = cdist(
|
||||
[submob.get_center() for submob in unlabelled_submobs],
|
||||
[submob.get_center() for submob in labelled_submobs]
|
||||
)
|
||||
_, indices = linear_sum_assignment(distance_matrix)
|
||||
labelled_submobs[:] = [labelled_submobs[index] for index in indices]
|
||||
|
||||
# Toolkits
|
||||
|
||||
def find_spans_by_selector(self, selector: Selector) -> list[Span]:
|
||||
def find_spans_by_single_selector(sel):
|
||||
if isinstance(sel, str):
|
||||
return [
|
||||
match_obj.span()
|
||||
for match_obj in re.finditer(re.escape(sel), self.string)
|
||||
]
|
||||
if isinstance(sel, re.Pattern):
|
||||
return [
|
||||
match_obj.span()
|
||||
for match_obj in sel.finditer(self.string)
|
||||
]
|
||||
if isinstance(sel, tuple) and len(sel) == 2 and all(
|
||||
isinstance(index, int) or index is None
|
||||
for index in sel
|
||||
):
|
||||
l = len(self.string)
|
||||
span = tuple(
|
||||
default_index if index is None else
|
||||
min(index, l) if index >= 0 else max(index + l, 0)
|
||||
for index, default_index in zip(sel, (0, l))
|
||||
)
|
||||
return [span]
|
||||
return None
|
||||
|
||||
result = find_spans_by_single_selector(selector)
|
||||
if result is None:
|
||||
result = []
|
||||
for sel in selector:
|
||||
spans = find_spans_by_single_selector(sel)
|
||||
if spans is None:
|
||||
raise TypeError(f"Invalid selector: '{sel}'")
|
||||
result.extend(spans)
|
||||
return list(filter(lambda span: span[0] <= span[1], result))
|
||||
|
||||
@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]
|
||||
|
||||
# Parsing
|
||||
|
||||
def parse(self) -> None:
|
||||
def get_substr(span: Span) -> str:
|
||||
return self.string[slice(*span)]
|
||||
|
||||
configured_items = self.get_configured_items()
|
||||
isolated_spans = self.find_spans_by_selector(self.isolate)
|
||||
protected_spans = self.find_spans_by_selector(self.protect)
|
||||
command_matches = self.get_command_matches(self.string)
|
||||
|
||||
def get_key(category, i, flag):
|
||||
def get_span_by_category(category, i):
|
||||
if category == 0:
|
||||
return configured_items[i][0]
|
||||
if category == 1:
|
||||
return isolated_spans[i]
|
||||
if category == 2:
|
||||
return protected_spans[i]
|
||||
return command_matches[i].span()
|
||||
|
||||
index, paired_index = get_span_by_category(category, i)[::flag]
|
||||
return (
|
||||
index,
|
||||
flag * (2 if index != paired_index else -1),
|
||||
-paired_index,
|
||||
flag * category,
|
||||
flag * i
|
||||
)
|
||||
|
||||
index_items = sorted([
|
||||
(category, i, flag)
|
||||
for category, item_length in enumerate((
|
||||
len(configured_items),
|
||||
len(isolated_spans),
|
||||
len(protected_spans),
|
||||
len(command_matches)
|
||||
))
|
||||
for i in range(item_length)
|
||||
for flag in (1, -1)
|
||||
], key=lambda t: get_key(*t))
|
||||
|
||||
inserted_items = []
|
||||
labelled_items = []
|
||||
overlapping_spans = []
|
||||
level_mismatched_spans = []
|
||||
|
||||
label = 1
|
||||
protect_level = 0
|
||||
bracket_stack = [0]
|
||||
bracket_count = 0
|
||||
open_command_stack = []
|
||||
open_stack = []
|
||||
for category, i, flag in index_items:
|
||||
if category >= 2:
|
||||
protect_level += flag
|
||||
if flag == 1 or category == 2:
|
||||
continue
|
||||
inserted_items.append((i, 0))
|
||||
command_match = command_matches[i]
|
||||
command_flag = self.get_command_flag(command_match)
|
||||
if command_flag == 1:
|
||||
bracket_count += 1
|
||||
bracket_stack.append(bracket_count)
|
||||
open_command_stack.append((len(inserted_items), i))
|
||||
continue
|
||||
if command_flag == 0:
|
||||
continue
|
||||
pos, i_ = open_command_stack.pop()
|
||||
bracket_stack.pop()
|
||||
open_command_match = command_matches[i_]
|
||||
attr_dict = self.get_attr_dict_from_command_pair(
|
||||
open_command_match, command_match
|
||||
)
|
||||
if attr_dict is None:
|
||||
continue
|
||||
span = (open_command_match.end(), command_match.start())
|
||||
labelled_items.append((span, attr_dict))
|
||||
inserted_items.insert(pos, (label, 1))
|
||||
inserted_items.insert(-1, (label, -1))
|
||||
label += 1
|
||||
continue
|
||||
if flag == 1:
|
||||
open_stack.append((
|
||||
len(inserted_items), category, i,
|
||||
protect_level, bracket_stack.copy()
|
||||
))
|
||||
continue
|
||||
span, attr_dict = configured_items[i] \
|
||||
if category == 0 else (isolated_spans[i], {})
|
||||
pos, category_, i_, protect_level_, bracket_stack_ \
|
||||
= open_stack.pop()
|
||||
if category_ != category or i_ != i:
|
||||
overlapping_spans.append(span)
|
||||
continue
|
||||
if protect_level_ or protect_level:
|
||||
continue
|
||||
if bracket_stack_ != bracket_stack:
|
||||
level_mismatched_spans.append(span)
|
||||
continue
|
||||
labelled_items.append((span, attr_dict))
|
||||
inserted_items.insert(pos, (label, 1))
|
||||
inserted_items.append((label, -1))
|
||||
label += 1
|
||||
labelled_items.insert(0, ((0, len(self.string)), {}))
|
||||
inserted_items.insert(0, (0, 1))
|
||||
inserted_items.append((0, -1))
|
||||
|
||||
if overlapping_spans:
|
||||
log.warning(
|
||||
"Partly overlapping substrings detected: %s",
|
||||
", ".join(
|
||||
f"'{get_substr(span)}'"
|
||||
for span in overlapping_spans
|
||||
)
|
||||
)
|
||||
if level_mismatched_spans:
|
||||
log.warning(
|
||||
"Cannot handle substrings: %s",
|
||||
", ".join(
|
||||
f"'{get_substr(span)}'"
|
||||
for span in level_mismatched_spans
|
||||
)
|
||||
)
|
||||
|
||||
def reconstruct_string(
|
||||
start_item: tuple[int, int],
|
||||
end_item: tuple[int, int],
|
||||
command_replace_func: Callable[[re.Match], str],
|
||||
command_insert_func: Callable[[int, int, dict[str, str]], str]
|
||||
) -> str:
|
||||
def get_edge_item(i: int, flag: int) -> tuple[Span, str]:
|
||||
if flag == 0:
|
||||
match_obj = command_matches[i]
|
||||
return (
|
||||
match_obj.span(),
|
||||
command_replace_func(match_obj)
|
||||
)
|
||||
span, attr_dict = labelled_items[i]
|
||||
index = span[flag < 0]
|
||||
return (
|
||||
(index, index),
|
||||
command_insert_func(i, flag, attr_dict)
|
||||
)
|
||||
|
||||
items = [
|
||||
get_edge_item(i, flag)
|
||||
for i, flag in inserted_items[slice(
|
||||
inserted_items.index(start_item),
|
||||
inserted_items.index(end_item) + 1
|
||||
)]
|
||||
]
|
||||
pieces = [
|
||||
get_substr((start, end))
|
||||
for start, end in zip(
|
||||
[interval_end for (_, interval_end), _ in items[:-1]],
|
||||
[interval_start for (interval_start, _), _ in items[1:]]
|
||||
)
|
||||
]
|
||||
interval_pieces = [piece for _, piece in items[1:-1]]
|
||||
return "".join(it.chain(*zip(pieces, (*interval_pieces, ""))))
|
||||
|
||||
self.labelled_spans = [span for span, _ in labelled_items]
|
||||
self.reconstruct_string = reconstruct_string
|
||||
|
||||
def get_content(self, is_labelled: bool) -> str:
|
||||
content = self.reconstruct_string(
|
||||
(0, 1), (0, -1),
|
||||
self.replace_for_content,
|
||||
lambda label, flag, attr_dict: self.get_command_string(
|
||||
attr_dict,
|
||||
is_end=flag < 0,
|
||||
label_hex=int_to_hex(label) if is_labelled else None
|
||||
)
|
||||
)
|
||||
prefix, suffix = self.get_content_prefix_and_suffix(
|
||||
is_labelled=is_labelled
|
||||
)
|
||||
return "".join((prefix, content, suffix))
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_command_matches(string: str) -> list[re.Match]:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_command_flag(match_obj: re.Match) -> int:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def replace_for_content(match_obj: re.Match) -> str:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def replace_for_matching(match_obj: re.Match) -> str:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_attr_dict_from_command_pair(
|
||||
open_command: re.Match, close_command: re.Match,
|
||||
) -> dict[str, str] | None:
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def get_command_string(
|
||||
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
|
||||
) -> str:
|
||||
return ""
|
||||
|
||||
@abstractmethod
|
||||
def get_content_prefix_and_suffix(
|
||||
self, is_labelled: bool
|
||||
) -> tuple[str, str]:
|
||||
return "", ""
|
||||
|
||||
# Selector
|
||||
|
||||
def get_submob_indices_list_by_span(
|
||||
self, arbitrary_span: Span
|
||||
) -> list[int]:
|
||||
return [
|
||||
submob_index
|
||||
for submob_index, label in enumerate(self.labels)
|
||||
if self.span_contains(arbitrary_span, self.labelled_spans[label])
|
||||
]
|
||||
|
||||
def get_specified_part_items(self) -> list[tuple[str, list[int]]]:
|
||||
return [
|
||||
(
|
||||
self.string[slice(*span)],
|
||||
self.get_submob_indices_list_by_span(span)
|
||||
)
|
||||
for span in self.labelled_spans[1:]
|
||||
]
|
||||
|
||||
def get_specified_substrings(self) -> list[str]:
|
||||
substrs = [
|
||||
self.string[slice(*span)]
|
||||
for span in self.labelled_spans[1:]
|
||||
]
|
||||
# Use dict.fromkeys to remove duplicates while retaining order
|
||||
return list(dict.fromkeys(substrs).keys())
|
||||
|
||||
def get_group_part_items(self) -> list[tuple[str, list[int]]]:
|
||||
if not self.labels:
|
||||
return []
|
||||
|
||||
def get_neighbouring_pairs(vals):
|
||||
return list(zip(vals[:-1], vals[1:]))
|
||||
|
||||
range_lens, group_labels = zip(*(
|
||||
(len(list(grouper)), val)
|
||||
for val, grouper in it.groupby(self.labels)
|
||||
))
|
||||
submob_indices_lists = [
|
||||
list(range(*submob_range))
|
||||
for submob_range in get_neighbouring_pairs(
|
||||
[0, *it.accumulate(range_lens)]
|
||||
)
|
||||
]
|
||||
labelled_spans = self.labelled_spans
|
||||
start_items = [
|
||||
(group_labels[0], 1),
|
||||
*(
|
||||
(curr_label, 1)
|
||||
if self.span_contains(
|
||||
labelled_spans[prev_label], labelled_spans[curr_label]
|
||||
)
|
||||
else (prev_label, -1)
|
||||
for prev_label, curr_label in get_neighbouring_pairs(
|
||||
group_labels
|
||||
)
|
||||
)
|
||||
]
|
||||
end_items = [
|
||||
*(
|
||||
(curr_label, -1)
|
||||
if self.span_contains(
|
||||
labelled_spans[next_label], labelled_spans[curr_label]
|
||||
)
|
||||
else (next_label, 1)
|
||||
for curr_label, next_label in get_neighbouring_pairs(
|
||||
group_labels
|
||||
)
|
||||
),
|
||||
(group_labels[-1], -1)
|
||||
]
|
||||
group_substrs = [
|
||||
re.sub(r"\s+", "", self.reconstruct_string(
|
||||
start_item, end_item,
|
||||
self.replace_for_matching,
|
||||
lambda label, flag, attr_dict: ""
|
||||
))
|
||||
for start_item, end_item in zip(start_items, end_items)
|
||||
]
|
||||
return list(zip(group_substrs, submob_indices_lists))
|
||||
|
||||
def get_submob_indices_lists_by_selector(
|
||||
self, selector: Selector
|
||||
) -> list[list[int]]:
|
||||
return list(filter(
|
||||
lambda indices_list: indices_list,
|
||||
[
|
||||
self.get_submob_indices_list_by_span(span)
|
||||
for span in self.find_spans_by_selector(selector)
|
||||
]
|
||||
))
|
||||
|
||||
def build_parts_from_indices_lists(
|
||||
self, indices_lists: list[list[int]]
|
||||
) -> VGroup:
|
||||
return VGroup(*(
|
||||
VGroup(*(
|
||||
self.submobjects[submob_index]
|
||||
for submob_index in indices_list
|
||||
))
|
||||
for indices_list in indices_lists
|
||||
))
|
||||
|
||||
def build_groups(self) -> VGroup:
|
||||
return self.build_parts_from_indices_lists([
|
||||
indices_list
|
||||
for _, indices_list in self.get_group_part_items()
|
||||
])
|
||||
|
||||
def select_parts(self, selector: Selector) -> VGroup:
|
||||
specified_substrings = self.get_specified_substrings()
|
||||
if isinstance(selector, (str, re.Pattern)) and selector not in specified_substrings:
|
||||
return self.select_unisolated_substring(selector)
|
||||
indices_list = self.get_submob_indices_lists_by_selector(selector)
|
||||
return self.build_parts_from_indices_lists(indices_list)
|
||||
|
||||
def __getitem__(self, value: int | slice | Selector) -> VMobject:
|
||||
if isinstance(value, (int, slice)):
|
||||
return super().__getitem__(value)
|
||||
return self.select_parts(value)
|
||||
|
||||
def select_part(self, selector: Selector, index: int = 0) -> VMobject:
|
||||
return self.select_parts(selector)[index]
|
||||
|
||||
def substr_to_path_count(self, substr: str) -> int:
|
||||
return len(re.sub(r"\s", "", substr))
|
||||
|
||||
def get_symbol_substrings(self):
|
||||
return list(re.sub(r"\s", "", self.string))
|
||||
|
||||
def select_unisolated_substring(self, pattern: str | re.Pattern) -> VGroup:
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(re.escape(pattern))
|
||||
result = []
|
||||
for match in re.finditer(pattern, self.string):
|
||||
index = match.start()
|
||||
start = self.substr_to_path_count(self.string[:index])
|
||||
substr = match.group()
|
||||
end = start + self.substr_to_path_count(substr)
|
||||
result.append(self[start:end])
|
||||
return VGroup(*result)
|
||||
|
||||
def set_parts_color(self, selector: Selector, color: ManimColor):
|
||||
self.select_parts(selector).set_color(color)
|
||||
return self
|
||||
|
||||
def set_parts_color_by_dict(self, color_map: dict[Selector, ManimColor]):
|
||||
for selector, color in color_map.items():
|
||||
self.set_parts_color(selector, color)
|
||||
return self
|
||||
|
||||
def get_string(self) -> str:
|
||||
return self.string
|
||||
@@ -1,528 +1,340 @@
|
||||
import itertools as it
|
||||
import re
|
||||
import string
|
||||
import warnings
|
||||
import os
|
||||
import hashlib
|
||||
from __future__ import annotations
|
||||
|
||||
from xml.dom import minidom
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from manimlib.constants import DEFAULT_STROKE_WIDTH
|
||||
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import DEGREES, PI
|
||||
import numpy as np
|
||||
import svgelements as se
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Line
|
||||
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.simple_functions import clip
|
||||
from manimlib.utils.iterables import hash_obj
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import ManimColor, Vect3Array
|
||||
|
||||
|
||||
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, list[VMobject]] = {}
|
||||
PATH_TO_POINTS: dict[str, Vect3Array] = {}
|
||||
|
||||
|
||||
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
|
||||
return np.array([x, y, 0.0])
|
||||
|
||||
|
||||
class SVGMobject(VMobject):
|
||||
CONFIG = {
|
||||
"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": {}
|
||||
}
|
||||
file_name: str = ""
|
||||
height: float | None = 2.0
|
||||
width: float | None = None
|
||||
|
||||
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 = "",
|
||||
svg_string: str = "",
|
||||
should_center: bool = True,
|
||||
height: float | None = None,
|
||||
width: float | None = None,
|
||||
# Style that overrides the original svg
|
||||
color: ManimColor = None,
|
||||
fill_color: ManimColor = None,
|
||||
fill_opacity: float | None = None,
|
||||
stroke_width: float | None = 0.0,
|
||||
stroke_color: ManimColor = None,
|
||||
stroke_opacity: float | None = None,
|
||||
# Style that fills only when not specified
|
||||
# If None, regarded as default values from svg standard
|
||||
svg_default: dict = dict(
|
||||
color=None,
|
||||
opacity=None,
|
||||
fill_color=None,
|
||||
fill_opacity=None,
|
||||
stroke_width=None,
|
||||
stroke_color=None,
|
||||
stroke_opacity=None,
|
||||
),
|
||||
path_string_config: dict = dict(),
|
||||
**kwargs
|
||||
):
|
||||
if svg_string != "":
|
||||
self.svg_string = svg_string
|
||||
elif file_name != "":
|
||||
self.svg_string = self.file_name_to_svg_string(file_name)
|
||||
elif self.file_name != "":
|
||||
self.file_name_to_svg_string(self.file_name)
|
||||
else:
|
||||
raise Exception("Must specify either a file_name or svg_string SVGMobject")
|
||||
|
||||
self.svg_default = dict(svg_default)
|
||||
self.path_string_config = dict(path_string_config)
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self.move_into_position()
|
||||
self.init_svg_mobject()
|
||||
self.ensure_positive_orientation()
|
||||
|
||||
def move_into_position(self):
|
||||
if self.should_center:
|
||||
# Rather than passing style into super().__init__
|
||||
# do it after svg has been taken in
|
||||
self.set_style(
|
||||
fill_color=color or fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_color=color or stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
stroke_opacity=stroke_opacity,
|
||||
)
|
||||
|
||||
# Initialize position
|
||||
height = height or self.height
|
||||
width = width or self.width
|
||||
|
||||
if should_center:
|
||||
self.center()
|
||||
if self.height is not None:
|
||||
self.set_height(self.height)
|
||||
if self.width is not None:
|
||||
self.set_width(self.width)
|
||||
if height is not None:
|
||||
self.set_height(height)
|
||||
if width is not None:
|
||||
self.set_width(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))
|
||||
def init_svg_mobject(self) -> None:
|
||||
hash_val = hash_obj(self.hash_seed)
|
||||
if hash_val in SVG_HASH_TO_MOB_MAP:
|
||||
submobs = [sm.copy() for sm in SVG_HASH_TO_MOB_MAP[hash_val]]
|
||||
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)]
|
||||
submobs = self.mobjects_from_svg_string(self.svg_string)
|
||||
SVG_HASH_TO_MOB_MAP[hash_val] = [sm.copy() for sm in submobs]
|
||||
|
||||
self.add(*submobs)
|
||||
self.flip(RIGHT) # Flip y
|
||||
|
||||
@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.svg_string
|
||||
)
|
||||
|
||||
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
|
||||
element_tree = ET.ElementTree(ET.fromstring(svg_string))
|
||||
new_tree = self.modify_xml_tree(element_tree)
|
||||
|
||||
# New svg based on tree contents
|
||||
data_stream = io.BytesIO()
|
||||
new_tree.write(data_stream)
|
||||
data_stream.seek(0)
|
||||
svg = se.SVG.parse(data_stream)
|
||||
data_stream.close()
|
||||
|
||||
return self.mobjects_from_svg(svg)
|
||||
|
||||
def file_name_to_svg_string(self, file_name: str) -> str:
|
||||
return Path(get_full_vector_image_path(file_name)).read_text()
|
||||
|
||||
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
|
||||
config_style_attrs = self.generate_config_style_dict()
|
||||
style_keys = (
|
||||
"fill",
|
||||
"fill-opacity",
|
||||
"stroke",
|
||||
"stroke-opacity",
|
||||
"stroke-width",
|
||||
"style"
|
||||
)
|
||||
root = element_tree.getroot()
|
||||
style_attrs = {
|
||||
k: v
|
||||
for k, v in root.attrib.items()
|
||||
if k in style_keys
|
||||
}
|
||||
|
||||
# Ignore other attributes in case that svgelements cannot parse them
|
||||
SVG_XMLNS = "{http://www.w3.org/2000/svg}"
|
||||
new_root = ET.Element("svg")
|
||||
config_style_node = ET.SubElement(new_root, f"{SVG_XMLNS}g", config_style_attrs)
|
||||
root_style_node = ET.SubElement(config_style_node, f"{SVG_XMLNS}g", style_attrs)
|
||||
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 g_to_mobjects(self, g_element):
|
||||
mob = VGroup(*self.get_mobjects_from(g_element))
|
||||
self.handle_transforms(g_element, mob)
|
||||
return mob.submobjects
|
||||
def mobjects_from_svg(self, svg: se.SVG) -> list[VMobject]:
|
||||
result = []
|
||||
for shape in svg.elements():
|
||||
if isinstance(shape, (se.Group, se.Use)):
|
||||
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, 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("Unsupported element type: %s", type(shape))
|
||||
continue
|
||||
if not mob.has_points():
|
||||
continue
|
||||
if isinstance(shape, se.GraphicObject):
|
||||
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
|
||||
|
||||
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 + "." + "-"
|
||||
@staticmethod
|
||||
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:
|
||||
mat = np.array([
|
||||
[matrix.a, matrix.c],
|
||||
[matrix.b, matrix.d]
|
||||
])
|
||||
return float(stripped_attr)
|
||||
vec = np.array([matrix.e, matrix.f, 0.0])
|
||||
mob.apply_matrix(mat)
|
||||
mob.shift(vec)
|
||||
return mob
|
||||
|
||||
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)
|
||||
@staticmethod
|
||||
def apply_style_to_mobject(
|
||||
mob: VMobject,
|
||||
shape: se.GraphicObject
|
||||
) -> VMobject:
|
||||
mob.set_style(
|
||||
stroke_width=shape.stroke_width,
|
||||
stroke_color=shape.stroke.hexrgb,
|
||||
stroke_opacity=shape.stroke.opacity,
|
||||
fill_color=shape.fill.hexrgb,
|
||||
fill_opacity=shape.fill.opacity
|
||||
)
|
||||
return mob
|
||||
|
||||
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 path_to_mobject(self, path: se.Path) -> VMobjectFromSVGPath:
|
||||
return VMobjectFromSVGPath(path, **self.path_string_config)
|
||||
|
||||
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")
|
||||
]
|
||||
result = Circle()
|
||||
result.stretch(rx, 0)
|
||||
result.stretch(ry, 1)
|
||||
result.shift(x * RIGHT + y * DOWN)
|
||||
return result
|
||||
def line_to_mobject(self, line: se.SimpleLine) -> 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_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
|
||||
fill_opacity = 1
|
||||
if fill_color in ["", "none", "#FFF", "#FFFFFF"] or Color(fill_color) == Color(WHITE):
|
||||
fill_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:
|
||||
def rect_to_mobject(self, rect: se.Rect) -> Rectangle:
|
||||
if rect.rx == 0 or rect.ry == 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=fill_opacity
|
||||
width=rect.width,
|
||||
height=rect.height,
|
||||
)
|
||||
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
|
||||
width=rect.width,
|
||||
height=rect.height * rect.rx / rect.ry,
|
||||
corner_radius=rect.rx
|
||||
)
|
||||
|
||||
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
|
||||
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 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
|
||||
def ellipse_to_mobject(self, ellipse: se.Circle | 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
|
||||
|
||||
transform = element.getAttribute('transform')
|
||||
def polygon_to_mobject(self, polygon: se.Polygon) -> Polygon:
|
||||
points = [
|
||||
_convert_point_to_3d(*point)
|
||||
for point in polygon
|
||||
]
|
||||
return Polygon(*points)
|
||||
|
||||
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
|
||||
def polyline_to_mobject(self, polyline: se.Polyline) -> Polyline:
|
||||
points = [
|
||||
_convert_point_to_3d(*point)
|
||||
for point in polyline
|
||||
]
|
||||
return Polyline(*points)
|
||||
|
||||
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)
|
||||
def text_to_mobject(self, text: se.Text):
|
||||
pass
|
||||
|
||||
|
||||
class VMobjectFromSVGPathstring(VMobject):
|
||||
CONFIG = {
|
||||
"long_lines": False,
|
||||
"should_subdivide_sharp_curves": False,
|
||||
"should_remove_null_curves": False,
|
||||
}
|
||||
|
||||
def __init__(self, path_string, **kwargs):
|
||||
self.path_string = path_string
|
||||
class VMobjectFromSVGPath(VMobject):
|
||||
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_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:
|
||||
# will be saved so that future calls for the same pathdon't need to
|
||||
# retrace the same computation.
|
||||
path_string = self.path_obj.d()
|
||||
if path_string not in PATH_TO_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())
|
||||
# Save for future use
|
||||
PATH_TO_POINTS[path_string] = self.get_points().copy()
|
||||
else:
|
||||
points = PATH_TO_POINTS[path_string]
|
||||
self.set_points(points)
|
||||
|
||||
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_commands(self):
|
||||
relative_point = ORIGIN
|
||||
for command, coord_string in self.get_commands_and_coord_strings():
|
||||
func, number_types_str = self.command_to_function(command)
|
||||
upper_command = command.upper()
|
||||
if upper_command == "Z":
|
||||
func() # `close_path` takes no arguments
|
||||
continue
|
||||
|
||||
number_types = np.array(list(number_types_str))
|
||||
n_numbers = len(number_types_str)
|
||||
number_groups = np.array(string_to_numbers(coord_string)).reshape((-1, n_numbers))
|
||||
|
||||
for numbers in number_groups:
|
||||
if command.islower():
|
||||
# Treat it as a relative command
|
||||
numbers[number_types == "x"] += relative_point[0]
|
||||
numbers[number_types == "y"] += relative_point[1]
|
||||
|
||||
if upper_command == "A":
|
||||
args = [*numbers[:5], np.array([*numbers[5:7], 0.0])]
|
||||
elif upper_command == "H":
|
||||
args = [np.array([numbers[0], relative_point[1], 0.0])]
|
||||
elif upper_command == "V":
|
||||
args = [np.array([relative_point[0], numbers[0], 0.0])]
|
||||
else:
|
||||
args = list(np.hstack((
|
||||
numbers.reshape((-1, 2)), np.zeros((n_numbers // 2, 1))
|
||||
)))
|
||||
func(*args)
|
||||
relative_point = self.get_last_point()
|
||||
|
||||
|
||||
def add_elliptical_arc_to(self, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, point):
|
||||
def close_to_zero(a, threshold=1e-5):
|
||||
return abs(a) < threshold
|
||||
|
||||
def solve_2d_linear_equation(a, b, c):
|
||||
"""
|
||||
Using Crammer's rule to solve the linear equation `[a b]x = c`
|
||||
where `a`, `b` and `c` are all 2d vectors.
|
||||
"""
|
||||
def det(a, b):
|
||||
return a[0] * b[1] - a[1] * b[0]
|
||||
d = det(a, b)
|
||||
if close_to_zero(d):
|
||||
raise Exception("Cannot handle 0 determinant.")
|
||||
return [det(c, b) / d, det(a, c) / d]
|
||||
|
||||
def get_arc_center_and_angles(x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, x1, y1):
|
||||
"""
|
||||
The parameter functions of an ellipse rotated `phi` radians counterclockwise is (on `alpha`):
|
||||
x = cx + rx * cos(alpha) * cos(phi) + ry * sin(alpha) * sin(phi),
|
||||
y = cy + rx * cos(alpha) * sin(phi) - ry * sin(alpha) * cos(phi).
|
||||
Now we have two points sitting on the ellipse: `(x0, y0)`, `(x1, y1)`, corresponding to 4 equations,
|
||||
and we want to hunt for 4 variables: `cx`, `cy`, `alpha0` and `alpha_1`.
|
||||
Let `d_alpha = alpha1 - alpha0`, then:
|
||||
if `sweep_flag = 0` and `large_arc_flag = 1`, then `PI <= d_alpha < 2 * PI`;
|
||||
if `sweep_flag = 0` and `large_arc_flag = 0`, then `0 < d_alpha <= PI`;
|
||||
if `sweep_flag = 1` and `large_arc_flag = 0`, then `-PI <= d_alpha < 0`;
|
||||
if `sweep_flag = 1` and `large_arc_flag = 1`, then `-2 * PI < d_alpha <= -PI`.
|
||||
"""
|
||||
xd = x1 - x0
|
||||
yd = y1 - y0
|
||||
if close_to_zero(xd) and close_to_zero(yd):
|
||||
raise Exception("Cannot find arc center since the start point and the end point meet.")
|
||||
# Find `p = cos(alpha1) - cos(alpha0)`, `q = sin(alpha1) - sin(alpha0)`
|
||||
eq0 = [rx * np.cos(phi), ry * np.sin(phi), xd]
|
||||
eq1 = [rx * np.sin(phi), -ry * np.cos(phi), yd]
|
||||
p, q = solve_2d_linear_equation(*zip(eq0, eq1))
|
||||
# Find `s = (alpha1 - alpha0) / 2`, `t = (alpha1 + alpha0) / 2`
|
||||
# If `sin(s) = 0`, this requires `p = q = 0`,
|
||||
# implying `xd = yd = 0`, which is impossible.
|
||||
sin_s = (p ** 2 + q ** 2) ** 0.5 / 2
|
||||
if sweep_flag:
|
||||
sin_s = -sin_s
|
||||
sin_s = clip(sin_s, -1, 1)
|
||||
s = np.arcsin(sin_s)
|
||||
if large_arc_flag:
|
||||
if not sweep_flag:
|
||||
s = PI - s
|
||||
else:
|
||||
s = -PI - s
|
||||
sin_t = -p / (2 * sin_s)
|
||||
cos_t = q / (2 * sin_s)
|
||||
cos_t = clip(cos_t, -1, 1)
|
||||
t = np.arccos(cos_t)
|
||||
if sin_t <= 0:
|
||||
t = -t
|
||||
# We can make sure `0 < abs(s) < PI`, `-PI <= t < PI`.
|
||||
alpha0 = t - s
|
||||
alpha_1 = t + s
|
||||
cx = x0 - rx * np.cos(alpha0) * np.cos(phi) - ry * np.sin(alpha0) * np.sin(phi)
|
||||
cy = y0 - rx * np.cos(alpha0) * np.sin(phi) + ry * np.sin(alpha0) * np.cos(phi)
|
||||
return cx, cy, alpha0, alpha_1
|
||||
|
||||
def get_point_on_ellipse(cx, cy, rx, ry, phi, angle):
|
||||
return np.array([
|
||||
cx + rx * np.cos(angle) * np.cos(phi) + ry * np.sin(angle) * np.sin(phi),
|
||||
cy + rx * np.cos(angle) * np.sin(phi) - ry * np.sin(angle) * np.cos(phi),
|
||||
0
|
||||
])
|
||||
|
||||
def convert_elliptical_arc_to_quadratic_bezier_curve(
|
||||
cx, cy, rx, ry, phi, start_angle, end_angle, n_components=8
|
||||
):
|
||||
theta = (end_angle - start_angle) / n_components / 2
|
||||
handles = np.array([
|
||||
get_point_on_ellipse(cx, cy, rx / np.cos(theta), ry / np.cos(theta), phi, a)
|
||||
for a in np.linspace(
|
||||
start_angle + theta,
|
||||
end_angle - theta,
|
||||
n_components,
|
||||
)
|
||||
])
|
||||
anchors = np.array([
|
||||
get_point_on_ellipse(cx, cy, rx, ry, phi, a)
|
||||
for a in np.linspace(
|
||||
start_angle + theta * 2,
|
||||
end_angle,
|
||||
n_components,
|
||||
)
|
||||
])
|
||||
return handles, anchors
|
||||
|
||||
phi = x_axis_rotation * DEGREES
|
||||
x0, y0 = self.get_last_point()[:2]
|
||||
cx, cy, start_angle, end_angle = get_arc_center_and_angles(
|
||||
x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, point[0], point[1]
|
||||
)
|
||||
handles, anchors = convert_elliptical_arc_to_quadratic_bezier_curve(
|
||||
cx, cy, rx, ry, phi, start_angle, end_angle
|
||||
)
|
||||
for handle, anchor in zip(handles, anchors):
|
||||
self.add_quadratic_bezier_curve_to(handle, anchor)
|
||||
|
||||
def command_to_function(self, command):
|
||||
return self.get_command_to_function_map()[command.upper()]
|
||||
|
||||
def get_command_to_function_map(self):
|
||||
"""
|
||||
Associates svg command to VMobject function, and
|
||||
the types of arguments it takes in
|
||||
"""
|
||||
return {
|
||||
"M": (self.start_new_path, "xy"),
|
||||
"L": (self.add_line_to, "xy"),
|
||||
"H": (self.add_line_to, "x"),
|
||||
"V": (self.add_line_to, "y"),
|
||||
"C": (self.add_cubic_bezier_curve_to, "xyxyxy"),
|
||||
"S": (self.add_smooth_cubic_curve_to, "xyxy"),
|
||||
"Q": (self.add_quadratic_bezier_curve_to, "xyxy"),
|
||||
"T": (self.add_smooth_curve_to, "xy"),
|
||||
"A": (self.add_elliptical_arc_to, "-----xy"),
|
||||
"Z": (self.close_path, ""),
|
||||
def handle_commands(self) -> None:
|
||||
segment_class_to_func_map = {
|
||||
se.Move: (self.start_new_path, ("end",)),
|
||||
se.Close: (self.close_path, ()),
|
||||
se.Line: (lambda p: self.add_line_to(p, allow_null_line=False), ("end",)),
|
||||
se.QuadraticBezier: (lambda c, e: self.add_quadratic_bezier_curve_to(c, e, allow_null_curve=False), ("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() - 2)
|
||||
|
||||
@@ -1,352 +1,269 @@
|
||||
from functools import reduce
|
||||
import operator as op
|
||||
import re
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from manimlib.mobject.svg.string_mobject import StringMobject
|
||||
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
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.color import color_to_hex
|
||||
from manimlib.utils.color import hex_to_int
|
||||
from manimlib.utils.tex_file_writing import latex_to_svg
|
||||
from manimlib.utils.tex import num_tex_symbols
|
||||
from manimlib.logger import log
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import ManimColor, Span, Selector, Self
|
||||
|
||||
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
|
||||
|
||||
tex_string_to_mob_map = {}
|
||||
class Tex(StringMobject):
|
||||
tex_environment: str = "align*"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*tex_strings: str,
|
||||
font_size: int = 48,
|
||||
alignment: str = R"\centering",
|
||||
template: str = "",
|
||||
additional_preamble: str = "",
|
||||
tex_to_color_map: dict = dict(),
|
||||
t2c: dict = dict(),
|
||||
isolate: Selector = [],
|
||||
use_labelled_svg: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
# Combine multi-string arg, but mark them to isolate
|
||||
if len(tex_strings) > 1:
|
||||
if isinstance(isolate, (str, re.Pattern, tuple)):
|
||||
isolate = [isolate]
|
||||
isolate = [*isolate, *tex_strings]
|
||||
|
||||
class SingleStringTex(VMobject):
|
||||
CONFIG = {
|
||||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0,
|
||||
"should_center": True,
|
||||
"font_size": 48,
|
||||
"height": None,
|
||||
"organize_left_to_right": False,
|
||||
"alignment": "\\centering",
|
||||
"math_mode": True,
|
||||
}
|
||||
tex_string = (" ".join(tex_strings)).strip()
|
||||
|
||||
def __init__(self, tex_string, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
assert(isinstance(tex_string, str))
|
||||
# Prevent from passing an empty string.
|
||||
if not tex_string.strip():
|
||||
tex_string = R"\\"
|
||||
|
||||
self.font_size = font_size
|
||||
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()
|
||||
self.alignment = alignment
|
||||
self.template = template
|
||||
self.additional_preamble = additional_preamble
|
||||
self.tex_to_color_map = dict(**t2c, **tex_to_color_map)
|
||||
|
||||
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):
|
||||
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
|
||||
super().__init__(
|
||||
tex_string,
|
||||
use_labelled_svg=use_labelled_svg,
|
||||
isolate=isolate,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_modified_expression(self, tex_string):
|
||||
return self.modify_special_strings(tex_string.strip())
|
||||
|
||||
def modify_special_strings(self, tex):
|
||||
tex = tex.strip()
|
||||
should_add_filler = reduce(op.or_, [
|
||||
# Fraction line needs something to be over
|
||||
tex == "\\over",
|
||||
tex == "\\overline",
|
||||
# Makesure sqrt has overbar
|
||||
tex == "\\sqrt",
|
||||
tex == "\\sqrt{",
|
||||
# Need to add blank subscript or superscript
|
||||
tex.endswith("_"),
|
||||
tex.endswith("^"),
|
||||
tex.endswith("dot"),
|
||||
])
|
||||
if should_add_filler:
|
||||
filler = "{\\quad}"
|
||||
tex += filler
|
||||
|
||||
if tex == "\\substack":
|
||||
tex = "\\quad"
|
||||
|
||||
if tex == "":
|
||||
tex = "\\quad"
|
||||
|
||||
# To keep files from starting with a line break
|
||||
if tex.startswith("\\\\"):
|
||||
tex = tex.replace("\\\\", "\\quad\\\\")
|
||||
|
||||
tex = self.balance_braces(tex)
|
||||
|
||||
# Handle imbalanced \left and \right
|
||||
num_lefts, num_rights = [
|
||||
len([
|
||||
s for s in tex.split(substr)[1:]
|
||||
if s and s[0] in "(){}[]|.\\"
|
||||
])
|
||||
for substr in ("\\left", "\\right")
|
||||
]
|
||||
if num_lefts != num_rights:
|
||||
tex = tex.replace("\\left", "\\big")
|
||||
tex = tex.replace("\\right", "\\big")
|
||||
|
||||
for context in ["array"]:
|
||||
begin_in = ("\\begin{%s}" % context) in tex
|
||||
end_in = ("\\end{%s}" % context) in tex
|
||||
if begin_in ^ end_in:
|
||||
# Just turn this into a blank string,
|
||||
# which means caller should leave a
|
||||
# stray \\begin{...} with other symbols
|
||||
tex = ""
|
||||
return tex
|
||||
|
||||
def balance_braces(self, tex):
|
||||
"""
|
||||
Makes Tex resiliant to unmatched braces
|
||||
"""
|
||||
num_unclosed_brackets = 0
|
||||
for char in tex:
|
||||
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):
|
||||
return self.tex_string
|
||||
|
||||
def organize_submobjects_left_to_right(self):
|
||||
self.sort(lambda p: p[0])
|
||||
return self
|
||||
|
||||
|
||||
class Tex(SingleStringTex):
|
||||
CONFIG = {
|
||||
"arg_separator": "",
|
||||
"isolate": [],
|
||||
"tex_to_color_map": {},
|
||||
}
|
||||
|
||||
def __init__(self, *tex_strings, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.tex_strings = self.break_up_tex_strings(tex_strings)
|
||||
full_string = self.arg_separator.join(self.tex_strings)
|
||||
super().__init__(full_string, **kwargs)
|
||||
self.break_up_by_substrings()
|
||||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
|
||||
|
||||
if self.organize_left_to_right:
|
||||
self.organize_submobjects_left_to_right()
|
||||
def get_svg_string_by_content(self, content: str) -> str:
|
||||
return latex_to_svg(content, self.template, self.additional_preamble, short_tex=self.tex_string)
|
||||
|
||||
def break_up_tex_strings(self, tex_strings):
|
||||
# Separate out any strings specified in the isolate
|
||||
# or tex_to_color_map lists.
|
||||
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 substrings_to_isolate
|
||||
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
|
||||
self.font_size *= scale_factor
|
||||
return self
|
||||
|
||||
# Parsing
|
||||
|
||||
@staticmethod
|
||||
def get_command_matches(string: str) -> list[re.Match]:
|
||||
# Lump together adjacent brace pairs
|
||||
pattern = re.compile(r"""
|
||||
(?P<command>\\(?:[a-zA-Z]+|.))
|
||||
|(?P<open>{+)
|
||||
|(?P<close>}+)
|
||||
""", flags=re.X | re.S)
|
||||
result = []
|
||||
open_stack = []
|
||||
for match_obj in pattern.finditer(string):
|
||||
if match_obj.group("open"):
|
||||
open_stack.append((match_obj.span(), len(result)))
|
||||
elif match_obj.group("close"):
|
||||
close_start, close_end = match_obj.span()
|
||||
while True:
|
||||
if not open_stack:
|
||||
raise ValueError("Missing '{' inserted")
|
||||
(open_start, open_end), index = open_stack.pop()
|
||||
n = min(open_end - open_start, close_end - close_start)
|
||||
result.insert(index, pattern.fullmatch(
|
||||
string, pos=open_end - n, endpos=open_end
|
||||
))
|
||||
result.append(pattern.fullmatch(
|
||||
string, pos=close_start, endpos=close_start + n
|
||||
))
|
||||
close_start += n
|
||||
if close_start < close_end:
|
||||
continue
|
||||
open_end -= n
|
||||
if open_start < open_end:
|
||||
open_stack.append(((open_start, open_end), index))
|
||||
break
|
||||
else:
|
||||
result.append(match_obj)
|
||||
if open_stack:
|
||||
raise ValueError("Missing '}' inserted")
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_command_flag(match_obj: re.Match) -> int:
|
||||
if match_obj.group("open"):
|
||||
return 1
|
||||
if match_obj.group("close"):
|
||||
return -1
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def replace_for_content(match_obj: re.Match) -> str:
|
||||
return match_obj.group()
|
||||
|
||||
@staticmethod
|
||||
def replace_for_matching(match_obj: re.Match) -> str:
|
||||
if match_obj.group("command"):
|
||||
return match_obj.group()
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_attr_dict_from_command_pair(
|
||||
open_command: re.Match, close_command: re.Match
|
||||
) -> dict[str, str] | None:
|
||||
if len(open_command.group()) >= 2:
|
||||
return {}
|
||||
return None
|
||||
|
||||
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
|
||||
return [
|
||||
(span, {})
|
||||
for selector in self.tex_to_color_map
|
||||
for span in self.find_spans_by_selector(selector)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_color_command(rgb_hex: str) -> str:
|
||||
rgb = hex_to_int(rgb_hex)
|
||||
rg, b = divmod(rgb, 256)
|
||||
r, g = divmod(rg, 256)
|
||||
return f"\\color[RGB]{{{r}, {g}, {b}}}"
|
||||
|
||||
@staticmethod
|
||||
def get_command_string(
|
||||
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
|
||||
) -> str:
|
||||
if label_hex is None:
|
||||
return ""
|
||||
if is_end:
|
||||
return "}}"
|
||||
return "{{" + Tex.get_color_command(label_hex)
|
||||
|
||||
def get_content_prefix_and_suffix(
|
||||
self, is_labelled: bool
|
||||
) -> tuple[str, str]:
|
||||
prefix_lines = []
|
||||
suffix_lines = []
|
||||
if not is_labelled:
|
||||
prefix_lines.append(self.get_color_command(
|
||||
color_to_hex(self.base_color)
|
||||
))
|
||||
if self.alignment:
|
||||
prefix_lines.append(self.alignment)
|
||||
if self.tex_environment:
|
||||
prefix_lines.append(f"\\begin{{{self.tex_environment}}}")
|
||||
suffix_lines.append(f"\\end{{{self.tex_environment}}}")
|
||||
return (
|
||||
"".join([line + "\n" for line in prefix_lines]),
|
||||
"".join(["\n" + line for line in suffix_lines])
|
||||
)
|
||||
pattern = "|".join(patterns)
|
||||
pieces = []
|
||||
for s in tex_strings:
|
||||
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):
|
||||
"""
|
||||
Reorganize existing submojects one layer
|
||||
deeper based on the structure of tex_strings (as a list
|
||||
of tex_strings)
|
||||
"""
|
||||
if len(self.tex_strings) == 1:
|
||||
submob = self.copy()
|
||||
self.set_submobjects([submob])
|
||||
return self
|
||||
new_submobjects = []
|
||||
curr_index = 0
|
||||
config = dict(self.CONFIG)
|
||||
config["alignment"] = ""
|
||||
for tex_string in self.tex_strings:
|
||||
tex_string = tex_string.strip()
|
||||
if len(tex_string) == 0:
|
||||
continue
|
||||
sub_tex_mob = SingleStringTex(tex_string, **config)
|
||||
num_submobs = len(sub_tex_mob)
|
||||
if num_submobs == 0:
|
||||
continue
|
||||
new_index = curr_index + num_submobs
|
||||
sub_tex_mob.set_submobjects(self[curr_index:new_index])
|
||||
new_submobjects.append(sub_tex_mob)
|
||||
curr_index = new_index
|
||||
self.set_submobjects(new_submobjects)
|
||||
return self
|
||||
# Method alias
|
||||
|
||||
def get_parts_by_tex(self, tex, substring=True, case_sensitive=True):
|
||||
def test(tex1, tex2):
|
||||
if not case_sensitive:
|
||||
tex1 = tex1.lower()
|
||||
tex2 = tex2.lower()
|
||||
if substring:
|
||||
return tex1 in tex2
|
||||
else:
|
||||
return tex1 == tex2
|
||||
def get_parts_by_tex(self, selector: Selector) -> VGroup:
|
||||
return self.select_parts(selector)
|
||||
|
||||
return VGroup(*filter(
|
||||
lambda m: isinstance(m, SingleStringTex) and test(tex, m.get_tex()),
|
||||
self.submobjects
|
||||
def get_part_by_tex(self, selector: Selector, index: int = 0) -> VMobject:
|
||||
return self.select_part(selector, index)
|
||||
|
||||
def set_color_by_tex(self, selector: Selector, color: ManimColor):
|
||||
return self.set_parts_color(selector, color)
|
||||
|
||||
def set_color_by_tex_to_color_map(
|
||||
self, color_map: dict[Selector, ManimColor]
|
||||
):
|
||||
return self.set_parts_color_by_dict(color_map)
|
||||
|
||||
def get_tex(self) -> str:
|
||||
return self.get_string()
|
||||
|
||||
# Specific to Tex
|
||||
def substr_to_path_count(self, substr: str) -> int:
|
||||
tex = self.get_tex()
|
||||
if len(self) != num_tex_symbols(tex):
|
||||
log.warning(f"Estimated size of {tex} does not match true size")
|
||||
return num_tex_symbols(substr)
|
||||
|
||||
def get_symbol_substrings(self):
|
||||
pattern = "|".join((
|
||||
# Tex commands
|
||||
r"\\[a-zA-Z]+",
|
||||
# And most single characters, with these exceptions
|
||||
r"[^\^\{\}\s\_\$\\\&]",
|
||||
))
|
||||
return re.findall(pattern, self.string)
|
||||
|
||||
def get_part_by_tex(self, tex, **kwargs):
|
||||
all_parts = self.get_parts_by_tex(tex, **kwargs)
|
||||
return all_parts[0] if all_parts else None
|
||||
def make_number_changeable(
|
||||
self,
|
||||
value: float | int | str,
|
||||
index: int = 0,
|
||||
replace_all: bool = False,
|
||||
**config,
|
||||
) -> VMobject:
|
||||
substr = str(value)
|
||||
parts = self.select_parts(substr)
|
||||
if len(parts) == 0:
|
||||
log.warning(f"{value} not found in Tex.make_number_changeable call")
|
||||
return VMobject()
|
||||
if index > len(parts) - 1:
|
||||
log.warning(f"Requested {index}th occurance of {value}, but only {len(parts)} exist")
|
||||
return VMobject()
|
||||
if not replace_all:
|
||||
parts = [parts[index]]
|
||||
|
||||
def set_color_by_tex(self, tex, color, **kwargs):
|
||||
self.get_parts_by_tex(tex, **kwargs).set_color(color)
|
||||
return self
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
|
||||
def set_color_by_tex_to_color_map(self, tex_to_color_map, **kwargs):
|
||||
for tex, color in list(tex_to_color_map.items()):
|
||||
self.set_color_by_tex(tex, color, **kwargs)
|
||||
return self
|
||||
decimal_mobs = []
|
||||
for part in parts:
|
||||
if "." in substr:
|
||||
num_decimal_places = len(substr.split(".")[1])
|
||||
else:
|
||||
num_decimal_places = 0
|
||||
decimal_mob = DecimalNumber(
|
||||
float(value),
|
||||
num_decimal_places=num_decimal_places,
|
||||
**config,
|
||||
)
|
||||
decimal_mob.replace(part)
|
||||
decimal_mob.match_style(part)
|
||||
if len(part) > 1:
|
||||
self.remove(*part[1:])
|
||||
self.replace_submobject(self.submobjects.index(part[0]), decimal_mob)
|
||||
decimal_mobs.append(decimal_mob)
|
||||
|
||||
def index_of_part(self, part, start=0):
|
||||
return self.submobjects.index(part, start)
|
||||
# Replace substr with something that looks like a tex command. This
|
||||
# is to ensure Tex.substr_to_path_count counts it correctly.
|
||||
self.string = self.string.replace(substr, R"\decimalmob", 1)
|
||||
|
||||
def index_of_part_by_tex(self, tex, start=0, **kwargs):
|
||||
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):
|
||||
if start_tex is None:
|
||||
start_index = 0
|
||||
else:
|
||||
start_index = self.index_of_part_by_tex(start_tex, **kwargs)
|
||||
|
||||
if stop_tex is None:
|
||||
return self[start_index:]
|
||||
else:
|
||||
stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
|
||||
return self[start_index:stop_index]
|
||||
|
||||
def sort_alphabetically(self):
|
||||
self.submobjects.sort(key=lambda m: m.get_tex())
|
||||
|
||||
def set_bstroke(self, color=BLACK, width=4):
|
||||
self.set_stroke(color, width, background=True)
|
||||
return self
|
||||
if replace_all:
|
||||
return VGroup(*decimal_mobs)
|
||||
return decimal_mobs[index]
|
||||
|
||||
|
||||
class TexText(Tex):
|
||||
CONFIG = {
|
||||
"math_mode": False,
|
||||
"arg_separator": "",
|
||||
}
|
||||
|
||||
|
||||
class BulletedList(TexText):
|
||||
CONFIG = {
|
||||
"buff": MED_LARGE_BUFF,
|
||||
"dot_scale_factor": 2,
|
||||
"alignment": "",
|
||||
}
|
||||
|
||||
def __init__(self, *items, **kwargs):
|
||||
line_separated_items = [s + "\\\\" for s in items]
|
||||
TexText.__init__(self, *line_separated_items, **kwargs)
|
||||
for part in self:
|
||||
dot = Tex("\\cdot").scale(self.dot_scale_factor)
|
||||
dot.next_to(part[0], LEFT, SMALL_BUFF)
|
||||
part.add_to_back(dot)
|
||||
self.arrange(
|
||||
DOWN,
|
||||
aligned_edge=LEFT,
|
||||
buff=self.buff
|
||||
)
|
||||
|
||||
def fade_all_but(self, index_or_string, opacity=0.5):
|
||||
arg = index_or_string
|
||||
if isinstance(arg, str):
|
||||
part = self.get_part_by_tex(arg)
|
||||
elif isinstance(arg, int):
|
||||
part = self.submobjects[arg]
|
||||
else:
|
||||
raise Exception("Expected int or string, got {0}".format(arg))
|
||||
for other_part in self.submobjects:
|
||||
if other_part is part:
|
||||
other_part.set_fill(opacity=1)
|
||||
else:
|
||||
other_part.set_fill(opacity=opacity)
|
||||
|
||||
|
||||
class TexFromPresetString(Tex):
|
||||
CONFIG = {
|
||||
# To be filled by subclasses
|
||||
"tex": None,
|
||||
"color": None,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
Tex.__init__(self, self.tex, **kwargs)
|
||||
self.set_color(self.color)
|
||||
|
||||
|
||||
class Title(TexText):
|
||||
CONFIG = {
|
||||
"scale_factor": 1,
|
||||
"include_underline": True,
|
||||
"underline_width": FRAME_WIDTH - 2,
|
||||
# This will override underline_width
|
||||
"match_underline_width_to_text": False,
|
||||
"underline_buff": MED_SMALL_BUFF,
|
||||
}
|
||||
|
||||
def __init__(self, *text_parts, **kwargs):
|
||||
TexText.__init__(self, *text_parts, **kwargs)
|
||||
self.scale(self.scale_factor)
|
||||
self.to_edge(UP)
|
||||
if self.include_underline:
|
||||
underline = Line(LEFT, RIGHT)
|
||||
underline.next_to(self, DOWN, buff=self.underline_buff)
|
||||
if self.match_underline_width_to_text:
|
||||
underline.match_width(self)
|
||||
else:
|
||||
underline.set_width(self.underline_width)
|
||||
self.add(underline)
|
||||
self.underline = underline
|
||||
tex_environment: str = ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.types.surface import Surface
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import BLUE, BLUE_D, BLUE_E, GREY_A, BLACK
|
||||
from manimlib.constants import IN, ORIGIN, OUT, RIGHT
|
||||
from manimlib.constants import PI, TAU
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.types.surface import SGroup
|
||||
from manimlib.mobject.types.surface import Surface
|
||||
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.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Square
|
||||
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 compass_directions
|
||||
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
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple, TypeVar
|
||||
from manimlib.typing import ManimColor, Vect3, Sequence
|
||||
|
||||
T = TypeVar("T", bound=Mobject)
|
||||
|
||||
|
||||
class SurfaceMesh(VGroup):
|
||||
CONFIG = {
|
||||
"resolution": (21, 11),
|
||||
"stroke_width": 1,
|
||||
"normal_nudge": 1e-2,
|
||||
"depth_test": True,
|
||||
"flat_stroke": False,
|
||||
}
|
||||
|
||||
def __init__(self, uv_surface, **kwargs):
|
||||
if not isinstance(uv_surface, Surface):
|
||||
raise Exception("uv_surface must be of type Surface")
|
||||
def __init__(
|
||||
self,
|
||||
uv_surface: Surface,
|
||||
resolution: Tuple[int, int] = (21, 11),
|
||||
stroke_width: float = 1,
|
||||
stroke_color: ManimColor = GREY_A,
|
||||
normal_nudge: float = 1e-2,
|
||||
depth_test: bool = True,
|
||||
joint_type: str = 'no_joint',
|
||||
**kwargs
|
||||
):
|
||||
self.uv_surface = uv_surface
|
||||
super().__init__(**kwargs)
|
||||
self.resolution = resolution
|
||||
self.normal_nudge = normal_nudge
|
||||
|
||||
def init_points(self):
|
||||
super().__init__(
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
depth_test=depth_test,
|
||||
joint_type=joint_type,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def init_points(self) -> None:
|
||||
uv_surface = self.uv_surface
|
||||
|
||||
full_nu, full_nv = uv_surface.resolution
|
||||
@@ -41,7 +63,7 @@ class SurfaceMesh(VGroup):
|
||||
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()
|
||||
points = uv_surface.get_points()
|
||||
normals = uv_surface.get_unit_normals()
|
||||
nudge = self.normal_nudge
|
||||
nudged_points = points + nudge * normals
|
||||
@@ -69,182 +91,286 @@ class SurfaceMesh(VGroup):
|
||||
# 3D shapes
|
||||
|
||||
class Sphere(Surface):
|
||||
CONFIG = {
|
||||
"resolution": (101, 51),
|
||||
"radius": 1,
|
||||
"u_range": (0, TAU),
|
||||
"v_range": (0, PI),
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
u_range: Tuple[float, float] = (0, TAU),
|
||||
v_range: Tuple[float, float] = (0, PI),
|
||||
resolution: Tuple[int, int] = (101, 51),
|
||||
radius: float = 1.0,
|
||||
**kwargs,
|
||||
):
|
||||
self.radius = radius
|
||||
super().__init__(
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
resolution=resolution,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
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),
|
||||
-np.cos(v)
|
||||
math.cos(u) * math.sin(v),
|
||||
math.sin(u) * math.sin(v),
|
||||
-math.cos(v)
|
||||
])
|
||||
|
||||
|
||||
class Torus(Surface):
|
||||
CONFIG = {
|
||||
"u_range": (0, TAU),
|
||||
"v_range": (0, TAU),
|
||||
"r1": 3,
|
||||
"r2": 1,
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
u_range: Tuple[float, float] = (0, TAU),
|
||||
v_range: Tuple[float, float] = (0, TAU),
|
||||
r1: float = 3.0,
|
||||
r2: float = 1.0,
|
||||
**kwargs,
|
||||
):
|
||||
self.r1 = r1
|
||||
self.r2 = r2
|
||||
super().__init__(
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
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
|
||||
return (self.r1 - self.r2 * math.cos(v)) * P - self.r2 * math.sin(v) * OUT
|
||||
|
||||
|
||||
class Cylinder(Surface):
|
||||
CONFIG = {
|
||||
"height": 2,
|
||||
"radius": 1,
|
||||
"axis": OUT,
|
||||
"u_range": (0, TAU),
|
||||
"v_range": (-1, 1),
|
||||
"resolution": (101, 11),
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
u_range: Tuple[float, float] = (0, TAU),
|
||||
v_range: Tuple[float, float] = (-1, 1),
|
||||
resolution: Tuple[int, int] = (101, 11),
|
||||
height: float = 2,
|
||||
radius: float = 1,
|
||||
axis: Vect3 = OUT,
|
||||
**kwargs,
|
||||
):
|
||||
self.height = height
|
||||
self.radius = radius
|
||||
self.axis = axis
|
||||
super().__init__(
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
resolution=resolution,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def init_points(self):
|
||||
super().init_points()
|
||||
self.scale(self.radius)
|
||||
self.set_depth(self.height, stretch=True)
|
||||
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 Cone(Cylinder):
|
||||
def __init__(
|
||||
self,
|
||||
u_range: Tuple[float, float] = (0, TAU),
|
||||
v_range: Tuple[float, float] = (0, 1),
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(u_range=u_range, v_range=v_range, *args, **kwargs)
|
||||
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([(1 - v) * np.cos(u), (1 - v) * np.sin(u), v])
|
||||
|
||||
|
||||
class Line3D(Cylinder):
|
||||
CONFIG = {
|
||||
"width": 0.05,
|
||||
"resolution": (21, 25)
|
||||
}
|
||||
|
||||
def __init__(self, start, end, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
start: Vect3,
|
||||
end: Vect3,
|
||||
width: float = 0.05,
|
||||
resolution: Tuple[int, int] = (21, 25),
|
||||
**kwargs
|
||||
):
|
||||
axis = end - start
|
||||
super().__init__(
|
||||
height=get_norm(axis),
|
||||
radius=self.width / 2,
|
||||
axis=axis
|
||||
radius=width / 2,
|
||||
axis=axis,
|
||||
resolution=resolution,
|
||||
**kwargs
|
||||
)
|
||||
self.shift((start + end) / 2)
|
||||
|
||||
|
||||
class Disk3D(Surface):
|
||||
CONFIG = {
|
||||
"radius": 1,
|
||||
"u_range": (0, 1),
|
||||
"v_range": (0, TAU),
|
||||
"resolution": (2, 25),
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
radius: float = 1,
|
||||
u_range: Tuple[float, float] = (0, 1),
|
||||
v_range: Tuple[float, float] = (0, TAU),
|
||||
resolution: Tuple[int, int] = (2, 100),
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
resolution=resolution,
|
||||
**kwargs,
|
||||
)
|
||||
self.scale(radius)
|
||||
|
||||
def init_points(self):
|
||||
super().init_points()
|
||||
self.scale(self.radius)
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return [
|
||||
u * np.cos(v),
|
||||
u * np.sin(v),
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([
|
||||
u * math.cos(v),
|
||||
u * math.sin(v),
|
||||
0
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
class Square3D(Surface):
|
||||
CONFIG = {
|
||||
"side_length": 2,
|
||||
"u_range": (-1, 1),
|
||||
"v_range": (-1, 1),
|
||||
"resolution": (2, 2),
|
||||
}
|
||||
def __init__(
|
||||
self,
|
||||
side_length: float = 2.0,
|
||||
u_range: Tuple[float, float] = (-1, 1),
|
||||
v_range: Tuple[float, float] = (-1, 1),
|
||||
resolution: Tuple[int, int] = (2, 2),
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
resolution=resolution,
|
||||
**kwargs
|
||||
)
|
||||
self.scale(side_length / 2)
|
||||
|
||||
def init_points(self):
|
||||
super().init_points()
|
||||
self.scale(self.side_length / 2)
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([u, v, 0])
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return [u, v, 0]
|
||||
|
||||
def square_to_cube_faces(square: T) -> list[T]:
|
||||
radius = square.get_height() / 2
|
||||
square.move_to(radius * OUT)
|
||||
result = [square.copy()]
|
||||
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
|
||||
|
||||
|
||||
class Cube(SGroup):
|
||||
CONFIG = {
|
||||
"color": BLUE,
|
||||
"opacity": 1,
|
||||
"gloss": 0.5,
|
||||
"square_resolution": (2, 2),
|
||||
"side_length": 2,
|
||||
"square_class": Square3D,
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
def __init__(
|
||||
self,
|
||||
color: ManimColor = BLUE,
|
||||
opacity: float = 1,
|
||||
shading: Tuple[float, float, float] = (0.1, 0.5, 0.1),
|
||||
square_resolution: Tuple[int, int] = (2, 2),
|
||||
side_length: float = 2,
|
||||
**kwargs,
|
||||
):
|
||||
face = Square3D(
|
||||
resolution=self.square_resolution,
|
||||
side_length=self.side_length,
|
||||
resolution=square_resolution,
|
||||
side_length=side_length,
|
||||
color=color,
|
||||
opacity=opacity,
|
||||
shading=shading,
|
||||
)
|
||||
self.add(*self.square_to_cube_faces(face))
|
||||
|
||||
@staticmethod
|
||||
def square_to_cube_faces(square):
|
||||
radius = square.get_height() / 2
|
||||
square.move_to(radius * OUT)
|
||||
result = [square]
|
||||
result.extend([
|
||||
square.copy().rotate(PI / 2, axis=vect, about_point=ORIGIN)
|
||||
for vect in compass_directions(4)
|
||||
])
|
||||
result.append(square.copy().rotate(PI, RIGHT, about_point=ORIGIN))
|
||||
return result
|
||||
|
||||
def _get_face(self):
|
||||
return Square3D(resolution=self.square_resolution)
|
||||
super().__init__(*square_to_cube_faces(face), **kwargs)
|
||||
|
||||
|
||||
class VCube(VGroup):
|
||||
CONFIG = {
|
||||
"fill_color": BLUE_D,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
"gloss": 0.5,
|
||||
"shadow": 0.5,
|
||||
}
|
||||
|
||||
def __init__(self, side_length=2, **kwargs):
|
||||
class Prism(Cube):
|
||||
def __init__(
|
||||
self,
|
||||
width: float = 3.0,
|
||||
height: float = 2.0,
|
||||
depth: float = 1.0,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
face = Square(side_length=side_length)
|
||||
face.get_triangulation()
|
||||
self.add(*Cube.square_to_cube_faces(face))
|
||||
self.init_colors()
|
||||
self.apply_depth_test()
|
||||
self.refresh_unit_normal()
|
||||
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,
|
||||
}
|
||||
class VGroup3D(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
*vmobjects: VMobject,
|
||||
depth_test: bool = True,
|
||||
shading: Tuple[float, float, float] = (0.2, 0.2, 0.2),
|
||||
joint_type: str = "no_joint",
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(*vmobjects, **kwargs)
|
||||
self.set_shading(*shading)
|
||||
self.set_joint_type(joint_type)
|
||||
if depth_test:
|
||||
self.apply_depth_test()
|
||||
|
||||
def init_points(self):
|
||||
# Star by creating two of the pentagons, meeting
|
||||
|
||||
class VCube(VGroup3D):
|
||||
def __init__(
|
||||
self,
|
||||
side_length: float = 2.0,
|
||||
fill_color: ManimColor = BLUE_D,
|
||||
fill_opacity: float = 1,
|
||||
stroke_width: float = 0,
|
||||
**kwargs
|
||||
):
|
||||
style = dict(
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_width=stroke_width,
|
||||
**kwargs
|
||||
)
|
||||
face = Square(side_length=side_length, **style)
|
||||
super().__init__(*square_to_cube_faces(face), **style)
|
||||
|
||||
|
||||
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(VGroup3D):
|
||||
def __init__(
|
||||
self,
|
||||
fill_color: ManimColor = BLUE_E,
|
||||
fill_opacity: float = 1,
|
||||
stroke_color: ManimColor = BLUE_E,
|
||||
stroke_width: float = 1,
|
||||
shading: Tuple[float, float, float] = (0.2, 0.2, 0.2),
|
||||
**kwargs,
|
||||
):
|
||||
style = dict(
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
shading=shading,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
# Start 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],
|
||||
np.array([phi, 1 / phi, 0]),
|
||||
np.array([1, 1, 1]),
|
||||
np.array([1 / phi, 0, phi]),
|
||||
np.array([1, -1, 1]),
|
||||
np.array([phi, -1 / phi, 0]),
|
||||
**style
|
||||
)
|
||||
pentagon2 = pentagon1.copy().stretch(-1, 2, about_point=ORIGIN)
|
||||
pentagon2.reverse_points()
|
||||
@@ -252,30 +378,29 @@ class Dodecahedron(VGroup):
|
||||
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):
|
||||
pentagons = [*x_pair, *y_pair, *z_pair]
|
||||
for pentagon in list(pentagons):
|
||||
pc = pentagon.copy()
|
||||
pc.apply_function(lambda p: -p)
|
||||
pc.reverse_points()
|
||||
self.add(pc)
|
||||
pentagons.append(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))
|
||||
super().__init__(*pentagons, **style)
|
||||
|
||||
|
||||
class Prism(Cube):
|
||||
CONFIG = {
|
||||
"dimensions": [3, 2, 1]
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
Cube.init_points(self)
|
||||
for dim, value in enumerate(self.dimensions):
|
||||
self.rescale_to_fit(value, dim, stretch=True)
|
||||
class Prismify(VGroup3D):
|
||||
def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):
|
||||
# At the moment, this assume stright edges
|
||||
vect = depth * direction
|
||||
pieces = [vmobject.copy()]
|
||||
points = vmobject.get_anchors()
|
||||
for p1, p2 in adjacent_pairs(points):
|
||||
wall = VMobject()
|
||||
wall.match_style(vmobject)
|
||||
wall.set_points_as_corners([p1, p2, p2 + vect, p1 + vect])
|
||||
pieces.append(wall)
|
||||
top = vmobject.copy()
|
||||
top.shift(vect)
|
||||
top.reverse_points()
|
||||
pieces.append(top)
|
||||
super().__init__(*pieces, **kwargs)
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import numpy as np
|
||||
import moderngl
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import GREY_C
|
||||
from manimlib.constants import YELLOW
|
||||
from manimlib.constants import ORIGIN
|
||||
import moderngl
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import GREY_C, YELLOW
|
||||
from manimlib.constants import ORIGIN, NULL_POINTS
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.types.point_cloud_mobject import PMobject
|
||||
from manimlib.utils.iterables import resize_preserving_order
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
from typing import Sequence, Tuple
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array, Self
|
||||
|
||||
|
||||
DEFAULT_DOT_RADIUS = 0.05
|
||||
@@ -15,41 +24,54 @@ DEFAULT_BUFF_RATIO = 0.5
|
||||
|
||||
|
||||
class DotCloud(PMobject):
|
||||
CONFIG = {
|
||||
"color": GREY_C,
|
||||
"opacity": 1,
|
||||
"radius": DEFAULT_DOT_RADIUS,
|
||||
"glow_factor": 0,
|
||||
"shader_folder": "true_dot",
|
||||
"render_primitive": moderngl.POINTS,
|
||||
"shader_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
('radius', np.float32, (1,)),
|
||||
('color', np.float32, (4,)),
|
||||
],
|
||||
}
|
||||
shader_folder: str = "true_dot"
|
||||
render_primitive: int = moderngl.POINTS
|
||||
data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
|
||||
('point', np.float32, (3,)),
|
||||
('radius', np.float32, (1,)),
|
||||
('rgba', np.float32, (4,)),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
points: Vect3Array = NULL_POINTS,
|
||||
color: ManimColor = GREY_C,
|
||||
opacity: float = 1.0,
|
||||
radius: float = DEFAULT_DOT_RADIUS,
|
||||
glow_factor: float = 0.0,
|
||||
anti_alias_width: float = 2.0,
|
||||
**kwargs
|
||||
):
|
||||
self.radius = radius
|
||||
self.glow_factor = glow_factor
|
||||
self.anti_alias_width = anti_alias_width
|
||||
|
||||
super().__init__(
|
||||
color=color,
|
||||
opacity=opacity,
|
||||
**kwargs
|
||||
)
|
||||
self.set_radius(self.radius)
|
||||
|
||||
def __init__(self, points=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if points is not None:
|
||||
self.set_points(points)
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
self.data["radii"] = np.zeros((1, 1))
|
||||
self.set_radius(self.radius)
|
||||
|
||||
def init_uniforms(self):
|
||||
def init_uniforms(self) -> None:
|
||||
super().init_uniforms()
|
||||
self.uniforms["glow_factor"] = self.glow_factor
|
||||
self.uniforms["anti_alias_width"] = self.anti_alias_width
|
||||
|
||||
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 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,
|
||||
) -> Self:
|
||||
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
|
||||
@@ -74,64 +96,90 @@ class DotCloud(PMobject):
|
||||
self.center()
|
||||
return self
|
||||
|
||||
def set_radii(self, radii):
|
||||
n_points = len(self.get_points())
|
||||
@Mobject.affects_data
|
||||
def set_radii(self, radii: npt.ArrayLike) -> Self:
|
||||
n_points = self.get_num_points()
|
||||
radii = np.array(radii).reshape((len(radii), 1))
|
||||
self.data["radii"] = resize_preserving_order(radii, n_points)
|
||||
self.data["radius"][:] = resize_with_interpolation(radii, n_points)
|
||||
self.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
def get_radii(self):
|
||||
return self.data["radii"]
|
||||
def get_radii(self) -> np.ndarray:
|
||||
return self.data["radius"]
|
||||
|
||||
def set_radius(self, radius):
|
||||
self.data["radii"][:] = radius
|
||||
@Mobject.affects_data
|
||||
def set_radius(self, radius: float) -> Self:
|
||||
data = self.data if self.get_num_points() > 0 else self._data_defaults
|
||||
data["radius"][:] = radius
|
||||
self.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
def get_radius(self):
|
||||
def get_radius(self) -> float:
|
||||
return self.get_radii().max()
|
||||
|
||||
def set_glow_factor(self, glow_factor):
|
||||
self.uniforms["glow_factor"] = glow_factor
|
||||
def scale_radii(self, scale_factor: float) -> Self:
|
||||
self.set_radius(scale_factor * self.get_radii())
|
||||
return self
|
||||
|
||||
def get_glow_factor(self):
|
||||
def set_glow_factor(self, glow_factor: float) -> Self:
|
||||
self.uniforms["glow_factor"] = glow_factor
|
||||
return self
|
||||
|
||||
def get_glow_factor(self) -> float:
|
||||
return self.uniforms["glow_factor"]
|
||||
|
||||
def compute_bounding_box(self):
|
||||
def compute_bounding_box(self) -> Vect3Array:
|
||||
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
|
||||
) -> Self:
|
||||
super().scale(scale_factor, **kwargs)
|
||||
if scale_radii:
|
||||
self.set_radii(scale_factor * self.get_radii())
|
||||
return self
|
||||
|
||||
def make_3d(self, reflectiveness=0.5, shadow=0.2):
|
||||
self.set_reflectiveness(reflectiveness)
|
||||
self.set_shadow(shadow)
|
||||
def make_3d(
|
||||
self,
|
||||
reflectiveness: float = 0.5,
|
||||
gloss: float = 0.1,
|
||||
shadow: float = 0.2
|
||||
) -> Self:
|
||||
self.set_shading(reflectiveness, gloss, shadow)
|
||||
self.apply_depth_test()
|
||||
return self
|
||||
|
||||
def get_shader_data(self):
|
||||
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=ORIGIN, **kwargs):
|
||||
super().__init__(points=[center], **kwargs)
|
||||
def __init__(self, center: Vect3 = ORIGIN, **kwargs):
|
||||
super().__init__(points=np.array([center]), **kwargs)
|
||||
|
||||
|
||||
class GlowDot(TrueDot):
|
||||
CONFIG = {
|
||||
"glow_factor": 2,
|
||||
"radius": DEFAULT_GLOW_DOT_RADIUS,
|
||||
"color": YELLOW,
|
||||
}
|
||||
class GlowDots(DotCloud):
|
||||
def __init__(
|
||||
self,
|
||||
points: Vect3Array = NULL_POINTS,
|
||||
color: ManimColor = YELLOW,
|
||||
radius: float = DEFAULT_GLOW_DOT_RADIUS,
|
||||
glow_factor: float = 2.0,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
points,
|
||||
color=color,
|
||||
radius=radius,
|
||||
glow_factor=glow_factor,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class GlowDot(GlowDots):
|
||||
def __init__(self, center: Vect3 = ORIGIN, **kwargs):
|
||||
super().__init__(points=np.array([center]), **kwargs)
|
||||
|
||||
@@ -1,53 +1,66 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import moderngl
|
||||
from PIL import Image
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.constants import DL, DR, UL, UR
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.bezier import inverse_interpolate
|
||||
from manimlib.utils.images import get_full_raster_image_path
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence, Tuple
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
|
||||
class ImageMobject(Mobject):
|
||||
CONFIG = {
|
||||
"height": 4,
|
||||
"opacity": 1,
|
||||
"shader_folder": "image",
|
||||
"shader_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
('im_coords', np.float32, (2,)),
|
||||
('opacity', np.float32, (1,)),
|
||||
]
|
||||
}
|
||||
shader_folder: str = "image"
|
||||
data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
|
||||
('point', np.float32, (3,)),
|
||||
('im_coords', np.float32, (2,)),
|
||||
('opacity', np.float32, (1,)),
|
||||
]
|
||||
render_primitive: int = moderngl.TRIANGLES
|
||||
|
||||
def __init__(self, filename, **kwargs):
|
||||
self.set_image_path(get_full_raster_image_path(filename))
|
||||
super().__init__(**kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
filename: str,
|
||||
height: float = 4.0,
|
||||
**kwargs
|
||||
):
|
||||
self.height = height
|
||||
self.image_path = get_full_raster_image_path(filename)
|
||||
self.image = Image.open(self.image_path)
|
||||
super().__init__(texture_paths={"Texture": self.image_path}, **kwargs)
|
||||
|
||||
def set_image_path(self, path):
|
||||
self.path = path
|
||||
self.image = Image.open(path)
|
||||
self.texture_paths = {"Texture": path}
|
||||
def init_data(self) -> None:
|
||||
super().init_data(length=6)
|
||||
self.data["point"][:] = [UL, DL, UR, DR, UR, DL]
|
||||
self.data["im_coords"][:] = [(0, 0), (0, 1), (1, 0), (1, 1), (1, 0), (0, 1)]
|
||||
self.data["opacity"][:] = self.opacity
|
||||
|
||||
def init_data(self):
|
||||
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):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
|
||||
@Mobject.affects_data
|
||||
def set_opacity(self, opacity: float, recurse: bool = True):
|
||||
self.data["opacity"][:, 0] = resize_with_interpolation(
|
||||
np.array(listify(opacity)),
|
||||
self.get_num_points()
|
||||
)
|
||||
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: Vect3) -> Vect3:
|
||||
x0, y0 = self.get_corner(UL)[:2]
|
||||
x1, y1 = self.get_corner(DR)[:2]
|
||||
x_alpha = inverse_interpolate(x0, x1, point[0])
|
||||
@@ -60,11 +73,5 @@ class ImageMobject(Mobject):
|
||||
rgb = self.image.getpixel((
|
||||
int((pw - 1) * x_alpha),
|
||||
int((ph - 1) * y_alpha),
|
||||
))
|
||||
))[:3]
|
||||
return np.array(rgb) / 255
|
||||
|
||||
def get_shader_data(self):
|
||||
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")
|
||||
return shader_data
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
from manimlib.constants import *
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.color import color_gradient
|
||||
from manimlib.utils.color import color_to_rgba
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.iterables import resize_array
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array, Vect4Array, Self
|
||||
|
||||
|
||||
class PMobject(Mobject):
|
||||
CONFIG = {
|
||||
"opacity": 1.0,
|
||||
}
|
||||
|
||||
def resize_points(self, size, resize_func=resize_array):
|
||||
# TODO
|
||||
for key in self.data:
|
||||
if key == "bounding_box":
|
||||
continue
|
||||
if len(self.data[key]) != size:
|
||||
self.data[key] = resize_array(self.data[key], size)
|
||||
return self
|
||||
|
||||
def set_points(self, points):
|
||||
def set_points(self, points: Vect3Array):
|
||||
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, rgbas=None, color=None, opacity=None):
|
||||
def add_points(
|
||||
self,
|
||||
points: Vect3Array,
|
||||
rgbas: Vect4Array | None = None,
|
||||
color: ManimColor | None = None,
|
||||
opacity: float | None = None
|
||||
) -> Self:
|
||||
"""
|
||||
points must be a Nx3 numpy array, as must rgbas if it is not None
|
||||
"""
|
||||
@@ -33,40 +36,44 @@ class PMobject(Mobject):
|
||||
# rgbas array will have been resized with points
|
||||
if color is not None:
|
||||
if opacity is None:
|
||||
opacity = self.data["rgbas"][-1, 3]
|
||||
new_rgbas = np.repeat(
|
||||
opacity = self.data["rgba"][-1, 3]
|
||||
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["rgba"][-len(rgbas):] = rgbas
|
||||
return self
|
||||
|
||||
def set_color_by_gradient(self, *colors):
|
||||
self.data["rgbas"] = np.array(list(map(
|
||||
def add_point(self, point: Vect3, rgba=None, color=None, opacity=None) -> Self:
|
||||
rgbas = None if rgba is None else [rgba]
|
||||
self.add_points([point], rgbas, color, opacity)
|
||||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def set_color_by_gradient(self, *colors: ManimColor) -> Self:
|
||||
self.data["rgba"][:] = np.array(list(map(
|
||||
color_to_rgba,
|
||||
color_gradient(colors, self.get_num_points())
|
||||
)))
|
||||
return self
|
||||
|
||||
def match_colors(self, pmobject):
|
||||
self.data["rgbas"][:] = resize_with_interpolation(
|
||||
pmobject.data["rgbas"], self.get_num_points()
|
||||
@Mobject.affects_data
|
||||
def match_colors(self, pmobject: PMobject) -> Self:
|
||||
self.data["rgba"][:] = resize_with_interpolation(
|
||||
pmobject.data["rgba"], self.get_num_points()
|
||||
)
|
||||
return self
|
||||
|
||||
def filter_out(self, condition):
|
||||
@Mobject.affects_data
|
||||
def filter_out(self, condition: Callable[[np.ndarray], bool]) -> Self:
|
||||
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]
|
||||
mob.data = mob.data[~np.apply_along_axis(condition, 1, mob.get_points())]
|
||||
return self
|
||||
|
||||
def sort_points(self, function=lambda p: p[0]):
|
||||
@Mobject.affects_data
|
||||
def sort_points(self, function: Callable[[Vect3], None] = lambda p: p[0]) -> Self:
|
||||
"""
|
||||
function is any map from R^3 to R
|
||||
"""
|
||||
@@ -74,44 +81,31 @@ class PMobject(Mobject):
|
||||
indices = np.argsort(
|
||||
np.apply_along_axis(function, 1, mob.get_points())
|
||||
)
|
||||
for key in mob.data:
|
||||
mob.data[key] = mob.data[key][indices]
|
||||
mob.data[:] = mob.data[indices]
|
||||
return self
|
||||
|
||||
def ingest_submobjects(self):
|
||||
for key in self.data:
|
||||
self.data[key] = np.vstack([
|
||||
sm.data[key]
|
||||
for sm in self.get_family()
|
||||
])
|
||||
@Mobject.affects_data
|
||||
def ingest_submobjects(self) -> Self:
|
||||
self.data = np.vstack([
|
||||
sm.data for sm in self.get_family()
|
||||
])
|
||||
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):
|
||||
@Mobject.affects_data
|
||||
def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float) -> Self:
|
||||
lower_index = int(a * pmobject.get_num_points())
|
||||
upper_index = int(b * pmobject.get_num_points())
|
||||
for key in self.data:
|
||||
if key == "bounding_box":
|
||||
continue
|
||||
self.data[key] = pmobject.data[key][lower_index:upper_index].copy()
|
||||
self.data = pmobject.data[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)
|
||||
|
||||
|
||||
class Point(PMobject):
|
||||
CONFIG = {
|
||||
"color": BLACK,
|
||||
}
|
||||
|
||||
def __init__(self, location=ORIGIN, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.add_points([location])
|
||||
self.add(*pmobs)
|
||||
|
||||
@@ -1,51 +1,75 @@
|
||||
import numpy as np
|
||||
import moderngl
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import *
|
||||
import moderngl
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import GREY
|
||||
from manimlib.constants import OUT
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.images import get_full_raster_image_path
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.space_ops import normalize_along_axis
|
||||
from manimlib.utils.space_ops import cross
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, Sequence, Tuple
|
||||
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array, Self
|
||||
|
||||
|
||||
class Surface(Mobject):
|
||||
CONFIG = {
|
||||
"u_range": (0, 1),
|
||||
"v_range": (0, 1),
|
||||
render_primitive: int = moderngl.TRIANGLES
|
||||
shader_folder: str = "surface"
|
||||
data_dtype: np.dtype = np.dtype([
|
||||
('point', np.float32, (3,)),
|
||||
('du_point', np.float32, (3,)),
|
||||
('dv_point', np.float32, (3,)),
|
||||
('rgba', np.float32, (4,)),
|
||||
])
|
||||
pointlike_data_keys = ['point', 'du_point', 'dv_point']
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: ManimColor = GREY,
|
||||
shading: Tuple[float, float, float] = (0.3, 0.2, 0.4),
|
||||
depth_test: bool = True,
|
||||
u_range: Tuple[float, float] = (0.0, 1.0),
|
||||
v_range: Tuple[float, float] = (0.0, 1.0),
|
||||
# Resolution counts number of points sampled, which for
|
||||
# each coordinate is one more than the the number of
|
||||
# rows/columns of approximating squares
|
||||
"resolution": (101, 101),
|
||||
"color": GREY,
|
||||
"opacity": 1.0,
|
||||
"reflectiveness": 0.3,
|
||||
"gloss": 0.1,
|
||||
"shadow": 0.4,
|
||||
"prefered_creation_axis": 1,
|
||||
resolution: Tuple[int, int] = (101, 101),
|
||||
prefered_creation_axis: int = 1,
|
||||
# For du and dv steps. Much smaller and numerical error
|
||||
# can crop up in the shaders.
|
||||
"epsilon": 1e-5,
|
||||
"render_primitive": moderngl.TRIANGLES,
|
||||
"depth_test": True,
|
||||
"shader_folder": "surface",
|
||||
"shader_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
('du_point', np.float32, (3,)),
|
||||
('dv_point', np.float32, (3,)),
|
||||
('color', np.float32, (4,)),
|
||||
]
|
||||
}
|
||||
epsilon: float = 1e-4,
|
||||
**kwargs
|
||||
):
|
||||
self.u_range = u_range
|
||||
self.v_range = v_range
|
||||
self.resolution = resolution
|
||||
self.prefered_creation_axis = prefered_creation_axis
|
||||
self.epsilon = epsilon
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
super().__init__(
|
||||
**kwargs,
|
||||
color=color,
|
||||
shading=shading,
|
||||
depth_test=depth_test,
|
||||
)
|
||||
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)
|
||||
|
||||
@Mobject.affects_data
|
||||
def init_points(self):
|
||||
dim = self.dim
|
||||
nu, nv = self.resolution
|
||||
@@ -56,25 +80,35 @@ class Surface(Mobject):
|
||||
# - Points generated by pure uv values
|
||||
# - Those generated by values nudged by du
|
||||
# - Those generated by values nudged by dv
|
||||
point_lists = []
|
||||
for (du, dv) in [(0, 0), (self.epsilon, 0), (0, self.epsilon)]:
|
||||
uv_grid = np.array([[[u + du, v + dv] for v in v_range] for u in u_range])
|
||||
point_grid = np.apply_along_axis(lambda p: self.uv_func(*p), 2, uv_grid)
|
||||
point_lists.append(point_grid.reshape((nu * nv, dim)))
|
||||
# Rather than tracking normal vectors, the points list will hold on to the
|
||||
# infinitesimal nudged values alongside the original values. This way, one
|
||||
# can perform all the manipulations they'd like to the surface, and normals
|
||||
# are still easily recoverable.
|
||||
self.set_points(np.vstack(point_lists))
|
||||
uv_grid = np.array([[[u, v] for v in v_range] for u in u_range])
|
||||
uv_plus_du = uv_grid.copy()
|
||||
uv_plus_du[:, :, 0] += self.epsilon
|
||||
uv_plus_dv = uv_grid.copy()
|
||||
uv_plus_dv[:, :, 1] += self.epsilon
|
||||
|
||||
def compute_triangle_indices(self):
|
||||
points, du_points, dv_points = [
|
||||
np.apply_along_axis(
|
||||
lambda p: self.uv_func(*p), 2, grid
|
||||
).reshape((nu * nv, dim))
|
||||
for grid in (uv_grid, uv_plus_du, uv_plus_dv)
|
||||
]
|
||||
self.set_points(points)
|
||||
self.data['du_point'][:] = du_points
|
||||
self.data['dv_point'][:] = dv_points
|
||||
|
||||
def apply_points_function(self, *args, **kwargs) -> Self:
|
||||
super().apply_points_function(*args, **kwargs)
|
||||
self.get_unit_normals()
|
||||
return self
|
||||
|
||||
def compute_triangle_indices(self) -> np.ndarray:
|
||||
# TODO, if there is an event which changes
|
||||
# the resolution of the surface, make sure
|
||||
# this is called.
|
||||
nu, nv = self.resolution
|
||||
if nu == 0 or nv == 0:
|
||||
self.triangle_indices = np.zeros(0, dtype=int)
|
||||
return
|
||||
return self.triangle_indices
|
||||
index_grid = np.arange(nu * nv).reshape((nu, nv))
|
||||
indices = np.zeros(6 * (nu - 1) * (nv - 1), dtype=int)
|
||||
indices[0::6] = index_grid[:-1, :-1].flatten() # Top left
|
||||
@@ -84,25 +118,28 @@ class Surface(Mobject):
|
||||
indices[4::6] = index_grid[+1:, :-1].flatten() # Bottom left
|
||||
indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right
|
||||
self.triangle_indices = indices
|
||||
|
||||
def get_triangle_indices(self):
|
||||
return self.triangle_indices
|
||||
|
||||
def get_surface_points_and_nudged_points(self):
|
||||
def get_triangle_indices(self) -> np.ndarray:
|
||||
return self.triangle_indices
|
||||
|
||||
def get_unit_normals(self) -> Vect3Array:
|
||||
points = self.get_points()
|
||||
k = len(points) // 3
|
||||
return points[:k], points[k:2 * k], points[2 * k:]
|
||||
|
||||
def get_unit_normals(self):
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
normals = np.cross(
|
||||
(du_points - s_points) / self.epsilon,
|
||||
(dv_points - s_points) / self.epsilon,
|
||||
crosses = cross(
|
||||
self.data['du_point'] - points,
|
||||
self.data['dv_point'] - points,
|
||||
)
|
||||
return normalize_along_axis(normals, 1)
|
||||
return normalize_along_axis(crosses, 1)
|
||||
|
||||
def pointwise_become_partial(self, smobject, a, b, axis=None):
|
||||
assert(isinstance(smobject, Surface))
|
||||
@Mobject.affects_data
|
||||
def pointwise_become_partial(
|
||||
self,
|
||||
smobject: "Surface",
|
||||
a: float,
|
||||
b: float,
|
||||
axis: int | None = None
|
||||
) -> Self:
|
||||
assert isinstance(smobject, Surface)
|
||||
if axis is None:
|
||||
axis = self.prefered_creation_axis
|
||||
if a <= 0 and b >= 1:
|
||||
@@ -110,17 +147,25 @@ class Surface(Mobject):
|
||||
return self
|
||||
|
||||
nu, nv = smobject.resolution
|
||||
self.set_points(np.vstack([
|
||||
self.get_partial_points_array(arr.copy(), a, b, (nu, nv, 3), axis=axis)
|
||||
for arr in smobject.get_surface_points_and_nudged_points()
|
||||
]))
|
||||
self.data['point'][:] = self.get_partial_points_array(
|
||||
smobject.data['point'], a, b,
|
||||
(nu, nv, 3),
|
||||
axis=axis
|
||||
)
|
||||
return self
|
||||
|
||||
def get_partial_points_array(self, points, a, b, resolution, axis):
|
||||
def get_partial_points_array(
|
||||
self,
|
||||
points: Vect3Array,
|
||||
a: float,
|
||||
b: float,
|
||||
resolution: Sequence[int],
|
||||
axis: int
|
||||
) -> Vect3Array:
|
||||
if len(points) == 0:
|
||||
return points
|
||||
nu, nv = resolution[:2]
|
||||
points = points.reshape(resolution)
|
||||
points = points.reshape(resolution).copy()
|
||||
max_index = resolution[axis] - 1
|
||||
lower_index, lower_residue = integer_interpolate(0, max_index, a)
|
||||
upper_index, upper_residue = integer_interpolate(0, max_index, b)
|
||||
@@ -149,45 +194,36 @@ class Surface(Mobject):
|
||||
).reshape(shape)
|
||||
return points.reshape((nu * nv, *resolution[2:]))
|
||||
|
||||
def sort_faces_back_to_front(self, vect=OUT):
|
||||
@Mobject.affects_data
|
||||
def sort_faces_back_to_front(self, vect: Vect3 = OUT) -> Self:
|
||||
tri_is = self.triangle_indices
|
||||
indices = list(range(len(tri_is) // 3))
|
||||
points = self.get_points()
|
||||
|
||||
def index_dot(index):
|
||||
return np.dot(points[tri_is[3 * index]], vect)
|
||||
|
||||
indices.sort(key=index_dot)
|
||||
dots = (points[tri_is[::3]] * vect).sum(1)
|
||||
indices = np.argsort(dots)
|
||||
for k in range(3):
|
||||
tri_is[k::3] = tri_is[k::3][indices]
|
||||
return self
|
||||
|
||||
def always_sort_to_camera(self, camera):
|
||||
self.add_updater(lambda m: m.sort_faces_back_to_front(
|
||||
camera.get_location() - self.get_center()
|
||||
))
|
||||
def always_sort_to_camera(self, camera: Camera) -> Self:
|
||||
def updater(surface: Surface):
|
||||
vect = camera.get_location() - surface.get_center()
|
||||
surface.sort_faces_back_to_front(vect)
|
||||
self.add_updater(updater)
|
||||
return self
|
||||
|
||||
# For shaders
|
||||
def get_shader_data(self):
|
||||
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:
|
||||
shader_data["point"] = s_points
|
||||
shader_data["du_point"] = du_points
|
||||
shader_data["dv_point"] = dv_points
|
||||
self.fill_in_shader_color_info(shader_data)
|
||||
return shader_data
|
||||
|
||||
def fill_in_shader_color_info(self, shader_data):
|
||||
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, u_range=(0, 1), v_range=(0, 1), **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__(u_range=u_range, v_range=v_range, **kwargs)
|
||||
|
||||
@@ -196,12 +232,12 @@ class ParametricSurface(Surface):
|
||||
|
||||
|
||||
class SGroup(Surface):
|
||||
CONFIG = {
|
||||
"resolution": (0, 0),
|
||||
}
|
||||
|
||||
def __init__(self, *parametric_surfaces, **kwargs):
|
||||
super().__init__(uv_func=None, **kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
*parametric_surfaces: Surface,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(resolution=(0, 0), **kwargs)
|
||||
self.add(*parametric_surfaces)
|
||||
|
||||
def init_points(self):
|
||||
@@ -209,18 +245,22 @@ class SGroup(Surface):
|
||||
|
||||
|
||||
class TexturedSurface(Surface):
|
||||
CONFIG = {
|
||||
"shader_folder": "textured_surface",
|
||||
"shader_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
('du_point', np.float32, (3,)),
|
||||
('dv_point', np.float32, (3,)),
|
||||
('im_coords', np.float32, (2,)),
|
||||
('opacity', np.float32, (1,)),
|
||||
]
|
||||
}
|
||||
shader_folder: str = "textured_surface"
|
||||
data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
|
||||
('point', np.float32, (3,)),
|
||||
('du_point', np.float32, (3,)),
|
||||
('dv_point', np.float32, (3,)),
|
||||
('im_coords', np.float32, (2,)),
|
||||
('opacity', np.float32, (1,)),
|
||||
]
|
||||
|
||||
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
|
||||
@@ -229,27 +269,33 @@ class TexturedSurface(Surface):
|
||||
self.num_textures = 1
|
||||
else:
|
||||
self.num_textures = 2
|
||||
self.texture_paths = {
|
||||
|
||||
texture_paths = {
|
||||
"LightTexture": get_full_raster_image_path(image_file),
|
||||
"DarkTexture": get_full_raster_image_path(dark_image_file),
|
||||
}
|
||||
|
||||
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
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
self.data["im_coords"] = np.zeros((0, 2))
|
||||
self.data["opacity"] = np.zeros((0, 1))
|
||||
self.u_range: Tuple[float, float] = uv_surface.u_range
|
||||
self.v_range: Tuple[float, float] = uv_surface.v_range
|
||||
self.resolution: Tuple[int, int] = uv_surface.resolution
|
||||
super().__init__(
|
||||
texture_paths=texture_paths,
|
||||
shading=tuple(uv_surface.shading),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@Mobject.affects_data
|
||||
def init_points(self):
|
||||
nu, nv = self.uv_surface.resolution
|
||||
self.set_points(self.uv_surface.get_points())
|
||||
surf = self.uv_surface
|
||||
nu, nv = surf.resolution
|
||||
self.resize_points(surf.get_num_points())
|
||||
self.resolution = surf.resolution
|
||||
self.data['point'][:] = surf.data['point']
|
||||
self.data['du_point'][:] = surf.data['du_point']
|
||||
self.data['dv_point'][:] = surf.data['dv_point']
|
||||
self.data['opacity'][:, 0] = surf.data["rgba"][:, 3]
|
||||
self.data["im_coords"] = np.array([
|
||||
[u, v]
|
||||
for u in np.linspace(0, 1, nu)
|
||||
@@ -260,15 +306,29 @@ class TexturedSurface(Surface):
|
||||
super().init_uniforms()
|
||||
self.uniforms["num_textures"] = self.num_textures
|
||||
|
||||
def init_colors(self):
|
||||
self.data["opacity"] = np.array([self.uv_surface.data["rgbas"][:, 3]])
|
||||
|
||||
def set_opacity(self, opacity, recurse=True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
|
||||
@Mobject.affects_data
|
||||
def set_opacity(self, opacity: float | Iterable[float]) -> Self:
|
||||
op_arr = np.array(listify(opacity))
|
||||
self.data["opacity"][:, 0] = resize_with_interpolation(op_arr, len(self.data))
|
||||
return self
|
||||
|
||||
def pointwise_become_partial(self, tsmobject, a, b, axis=1):
|
||||
def set_color(
|
||||
self,
|
||||
color: ManimColor | Iterable[ManimColor] | None,
|
||||
opacity: float | Iterable[float] | None = None,
|
||||
recurse: bool = True
|
||||
) -> Self:
|
||||
if opacity is not None:
|
||||
self.set_opacity(opacity)
|
||||
return self
|
||||
|
||||
def pointwise_become_partial(
|
||||
self,
|
||||
tsmobject: "TexturedSurface",
|
||||
a: float,
|
||||
b: float,
|
||||
axis: int = 1
|
||||
) -> Self:
|
||||
super().pointwise_become_partial(tsmobject, a, b, axis)
|
||||
im_coords = self.data["im_coords"]
|
||||
im_coords[:] = tsmobject.data["im_coords"]
|
||||
@@ -279,8 +339,3 @@ class TexturedSurface(Surface):
|
||||
im_coords, a, b, (nu, nv, 2), axis
|
||||
)
|
||||
return self
|
||||
|
||||
def fill_in_shader_color_info(self, shader_data):
|
||||
self.read_data_to_shader(shader_data, "opacity", "opacity")
|
||||
self.read_data_to_shader(shader_data, "im_coords", "im_coords")
|
||||
return shader_data
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,14 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import listify
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import Self
|
||||
|
||||
|
||||
class ValueTracker(Mobject):
|
||||
"""
|
||||
@@ -11,33 +17,34 @@ class ValueTracker(Mobject):
|
||||
uses for its update function, and by treating it as a mobject it can
|
||||
still be animated and manipulated just like anything else.
|
||||
"""
|
||||
CONFIG = {
|
||||
"value_type": np.float64,
|
||||
}
|
||||
value_type: type = np.float64
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
value: float | complex | np.ndarray = 0,
|
||||
**kwargs
|
||||
):
|
||||
self.value = value
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
self.data["value"] = np.array(
|
||||
def init_uniforms(self) -> None:
|
||||
super().init_uniforms()
|
||||
self.uniforms["value"] = np.array(
|
||||
listify(self.value),
|
||||
ndmin=2,
|
||||
dtype=self.value_type,
|
||||
)
|
||||
|
||||
def get_value(self):
|
||||
result = self.data["value"][0, :]
|
||||
def get_value(self) -> float | complex | np.ndarray:
|
||||
result = self.uniforms["value"]
|
||||
if len(result) == 1:
|
||||
return result[0]
|
||||
return result
|
||||
|
||||
def set_value(self, value):
|
||||
self.data["value"][0, :] = value
|
||||
def set_value(self, value: float | complex | np.ndarray) -> Self:
|
||||
self.uniforms["value"][:] = 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)
|
||||
|
||||
|
||||
@@ -48,14 +55,12 @@ 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))
|
||||
|
||||
|
||||
class ComplexValueTracker(ValueTracker):
|
||||
CONFIG = {
|
||||
"value_type": np.complex128
|
||||
}
|
||||
value_type: type = np.complex128
|
||||
|
||||
@@ -1,25 +1,41 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
import random
|
||||
|
||||
from manimlib.constants import *
|
||||
import numpy as np
|
||||
from scipy.integrate import solve_ivp
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
||||
from manimlib.constants import WHITE
|
||||
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.bezier import inverse_interpolate
|
||||
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.color import get_color_map
|
||||
from manimlib.utils.iterables import cartesian_product
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.simple_functions import sigmoid
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
def get_vectorized_rgb_gradient_function(min_value, max_value, color_map):
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, Sequence, TypeVar, Tuple, Optional
|
||||
from manimlib.typing import ManimColor, Vect3, VectN, VectArray, Vect3Array, Vect4Array
|
||||
|
||||
from manimlib.mobject.coordinate_systems import CoordinateSystem
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
#### Delete these two ###
|
||||
def get_vectorized_rgb_gradient_function(
|
||||
min_value: T,
|
||||
max_value: T,
|
||||
color_map: str
|
||||
) -> Callable[[VectN], Vect3Array]:
|
||||
rgbs = np.array(get_colormap_list(color_map))
|
||||
|
||||
def func(values):
|
||||
@@ -34,15 +50,34 @@ def get_vectorized_rgb_gradient_function(min_value, max_value, color_map):
|
||||
inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3))
|
||||
result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas)
|
||||
return result
|
||||
|
||||
return func
|
||||
|
||||
|
||||
def get_rgb_gradient_function(min_value, max_value, color_map):
|
||||
def get_rgb_gradient_function(
|
||||
min_value: T,
|
||||
max_value: T,
|
||||
color_map: str
|
||||
) -> Callable[[float], Vect3]:
|
||||
vectorized_func = get_vectorized_rgb_gradient_function(min_value, max_value, color_map)
|
||||
return lambda value: vectorized_func([value])[0]
|
||||
return lambda value: vectorized_func(np.array([value]))[0]
|
||||
####
|
||||
|
||||
|
||||
def move_along_vector_field(mobject, func):
|
||||
def ode_solution_points(function, state0, time, dt=0.01):
|
||||
solution = solve_ivp(
|
||||
lambda t, state: function(state),
|
||||
t_span=(0, time),
|
||||
y0=state0,
|
||||
t_eval=np.arange(0, time, dt)
|
||||
)
|
||||
return solution.y.T
|
||||
|
||||
|
||||
def move_along_vector_field(
|
||||
mobject: Mobject,
|
||||
func: Callable[[Vect3], Vect3]
|
||||
) -> Mobject:
|
||||
mobject.add_updater(
|
||||
lambda m, dt: m.shift(
|
||||
func(m.get_center()) * dt
|
||||
@@ -51,7 +86,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[[Vect3], Vect3]
|
||||
) -> Mobject:
|
||||
def apply_nudge(mob, dt):
|
||||
for submob in mob:
|
||||
x, y = submob.get_center()[:2]
|
||||
@@ -62,155 +100,321 @@ def move_submobjects_along_vector_field(mobject, func):
|
||||
return mobject
|
||||
|
||||
|
||||
def move_points_along_vector_field(mobject, func, coordinate_system):
|
||||
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):
|
||||
mobject.apply_function(
|
||||
def apply_nudge(mob, dt):
|
||||
mob.apply_function(
|
||||
lambda p: p + (cs.c2p(*func(*cs.p2c(p))) - origin) * dt
|
||||
)
|
||||
mobject.add_updater(apply_nudge)
|
||||
return mobject
|
||||
|
||||
|
||||
def get_sample_points_from_coordinate_system(coordinate_system, step_multiple):
|
||||
def get_sample_coords(
|
||||
coordinate_system: CoordinateSystem,
|
||||
density: float = 1.0
|
||||
) -> it.product[tuple[Vect3, ...]]:
|
||||
ranges = []
|
||||
for range_args in coordinate_system.get_all_ranges():
|
||||
_min, _max, step = range_args
|
||||
step *= step_multiple
|
||||
step /= density
|
||||
ranges.append(np.arange(_min, _max + step, step))
|
||||
return it.product(*ranges)
|
||||
return np.array(list(it.product(*ranges)))
|
||||
|
||||
|
||||
def vectorize(pointwise_function: Callable[[Tuple], Tuple]):
|
||||
def v_func(coords_array: VectArray) -> VectArray:
|
||||
return np.array([pointwise_function(*coords) for coords in coords_array])
|
||||
|
||||
return v_func
|
||||
|
||||
|
||||
# Mobjects
|
||||
|
||||
class VectorField(VGroup):
|
||||
CONFIG = {
|
||||
"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, coordinate_system, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
class VectorField(VMobject):
|
||||
def __init__(
|
||||
self,
|
||||
# Vectorized function: Takes in an array of coordinates, returns an array of outputs.
|
||||
func: Callable[[VectArray], VectArray],
|
||||
# Typically a set of Axes or NumberPlane
|
||||
coordinate_system: CoordinateSystem,
|
||||
density: float = 2.0,
|
||||
magnitude_range: Optional[Tuple[float, float]] = None,
|
||||
color: Optional[ManimColor] = None,
|
||||
color_map_name: Optional[str] = "3b1b_colormap",
|
||||
color_map: Optional[Callable[[Sequence[float]], Vect4Array]] = None,
|
||||
stroke_opacity: float = 1.0,
|
||||
stroke_width: float = 3,
|
||||
tip_width_ratio: float = 4,
|
||||
tip_len_to_width: float = 0.01,
|
||||
max_vect_len: float | None = None,
|
||||
max_vect_len_to_step_size: float = 0.8,
|
||||
flat_stroke: bool = False,
|
||||
norm_to_opacity_func=None, # TODO, check on this
|
||||
**kwargs
|
||||
):
|
||||
self.func = func
|
||||
self.coordinate_system = coordinate_system
|
||||
self.value_to_rgb = get_rgb_gradient_function(
|
||||
*self.magnitude_range, self.color_map,
|
||||
)
|
||||
self.stroke_width = stroke_width
|
||||
self.tip_width_ratio = tip_width_ratio
|
||||
self.tip_len_to_width = tip_len_to_width
|
||||
self.norm_to_opacity_func = norm_to_opacity_func
|
||||
|
||||
samples = get_sample_points_from_coordinate_system(
|
||||
coordinate_system, self.step_multiple
|
||||
# Search for sample_points
|
||||
self.sample_coords = get_sample_coords(coordinate_system, density)
|
||||
self.update_sample_points()
|
||||
|
||||
if max_vect_len is None:
|
||||
step_size = get_norm(self.sample_points[1] - self.sample_points[0])
|
||||
self.max_displayed_vect_len = max_vect_len_to_step_size * step_size
|
||||
else:
|
||||
self.max_displayed_vect_len = max_vect_len * coordinate_system.get_x_unit_size()
|
||||
|
||||
# Prepare the color map
|
||||
if magnitude_range is None:
|
||||
max_value = max(map(get_norm, func(self.sample_coords)))
|
||||
magnitude_range = (0, max_value)
|
||||
|
||||
self.magnitude_range = magnitude_range
|
||||
|
||||
if color is not None:
|
||||
self.color_map = None
|
||||
else:
|
||||
self.color_map = color_map or get_color_map(color_map_name)
|
||||
|
||||
self.init_base_stroke_width_array(len(self.sample_coords))
|
||||
|
||||
super().__init__(
|
||||
stroke_opacity=stroke_opacity,
|
||||
flat_stroke=flat_stroke,
|
||||
**kwargs
|
||||
)
|
||||
self.add(*(
|
||||
self.get_vector(coords)
|
||||
for coords in samples
|
||||
self.set_stroke(color, stroke_width)
|
||||
self.update_vectors()
|
||||
|
||||
def init_points(self):
|
||||
n_samples = len(self.sample_coords)
|
||||
self.set_points(np.zeros((8 * n_samples - 1, 3)))
|
||||
self.set_joint_type('no_joint')
|
||||
|
||||
def get_sample_points(
|
||||
self,
|
||||
center: np.ndarray,
|
||||
width: float,
|
||||
height: float,
|
||||
depth: float,
|
||||
x_density: float,
|
||||
y_density: float,
|
||||
z_density: float
|
||||
) -> np.ndarray:
|
||||
to_corner = np.array([width / 2, height / 2, depth / 2])
|
||||
spacings = 1.0 / np.array([x_density, y_density, z_density])
|
||||
to_corner = spacings * (to_corner / spacings).astype(int)
|
||||
lower_corner = center - to_corner
|
||||
upper_corner = center + to_corner + spacings
|
||||
return cartesian_product(*(
|
||||
np.arange(low, high, space)
|
||||
for low, high, space in zip(lower_corner, upper_corner, spacings)
|
||||
))
|
||||
|
||||
def get_vector(self, coords, **kwargs):
|
||||
vector_config = merge_dicts_recursively(
|
||||
self.vector_config,
|
||||
kwargs
|
||||
)
|
||||
def init_base_stroke_width_array(self, n_sample_points):
|
||||
arr = np.ones(8 * n_sample_points - 1)
|
||||
arr[4::8] = self.tip_width_ratio
|
||||
arr[5::8] = self.tip_width_ratio * 0.5
|
||||
arr[6::8] = 0
|
||||
arr[7::8] = 0
|
||||
self.base_stroke_width_array = arr
|
||||
|
||||
output = np.array(self.func(*coords))
|
||||
norm = get_norm(output)
|
||||
if norm > 0:
|
||||
output *= self.length_func(norm) / norm
|
||||
def set_sample_coords(self, sample_coords: VectArray):
|
||||
self.sample_coords = sample_coords
|
||||
return self
|
||||
|
||||
origin = self.coordinate_system.get_origin()
|
||||
_input = self.coordinate_system.c2p(*coords)
|
||||
_output = self.coordinate_system.c2p(*output)
|
||||
def set_stroke(self, color=None, width=None, opacity=None, behind=None, flat=None, recurse=True):
|
||||
super().set_stroke(color, None, opacity, behind, flat, recurse)
|
||||
if width is not None:
|
||||
self.set_stroke_width(float(width))
|
||||
return self
|
||||
|
||||
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
|
||||
def set_stroke_width(self, width: float):
|
||||
if self.get_num_points() > 0:
|
||||
self.get_stroke_widths()[:] = width * self.base_stroke_width_array
|
||||
self.stroke_width = width
|
||||
return self
|
||||
|
||||
def update_sample_points(self):
|
||||
self.sample_points = self.coordinate_system.c2p(*self.sample_coords.T)
|
||||
|
||||
def update_vectors(self):
|
||||
tip_width = self.tip_width_ratio * self.stroke_width
|
||||
tip_len = self.tip_len_to_width * tip_width
|
||||
|
||||
# Outputs in the coordinate system
|
||||
outputs = self.func(self.sample_coords)
|
||||
output_norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis]
|
||||
|
||||
# Corresponding vector values in global coordinates
|
||||
out_vects = self.coordinate_system.c2p(*outputs.T) - self.coordinate_system.get_origin()
|
||||
out_vect_norms = np.linalg.norm(out_vects, axis=1)[:, np.newaxis]
|
||||
unit_outputs = np.zeros_like(out_vects)
|
||||
np.true_divide(out_vects, out_vect_norms, out=unit_outputs, where=(out_vect_norms > 0))
|
||||
|
||||
# How long should the arrows be drawn, in global coordinates
|
||||
max_len = self.max_displayed_vect_len
|
||||
if max_len < np.inf:
|
||||
drawn_norms = max_len * np.tanh(out_vect_norms / max_len)
|
||||
else:
|
||||
drawn_norms = out_vect_norms
|
||||
|
||||
# What's the distance from the base of an arrow to
|
||||
# the base of its head?
|
||||
dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf) # Mixing units!
|
||||
|
||||
# Set all points
|
||||
points = self.get_points()
|
||||
points[0::8] = self.sample_points
|
||||
points[2::8] = self.sample_points + dist_to_head_base * unit_outputs
|
||||
points[4::8] = points[2::8]
|
||||
points[6::8] = self.sample_points + drawn_norms * unit_outputs
|
||||
for i in (1, 3, 5):
|
||||
points[i::8] = 0.5 * (points[i - 1::8] + points[i + 1::8])
|
||||
points[7::8] = points[6:-1:8]
|
||||
|
||||
# Adjust stroke widths
|
||||
width_arr = self.stroke_width * self.base_stroke_width_array
|
||||
width_scalars = np.clip(drawn_norms / tip_len, 0, 1)
|
||||
width_scalars = np.repeat(width_scalars, 8)[:-1]
|
||||
self.get_stroke_widths()[:] = width_scalars * width_arr
|
||||
|
||||
# Potentially adjust opacity and color
|
||||
if self.color_map is not None:
|
||||
self.get_stroke_colors() # Ensures the array is updated to appropriate length
|
||||
low, high = self.magnitude_range
|
||||
self.data['stroke_rgba'][:, :3] = self.color_map(
|
||||
inverse_interpolate(low, high, np.repeat(output_norms, 8)[:-1])
|
||||
)[:, :3]
|
||||
|
||||
if self.norm_to_opacity_func is not None:
|
||||
self.get_stroke_opacities()[:] = self.norm_to_opacity_func(
|
||||
np.repeat(output_norms, 8)[:-1]
|
||||
)
|
||||
|
||||
self.note_changed_data()
|
||||
return self
|
||||
|
||||
|
||||
class TimeVaryingVectorField(VectorField):
|
||||
def __init__(
|
||||
self,
|
||||
# Takes in an array of points and a float for time
|
||||
time_func: Callable[[VectArray, float], VectArray],
|
||||
coordinate_system: CoordinateSystem,
|
||||
**kwargs
|
||||
):
|
||||
self.time = 0
|
||||
|
||||
def func(coords):
|
||||
return time_func(coords, self.time)
|
||||
|
||||
super().__init__(func, coordinate_system, **kwargs)
|
||||
self.add_updater(lambda m, dt: m.increment_time(dt))
|
||||
self.always.update_vectors()
|
||||
|
||||
def increment_time(self, dt):
|
||||
self.time += dt
|
||||
|
||||
|
||||
class StreamLines(VGroup):
|
||||
CONFIG = {
|
||||
"step_multiple": 0.5,
|
||||
"n_repeats": 1,
|
||||
"noise_factor": None,
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[VectArray], VectArray],
|
||||
coordinate_system: CoordinateSystem,
|
||||
density: float = 1.0,
|
||||
n_repeats: int = 1,
|
||||
noise_factor: float | None = None,
|
||||
# Config for drawing lines
|
||||
"dt": 0.05,
|
||||
"arc_len": 3,
|
||||
"max_time_steps": 200,
|
||||
"n_samples_per_line": 10,
|
||||
"cutoff_norm": 15,
|
||||
solution_time: float = 3,
|
||||
dt: float = 0.05,
|
||||
arc_len: float = 3,
|
||||
max_time_steps: int = 200,
|
||||
n_samples_per_line: int = 10,
|
||||
cutoff_norm: float = 15,
|
||||
# Style info
|
||||
"stroke_width": 1,
|
||||
"stroke_color": WHITE,
|
||||
"stroke_opacity": 1,
|
||||
"color_by_magnitude": True,
|
||||
"magnitude_range": (0, 2.0),
|
||||
"taper_stroke_width": False,
|
||||
"color_map": "3b1b_colormap",
|
||||
}
|
||||
|
||||
def __init__(self, func, coordinate_system, **kwargs):
|
||||
stroke_width: float = 1.0,
|
||||
stroke_color: ManimColor = WHITE,
|
||||
stroke_opacity: float = 1,
|
||||
color_by_magnitude: bool = True,
|
||||
magnitude_range: Tuple[float, float] = (0, 2.0),
|
||||
taper_stroke_width: bool = False,
|
||||
color_map: str = "3b1b_colormap",
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.func = func
|
||||
self.coordinate_system = coordinate_system
|
||||
self.density = density
|
||||
self.n_repeats = n_repeats
|
||||
self.noise_factor = noise_factor
|
||||
self.solution_time = solution_time
|
||||
self.dt = dt
|
||||
self.arc_len = arc_len
|
||||
self.max_time_steps = max_time_steps
|
||||
self.n_samples_per_line = n_samples_per_line
|
||||
self.cutoff_norm = cutoff_norm
|
||||
self.stroke_width = stroke_width
|
||||
self.stroke_color = stroke_color
|
||||
self.stroke_opacity = stroke_opacity
|
||||
self.color_by_magnitude = color_by_magnitude
|
||||
self.magnitude_range = magnitude_range
|
||||
self.taper_stroke_width = taper_stroke_width
|
||||
self.color_map = color_map
|
||||
|
||||
self.draw_lines()
|
||||
self.init_style()
|
||||
|
||||
def point_func(self, point):
|
||||
in_coords = self.coordinate_system.p2c(point)
|
||||
out_coords = self.func(*in_coords)
|
||||
return self.coordinate_system.c2p(*out_coords)
|
||||
def point_func(self, points: Vect3Array) -> Vect3:
|
||||
in_coords = np.array(self.coordinate_system.p2c(points)).T
|
||||
out_coords = self.func(in_coords)
|
||||
origin = self.coordinate_system.get_origin()
|
||||
return self.coordinate_system.c2p(*out_coords.T) - origin
|
||||
|
||||
def draw_lines(self):
|
||||
def draw_lines(self) -> None:
|
||||
lines = []
|
||||
origin = self.coordinate_system.get_origin()
|
||||
for point in self.get_start_points():
|
||||
points = [point]
|
||||
total_arc_len = 0
|
||||
time = 0
|
||||
for x in range(self.max_time_steps):
|
||||
time += self.dt
|
||||
last_point = points[-1]
|
||||
new_point = last_point + self.dt * (self.point_func(last_point) - origin)
|
||||
points.append(new_point)
|
||||
total_arc_len += get_norm(new_point - last_point)
|
||||
if get_norm(last_point) > self.cutoff_norm:
|
||||
break
|
||||
if total_arc_len > self.arc_len:
|
||||
break
|
||||
|
||||
# Todo, it feels like coordinate system should just have
|
||||
# the ODE solver built into it, no?
|
||||
lines = []
|
||||
for coords in self.get_sample_coords():
|
||||
solution_coords = ode_solution_points(self.func, coords, self.solution_time, self.dt)
|
||||
line = VMobject()
|
||||
line.virtual_time = time
|
||||
step = max(1, int(len(points) / self.n_samples_per_line))
|
||||
line.set_points_as_corners(points[::step])
|
||||
line.make_approximately_smooth()
|
||||
line.set_points_smoothly(self.coordinate_system.c2p(*solution_coords.T))
|
||||
# TODO, account for arc length somehow?
|
||||
line.virtual_time = self.solution_time
|
||||
lines.append(line)
|
||||
self.set_submobjects(lines)
|
||||
|
||||
def get_start_points(self):
|
||||
def get_sample_coords(self):
|
||||
cs = self.coordinate_system
|
||||
sample_coords = get_sample_points_from_coordinate_system(
|
||||
cs, self.step_multiple,
|
||||
)
|
||||
sample_coords = get_sample_coords(cs, self.density)
|
||||
|
||||
noise_factor = self.noise_factor
|
||||
if noise_factor is None:
|
||||
noise_factor = cs.x_range[2] * self.step_multiple * 0.5
|
||||
noise_factor = (cs.get_x_unit_size() / self.density) * 0.5
|
||||
|
||||
return np.array([
|
||||
cs.c2p(*coords) + noise_factor * np.random.random(3)
|
||||
coords + noise_factor * np.random.random(coords.shape)
|
||||
for n in range(self.n_repeats)
|
||||
for coords in sample_coords
|
||||
])
|
||||
|
||||
def init_style(self):
|
||||
def init_style(self) -> None:
|
||||
if self.color_by_magnitude:
|
||||
values_to_rgbs = get_vectorized_rgb_gradient_function(
|
||||
*self.magnitude_range, self.color_map,
|
||||
@@ -237,59 +441,35 @@ class StreamLines(VGroup):
|
||||
|
||||
|
||||
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, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
stream_lines: StreamLines,
|
||||
lag_range: float = 4,
|
||||
rate_multiple: float = 1.0,
|
||||
line_anim_config: dict = dict(
|
||||
rate_func=linear,
|
||||
time_width=1.0,
|
||||
),
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.stream_lines = stream_lines
|
||||
|
||||
for line in stream_lines:
|
||||
line.anim = self.line_anim_class(
|
||||
line.anim = VShowPassingFlash(
|
||||
line,
|
||||
run_time=line.virtual_time,
|
||||
**self.line_anim_config,
|
||||
run_time=line.virtual_time / rate_multiple,
|
||||
**line_anim_config,
|
||||
)
|
||||
line.anim.begin()
|
||||
line.time = -self.lag_range * random.random()
|
||||
line.time = -lag_range * np.random.random()
|
||||
self.add(line.anim.mobject)
|
||||
|
||||
self.add_updater(lambda m, dt: m.update(dt))
|
||||
|
||||
def update(self, 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,
|
||||
"time_width": 0.1,
|
||||
"remover": True
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
max_stroke_width = vmobject.get_stroke_width()
|
||||
max_time_width = kwargs.pop("time_width", self.time_width)
|
||||
AnimationGroup.__init__(self, *[
|
||||
VShowPassingFlash(
|
||||
vmobject.deepcopy().set_stroke(width=stroke_width),
|
||||
time_width=time_width,
|
||||
**kwargs
|
||||
)
|
||||
for stroke_width, time_width in zip(
|
||||
np.linspace(0, max_stroke_width, self.n_segments),
|
||||
np.linspace(max_time_width, 0, self.n_segments)
|
||||
)
|
||||
])
|
||||
|
||||
176
manimlib/module_loader.py
Normal file
176
manimlib/module_loader.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.logger import log
|
||||
|
||||
Module = importlib.util.types.ModuleType
|
||||
|
||||
|
||||
class ModuleLoader:
|
||||
"""
|
||||
Utility class to load a module from a file and handle its imports.
|
||||
|
||||
Most parts of this class are only needed for the reload functionality,
|
||||
while the `get_module` method is the main entry point to import a module.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_module(file_name: str | None, is_during_reload=False) -> Module | None:
|
||||
"""
|
||||
Imports a module from a file and returns it.
|
||||
|
||||
During reload (when the user calls `reload()` in the IPython shell), we
|
||||
also track the imported modules and reload them as well (they would be
|
||||
cached otherwise). See the reload_manager where the reload parameter is set.
|
||||
|
||||
Note that `exec_module()` is called twice when reloading a module:
|
||||
1. In exec_module_and_track_imports to track the imports
|
||||
2. Here to actually execute the module again with the respective
|
||||
imported modules reloaded.
|
||||
"""
|
||||
if file_name is None:
|
||||
return None
|
||||
|
||||
module_name = file_name.replace(os.sep, ".").replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
|
||||
if is_during_reload:
|
||||
imported_modules = ModuleLoader._exec_module_and_track_imports(spec, module)
|
||||
reloaded_modules_tracker = set()
|
||||
ModuleLoader._reload_modules(imported_modules, reloaded_modules_tracker)
|
||||
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
@staticmethod
|
||||
def _exec_module_and_track_imports(spec, module: Module) -> set[str]:
|
||||
"""
|
||||
Executes the given module (imports it) and returns all the modules that
|
||||
are imported during its execution.
|
||||
|
||||
This is achieved by replacing the __import__ function with a custom one
|
||||
that tracks the imported modules. At the end, the original __import__
|
||||
built-in function is restored.
|
||||
"""
|
||||
imported_modules: set[str] = set()
|
||||
original_import = builtins.__import__
|
||||
|
||||
def tracked_import(name, globals=None, locals=None, fromlist=(), level=0):
|
||||
"""
|
||||
Custom __import__ function that does exactly the same as the original
|
||||
one, but also tracks the imported modules by means of adding their
|
||||
names to a set.
|
||||
"""
|
||||
result = original_import(name, globals, locals, fromlist, level)
|
||||
imported_modules.add(name)
|
||||
return result
|
||||
|
||||
builtins.__import__ = tracked_import
|
||||
|
||||
try:
|
||||
module_name = module.__name__
|
||||
log.debug('Reloading module "%s"', module_name)
|
||||
|
||||
spec.loader.exec_module(module)
|
||||
finally:
|
||||
builtins.__import__ = original_import
|
||||
|
||||
return imported_modules
|
||||
|
||||
@staticmethod
|
||||
def _reload_modules(modules: set[str], reloaded_modules_tracker: set[str]):
|
||||
"""
|
||||
Out of the given modules, reloads the ones that were not already imported.
|
||||
|
||||
We skip modules that are not user-defined (see `is_user_defined_module()`).
|
||||
"""
|
||||
for mod in modules:
|
||||
if mod in reloaded_modules_tracker:
|
||||
continue
|
||||
|
||||
if not ModuleLoader._is_user_defined_module(mod):
|
||||
continue
|
||||
|
||||
module = sys.modules[mod]
|
||||
ModuleLoader._deep_reload(module, reloaded_modules_tracker)
|
||||
|
||||
reloaded_modules_tracker.add(mod)
|
||||
|
||||
@staticmethod
|
||||
def _is_user_defined_module(mod: str) -> bool:
|
||||
"""
|
||||
Returns whether the given module is user-defined or not.
|
||||
|
||||
A module is considered user-defined if
|
||||
- it is not part of the standard library
|
||||
- AND it is not an external library (site-packages or dist-packages)
|
||||
"""
|
||||
if mod not in sys.modules:
|
||||
return False
|
||||
|
||||
if mod in sys.builtin_module_names:
|
||||
return False
|
||||
|
||||
module = sys.modules[mod]
|
||||
module_path = getattr(module, "__file__", None)
|
||||
if module_path is None:
|
||||
return False
|
||||
module_path = os.path.abspath(module_path)
|
||||
|
||||
# External libraries (site-packages or dist-packages), e.g. numpy
|
||||
if "site-packages" in module_path or "dist-packages" in module_path:
|
||||
return False
|
||||
|
||||
# Standard lib
|
||||
standard_lib_path = sysconfig.get_path("stdlib")
|
||||
if module_path.startswith(standard_lib_path):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _deep_reload(module: Module, reloaded_modules_tracker: set[str]):
|
||||
"""
|
||||
Recursively reloads modules imported by the given module.
|
||||
|
||||
Only user-defined modules are reloaded, see `is_user_defined_module()`.
|
||||
"""
|
||||
ignore_manimlib_modules = manim_config.ignore_manimlib_modules_on_reload
|
||||
if ignore_manimlib_modules and module.__name__.startswith("manimlib"):
|
||||
return
|
||||
if module.__name__.startswith("manimlib.config"):
|
||||
# We don't want to reload global manim_config
|
||||
return
|
||||
|
||||
if not hasattr(module, "__dict__"):
|
||||
return
|
||||
|
||||
# Prevent reloading the same module multiple times
|
||||
if module.__name__ in reloaded_modules_tracker:
|
||||
return
|
||||
reloaded_modules_tracker.add(module.__name__)
|
||||
|
||||
# Recurse for all imported modules
|
||||
for _attr_name, attr_value in module.__dict__.items():
|
||||
if isinstance(attr_value, Module):
|
||||
if ModuleLoader._is_user_defined_module(attr_value.__name__):
|
||||
ModuleLoader._deep_reload(attr_value, reloaded_modules_tracker)
|
||||
|
||||
# Also reload modules that are part of a class or function
|
||||
# e.g. when importing `from custom_module import CustomClass`
|
||||
elif hasattr(attr_value, "__module__"):
|
||||
attr_module_name = attr_value.__module__
|
||||
if ModuleLoader._is_user_defined_module(attr_module_name):
|
||||
attr_module = sys.modules[attr_module_name]
|
||||
ModuleLoader._deep_reload(attr_module, reloaded_modules_tracker)
|
||||
|
||||
# Reload
|
||||
log.debug('Reloading module "%s"', module.__name__)
|
||||
importlib.reload(module)
|
||||
@@ -1 +0,0 @@
|
||||
This folder contains a collection of various things that were built for a video at some point, but were really one-off and should be given more careful consideration before being brought into the main library. In particular, there is really no guarantee of these being fully functional.
|
||||
@@ -1,101 +0,0 @@
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class RearrangeEquation(Scene):
|
||||
def construct(
|
||||
self,
|
||||
start_terms,
|
||||
end_terms,
|
||||
index_map,
|
||||
path_arc=np.pi,
|
||||
start_transform=None,
|
||||
end_transform=None,
|
||||
leave_start_terms=False,
|
||||
transform_kwargs={},
|
||||
):
|
||||
transform_kwargs["path_func"] = path
|
||||
start_mobs, end_mobs = self.get_mobs_from_terms(
|
||||
start_terms, end_terms
|
||||
)
|
||||
if start_transform:
|
||||
start_mobs = start_transform(Mobject(*start_mobs)).split()
|
||||
if end_transform:
|
||||
end_mobs = end_transform(Mobject(*end_mobs)).split()
|
||||
unmatched_start_indices = set(range(len(start_mobs)))
|
||||
unmatched_end_indices = set(range(len(end_mobs)))
|
||||
unmatched_start_indices.difference_update(
|
||||
[n % len(start_mobs) for n in index_map]
|
||||
)
|
||||
unmatched_end_indices.difference_update(
|
||||
[n % len(end_mobs) for n in list(index_map.values())]
|
||||
)
|
||||
mobject_pairs = [
|
||||
(start_mobs[a], end_mobs[b])
|
||||
for a, b in index_map.items()
|
||||
] + [
|
||||
(Point(end_mobs[b].get_center()), end_mobs[b])
|
||||
for b in unmatched_end_indices
|
||||
]
|
||||
if not leave_start_terms:
|
||||
mobject_pairs += [
|
||||
(start_mobs[a], Point(start_mobs[a].get_center()))
|
||||
for a in unmatched_start_indices
|
||||
]
|
||||
|
||||
self.add(*start_mobs)
|
||||
if leave_start_terms:
|
||||
self.add(Mobject(*start_mobs))
|
||||
self.wait()
|
||||
self.play(*[
|
||||
Transform(*pair, **transform_kwargs)
|
||||
for pair in mobject_pairs
|
||||
])
|
||||
self.wait()
|
||||
|
||||
def get_mobs_from_terms(self, start_terms, end_terms):
|
||||
"""
|
||||
Need to ensure that all image mobjects for a tex expression
|
||||
stemming from the same string are point-for-point copies of one
|
||||
and other. This makes transitions much smoother, and not look
|
||||
like point-clouds.
|
||||
"""
|
||||
num_start_terms = len(start_terms)
|
||||
all_mobs = np.array(
|
||||
Tex(start_terms).split() + Tex(end_terms).split())
|
||||
all_terms = np.array(start_terms + end_terms)
|
||||
for term in set(all_terms):
|
||||
matches = all_terms == term
|
||||
if sum(matches) > 1:
|
||||
base_mob = all_mobs[list(all_terms).index(term)]
|
||||
all_mobs[matches] = [
|
||||
base_mob.copy().replace(target_mob)
|
||||
for target_mob in all_mobs[matches]
|
||||
]
|
||||
return all_mobs[:num_start_terms], all_mobs[num_start_terms:]
|
||||
|
||||
|
||||
class FlipThroughSymbols(Animation):
|
||||
CONFIG = {
|
||||
"start_center": ORIGIN,
|
||||
"end_center": ORIGIN,
|
||||
}
|
||||
|
||||
def __init__(self, tex_list, **kwargs):
|
||||
mobject = Tex(self.curr_tex).shift(start_center)
|
||||
Animation.__init__(self, mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
new_tex = self.tex_list[np.ceil(alpha * len(self.tex_list)) - 1]
|
||||
|
||||
if new_tex != self.curr_tex:
|
||||
self.curr_tex = new_tex
|
||||
self.mobject = Tex(new_tex).shift(self.start_center)
|
||||
if not all(self.start_center == self.end_center):
|
||||
self.mobject.center().shift(
|
||||
(1 - alpha) * self.start_center + alpha * self.end_center
|
||||
)
|
||||
@@ -1,188 +0,0 @@
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.numbers import Integer
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject, VGroup
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.utils.simple_functions import choose
|
||||
|
||||
|
||||
DEFAULT_COUNT_NUM_OFFSET = (FRAME_X_RADIUS - 1, FRAME_Y_RADIUS - 1, 0)
|
||||
DEFAULT_COUNT_RUN_TIME = 5.0
|
||||
|
||||
|
||||
class CountingScene(Scene):
|
||||
def count(self, items, item_type="mobject", *args, **kwargs):
|
||||
if item_type == "mobject":
|
||||
self.count_mobjects(items, *args, **kwargs)
|
||||
elif item_type == "region":
|
||||
self.count_regions(items, *args, **kwargs)
|
||||
else:
|
||||
raise Exception("Unknown item_type, should be mobject or region")
|
||||
return self
|
||||
|
||||
def count_mobjects(
|
||||
self, mobjects, mode="highlight",
|
||||
color="red",
|
||||
display_numbers=True,
|
||||
num_offset=DEFAULT_COUNT_NUM_OFFSET,
|
||||
run_time=DEFAULT_COUNT_RUN_TIME,
|
||||
):
|
||||
"""
|
||||
Note, leaves final number mobject as "number" attribute
|
||||
|
||||
mode can be "highlight", "show_creation" or "show", otherwise
|
||||
a warning is given and nothing is animating during the count
|
||||
"""
|
||||
if len(mobjects) > 50: # TODO
|
||||
raise Exception("I don't know if you should be counting \
|
||||
too many mobjects...")
|
||||
if len(mobjects) == 0:
|
||||
raise Exception("Counting mobject list of length 0")
|
||||
if mode not in ["highlight", "show_creation", "show"]:
|
||||
raise Warning("Unknown mode")
|
||||
frame_time = run_time / len(mobjects)
|
||||
if mode == "highlight":
|
||||
self.add(*mobjects)
|
||||
for mob, num in zip(mobjects, it.count(1)):
|
||||
if display_numbers:
|
||||
num_mob = Tex(str(num))
|
||||
num_mob.center().shift(num_offset)
|
||||
self.add(num_mob)
|
||||
if mode == "highlight":
|
||||
original_color = mob.color
|
||||
mob.set_color(color)
|
||||
self.wait(frame_time)
|
||||
mob.set_color(original_color)
|
||||
if mode == "show_creation":
|
||||
self.play(ShowCreation(mob, run_time=frame_time))
|
||||
if mode == "show":
|
||||
self.add(mob)
|
||||
self.wait(frame_time)
|
||||
if display_numbers:
|
||||
self.remove(num_mob)
|
||||
if display_numbers:
|
||||
self.add(num_mob)
|
||||
self.number = num_mob
|
||||
return self
|
||||
|
||||
def count_regions(self, regions,
|
||||
mode="one_at_a_time",
|
||||
num_offset=DEFAULT_COUNT_NUM_OFFSET,
|
||||
run_time=DEFAULT_COUNT_RUN_TIME,
|
||||
**unused_kwargsn):
|
||||
if mode not in ["one_at_a_time", "show_all"]:
|
||||
raise Warning("Unknown mode")
|
||||
frame_time = run_time / (len(regions))
|
||||
for region, count in zip(regions, it.count(1)):
|
||||
num_mob = Tex(str(count))
|
||||
num_mob.center().shift(num_offset)
|
||||
self.add(num_mob)
|
||||
self.set_color_region(region)
|
||||
self.wait(frame_time)
|
||||
if mode == "one_at_a_time":
|
||||
self.reset_background()
|
||||
self.remove(num_mob)
|
||||
self.add(num_mob)
|
||||
self.number = num_mob
|
||||
return self
|
||||
|
||||
|
||||
def combinationMobject(n, k):
|
||||
return Integer(choose(n, k))
|
||||
|
||||
|
||||
class GeneralizedPascalsTriangle(VMobject):
|
||||
CONFIG = {
|
||||
"nrows": 7,
|
||||
"height": FRAME_HEIGHT - 1,
|
||||
"width": 1.5 * FRAME_X_RADIUS,
|
||||
"portion_to_fill": 0.7,
|
||||
"submob_class": combinationMobject,
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
self.cell_height = float(self.height) / self.nrows
|
||||
self.cell_width = float(self.width) / self.nrows
|
||||
self.bottom_left = (self.cell_width * self.nrows / 2.0) * LEFT + \
|
||||
(self.cell_height * self.nrows / 2.0) * DOWN
|
||||
self.coords_to_mobs = {}
|
||||
self.coords = [
|
||||
(n, k)
|
||||
for n in range(self.nrows)
|
||||
for k in range(n + 1)
|
||||
]
|
||||
for n, k in self.coords:
|
||||
center = self.coords_to_center(n, k)
|
||||
num_mob = self.submob_class(n, k) # Tex(str(num))
|
||||
scale_factor = min(
|
||||
1,
|
||||
self.portion_to_fill * self.cell_height / num_mob.get_height(),
|
||||
self.portion_to_fill * self.cell_width / num_mob.get_width(),
|
||||
)
|
||||
num_mob.center().scale(scale_factor).shift(center)
|
||||
if n not in self.coords_to_mobs:
|
||||
self.coords_to_mobs[n] = {}
|
||||
self.coords_to_mobs[n][k] = num_mob
|
||||
self.add(*[
|
||||
self.coords_to_mobs[n][k]
|
||||
for n, k in self.coords
|
||||
])
|
||||
return self
|
||||
|
||||
def coords_to_center(self, n, k):
|
||||
x_offset = self.cell_width * (k + self.nrows / 2.0 - n / 2.0)
|
||||
y_offset = self.cell_height * (self.nrows - n)
|
||||
return self.bottom_left + x_offset * RIGHT + y_offset * UP
|
||||
|
||||
def generate_n_choose_k_mobs(self):
|
||||
self.coords_to_n_choose_k = {}
|
||||
for n, k in self.coords:
|
||||
nck_mob = Tex(r"{%d \choose %d}" % (n, k))
|
||||
scale_factor = min(
|
||||
1,
|
||||
self.portion_to_fill * self.cell_height / nck_mob.get_height(),
|
||||
self.portion_to_fill * self.cell_width / nck_mob.get_width(),
|
||||
)
|
||||
center = self.coords_to_mobs[n][k].get_center()
|
||||
nck_mob.center().scale(scale_factor).shift(center)
|
||||
if n not in self.coords_to_n_choose_k:
|
||||
self.coords_to_n_choose_k[n] = {}
|
||||
self.coords_to_n_choose_k[n][k] = nck_mob
|
||||
return self
|
||||
|
||||
def fill_with_n_choose_k(self):
|
||||
if not hasattr(self, "coords_to_n_choose_k"):
|
||||
self.generate_n_choose_k_mobs()
|
||||
self.set_submobjects([])
|
||||
self.add(*[
|
||||
self.coords_to_n_choose_k[n][k]
|
||||
for n, k in self.coords
|
||||
])
|
||||
return self
|
||||
|
||||
def generate_sea_of_zeros(self):
|
||||
zero = Tex("0")
|
||||
self.sea_of_zeros = []
|
||||
for n in range(self.nrows):
|
||||
for a in range((self.nrows - n) / 2 + 1):
|
||||
for k in (n + a + 1, -a - 1):
|
||||
self.coords.append((n, k))
|
||||
mob = zero.copy()
|
||||
mob.shift(self.coords_to_center(n, k))
|
||||
self.coords_to_mobs[n][k] = mob
|
||||
self.add(mob)
|
||||
return self
|
||||
|
||||
def get_lowest_row(self):
|
||||
n = self.nrows - 1
|
||||
lowest_row = VGroup(*[
|
||||
self.coords_to_mobs[n][k]
|
||||
for k in range(n + 1)
|
||||
])
|
||||
return lowest_row
|
||||
|
||||
|
||||
class PascalsTriangle(GeneralizedPascalsTriangle):
|
||||
CONFIG = {
|
||||
"submob_class": combinationMobject,
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.movement import ComplexHomotopy
|
||||
from manimlib.animation.transform import MoveToTarget
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.coordinate_systems import ComplexPlane
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
# TODO, refactor this full scene
|
||||
class ComplexTransformationScene(Scene):
|
||||
CONFIG = {
|
||||
"plane_config": {},
|
||||
"background_fade_factor": 0.5,
|
||||
"use_multicolored_plane": False,
|
||||
"vert_start_color": BLUE, # TODO
|
||||
"vert_end_color": BLUE,
|
||||
"horiz_start_color": BLUE,
|
||||
"horiz_end_color": BLUE,
|
||||
"num_anchors_to_add_per_line": 50,
|
||||
"post_transformation_stroke_width": None,
|
||||
"default_apply_complex_function_kwargs": {
|
||||
"run_time": 5,
|
||||
},
|
||||
"background_label_scale_val": 0.5,
|
||||
"include_coordinate_labels": True,
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
self.foreground_mobjects = []
|
||||
self.transformable_mobjects = []
|
||||
self.add_background_plane()
|
||||
if self.include_coordinate_labels:
|
||||
self.add_coordinate_labels()
|
||||
|
||||
def add_foreground_mobject(self, mobject):
|
||||
self.add_foreground_mobjects(mobject)
|
||||
|
||||
def add_transformable_mobjects(self, *mobjects):
|
||||
self.transformable_mobjects += list(mobjects)
|
||||
self.add(*mobjects)
|
||||
|
||||
def add_foreground_mobjects(self, *mobjects):
|
||||
self.foreground_mobjects += list(mobjects)
|
||||
Scene.add(self, *mobjects)
|
||||
|
||||
def add(self, *mobjects):
|
||||
Scene.add(self, *list(mobjects) + self.foreground_mobjects)
|
||||
|
||||
def play(self, *animations, **kwargs):
|
||||
Scene.play(
|
||||
self,
|
||||
*list(animations) + list(map(Animation, self.foreground_mobjects)),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def add_background_plane(self):
|
||||
background = ComplexPlane(**self.plane_config)
|
||||
background.fade(self.background_fade_factor)
|
||||
self.add(background)
|
||||
self.background = background
|
||||
|
||||
def add_coordinate_labels(self):
|
||||
self.background.add_coordinates()
|
||||
self.add(self.background)
|
||||
|
||||
def add_transformable_plane(self, **kwargs):
|
||||
self.plane = self.get_transformable_plane()
|
||||
self.add(self.plane)
|
||||
|
||||
def get_transformable_plane(self, x_range=None, y_range=None):
|
||||
"""
|
||||
x_range and y_range would be tuples (min, max)
|
||||
"""
|
||||
plane_config = dict(self.plane_config)
|
||||
shift_val = ORIGIN
|
||||
if x_range is not None:
|
||||
x_min, x_max = x_range
|
||||
plane_config["x_radius"] = x_max - x_min
|
||||
shift_val += (x_max + x_min) * RIGHT / 2.
|
||||
if y_range is not None:
|
||||
y_min, y_max = y_range
|
||||
plane_config["y_radius"] = y_max - y_min
|
||||
shift_val += (y_max + y_min) * UP / 2.
|
||||
plane = ComplexPlane(**plane_config)
|
||||
plane.shift(shift_val)
|
||||
if self.use_multicolored_plane:
|
||||
self.paint_plane(plane)
|
||||
return plane
|
||||
|
||||
def prepare_for_transformation(self, mob):
|
||||
if hasattr(mob, "prepare_for_nonlinear_transform"):
|
||||
mob.prepare_for_nonlinear_transform(
|
||||
self.num_anchors_to_add_per_line
|
||||
)
|
||||
# TODO...
|
||||
|
||||
def paint_plane(self, plane):
|
||||
for lines in planes, plane.secondary_lines:
|
||||
lines.set_color_by_gradient(
|
||||
self.vert_start_color,
|
||||
self.vert_end_color,
|
||||
self.horiz_start_color,
|
||||
self.horiz_end_color,
|
||||
)
|
||||
# plane.axes.set_color_by_gradient(
|
||||
# self.horiz_start_color,
|
||||
# self.vert_start_color
|
||||
# )
|
||||
|
||||
def z_to_point(self, z):
|
||||
return self.background.number_to_point(z)
|
||||
|
||||
def get_transformer(self, **kwargs):
|
||||
transform_kwargs = dict(self.default_apply_complex_function_kwargs)
|
||||
transform_kwargs.update(kwargs)
|
||||
transformer = VGroup()
|
||||
if hasattr(self, "plane"):
|
||||
self.prepare_for_transformation(self.plane)
|
||||
transformer.add(self.plane)
|
||||
transformer.add(*self.transformable_mobjects)
|
||||
return transformer, transform_kwargs
|
||||
|
||||
def apply_complex_function(self, func, added_anims=[], **kwargs):
|
||||
transformer, transform_kwargs = self.get_transformer(**kwargs)
|
||||
transformer.generate_target()
|
||||
# Rescale, apply function, scale back
|
||||
transformer.target.shift(-self.background.get_center_point())
|
||||
transformer.target.scale(1. / self.background.unit_size)
|
||||
transformer.target.apply_complex_function(func)
|
||||
transformer.target.scale(self.background.unit_size)
|
||||
transformer.target.shift(self.background.get_center_point())
|
||||
#
|
||||
|
||||
for mob in transformer.target[0].family_members_with_points():
|
||||
mob.make_smooth()
|
||||
if self.post_transformation_stroke_width is not None:
|
||||
transformer.target.set_stroke(
|
||||
width=self.post_transformation_stroke_width)
|
||||
self.play(
|
||||
MoveToTarget(transformer, **transform_kwargs),
|
||||
*added_anims
|
||||
)
|
||||
|
||||
def apply_complex_homotopy(self, complex_homotopy, added_anims=[], **kwargs):
|
||||
transformer, transform_kwargs = self.get_transformer(**kwargs)
|
||||
|
||||
# def homotopy(x, y, z, t):
|
||||
# output = complex_homotopy(complex(x, y), t)
|
||||
# rescaled_output = self.z_to_point(output)
|
||||
# return (rescaled_output.real, rescaled_output.imag, z)
|
||||
|
||||
self.play(
|
||||
ComplexHomotopy(complex_homotopy, transformer, **transform_kwargs),
|
||||
*added_anims
|
||||
)
|
||||
@@ -1,263 +0,0 @@
|
||||
from manimlib.animation.creation import ShowCreation
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.animation.transform import MoveToTarget
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Dot
|
||||
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 = {
|
||||
"digit_place_colors": [YELLOW, MAROON_B, RED, GREEN, BLUE, PURPLE_D],
|
||||
"counting_dot_starting_position": (FRAME_X_RADIUS - 1) * RIGHT + (FRAME_Y_RADIUS - 1) * UP,
|
||||
"count_dot_starting_radius": 0.5,
|
||||
"dot_configuration_height": 2,
|
||||
"ones_configuration_location": UP + 2 * RIGHT,
|
||||
"num_scale_factor": 2,
|
||||
"num_start_location": 2 * DOWN,
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
self.dots = VGroup()
|
||||
self.number = 0
|
||||
self.max_place = 0
|
||||
self.number_mob = VGroup(Tex(str(self.number)))
|
||||
self.number_mob.scale(self.num_scale_factor)
|
||||
self.number_mob.shift(self.num_start_location)
|
||||
|
||||
self.dot_templates = []
|
||||
self.dot_template_iterators = []
|
||||
self.curr_configurations = []
|
||||
|
||||
self.arrows = VGroup()
|
||||
|
||||
self.add(self.number_mob)
|
||||
|
||||
def get_template_configuration(self, place):
|
||||
# This should probably be replaced for non-base-10 counting scenes
|
||||
down_right = (0.5) * RIGHT + (np.sqrt(3) / 2) * DOWN
|
||||
result = []
|
||||
for down_right_steps in range(5):
|
||||
for left_steps in range(down_right_steps):
|
||||
result.append(
|
||||
down_right_steps * down_right + left_steps * LEFT
|
||||
)
|
||||
return reversed(result[:self.get_place_max(place)])
|
||||
|
||||
def get_dot_template(self, place):
|
||||
# This should be replaced for non-base-10 counting scenes
|
||||
dots = VGroup(*[
|
||||
Dot(
|
||||
point,
|
||||
radius=0.25,
|
||||
fill_opacity=0,
|
||||
stroke_width=2,
|
||||
stroke_color=WHITE,
|
||||
)
|
||||
for point in self.get_template_configuration(place)
|
||||
])
|
||||
dots.set_height(self.dot_configuration_height)
|
||||
return dots
|
||||
|
||||
def add_configuration(self):
|
||||
new_template = self.get_dot_template(len(self.dot_templates))
|
||||
new_template.move_to(self.ones_configuration_location)
|
||||
left_vect = (new_template.get_width() + LARGE_BUFF) * LEFT
|
||||
new_template.shift(
|
||||
left_vect * len(self.dot_templates)
|
||||
)
|
||||
self.dot_templates.append(new_template)
|
||||
self.dot_template_iterators.append(
|
||||
it.cycle(new_template)
|
||||
)
|
||||
self.curr_configurations.append(VGroup())
|
||||
|
||||
def count(self, max_val, run_time_per_anim=1):
|
||||
for x in range(max_val):
|
||||
self.increment(run_time_per_anim)
|
||||
|
||||
def increment(self, run_time_per_anim=1):
|
||||
moving_dot = Dot(
|
||||
self.counting_dot_starting_position,
|
||||
radius=self.count_dot_starting_radius,
|
||||
color=self.digit_place_colors[0],
|
||||
)
|
||||
moving_dot.generate_target()
|
||||
moving_dot.set_fill(opacity=0)
|
||||
kwargs = {
|
||||
"run_time": run_time_per_anim
|
||||
}
|
||||
|
||||
continue_rolling_over = True
|
||||
first_move = True
|
||||
place = 0
|
||||
while continue_rolling_over:
|
||||
added_anims = []
|
||||
if first_move:
|
||||
added_anims += self.get_digit_increment_animations()
|
||||
first_move = False
|
||||
moving_dot.target.replace(
|
||||
next(self.dot_template_iterators[place])
|
||||
)
|
||||
self.play(MoveToTarget(moving_dot), *added_anims, **kwargs)
|
||||
self.curr_configurations[place].add(moving_dot)
|
||||
|
||||
if len(self.curr_configurations[place].split()) == self.get_place_max(place):
|
||||
full_configuration = self.curr_configurations[place]
|
||||
self.curr_configurations[place] = VGroup()
|
||||
place += 1
|
||||
center = full_configuration.get_center_of_mass()
|
||||
radius = 0.6 * max(
|
||||
full_configuration.get_width(),
|
||||
full_configuration.get_height(),
|
||||
)
|
||||
circle = Circle(
|
||||
radius=radius,
|
||||
stroke_width=0,
|
||||
fill_color=self.digit_place_colors[place],
|
||||
fill_opacity=0.5,
|
||||
)
|
||||
circle.move_to(center)
|
||||
moving_dot = VGroup(circle, full_configuration)
|
||||
moving_dot.generate_target()
|
||||
moving_dot[0].set_fill(opacity=0)
|
||||
else:
|
||||
continue_rolling_over = False
|
||||
|
||||
def get_digit_increment_animations(self):
|
||||
result = []
|
||||
self.number += 1
|
||||
is_next_digit = self.is_next_digit()
|
||||
if is_next_digit:
|
||||
self.max_place += 1
|
||||
new_number_mob = self.get_number_mob(self.number)
|
||||
new_number_mob.move_to(self.number_mob, RIGHT)
|
||||
if is_next_digit:
|
||||
self.add_configuration()
|
||||
place = len(new_number_mob.split()) - 1
|
||||
result.append(FadeIn(self.dot_templates[place]))
|
||||
arrow = Arrow(
|
||||
new_number_mob[place].get_top(),
|
||||
self.dot_templates[place].get_bottom(),
|
||||
color=self.digit_place_colors[place]
|
||||
)
|
||||
self.arrows.add(arrow)
|
||||
result.append(ShowCreation(arrow))
|
||||
result.append(Transform(
|
||||
self.number_mob, new_number_mob,
|
||||
lag_ratio=0.5
|
||||
))
|
||||
return result
|
||||
|
||||
def get_number_mob(self, num):
|
||||
result = VGroup()
|
||||
place = 0
|
||||
max_place = self.max_place
|
||||
while place < max_place:
|
||||
digit = Tex(str(self.get_place_num(num, place)))
|
||||
if place >= len(self.digit_place_colors):
|
||||
self.digit_place_colors += self.digit_place_colors
|
||||
digit.set_color(self.digit_place_colors[place])
|
||||
digit.scale(self.num_scale_factor)
|
||||
digit.next_to(result, LEFT, buff=SMALL_BUFF, aligned_edge=DOWN)
|
||||
result.add(digit)
|
||||
place += 1
|
||||
return result
|
||||
|
||||
def is_next_digit(self):
|
||||
return False
|
||||
|
||||
def get_place_num(self, num, place):
|
||||
return 0
|
||||
|
||||
def get_place_max(self, place):
|
||||
return 0
|
||||
|
||||
|
||||
class PowerCounter(CountingScene):
|
||||
def is_next_digit(self):
|
||||
number = self.number
|
||||
while number > 1:
|
||||
if number % self.base != 0:
|
||||
return False
|
||||
number /= self.base
|
||||
return True
|
||||
|
||||
def get_place_max(self, place):
|
||||
return self.base
|
||||
|
||||
def get_place_num(self, num, place):
|
||||
return (num / (self.base ** place)) % self.base
|
||||
|
||||
|
||||
class CountInDecimal(PowerCounter):
|
||||
CONFIG = {
|
||||
"base": 10,
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
for x in range(11):
|
||||
self.increment()
|
||||
for x in range(85):
|
||||
self.increment(0.25)
|
||||
for x in range(20):
|
||||
self.increment()
|
||||
|
||||
|
||||
class CountInTernary(PowerCounter):
|
||||
CONFIG = {
|
||||
"base": 3,
|
||||
"dot_configuration_height": 1,
|
||||
"ones_configuration_location": UP + 4 * RIGHT
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
self.count(27)
|
||||
|
||||
# def get_template_configuration(self, place):
|
||||
# return [ORIGIN, UP]
|
||||
|
||||
|
||||
class CountInBinaryTo256(PowerCounter):
|
||||
CONFIG = {
|
||||
"base": 2,
|
||||
"dot_configuration_height": 1,
|
||||
"ones_configuration_location": UP + 5 * RIGHT
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
self.count(128, 0.3)
|
||||
|
||||
def get_template_configuration(self, place):
|
||||
return [ORIGIN, UP]
|
||||
|
||||
|
||||
class FactorialBase(CountingScene):
|
||||
CONFIG = {
|
||||
"dot_configuration_height": 1,
|
||||
"ones_configuration_location": UP + 4 * RIGHT
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
self.count(30, 0.4)
|
||||
|
||||
def is_next_digit(self):
|
||||
return self.number == self.factorial(self.max_place + 1)
|
||||
|
||||
def get_place_max(self, place):
|
||||
return place + 2
|
||||
|
||||
def get_place_num(self, num, place):
|
||||
return (num / self.factorial(place + 1)) % self.get_place_max(place)
|
||||
|
||||
def factorial(self, n):
|
||||
if (n == 1):
|
||||
return 1
|
||||
else:
|
||||
return n * self.factorial(n - 1)
|
||||
@@ -1,676 +0,0 @@
|
||||
from functools import reduce
|
||||
|
||||
from manimlib.constants import *
|
||||
# from manimlib.for_3b1b_videos.pi_creature import PiCreature
|
||||
# from manimlib.for_3b1b_videos.pi_creature import Randolph
|
||||
# from manimlib.for_3b1b_videos.pi_creature import get_all_pi_creature_modes
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Polygon
|
||||
from manimlib.mobject.geometry import RegularPolygon
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.color import color_gradient
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import center_of_mass
|
||||
from manimlib.utils.space_ops import compass_directions
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
from manimlib.utils.space_ops import rotation_matrix
|
||||
|
||||
|
||||
def rotate(points, angle=np.pi, axis=OUT):
|
||||
if axis is None:
|
||||
return points
|
||||
matrix = rotation_matrix(angle, axis)
|
||||
points = np.dot(points, np.transpose(matrix))
|
||||
return points
|
||||
|
||||
|
||||
def fractalify(vmobject, order=3, *args, **kwargs):
|
||||
for x in range(order):
|
||||
fractalification_iteration(vmobject)
|
||||
return vmobject
|
||||
|
||||
|
||||
def fractalification_iteration(vmobject, dimension=1.05, num_inserted_anchors_range=list(range(1, 4))):
|
||||
num_points = vmobject.get_num_points()
|
||||
if num_points > 0:
|
||||
# original_anchors = vmobject.get_anchors()
|
||||
original_anchors = [
|
||||
vmobject.point_from_proportion(x)
|
||||
for x in np.linspace(0, 1 - 1. / num_points, num_points)
|
||||
]
|
||||
new_anchors = []
|
||||
for p1, p2, in zip(original_anchors, original_anchors[1:]):
|
||||
num_inserts = random.choice(num_inserted_anchors_range)
|
||||
inserted_points = [
|
||||
interpolate(p1, p2, alpha)
|
||||
for alpha in np.linspace(0, 1, num_inserts + 2)[1:-1]
|
||||
]
|
||||
mass_scaling_factor = 1. / (num_inserts + 1)
|
||||
length_scaling_factor = mass_scaling_factor**(1. / dimension)
|
||||
target_length = get_norm(p1 - p2) * length_scaling_factor
|
||||
curr_length = get_norm(p1 - p2) * mass_scaling_factor
|
||||
# offset^2 + curr_length^2 = target_length^2
|
||||
offset_len = np.sqrt(target_length**2 - curr_length**2)
|
||||
unit_vect = (p1 - p2) / get_norm(p1 - p2)
|
||||
offset_unit_vect = rotate_vector(unit_vect, np.pi / 2)
|
||||
inserted_points = [
|
||||
point + u * offset_len * offset_unit_vect
|
||||
for u, point in zip(it.cycle([-1, 1]), inserted_points)
|
||||
]
|
||||
new_anchors += [p1] + inserted_points
|
||||
new_anchors.append(original_anchors[-1])
|
||||
vmobject.set_points_as_corners(new_anchors)
|
||||
vmobject.set_submobjects([
|
||||
fractalification_iteration(
|
||||
submob, dimension, num_inserted_anchors_range)
|
||||
for submob in vmobject.submobjects
|
||||
])
|
||||
return vmobject
|
||||
|
||||
|
||||
class SelfSimilarFractal(VMobject):
|
||||
CONFIG = {
|
||||
"order": 5,
|
||||
"num_subparts": 3,
|
||||
"height": 4,
|
||||
"colors": [RED, WHITE],
|
||||
"stroke_width": 1,
|
||||
"fill_opacity": 1,
|
||||
}
|
||||
|
||||
def init_colors(self):
|
||||
VMobject.init_colors(self)
|
||||
self.set_color_by_gradient(*self.colors)
|
||||
|
||||
def init_points(self):
|
||||
order_n_self = self.get_order_n_self(self.order)
|
||||
if self.order == 0:
|
||||
self.set_submobjects([order_n_self])
|
||||
else:
|
||||
self.set_submobjects(order_n_self.submobjects)
|
||||
return self
|
||||
|
||||
def get_order_n_self(self, order):
|
||||
if order == 0:
|
||||
result = self.get_seed_shape()
|
||||
else:
|
||||
lower_order = self.get_order_n_self(order - 1)
|
||||
subparts = [
|
||||
lower_order.copy()
|
||||
for x in range(self.num_subparts)
|
||||
]
|
||||
self.arrange_subparts(*subparts)
|
||||
result = VGroup(*subparts)
|
||||
|
||||
result.set_height(self.height)
|
||||
result.center()
|
||||
return result
|
||||
|
||||
def get_seed_shape(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def arrange_subparts(self, *subparts):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
|
||||
class Sierpinski(SelfSimilarFractal):
|
||||
def get_seed_shape(self):
|
||||
return Polygon(
|
||||
RIGHT, np.sqrt(3) * UP, LEFT,
|
||||
)
|
||||
|
||||
def arrange_subparts(self, *subparts):
|
||||
tri1, tri2, tri3 = subparts
|
||||
tri1.move_to(tri2.get_corner(DOWN + LEFT), UP)
|
||||
tri3.move_to(tri2.get_corner(DOWN + RIGHT), UP)
|
||||
|
||||
|
||||
class DiamondFractal(SelfSimilarFractal):
|
||||
CONFIG = {
|
||||
"num_subparts": 4,
|
||||
"height": 4,
|
||||
"colors": [GREEN_E, YELLOW],
|
||||
}
|
||||
|
||||
def get_seed_shape(self):
|
||||
return RegularPolygon(n=4)
|
||||
|
||||
def arrange_subparts(self, *subparts):
|
||||
# VGroup(*subparts).rotate(np.pi/4)
|
||||
for part, vect in zip(subparts, compass_directions(start_vect=UP + RIGHT)):
|
||||
part.next_to(ORIGIN, vect, buff=0)
|
||||
VGroup(*subparts).rotate(np.pi / 4, about_point=ORIGIN)
|
||||
|
||||
|
||||
class PentagonalFractal(SelfSimilarFractal):
|
||||
CONFIG = {
|
||||
"num_subparts": 5,
|
||||
"colors": [MAROON_B, YELLOW, RED],
|
||||
"height": 6,
|
||||
}
|
||||
|
||||
def get_seed_shape(self):
|
||||
return RegularPolygon(n=5, start_angle=np.pi / 2)
|
||||
|
||||
def arrange_subparts(self, *subparts):
|
||||
for x, part in enumerate(subparts):
|
||||
part.shift(0.95 * part.get_height() * UP)
|
||||
part.rotate(2 * np.pi * x / 5, about_point=ORIGIN)
|
||||
|
||||
|
||||
class PentagonalPiCreatureFractal(PentagonalFractal):
|
||||
def init_colors(self):
|
||||
SelfSimilarFractal.init_colors(self)
|
||||
internal_pis = [
|
||||
pi
|
||||
for pi in self.get_family()
|
||||
if isinstance(pi, PiCreature)
|
||||
]
|
||||
colors = color_gradient(self.colors, len(internal_pis))
|
||||
for pi, color in zip(internal_pis, colors):
|
||||
pi.init_colors()
|
||||
pi.body.set_stroke(color, width=0.5)
|
||||
pi.set_color(color)
|
||||
|
||||
def get_seed_shape(self):
|
||||
return Randolph(mode="shruggie")
|
||||
|
||||
def arrange_subparts(self, *subparts):
|
||||
for part in subparts:
|
||||
part.rotate(2 * np.pi / 5, about_point=ORIGIN)
|
||||
PentagonalFractal.arrange_subparts(self, *subparts)
|
||||
|
||||
|
||||
class PiCreatureFractal(VMobject):
|
||||
CONFIG = {
|
||||
"order": 7,
|
||||
"scale_val": 2.5,
|
||||
"start_mode": "hooray",
|
||||
"height": 6,
|
||||
"colors": [
|
||||
BLUE_D, BLUE_B, MAROON_B, MAROON_D, GREY,
|
||||
YELLOW, RED, GREY_BROWN, RED, RED_E,
|
||||
],
|
||||
"random_seed": 0,
|
||||
"stroke_width": 0,
|
||||
}
|
||||
|
||||
def init_colors(self):
|
||||
VMobject.init_colors(self)
|
||||
internal_pis = [
|
||||
pi
|
||||
for pi in self.get_family()
|
||||
if isinstance(pi, PiCreature)
|
||||
]
|
||||
random.seed(self.random_seed)
|
||||
for pi in reversed(internal_pis):
|
||||
color = random.choice(self.colors)
|
||||
pi.set_color(color)
|
||||
pi.set_stroke(color, width=0)
|
||||
|
||||
def init_points(self):
|
||||
random.seed(self.random_seed)
|
||||
modes = get_all_pi_creature_modes()
|
||||
seed = PiCreature(mode=self.start_mode)
|
||||
seed.set_height(self.height)
|
||||
seed.to_edge(DOWN)
|
||||
creatures = [seed]
|
||||
self.add(VGroup(seed))
|
||||
for x in range(self.order):
|
||||
new_creatures = []
|
||||
for creature in creatures:
|
||||
for eye, vect in zip(creature.eyes, [LEFT, RIGHT]):
|
||||
new_creature = PiCreature(
|
||||
mode=random.choice(modes)
|
||||
)
|
||||
new_creature.set_height(
|
||||
self.scale_val * eye.get_height()
|
||||
)
|
||||
new_creature.next_to(
|
||||
eye, vect,
|
||||
buff=0,
|
||||
aligned_edge=DOWN
|
||||
)
|
||||
new_creatures.append(new_creature)
|
||||
creature.look_at(random.choice(new_creatures))
|
||||
self.add_to_back(VGroup(*new_creatures))
|
||||
creatures = new_creatures
|
||||
|
||||
# def init_colors(self):
|
||||
# VMobject.init_colors(self)
|
||||
# self.set_color_by_gradient(*self.colors)
|
||||
|
||||
|
||||
class WonkyHexagonFractal(SelfSimilarFractal):
|
||||
CONFIG = {
|
||||
"num_subparts": 7
|
||||
}
|
||||
|
||||
def get_seed_shape(self):
|
||||
return RegularPolygon(n=6)
|
||||
|
||||
def arrange_subparts(self, *subparts):
|
||||
for i, piece in enumerate(subparts):
|
||||
piece.rotate(i * np.pi / 12, about_point=ORIGIN)
|
||||
p1, p2, p3, p4, p5, p6, p7 = subparts
|
||||
center_row = VGroup(p1, p4, p7)
|
||||
center_row.arrange(RIGHT, buff=0)
|
||||
for p in p2, p3, p5, p6:
|
||||
p.set_width(p1.get_width())
|
||||
p2.move_to(p1.get_top(), DOWN + LEFT)
|
||||
p3.move_to(p1.get_bottom(), UP + LEFT)
|
||||
p5.move_to(p4.get_top(), DOWN + LEFT)
|
||||
p6.move_to(p4.get_bottom(), UP + LEFT)
|
||||
|
||||
|
||||
class CircularFractal(SelfSimilarFractal):
|
||||
CONFIG = {
|
||||
"num_subparts": 3,
|
||||
"colors": [GREEN, BLUE, GREY]
|
||||
}
|
||||
|
||||
def get_seed_shape(self):
|
||||
return Circle()
|
||||
|
||||
def arrange_subparts(self, *subparts):
|
||||
if not hasattr(self, "been_here"):
|
||||
self.num_subparts = 3 + self.order
|
||||
self.been_here = True
|
||||
for i, part in enumerate(subparts):
|
||||
theta = np.pi / self.num_subparts
|
||||
part.next_to(
|
||||
ORIGIN, UP,
|
||||
buff=self.height / (2 * np.tan(theta))
|
||||
)
|
||||
part.rotate(i * 2 * np.pi / self.num_subparts, about_point=ORIGIN)
|
||||
self.num_subparts -= 1
|
||||
|
||||
######## Space filling curves ############
|
||||
|
||||
|
||||
class JaggedCurvePiece(VMobject):
|
||||
def insert_n_curves(self, n):
|
||||
if self.get_num_curves() == 0:
|
||||
self.set_points(np.zeros((1, 3)))
|
||||
anchors = self.get_anchors()
|
||||
indices = np.linspace(
|
||||
0, len(anchors) - 1, n + len(anchors)
|
||||
).astype('int')
|
||||
self.set_points_as_corners(anchors[indices])
|
||||
|
||||
|
||||
class FractalCurve(VMobject):
|
||||
CONFIG = {
|
||||
"radius": 3,
|
||||
"order": 5,
|
||||
"colors": [RED, GREEN],
|
||||
"num_submobjects": 20,
|
||||
"monochromatic": False,
|
||||
"order_to_stroke_width_map": {
|
||||
3: 3,
|
||||
4: 2,
|
||||
5: 1,
|
||||
},
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
points = self.get_anchor_points()
|
||||
self.set_points_as_corners(points)
|
||||
if not self.monochromatic:
|
||||
alphas = np.linspace(0, 1, self.num_submobjects)
|
||||
for alpha_pair in zip(alphas, alphas[1:]):
|
||||
submobject = JaggedCurvePiece()
|
||||
submobject.pointwise_become_partial(
|
||||
self, *alpha_pair
|
||||
)
|
||||
self.add(submobject)
|
||||
self.set_points(np.zeros((0, 3)))
|
||||
|
||||
def init_colors(self):
|
||||
VMobject.init_colors(self)
|
||||
self.set_color_by_gradient(*self.colors)
|
||||
for order in sorted(self.order_to_stroke_width_map.keys()):
|
||||
if self.order >= order:
|
||||
self.set_stroke(width=self.order_to_stroke_width_map[order])
|
||||
|
||||
def get_anchor_points(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
|
||||
class LindenmayerCurve(FractalCurve):
|
||||
CONFIG = {
|
||||
"axiom": "A",
|
||||
"rule": {},
|
||||
"scale_factor": 2,
|
||||
"radius": 3,
|
||||
"start_step": RIGHT,
|
||||
"angle": np.pi / 2,
|
||||
}
|
||||
|
||||
def expand_command_string(self, command):
|
||||
result = ""
|
||||
for letter in command:
|
||||
if letter in self.rule:
|
||||
result += self.rule[letter]
|
||||
else:
|
||||
result += letter
|
||||
return result
|
||||
|
||||
def get_command_string(self):
|
||||
result = self.axiom
|
||||
for x in range(self.order):
|
||||
result = self.expand_command_string(result)
|
||||
return result
|
||||
|
||||
def get_anchor_points(self):
|
||||
step = float(self.radius) * self.start_step
|
||||
step /= (self.scale_factor**self.order)
|
||||
curr = np.zeros(3)
|
||||
result = [curr]
|
||||
for letter in self.get_command_string():
|
||||
if letter == "+":
|
||||
step = rotate(step, self.angle)
|
||||
elif letter == "-":
|
||||
step = rotate(step, -self.angle)
|
||||
else:
|
||||
curr = curr + step
|
||||
result.append(curr)
|
||||
return np.array(result) - center_of_mass(result)
|
||||
|
||||
|
||||
class SelfSimilarSpaceFillingCurve(FractalCurve):
|
||||
CONFIG = {
|
||||
"offsets": [],
|
||||
# keys must awkwardly be in string form...
|
||||
"offset_to_rotation_axis": {},
|
||||
"scale_factor": 2,
|
||||
"radius_scale_factor": 0.5,
|
||||
}
|
||||
|
||||
def transform(self, points, offset):
|
||||
"""
|
||||
How to transform the copy of points shifted by
|
||||
offset. Generally meant to be extended in subclasses
|
||||
"""
|
||||
copy = np.array(points)
|
||||
if str(offset) in self.offset_to_rotation_axis:
|
||||
copy = rotate(
|
||||
copy,
|
||||
axis=self.offset_to_rotation_axis[str(offset)]
|
||||
)
|
||||
copy /= self.scale_factor,
|
||||
copy += offset * self.radius * self.radius_scale_factor
|
||||
return copy
|
||||
|
||||
def refine_into_subparts(self, points):
|
||||
transformed_copies = [
|
||||
self.transform(points, offset)
|
||||
for offset in self.offsets
|
||||
]
|
||||
return reduce(
|
||||
lambda a, b: np.append(a, b, axis=0),
|
||||
transformed_copies
|
||||
)
|
||||
|
||||
def get_anchor_points(self):
|
||||
points = np.zeros((1, 3))
|
||||
for count in range(self.order):
|
||||
points = self.refine_into_subparts(points)
|
||||
return points
|
||||
|
||||
def generate_grid(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
|
||||
class HilbertCurve(SelfSimilarSpaceFillingCurve):
|
||||
CONFIG = {
|
||||
"offsets": [
|
||||
LEFT + DOWN,
|
||||
LEFT + UP,
|
||||
RIGHT + UP,
|
||||
RIGHT + DOWN,
|
||||
],
|
||||
"offset_to_rotation_axis": {
|
||||
str(LEFT + DOWN): RIGHT + UP,
|
||||
str(RIGHT + DOWN): RIGHT + DOWN,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class HilbertCurve3D(SelfSimilarSpaceFillingCurve):
|
||||
CONFIG = {
|
||||
"offsets": [
|
||||
RIGHT + DOWN + IN,
|
||||
LEFT + DOWN + IN,
|
||||
LEFT + DOWN + OUT,
|
||||
RIGHT + DOWN + OUT,
|
||||
RIGHT + UP + OUT,
|
||||
LEFT + UP + OUT,
|
||||
LEFT + UP + IN,
|
||||
RIGHT + UP + IN,
|
||||
],
|
||||
"offset_to_rotation_axis_and_angle": {
|
||||
str(RIGHT + DOWN + IN): (LEFT + UP + OUT, 2 * np.pi / 3),
|
||||
str(LEFT + DOWN + IN): (RIGHT + DOWN + IN, 2 * np.pi / 3),
|
||||
str(LEFT + DOWN + OUT): (RIGHT + DOWN + IN, 2 * np.pi / 3),
|
||||
str(RIGHT + DOWN + OUT): (UP, np.pi),
|
||||
str(RIGHT + UP + OUT): (UP, np.pi),
|
||||
str(LEFT + UP + OUT): (LEFT + DOWN + OUT, 2 * np.pi / 3),
|
||||
str(LEFT + UP + IN): (LEFT + DOWN + OUT, 2 * np.pi / 3),
|
||||
str(RIGHT + UP + IN): (RIGHT + UP + IN, 2 * np.pi / 3),
|
||||
},
|
||||
}
|
||||
# Rewrote transform method to include the rotation angle
|
||||
|
||||
def transform(self, points, offset):
|
||||
copy = np.array(points)
|
||||
copy = rotate(
|
||||
copy,
|
||||
axis=self.offset_to_rotation_axis_and_angle[str(offset)][0],
|
||||
angle=self.offset_to_rotation_axis_and_angle[str(offset)][1],
|
||||
)
|
||||
copy /= self.scale_factor,
|
||||
copy += offset * self.radius * self.radius_scale_factor
|
||||
return copy
|
||||
|
||||
|
||||
class PeanoCurve(SelfSimilarSpaceFillingCurve):
|
||||
CONFIG = {
|
||||
"colors": [PURPLE, TEAL],
|
||||
"offsets": [
|
||||
LEFT + DOWN,
|
||||
LEFT,
|
||||
LEFT + UP,
|
||||
UP,
|
||||
ORIGIN,
|
||||
DOWN,
|
||||
RIGHT + DOWN,
|
||||
RIGHT,
|
||||
RIGHT + UP,
|
||||
],
|
||||
"offset_to_rotation_axis": {
|
||||
str(LEFT): UP,
|
||||
str(UP): RIGHT,
|
||||
str(ORIGIN): LEFT + UP,
|
||||
str(DOWN): RIGHT,
|
||||
str(RIGHT): UP,
|
||||
},
|
||||
"scale_factor": 3,
|
||||
"radius_scale_factor": 2.0 / 3,
|
||||
}
|
||||
|
||||
|
||||
class TriangleFillingCurve(SelfSimilarSpaceFillingCurve):
|
||||
CONFIG = {
|
||||
"colors": [MAROON, YELLOW],
|
||||
"offsets": [
|
||||
LEFT / 4. + DOWN / 6.,
|
||||
ORIGIN,
|
||||
RIGHT / 4. + DOWN / 6.,
|
||||
UP / 3.,
|
||||
],
|
||||
"offset_to_rotation_axis": {
|
||||
str(ORIGIN): RIGHT,
|
||||
str(UP / 3.): UP,
|
||||
},
|
||||
"scale_factor": 2,
|
||||
"radius_scale_factor": 1.5,
|
||||
}
|
||||
|
||||
# class HexagonFillingCurve(SelfSimilarSpaceFillingCurve):
|
||||
# CONFIG = {
|
||||
# "start_color" : WHITE,
|
||||
# "end_color" : BLUE_D,
|
||||
# "axis_offset_pairs" : [
|
||||
# (None, 1.5*DOWN + 0.5*np.sqrt(3)*LEFT),
|
||||
# (UP+np.sqrt(3)*RIGHT, 1.5*DOWN + 0.5*np.sqrt(3)*RIGHT),
|
||||
# (np.sqrt(3)*UP+RIGHT, ORIGIN),
|
||||
# ((UP, RIGHT), np.sqrt(3)*LEFT),
|
||||
# (None, 1.5*UP + 0.5*np.sqrt(3)*LEFT),
|
||||
# (None, 1.5*UP + 0.5*np.sqrt(3)*RIGHT),
|
||||
# (RIGHT, np.sqrt(3)*RIGHT),
|
||||
# ],
|
||||
# "scale_factor" : 3,
|
||||
# "radius_scale_factor" : 2/(3*np.sqrt(3)),
|
||||
# }
|
||||
|
||||
# def refine_into_subparts(self, points):
|
||||
# return SelfSimilarSpaceFillingCurve.refine_into_subparts(
|
||||
# self,
|
||||
# rotate(points, np.pi/6, IN)
|
||||
# )
|
||||
|
||||
|
||||
class UtahFillingCurve(SelfSimilarSpaceFillingCurve):
|
||||
CONFIG = {
|
||||
"colors": [WHITE, BLUE_D],
|
||||
"axis_offset_pairs": [
|
||||
|
||||
],
|
||||
"scale_factor": 3,
|
||||
"radius_scale_factor": 2 / (3 * np.sqrt(3)),
|
||||
}
|
||||
|
||||
|
||||
class FlowSnake(LindenmayerCurve):
|
||||
CONFIG = {
|
||||
"colors": [YELLOW, GREEN],
|
||||
"axiom": "A",
|
||||
"rule": {
|
||||
"A": "A-B--B+A++AA+B-",
|
||||
"B": "+A-BB--B-A++A+B",
|
||||
},
|
||||
"radius": 6, # TODO, this is innaccurate
|
||||
"scale_factor": np.sqrt(7),
|
||||
"start_step": RIGHT,
|
||||
"angle": -np.pi / 3,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
LindenmayerCurve.__init__(self, **kwargs)
|
||||
self.rotate(-self.order * np.pi / 9, about_point=ORIGIN)
|
||||
|
||||
|
||||
class SierpinskiCurve(LindenmayerCurve):
|
||||
CONFIG = {
|
||||
"colors": [RED, WHITE],
|
||||
"axiom": "B",
|
||||
"rule": {
|
||||
"A": "+B-A-B+",
|
||||
"B": "-A+B+A-",
|
||||
},
|
||||
"radius": 6, # TODO, this is innaccurate
|
||||
"scale_factor": 2,
|
||||
"start_step": RIGHT,
|
||||
"angle": -np.pi / 3,
|
||||
}
|
||||
|
||||
|
||||
class KochSnowFlake(LindenmayerCurve):
|
||||
CONFIG = {
|
||||
"colors": [BLUE_D, WHITE, BLUE_D],
|
||||
"axiom": "A--A--A--",
|
||||
"rule": {
|
||||
"A": "A+A--A+A"
|
||||
},
|
||||
"radius": 4,
|
||||
"scale_factor": 3,
|
||||
"start_step": RIGHT,
|
||||
"angle": np.pi / 3,
|
||||
"order_to_stroke_width_map": {
|
||||
3: 3,
|
||||
5: 2,
|
||||
6: 1,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.scale_factor = 2 * (1 + np.cos(self.angle))
|
||||
LindenmayerCurve.__init__(self, **kwargs)
|
||||
|
||||
|
||||
class KochCurve(KochSnowFlake):
|
||||
CONFIG = {
|
||||
"axiom": "A--"
|
||||
}
|
||||
|
||||
|
||||
class QuadraticKoch(LindenmayerCurve):
|
||||
CONFIG = {
|
||||
"colors": [YELLOW, WHITE, MAROON_B],
|
||||
"axiom": "A",
|
||||
"rule": {
|
||||
"A": "A+A-A-AA+A+A-A"
|
||||
},
|
||||
"radius": 4,
|
||||
"scale_factor": 4,
|
||||
"start_step": RIGHT,
|
||||
"angle": np.pi / 2
|
||||
}
|
||||
|
||||
|
||||
class QuadraticKochIsland(QuadraticKoch):
|
||||
CONFIG = {
|
||||
"axiom": "A+A+A+A"
|
||||
}
|
||||
|
||||
|
||||
class StellarCurve(LindenmayerCurve):
|
||||
CONFIG = {
|
||||
"start_color": RED,
|
||||
"end_color": BLUE_E,
|
||||
"rule": {
|
||||
"A": "+B-A-B+A-B+",
|
||||
"B": "-A+B+A-B+A-",
|
||||
},
|
||||
"scale_factor": 3,
|
||||
"angle": 2 * np.pi / 5,
|
||||
}
|
||||
|
||||
|
||||
class SnakeCurve(FractalCurve):
|
||||
CONFIG = {
|
||||
"start_color": BLUE,
|
||||
"end_color": YELLOW,
|
||||
}
|
||||
|
||||
def get_anchor_points(self):
|
||||
result = []
|
||||
resolution = 2**self.order
|
||||
step = 2.0 * self.radius / resolution
|
||||
lower_left = ORIGIN + \
|
||||
LEFT * (self.radius - step / 2) + \
|
||||
DOWN * (self.radius - step / 2)
|
||||
|
||||
for y in range(resolution):
|
||||
x_range = list(range(resolution))
|
||||
if y % 2 == 0:
|
||||
x_range.reverse()
|
||||
for x in x_range:
|
||||
result.append(
|
||||
lower_left + x * step * RIGHT + y * step * UP
|
||||
)
|
||||
return result
|
||||
@@ -1,566 +0,0 @@
|
||||
import itertools as it
|
||||
|
||||
from manimlib.animation.creation import Write, DrawBorderThenFill, ShowCreation
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.animation.update import UpdateFromAlphaFunc
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.functions import ParametricCurve
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import RegularPolygon
|
||||
from manimlib.mobject.number_line import NumberLine
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VectorizedPoint
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.color import color_gradient
|
||||
from manimlib.utils.color import invert_color
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
|
||||
# TODO, this class should be deprecated, with all its
|
||||
# functionality moved to Axes and handled at the mobject
|
||||
# level rather than the scene level
|
||||
|
||||
|
||||
class GraphScene(Scene):
|
||||
CONFIG = {
|
||||
"x_min": -1,
|
||||
"x_max": 10,
|
||||
"x_axis_width": 9,
|
||||
"x_tick_frequency": 1,
|
||||
"x_leftmost_tick": None, # Change if different from x_min
|
||||
"x_labeled_nums": None,
|
||||
"x_axis_label": "$x$",
|
||||
"y_min": -1,
|
||||
"y_max": 10,
|
||||
"y_axis_height": 6,
|
||||
"y_tick_frequency": 1,
|
||||
"y_bottom_tick": None, # Change if different from y_min
|
||||
"y_labeled_nums": None,
|
||||
"y_axis_label": "$y$",
|
||||
"axes_color": GREY,
|
||||
"graph_origin": 2.5 * DOWN + 4 * LEFT,
|
||||
"exclude_zero_label": True,
|
||||
"default_graph_colors": [BLUE, GREEN, YELLOW],
|
||||
"default_derivative_color": GREEN,
|
||||
"default_input_color": YELLOW,
|
||||
"default_riemann_start_color": BLUE,
|
||||
"default_riemann_end_color": GREEN,
|
||||
"area_opacity": 0.8,
|
||||
"num_rects": 50,
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
self.default_graph_colors_cycle = it.cycle(self.default_graph_colors)
|
||||
|
||||
self.left_T_label = VGroup()
|
||||
self.left_v_line = VGroup()
|
||||
self.right_T_label = VGroup()
|
||||
self.right_v_line = VGroup()
|
||||
|
||||
def setup_axes(self, animate=False):
|
||||
# TODO, once eoc is done, refactor this to be less redundant.
|
||||
x_num_range = float(self.x_max - self.x_min)
|
||||
self.space_unit_to_x = self.x_axis_width / x_num_range
|
||||
if self.x_labeled_nums is None:
|
||||
self.x_labeled_nums = []
|
||||
if self.x_leftmost_tick is None:
|
||||
self.x_leftmost_tick = self.x_min
|
||||
x_axis = NumberLine(
|
||||
x_min=self.x_min,
|
||||
x_max=self.x_max,
|
||||
unit_size=self.space_unit_to_x,
|
||||
tick_frequency=self.x_tick_frequency,
|
||||
leftmost_tick=self.x_leftmost_tick,
|
||||
numbers_with_elongated_ticks=self.x_labeled_nums,
|
||||
color=self.axes_color
|
||||
)
|
||||
x_axis.shift(self.graph_origin - x_axis.number_to_point(0))
|
||||
if len(self.x_labeled_nums) > 0:
|
||||
if self.exclude_zero_label:
|
||||
self.x_labeled_nums = [x for x in self.x_labeled_nums if x != 0]
|
||||
x_axis.add_numbers(self.x_labeled_nums)
|
||||
if self.x_axis_label:
|
||||
x_label = TexText(self.x_axis_label)
|
||||
x_label.next_to(
|
||||
x_axis.get_tick_marks(), UP + RIGHT,
|
||||
buff=SMALL_BUFF
|
||||
)
|
||||
x_label.shift_onto_screen()
|
||||
x_axis.add(x_label)
|
||||
self.x_axis_label_mob = x_label
|
||||
|
||||
y_num_range = float(self.y_max - self.y_min)
|
||||
self.space_unit_to_y = self.y_axis_height / y_num_range
|
||||
|
||||
if self.y_labeled_nums is None:
|
||||
self.y_labeled_nums = []
|
||||
if self.y_bottom_tick is None:
|
||||
self.y_bottom_tick = self.y_min
|
||||
y_axis = NumberLine(
|
||||
x_min=self.y_min,
|
||||
x_max=self.y_max,
|
||||
unit_size=self.space_unit_to_y,
|
||||
tick_frequency=self.y_tick_frequency,
|
||||
leftmost_tick=self.y_bottom_tick,
|
||||
numbers_with_elongated_ticks=self.y_labeled_nums,
|
||||
color=self.axes_color,
|
||||
line_to_number_vect=LEFT,
|
||||
label_direction=LEFT,
|
||||
)
|
||||
y_axis.shift(self.graph_origin - y_axis.number_to_point(0))
|
||||
y_axis.rotate(np.pi / 2, about_point=y_axis.number_to_point(0))
|
||||
if len(self.y_labeled_nums) > 0:
|
||||
if self.exclude_zero_label:
|
||||
self.y_labeled_nums = [y for y in self.y_labeled_nums if y != 0]
|
||||
y_axis.add_numbers(self.y_labeled_nums)
|
||||
if self.y_axis_label:
|
||||
y_label = TexText(self.y_axis_label)
|
||||
y_label.next_to(
|
||||
y_axis.get_corner(UP + RIGHT), UP + RIGHT,
|
||||
buff=SMALL_BUFF
|
||||
)
|
||||
y_label.shift_onto_screen()
|
||||
y_axis.add(y_label)
|
||||
self.y_axis_label_mob = y_label
|
||||
|
||||
if animate:
|
||||
self.play(Write(VGroup(x_axis, y_axis)))
|
||||
else:
|
||||
self.add(x_axis, y_axis)
|
||||
self.x_axis, self.y_axis = self.axes = VGroup(x_axis, y_axis)
|
||||
self.default_graph_colors = it.cycle(self.default_graph_colors)
|
||||
|
||||
def coords_to_point(self, x, y):
|
||||
assert(hasattr(self, "x_axis") and hasattr(self, "y_axis"))
|
||||
result = self.x_axis.number_to_point(x)[0] * RIGHT
|
||||
result += self.y_axis.number_to_point(y)[1] * UP
|
||||
return result
|
||||
|
||||
def point_to_coords(self, point):
|
||||
return (self.x_axis.point_to_number(point),
|
||||
self.y_axis.point_to_number(point))
|
||||
|
||||
def get_graph(
|
||||
self, func,
|
||||
color=None,
|
||||
x_min=None,
|
||||
x_max=None,
|
||||
**kwargs
|
||||
):
|
||||
if color is None:
|
||||
color = next(self.default_graph_colors_cycle)
|
||||
if x_min is None:
|
||||
x_min = self.x_min
|
||||
if x_max is None:
|
||||
x_max = self.x_max
|
||||
|
||||
def parameterized_function(alpha):
|
||||
x = interpolate(x_min, x_max, alpha)
|
||||
y = func(x)
|
||||
if not np.isfinite(y):
|
||||
y = self.y_max
|
||||
return self.coords_to_point(x, y)
|
||||
|
||||
graph = ParametricCurve(
|
||||
parameterized_function,
|
||||
color=color,
|
||||
**kwargs
|
||||
)
|
||||
graph.underlying_function = func
|
||||
return graph
|
||||
|
||||
def input_to_graph_point(self, x, graph):
|
||||
return self.coords_to_point(x, graph.underlying_function(x))
|
||||
|
||||
def angle_of_tangent(self, x, graph, dx=0.01):
|
||||
vect = self.input_to_graph_point(
|
||||
x + dx, graph) - self.input_to_graph_point(x, graph)
|
||||
return angle_of_vector(vect)
|
||||
|
||||
def slope_of_tangent(self, *args, **kwargs):
|
||||
return np.tan(self.angle_of_tangent(*args, **kwargs))
|
||||
|
||||
def get_derivative_graph(self, graph, dx=0.01, **kwargs):
|
||||
if "color" not in kwargs:
|
||||
kwargs["color"] = self.default_derivative_color
|
||||
|
||||
def deriv(x):
|
||||
return self.slope_of_tangent(x, graph, dx) / self.space_unit_to_y
|
||||
return self.get_graph(deriv, **kwargs)
|
||||
|
||||
def get_graph_label(
|
||||
self,
|
||||
graph,
|
||||
label="f(x)",
|
||||
x_val=None,
|
||||
direction=RIGHT,
|
||||
buff=MED_SMALL_BUFF,
|
||||
color=None,
|
||||
):
|
||||
label = Tex(label)
|
||||
color = color or graph.get_color()
|
||||
label.set_color(color)
|
||||
if x_val is None:
|
||||
# Search from right to left
|
||||
for x in np.linspace(self.x_max, self.x_min, 100):
|
||||
point = self.input_to_graph_point(x, graph)
|
||||
if point[1] < FRAME_Y_RADIUS:
|
||||
break
|
||||
x_val = x
|
||||
label.next_to(
|
||||
self.input_to_graph_point(x_val, graph),
|
||||
direction,
|
||||
buff=buff
|
||||
)
|
||||
label.shift_onto_screen()
|
||||
return label
|
||||
|
||||
def get_riemann_rectangles(
|
||||
self,
|
||||
graph,
|
||||
x_min=None,
|
||||
x_max=None,
|
||||
dx=0.1,
|
||||
input_sample_type="left",
|
||||
stroke_width=1,
|
||||
stroke_color=BLACK,
|
||||
fill_opacity=1,
|
||||
start_color=None,
|
||||
end_color=None,
|
||||
show_signed_area=True,
|
||||
width_scale_factor=1.001
|
||||
):
|
||||
x_min = x_min if x_min is not None else self.x_min
|
||||
x_max = x_max if x_max is not None else self.x_max
|
||||
if start_color is None:
|
||||
start_color = self.default_riemann_start_color
|
||||
if end_color is None:
|
||||
end_color = self.default_riemann_end_color
|
||||
rectangles = VGroup()
|
||||
x_range = np.arange(x_min, x_max, dx)
|
||||
colors = color_gradient([start_color, end_color], len(x_range))
|
||||
for x, color in zip(x_range, colors):
|
||||
if input_sample_type == "left":
|
||||
sample_input = x
|
||||
elif input_sample_type == "right":
|
||||
sample_input = x + dx
|
||||
elif input_sample_type == "center":
|
||||
sample_input = x + 0.5 * dx
|
||||
else:
|
||||
raise Exception("Invalid input sample type")
|
||||
graph_point = self.input_to_graph_point(sample_input, graph)
|
||||
points = VGroup(*list(map(VectorizedPoint, [
|
||||
self.coords_to_point(x, 0),
|
||||
self.coords_to_point(x + width_scale_factor * dx, 0),
|
||||
graph_point
|
||||
])))
|
||||
|
||||
rect = Rectangle()
|
||||
rect.replace(points, stretch=True)
|
||||
if graph_point[1] < self.graph_origin[1] and show_signed_area:
|
||||
fill_color = invert_color(color)
|
||||
else:
|
||||
fill_color = color
|
||||
rect.set_fill(fill_color, opacity=fill_opacity)
|
||||
rect.set_stroke(stroke_color, width=stroke_width)
|
||||
rectangles.add(rect)
|
||||
return rectangles
|
||||
|
||||
def get_riemann_rectangles_list(
|
||||
self,
|
||||
graph,
|
||||
n_iterations,
|
||||
max_dx=0.5,
|
||||
power_base=2,
|
||||
stroke_width=1,
|
||||
**kwargs
|
||||
):
|
||||
return [
|
||||
self.get_riemann_rectangles(
|
||||
graph=graph,
|
||||
dx=float(max_dx) / (power_base**n),
|
||||
stroke_width=float(stroke_width) / (power_base**n),
|
||||
**kwargs
|
||||
)
|
||||
for n in range(n_iterations)
|
||||
]
|
||||
|
||||
def get_area(self, graph, t_min, t_max):
|
||||
numerator = max(t_max - t_min, 0.0001)
|
||||
dx = float(numerator) / self.num_rects
|
||||
return self.get_riemann_rectangles(
|
||||
graph,
|
||||
x_min=t_min,
|
||||
x_max=t_max,
|
||||
dx=dx,
|
||||
stroke_width=0,
|
||||
).set_fill(opacity=self.area_opacity)
|
||||
|
||||
def transform_between_riemann_rects(self, curr_rects, new_rects, **kwargs):
|
||||
transform_kwargs = {
|
||||
"run_time": 2,
|
||||
"lag_ratio": 0.5
|
||||
}
|
||||
added_anims = kwargs.get("added_anims", [])
|
||||
transform_kwargs.update(kwargs)
|
||||
curr_rects.align_family(new_rects)
|
||||
x_coords = set() # Keep track of new repetitions
|
||||
for rect in curr_rects:
|
||||
x = rect.get_center()[0]
|
||||
if x in x_coords:
|
||||
rect.set_fill(opacity=0)
|
||||
else:
|
||||
x_coords.add(x)
|
||||
self.play(
|
||||
Transform(curr_rects, new_rects, **transform_kwargs),
|
||||
*added_anims
|
||||
)
|
||||
|
||||
def get_vertical_line_to_graph(
|
||||
self,
|
||||
x, graph,
|
||||
line_class=Line,
|
||||
**line_kwargs
|
||||
):
|
||||
if "color" not in line_kwargs:
|
||||
line_kwargs["color"] = graph.get_color()
|
||||
return line_class(
|
||||
self.coords_to_point(x, 0),
|
||||
self.input_to_graph_point(x, graph),
|
||||
**line_kwargs
|
||||
)
|
||||
|
||||
def get_vertical_lines_to_graph(
|
||||
self, graph,
|
||||
x_min=None,
|
||||
x_max=None,
|
||||
num_lines=20,
|
||||
**kwargs
|
||||
):
|
||||
x_min = x_min or self.x_min
|
||||
x_max = x_max or self.x_max
|
||||
return VGroup(*[
|
||||
self.get_vertical_line_to_graph(x, graph, **kwargs)
|
||||
for x in np.linspace(x_min, x_max, num_lines)
|
||||
])
|
||||
|
||||
def get_secant_slope_group(
|
||||
self,
|
||||
x, graph,
|
||||
dx=None,
|
||||
dx_line_color=None,
|
||||
df_line_color=None,
|
||||
dx_label=None,
|
||||
df_label=None,
|
||||
include_secant_line=True,
|
||||
secant_line_color=None,
|
||||
secant_line_length=10,
|
||||
):
|
||||
"""
|
||||
Resulting group is of the form VGroup(
|
||||
dx_line,
|
||||
df_line,
|
||||
dx_label, (if applicable)
|
||||
df_label, (if applicable)
|
||||
secant_line, (if applicable)
|
||||
)
|
||||
with attributes of those names.
|
||||
"""
|
||||
kwargs = locals()
|
||||
kwargs.pop("self")
|
||||
group = VGroup()
|
||||
group.kwargs = kwargs
|
||||
|
||||
dx = dx or float(self.x_max - self.x_min) / 10
|
||||
dx_line_color = dx_line_color or self.default_input_color
|
||||
df_line_color = df_line_color or graph.get_color()
|
||||
|
||||
p1 = self.input_to_graph_point(x, graph)
|
||||
p2 = self.input_to_graph_point(x + dx, graph)
|
||||
interim_point = p2[0] * RIGHT + p1[1] * UP
|
||||
|
||||
group.dx_line = Line(
|
||||
p1, interim_point,
|
||||
color=dx_line_color
|
||||
)
|
||||
group.df_line = Line(
|
||||
interim_point, p2,
|
||||
color=df_line_color
|
||||
)
|
||||
group.add(group.dx_line, group.df_line)
|
||||
|
||||
labels = VGroup()
|
||||
if dx_label is not None:
|
||||
group.dx_label = Tex(dx_label)
|
||||
labels.add(group.dx_label)
|
||||
group.add(group.dx_label)
|
||||
if df_label is not None:
|
||||
group.df_label = Tex(df_label)
|
||||
labels.add(group.df_label)
|
||||
group.add(group.df_label)
|
||||
|
||||
if len(labels) > 0:
|
||||
max_width = 0.8 * group.dx_line.get_width()
|
||||
max_height = 0.8 * group.df_line.get_height()
|
||||
if labels.get_width() > max_width:
|
||||
labels.set_width(max_width)
|
||||
if labels.get_height() > max_height:
|
||||
labels.set_height(max_height)
|
||||
|
||||
if dx_label is not None:
|
||||
group.dx_label.next_to(
|
||||
group.dx_line,
|
||||
np.sign(dx) * DOWN,
|
||||
buff=group.dx_label.get_height() / 2
|
||||
)
|
||||
group.dx_label.set_color(group.dx_line.get_color())
|
||||
|
||||
if df_label is not None:
|
||||
group.df_label.next_to(
|
||||
group.df_line,
|
||||
np.sign(dx) * RIGHT,
|
||||
buff=group.df_label.get_height() / 2
|
||||
)
|
||||
group.df_label.set_color(group.df_line.get_color())
|
||||
|
||||
if include_secant_line:
|
||||
secant_line_color = secant_line_color or self.default_derivative_color
|
||||
group.secant_line = Line(p1, p2, color=secant_line_color)
|
||||
group.secant_line.scale(
|
||||
secant_line_length / group.secant_line.get_length()
|
||||
)
|
||||
group.add(group.secant_line)
|
||||
|
||||
return group
|
||||
|
||||
def add_T_label(self, x_val, side=RIGHT, label=None, color=WHITE, animated=False, **kwargs):
|
||||
triangle = RegularPolygon(n=3, start_angle=np.pi / 2)
|
||||
triangle.set_height(MED_SMALL_BUFF)
|
||||
triangle.move_to(self.coords_to_point(x_val, 0), UP)
|
||||
triangle.set_fill(color, 1)
|
||||
triangle.set_stroke(width=0)
|
||||
if label is None:
|
||||
T_label = Tex(self.variable_point_label, fill_color=color)
|
||||
else:
|
||||
T_label = Tex(label, fill_color=color)
|
||||
|
||||
T_label.next_to(triangle, DOWN)
|
||||
v_line = self.get_vertical_line_to_graph(
|
||||
x_val, self.v_graph,
|
||||
color=YELLOW
|
||||
)
|
||||
|
||||
if animated:
|
||||
self.play(
|
||||
DrawBorderThenFill(triangle),
|
||||
ShowCreation(v_line),
|
||||
Write(T_label, run_time=1),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if np.all(side == LEFT):
|
||||
self.left_T_label_group = VGroup(T_label, triangle)
|
||||
self.left_v_line = v_line
|
||||
self.add(self.left_T_label_group, self.left_v_line)
|
||||
elif np.all(side == RIGHT):
|
||||
self.right_T_label_group = VGroup(T_label, triangle)
|
||||
self.right_v_line = v_line
|
||||
self.add(self.right_T_label_group, self.right_v_line)
|
||||
|
||||
def get_animation_integral_bounds_change(
|
||||
self,
|
||||
graph,
|
||||
new_t_min,
|
||||
new_t_max,
|
||||
fade_close_to_origin=True,
|
||||
run_time=1.0
|
||||
):
|
||||
curr_t_min = self.x_axis.point_to_number(self.area.get_left())
|
||||
curr_t_max = self.x_axis.point_to_number(self.area.get_right())
|
||||
if new_t_min is None:
|
||||
new_t_min = curr_t_min
|
||||
if new_t_max is None:
|
||||
new_t_max = curr_t_max
|
||||
|
||||
group = VGroup(self.area)
|
||||
group.add(self.left_v_line)
|
||||
group.add(self.left_T_label_group)
|
||||
group.add(self.right_v_line)
|
||||
group.add(self.right_T_label_group)
|
||||
|
||||
def update_group(group, alpha):
|
||||
area, left_v_line, left_T_label, right_v_line, right_T_label = group
|
||||
t_min = interpolate(curr_t_min, new_t_min, alpha)
|
||||
t_max = interpolate(curr_t_max, new_t_max, alpha)
|
||||
new_area = self.get_area(graph, t_min, t_max)
|
||||
|
||||
new_left_v_line = self.get_vertical_line_to_graph(
|
||||
t_min, graph
|
||||
)
|
||||
new_left_v_line.set_color(left_v_line.get_color())
|
||||
left_T_label.move_to(new_left_v_line.get_bottom(), UP)
|
||||
|
||||
new_right_v_line = self.get_vertical_line_to_graph(
|
||||
t_max, graph
|
||||
)
|
||||
new_right_v_line.set_color(right_v_line.get_color())
|
||||
right_T_label.move_to(new_right_v_line.get_bottom(), UP)
|
||||
|
||||
# Fade close to 0
|
||||
if fade_close_to_origin:
|
||||
if len(left_T_label) > 0:
|
||||
left_T_label[0].set_fill(opacity=min(1, np.abs(t_min)))
|
||||
if len(right_T_label) > 0:
|
||||
right_T_label[0].set_fill(opacity=min(1, np.abs(t_max)))
|
||||
|
||||
Transform(area, new_area).update(1)
|
||||
Transform(left_v_line, new_left_v_line).update(1)
|
||||
Transform(right_v_line, new_right_v_line).update(1)
|
||||
return group
|
||||
|
||||
return UpdateFromAlphaFunc(group, update_group, run_time=run_time)
|
||||
|
||||
def animate_secant_slope_group_change(
|
||||
self, secant_slope_group,
|
||||
target_dx=None,
|
||||
target_x=None,
|
||||
run_time=3,
|
||||
added_anims=None,
|
||||
**anim_kwargs
|
||||
):
|
||||
if target_dx is None and target_x is None:
|
||||
raise Exception(
|
||||
"At least one of target_x and target_dx must not be None")
|
||||
if added_anims is None:
|
||||
added_anims = []
|
||||
|
||||
start_dx = secant_slope_group.kwargs["dx"]
|
||||
start_x = secant_slope_group.kwargs["x"]
|
||||
if target_dx is None:
|
||||
target_dx = start_dx
|
||||
if target_x is None:
|
||||
target_x = start_x
|
||||
|
||||
def update_func(group, alpha):
|
||||
dx = interpolate(start_dx, target_dx, alpha)
|
||||
x = interpolate(start_x, target_x, alpha)
|
||||
kwargs = dict(secant_slope_group.kwargs)
|
||||
kwargs["dx"] = dx
|
||||
kwargs["x"] = x
|
||||
new_group = self.get_secant_slope_group(**kwargs)
|
||||
group.become(new_group)
|
||||
return group
|
||||
|
||||
self.play(
|
||||
UpdateFromAlphaFunc(
|
||||
secant_slope_group, update_func,
|
||||
run_time=run_time,
|
||||
**anim_kwargs
|
||||
),
|
||||
*added_anims
|
||||
)
|
||||
secant_slope_group.kwargs["x"] = target_x
|
||||
secant_slope_group.kwargs["dx"] = target_dx
|
||||
@@ -1,414 +0,0 @@
|
||||
from functools import reduce
|
||||
import itertools as it
|
||||
import operator as op
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.utils.rate_functions import there_and_back
|
||||
from manimlib.utils.space_ops import center_of_mass
|
||||
|
||||
|
||||
class Graph():
|
||||
def __init__(self):
|
||||
# List of points in R^3
|
||||
# vertices = []
|
||||
# List of pairs of indices of vertices
|
||||
# edges = []
|
||||
# List of tuples of indices of vertices. The last should
|
||||
# be a cycle whose interior is the entire graph, and when
|
||||
# regions are computed its complement will be taken.
|
||||
# region_cycles = []
|
||||
|
||||
self.construct()
|
||||
|
||||
def construct(self):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
|
||||
class CubeGraph(Graph):
|
||||
"""
|
||||
5 7
|
||||
12
|
||||
03
|
||||
4 6
|
||||
"""
|
||||
|
||||
def construct(self):
|
||||
self.vertices = [
|
||||
(x, y, 0)
|
||||
for r in (1, 2)
|
||||
for x, y in it.product([-r, r], [-r, r])
|
||||
]
|
||||
self.edges = [
|
||||
(0, 1),
|
||||
(0, 2),
|
||||
(3, 1),
|
||||
(3, 2),
|
||||
(4, 5),
|
||||
(4, 6),
|
||||
(7, 5),
|
||||
(7, 6),
|
||||
(0, 4),
|
||||
(1, 5),
|
||||
(2, 6),
|
||||
(3, 7),
|
||||
]
|
||||
self.region_cycles = [
|
||||
[0, 2, 3, 1],
|
||||
[4, 0, 1, 5],
|
||||
[4, 6, 2, 0],
|
||||
[6, 7, 3, 2],
|
||||
[7, 5, 1, 3],
|
||||
[4, 6, 7, 5], # By convention, last region will be "outside"
|
||||
]
|
||||
|
||||
|
||||
class SampleGraph(Graph):
|
||||
"""
|
||||
4 2 3 8
|
||||
0 1
|
||||
7
|
||||
5 6
|
||||
"""
|
||||
|
||||
def construct(self):
|
||||
self.vertices = [
|
||||
(0, 0, 0),
|
||||
(2, 0, 0),
|
||||
(1, 2, 0),
|
||||
(3, 2, 0),
|
||||
(-1, 2, 0),
|
||||
(-2, -2, 0),
|
||||
(2, -2, 0),
|
||||
(4, -1, 0),
|
||||
(6, 2, 0),
|
||||
]
|
||||
self.edges = [
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(3, 2),
|
||||
(2, 4),
|
||||
(4, 0),
|
||||
(2, 0),
|
||||
(4, 5),
|
||||
(0, 5),
|
||||
(1, 5),
|
||||
(5, 6),
|
||||
(6, 7),
|
||||
(7, 1),
|
||||
(7, 8),
|
||||
(8, 3),
|
||||
]
|
||||
self.region_cycles = [
|
||||
(0, 1, 2),
|
||||
(1, 3, 2),
|
||||
(2, 4, 0),
|
||||
(4, 5, 0),
|
||||
(0, 5, 1),
|
||||
(1, 5, 6, 7),
|
||||
(1, 7, 8, 3),
|
||||
(4, 5, 6, 7, 8, 3, 2),
|
||||
]
|
||||
|
||||
|
||||
class OctohedronGraph(Graph):
|
||||
"""
|
||||
3
|
||||
|
||||
1 0
|
||||
2
|
||||
4 5
|
||||
"""
|
||||
|
||||
def construct(self):
|
||||
self.vertices = [
|
||||
(r * np.cos(angle), r * np.sin(angle) - 1, 0)
|
||||
for r, s in [(1, 0), (3, 3)]
|
||||
for angle in (np.pi / 6) * np.array([s, 4 + s, 8 + s])
|
||||
]
|
||||
self.edges = [
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 0),
|
||||
(5, 0),
|
||||
(0, 3),
|
||||
(3, 5),
|
||||
(3, 1),
|
||||
(3, 4),
|
||||
(1, 4),
|
||||
(4, 2),
|
||||
(4, 5),
|
||||
(5, 2),
|
||||
]
|
||||
self.region_cycles = [
|
||||
(0, 1, 2),
|
||||
(0, 5, 3),
|
||||
(3, 1, 0),
|
||||
(3, 4, 1),
|
||||
(1, 4, 2),
|
||||
(2, 4, 5),
|
||||
(5, 0, 2),
|
||||
(3, 4, 5),
|
||||
]
|
||||
|
||||
|
||||
class CompleteGraph(Graph):
|
||||
def __init__(self, num_vertices, radius=3):
|
||||
self.num_vertices = num_vertices
|
||||
self.radius = radius
|
||||
Graph.__init__(self)
|
||||
|
||||
def construct(self):
|
||||
self.vertices = [
|
||||
(self.radius * np.cos(theta), self.radius * np.sin(theta), 0)
|
||||
for x in range(self.num_vertices)
|
||||
for theta in [2 * np.pi * x / self.num_vertices]
|
||||
]
|
||||
self.edges = it.combinations(list(range(self.num_vertices)), 2)
|
||||
|
||||
def __str__(self):
|
||||
return Graph.__str__(self) + str(self.num_vertices)
|
||||
|
||||
|
||||
class DiscreteGraphScene(Scene):
|
||||
args_list = [
|
||||
(CubeGraph(),),
|
||||
(SampleGraph(),),
|
||||
(OctohedronGraph(),),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def args_to_string(*args):
|
||||
return str(args[0])
|
||||
|
||||
def __init__(self, graph, *args, **kwargs):
|
||||
# See CubeGraph() above for format of graph
|
||||
self.graph = graph
|
||||
Scene.__init__(self, *args, **kwargs)
|
||||
|
||||
def construct(self):
|
||||
self._points = list(map(np.array, self.graph.vertices))
|
||||
self.vertices = self.dots = [Dot(p) for p in self._points]
|
||||
self.edges = self.lines = [
|
||||
Line(self._points[i], self._points[j])
|
||||
for i, j in self.graph.edges
|
||||
]
|
||||
self.add(*self.dots + self.edges)
|
||||
|
||||
def generate_regions(self):
|
||||
regions = [
|
||||
self.region_from_cycle(cycle)
|
||||
for cycle in self.graph.region_cycles
|
||||
]
|
||||
regions[-1].complement() # Outer region painted outwardly...
|
||||
self.regions = regions
|
||||
|
||||
def region_from_cycle(self, cycle):
|
||||
point_pairs = [
|
||||
[
|
||||
self._points[cycle[i]],
|
||||
self._points[cycle[(i + 1) % len(cycle)]]
|
||||
]
|
||||
for i in range(len(cycle))
|
||||
]
|
||||
return region_from_line_boundary(
|
||||
*point_pairs, shape=self.shape
|
||||
)
|
||||
|
||||
def draw_vertices(self, **kwargs):
|
||||
self.clear()
|
||||
self.play(ShowCreation(Mobject(*self.vertices), **kwargs))
|
||||
|
||||
def draw_edges(self):
|
||||
self.play(*[
|
||||
ShowCreation(edge, run_time=1.0)
|
||||
for edge in self.edges
|
||||
])
|
||||
|
||||
def accent_vertices(self, **kwargs):
|
||||
self.remove(*self.vertices)
|
||||
start = Mobject(*self.vertices)
|
||||
end = Mobject(*[
|
||||
Dot(point, radius=3 * Dot.DEFAULT_RADIUS, color="lightgreen")
|
||||
for point in self._points
|
||||
])
|
||||
self.play(Transform(
|
||||
start, end, rate_func=there_and_back,
|
||||
**kwargs
|
||||
))
|
||||
self.remove(start)
|
||||
self.add(*self.vertices)
|
||||
|
||||
def replace_vertices_with(self, mobject):
|
||||
mobject.center()
|
||||
diameter = max(mobject.get_height(), mobject.get_width())
|
||||
self.play(*[
|
||||
CounterclockwiseTransform(
|
||||
vertex,
|
||||
mobject.copy().shift(vertex.get_center())
|
||||
)
|
||||
for vertex in self.vertices
|
||||
] + [
|
||||
ApplyMethod(
|
||||
edge.scale,
|
||||
(edge.get_length() - diameter) / edge.get_length()
|
||||
)
|
||||
for edge in self.edges
|
||||
])
|
||||
|
||||
def annotate_edges(self, mobject, fade_in=True, **kwargs):
|
||||
angles = list(map(np.arctan, list(map(Line.get_slope, self.edges))))
|
||||
self.edge_annotations = [
|
||||
mobject.copy().rotate(angle).move_to(edge.get_center())
|
||||
for angle, edge in zip(angles, self.edges)
|
||||
]
|
||||
if fade_in:
|
||||
self.play(*[
|
||||
FadeIn(ann, **kwargs)
|
||||
for ann in self.edge_annotations
|
||||
])
|
||||
|
||||
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(*[
|
||||
Line(self._points[i], self._points[j]).set_color(color)
|
||||
for i, j in zip(cycle, next_in_cycle)
|
||||
])
|
||||
self.play(
|
||||
ShowCreation(self.traced_cycle),
|
||||
run_time=run_time
|
||||
)
|
||||
|
||||
def generate_spanning_tree(self, root=0, color="yellow"):
|
||||
self.spanning_tree_root = 0
|
||||
pairs = deepcopy(self.graph.edges)
|
||||
pairs += [tuple(reversed(pair)) for pair in pairs]
|
||||
self.spanning_tree_index_pairs = []
|
||||
curr = root
|
||||
spanned_vertices = set([curr])
|
||||
to_check = set([curr])
|
||||
while len(to_check) > 0:
|
||||
curr = to_check.pop()
|
||||
for pair in pairs:
|
||||
if pair[0] == curr and pair[1] not in spanned_vertices:
|
||||
self.spanning_tree_index_pairs.append(pair)
|
||||
spanned_vertices.add(pair[1])
|
||||
to_check.add(pair[1])
|
||||
self.spanning_tree = Mobject(*[
|
||||
Line(
|
||||
self._points[pair[0]],
|
||||
self._points[pair[1]]
|
||||
).set_color(color)
|
||||
for pair in self.spanning_tree_index_pairs
|
||||
])
|
||||
|
||||
def generate_treeified_spanning_tree(self):
|
||||
bottom = -FRAME_Y_RADIUS + 1
|
||||
x_sep = 1
|
||||
y_sep = 2
|
||||
if not hasattr(self, "spanning_tree"):
|
||||
self.generate_spanning_tree()
|
||||
root = self.spanning_tree_root
|
||||
color = self.spanning_tree.get_color()
|
||||
indices = list(range(len(self._points)))
|
||||
# Build dicts
|
||||
parent_of = dict([
|
||||
tuple(reversed(pair))
|
||||
for pair in self.spanning_tree_index_pairs
|
||||
])
|
||||
children_of = dict([(index, []) for index in indices])
|
||||
for child in parent_of:
|
||||
children_of[parent_of[child]].append(child)
|
||||
|
||||
x_coord_of = {root: 0}
|
||||
y_coord_of = {root: bottom}
|
||||
# width to allocate to a given node, computed as
|
||||
# the maximum number of decendents in a single generation,
|
||||
# minus 1, multiplied by x_sep
|
||||
width_of = {}
|
||||
for index in indices:
|
||||
next_generation = children_of[index]
|
||||
curr_max = max(1, len(next_generation))
|
||||
while next_generation != []:
|
||||
next_generation = reduce(op.add, [
|
||||
children_of[node]
|
||||
for node in next_generation
|
||||
])
|
||||
curr_max = max(curr_max, len(next_generation))
|
||||
width_of[index] = x_sep * (curr_max - 1)
|
||||
to_process = [root]
|
||||
while to_process != []:
|
||||
index = to_process.pop()
|
||||
if index not in y_coord_of:
|
||||
y_coord_of[index] = y_sep + y_coord_of[parent_of[index]]
|
||||
children = children_of[index]
|
||||
left_hand = x_coord_of[index] - width_of[index] / 2.0
|
||||
for child in children:
|
||||
x_coord_of[child] = left_hand + width_of[child] / 2.0
|
||||
left_hand += width_of[child] + x_sep
|
||||
to_process += children
|
||||
|
||||
new_points = [
|
||||
np.array([
|
||||
x_coord_of[index],
|
||||
y_coord_of[index],
|
||||
0
|
||||
])
|
||||
for index in indices
|
||||
]
|
||||
self.treeified_spanning_tree = Mobject(*[
|
||||
Line(new_points[i], new_points[j]).set_color(color)
|
||||
for i, j in self.spanning_tree_index_pairs
|
||||
])
|
||||
|
||||
def generate_dual_graph(self):
|
||||
point_at_infinity = np.array([np.inf] * 3)
|
||||
cycles = self.graph.region_cycles
|
||||
self.dual_points = [
|
||||
center_of_mass([
|
||||
self._points[index]
|
||||
for index in cycle
|
||||
])
|
||||
for cycle in cycles
|
||||
]
|
||||
self.dual_vertices = [
|
||||
Dot(point).set_color("green")
|
||||
for point in self.dual_points
|
||||
]
|
||||
self.dual_vertices[-1] = Circle().scale(FRAME_X_RADIUS + FRAME_Y_RADIUS)
|
||||
self.dual_points[-1] = point_at_infinity
|
||||
|
||||
self.dual_edges = []
|
||||
for pair in self.graph.edges:
|
||||
dual_point_pair = []
|
||||
for cycle in cycles:
|
||||
if not (pair[0] in cycle and pair[1] in cycle):
|
||||
continue
|
||||
index1, index2 = cycle.index(pair[0]), cycle.index(pair[1])
|
||||
if abs(index1 - index2) in [1, len(cycle) - 1]:
|
||||
dual_point_pair.append(
|
||||
self.dual_points[cycles.index(cycle)]
|
||||
)
|
||||
assert(len(dual_point_pair) == 2)
|
||||
for i in 0, 1:
|
||||
if all(dual_point_pair[i] == point_at_infinity):
|
||||
new_point = np.array(dual_point_pair[1 - i])
|
||||
vect = center_of_mass([
|
||||
self._points[pair[0]],
|
||||
self._points[pair[1]]
|
||||
]) - new_point
|
||||
new_point += FRAME_X_RADIUS * vect / get_norm(vect)
|
||||
dual_point_pair[i] = new_point
|
||||
self.dual_edges.append(
|
||||
Line(*dual_point_pair).set_color()
|
||||
)
|
||||
@@ -1,602 +0,0 @@
|
||||
from traceback import *
|
||||
|
||||
from scipy.spatial import ConvexHull
|
||||
|
||||
from manimlib.animation.composition import LaggedStartMap
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.animation.fading import FadeOut
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import AnnularSector
|
||||
from manimlib.mobject.geometry import Annulus
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VectorizedPoint
|
||||
from manimlib.utils.space_ops import angle_between_vectors
|
||||
from manimlib.utils.space_ops import project_along_vector
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
from manimlib.utils.space_ops import z_to_vector
|
||||
|
||||
LIGHT_COLOR = YELLOW
|
||||
SHADOW_COLOR = BLACK
|
||||
SWITCH_ON_RUN_TIME = 1.5
|
||||
FAST_SWITCH_ON_RUN_TIME = 0.1
|
||||
NUM_LEVELS = 30
|
||||
NUM_CONES = 7 # in first lighthouse scene
|
||||
NUM_VISIBLE_CONES = 5 # ibidem
|
||||
ARC_TIP_LENGTH = 0.2
|
||||
AMBIENT_FULL = 0.8
|
||||
AMBIENT_DIMMED = 0.5
|
||||
SPOTLIGHT_FULL = 0.8
|
||||
SPOTLIGHT_DIMMED = 0.5
|
||||
LIGHTHOUSE_HEIGHT = 0.8
|
||||
|
||||
DEGREES = TAU / 360
|
||||
|
||||
|
||||
def inverse_power_law(maxint, scale, cutoff, exponent):
|
||||
return (lambda r: maxint * (cutoff / (r / scale + cutoff))**exponent)
|
||||
|
||||
|
||||
def inverse_quadratic(maxint, scale, cutoff):
|
||||
return inverse_power_law(maxint, scale, cutoff, 2)
|
||||
|
||||
|
||||
class SwitchOn(LaggedStartMap):
|
||||
CONFIG = {
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": SWITCH_ON_RUN_TIME
|
||||
}
|
||||
|
||||
def __init__(self, light, **kwargs):
|
||||
if (not isinstance(light, AmbientLight) and not isinstance(light, Spotlight)):
|
||||
raise Exception(
|
||||
"Only AmbientLights and Spotlights can be switched on")
|
||||
LaggedStartMap.__init__(
|
||||
self, FadeIn, light, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class SwitchOff(LaggedStartMap):
|
||||
CONFIG = {
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": SWITCH_ON_RUN_TIME
|
||||
}
|
||||
|
||||
def __init__(self, light, **kwargs):
|
||||
if (not isinstance(light, AmbientLight) and not isinstance(light, Spotlight)):
|
||||
raise Exception(
|
||||
"Only AmbientLights and Spotlights can be switched off")
|
||||
light.set_submobjects(light.submobjects[::-1])
|
||||
LaggedStartMap.__init__(self, FadeOut, light, **kwargs)
|
||||
light.set_submobjects(light.submobjects[::-1])
|
||||
|
||||
|
||||
class Lighthouse(SVGMobject):
|
||||
CONFIG = {
|
||||
"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)
|
||||
|
||||
|
||||
class AmbientLight(VMobject):
|
||||
|
||||
# Parameters are:
|
||||
# * a source point
|
||||
# * an opacity function
|
||||
# * a light color
|
||||
# * a max opacity
|
||||
# * a radius (larger than the opacity's dropoff length)
|
||||
# * the number of subdivisions (levels, annuli)
|
||||
|
||||
CONFIG = {
|
||||
"source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0),
|
||||
"opacity_function": lambda r: 1.0 / (r + 1.0)**2,
|
||||
"color": LIGHT_COLOR,
|
||||
"max_opacity": 1.0,
|
||||
"num_levels": NUM_LEVELS,
|
||||
"radius": 5.0
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
# in theory, this method is only called once, right?
|
||||
# so removing submobs shd not be necessary
|
||||
#
|
||||
# Note: Usually, yes, it is only called within Mobject.__init__,
|
||||
# but there is no strong guarantee of that, and you may want certain
|
||||
# update functions to regenerate points here and there.
|
||||
for submob in self.submobjects:
|
||||
self.remove(submob)
|
||||
|
||||
self.add(self.source_point)
|
||||
|
||||
# create annuli
|
||||
self.radius = float(self.radius)
|
||||
dr = self.radius / self.num_levels
|
||||
for r in np.arange(0, self.radius, dr):
|
||||
alpha = self.max_opacity * self.opacity_function(r)
|
||||
annulus = Annulus(
|
||||
inner_radius=r,
|
||||
outer_radius=r + dr,
|
||||
color=self.color,
|
||||
fill_opacity=alpha
|
||||
)
|
||||
annulus.move_to(self.get_source_point())
|
||||
self.add(annulus)
|
||||
|
||||
def move_source_to(self, point):
|
||||
# old_source_point = self.get_source_point()
|
||||
# self.shift(point - old_source_point)
|
||||
self.move_to(point)
|
||||
|
||||
return self
|
||||
|
||||
def get_source_point(self):
|
||||
return self.source_point.get_location()
|
||||
|
||||
def dimming(self, new_alpha):
|
||||
old_alpha = self.max_opacity
|
||||
self.max_opacity = new_alpha
|
||||
for submob in self.submobjects:
|
||||
old_submob_alpha = submob.fill_opacity
|
||||
new_submob_alpha = old_submob_alpha * new_alpha / old_alpha
|
||||
submob.set_fill(opacity=new_submob_alpha)
|
||||
|
||||
|
||||
class Spotlight(VMobject):
|
||||
CONFIG = {
|
||||
"source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0),
|
||||
"opacity_function": lambda r: 1.0 / (r / 2 + 1.0)**2,
|
||||
"color": GREEN, # LIGHT_COLOR,
|
||||
"max_opacity": 1.0,
|
||||
"num_levels": 10,
|
||||
"radius": 10.0,
|
||||
"screen": None,
|
||||
"camera_mob": None
|
||||
}
|
||||
|
||||
def projection_direction(self):
|
||||
# Note: This seems reasonable, though for it to work you'd
|
||||
# need to be sure that any 3d scene including a spotlight
|
||||
# somewhere assigns that spotlights "camera" attribute
|
||||
# to be the camera associated with that scene.
|
||||
if self.camera_mob is None:
|
||||
return OUT
|
||||
else:
|
||||
[phi, theta, r] = self.camera_mob.get_center()
|
||||
v = np.array([np.sin(phi) * np.cos(theta),
|
||||
np.sin(phi) * np.sin(theta), np.cos(phi)])
|
||||
return v # /get_norm(v)
|
||||
|
||||
def project(self, point):
|
||||
v = self.projection_direction()
|
||||
w = project_along_vector(point, v)
|
||||
return w
|
||||
|
||||
def get_source_point(self):
|
||||
return self.source_point.get_location()
|
||||
|
||||
def init_points(self):
|
||||
self.set_submobjects([])
|
||||
|
||||
self.add(self.source_point)
|
||||
|
||||
if self.screen is not None:
|
||||
# look for the screen and create annular sectors
|
||||
lower_angle, upper_angle = self.viewing_angles(self.screen)
|
||||
self.radius = float(self.radius)
|
||||
dr = self.radius / self.num_levels
|
||||
lower_ray, upper_ray = self.viewing_rays(self.screen)
|
||||
|
||||
for r in np.arange(0, self.radius, dr):
|
||||
new_sector = self.new_sector(r, dr, lower_angle, upper_angle)
|
||||
self.add(new_sector)
|
||||
|
||||
def new_sector(self, r, dr, lower_angle, upper_angle):
|
||||
alpha = self.max_opacity * self.opacity_function(r)
|
||||
annular_sector = AnnularSector(
|
||||
inner_radius=r,
|
||||
outer_radius=r + dr,
|
||||
color=self.color,
|
||||
fill_opacity=alpha,
|
||||
start_angle=lower_angle,
|
||||
angle=upper_angle - lower_angle
|
||||
)
|
||||
# rotate (not project) it into the viewing plane
|
||||
rotation_matrix = z_to_vector(self.projection_direction())
|
||||
annular_sector.apply_matrix(rotation_matrix)
|
||||
# now rotate it inside that plane
|
||||
rotated_RIGHT = np.dot(RIGHT, rotation_matrix.T)
|
||||
projected_RIGHT = self.project(RIGHT)
|
||||
omega = angle_between_vectors(rotated_RIGHT, projected_RIGHT)
|
||||
annular_sector.rotate(omega, axis=self.projection_direction())
|
||||
annular_sector.move_arc_center_to(self.get_source_point())
|
||||
|
||||
return annular_sector
|
||||
|
||||
def viewing_angle_of_point(self, point):
|
||||
# as measured from the positive x-axis
|
||||
v1 = self.project(RIGHT)
|
||||
v2 = self.project(np.array(point) - self.get_source_point())
|
||||
absolute_angle = angle_between_vectors(v1, v2)
|
||||
# determine the angle's sign depending on their plane's
|
||||
# choice of orientation. That choice is set by the camera
|
||||
# position, i. e. projection direction
|
||||
|
||||
if np.dot(self.projection_direction(), np.cross(v1, v2)) > 0:
|
||||
return absolute_angle
|
||||
else:
|
||||
return -absolute_angle
|
||||
|
||||
def viewing_angles(self, screen):
|
||||
|
||||
screen_points = screen.get_anchors()
|
||||
projected_screen_points = list(map(self.project, screen_points))
|
||||
|
||||
viewing_angles = np.array(list(map(self.viewing_angle_of_point,
|
||||
projected_screen_points)))
|
||||
|
||||
lower_angle = upper_angle = 0
|
||||
if len(viewing_angles) != 0:
|
||||
lower_angle = np.min(viewing_angles)
|
||||
upper_angle = np.max(viewing_angles)
|
||||
|
||||
if upper_angle - lower_angle > TAU / 2:
|
||||
lower_angle, upper_angle = upper_angle, lower_angle + TAU
|
||||
return lower_angle, upper_angle
|
||||
|
||||
def viewing_rays(self, screen):
|
||||
|
||||
lower_angle, upper_angle = self.viewing_angles(screen)
|
||||
projected_RIGHT = self.project(
|
||||
RIGHT) / get_norm(self.project(RIGHT))
|
||||
lower_ray = rotate_vector(
|
||||
projected_RIGHT, lower_angle, axis=self.projection_direction())
|
||||
upper_ray = rotate_vector(
|
||||
projected_RIGHT, upper_angle, axis=self.projection_direction())
|
||||
|
||||
return lower_ray, upper_ray
|
||||
|
||||
def opening_angle(self):
|
||||
l, u = self.viewing_angles(self.screen)
|
||||
return u - l
|
||||
|
||||
def start_angle(self):
|
||||
l, u = self.viewing_angles(self.screen)
|
||||
return l
|
||||
|
||||
def stop_angle(self):
|
||||
l, u = self.viewing_angles(self.screen)
|
||||
return u
|
||||
|
||||
def move_source_to(self, point):
|
||||
self.source_point.set_location(np.array(point))
|
||||
# self.source_point.move_to(np.array(point))
|
||||
# self.move_to(point)
|
||||
self.update_sectors()
|
||||
return self
|
||||
|
||||
def update_sectors(self):
|
||||
if self.screen is None:
|
||||
return
|
||||
for submob in self.submobjects:
|
||||
if type(submob) == AnnularSector:
|
||||
lower_angle, upper_angle = self.viewing_angles(self.screen)
|
||||
# dr = submob.outer_radius - submob.inner_radius
|
||||
dr = self.radius / self.num_levels
|
||||
new_submob = self.new_sector(
|
||||
submob.inner_radius, dr, lower_angle, upper_angle
|
||||
)
|
||||
# submob.points = new_submob.points
|
||||
# submob.set_fill(opacity = 10 * self.opacity_function(submob.outer_radius))
|
||||
Transform(submob, new_submob).update(1)
|
||||
|
||||
def dimming(self, new_alpha):
|
||||
old_alpha = self.max_opacity
|
||||
self.max_opacity = new_alpha
|
||||
for submob in self.submobjects:
|
||||
# Note: Maybe it'd be best to have a Shadow class so that the
|
||||
# type can be checked directly?
|
||||
if type(submob) != AnnularSector:
|
||||
# it's the shadow, don't dim it
|
||||
continue
|
||||
old_submob_alpha = submob.fill_opacity
|
||||
new_submob_alpha = old_submob_alpha * new_alpha / old_alpha
|
||||
submob.set_fill(opacity=new_submob_alpha)
|
||||
|
||||
def change_opacity_function(self, new_f):
|
||||
self.opacity_function = new_f
|
||||
dr = self.radius / self.num_levels
|
||||
|
||||
sectors = []
|
||||
for submob in self.submobjects:
|
||||
if type(submob) == AnnularSector:
|
||||
sectors.append(submob)
|
||||
|
||||
for (r, submob) in zip(np.arange(0, self.radius, dr), sectors):
|
||||
if type(submob) != AnnularSector:
|
||||
# it's the shadow, don't dim it
|
||||
continue
|
||||
alpha = self.opacity_function(r)
|
||||
submob.set_fill(opacity=alpha)
|
||||
|
||||
# Warning: This class is likely quite buggy.
|
||||
|
||||
|
||||
class LightSource(VMobject):
|
||||
# combines:
|
||||
# a lighthouse
|
||||
# an ambient light
|
||||
# a spotlight
|
||||
# and a shadow
|
||||
CONFIG = {
|
||||
"source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0),
|
||||
"color": LIGHT_COLOR,
|
||||
"num_levels": 10,
|
||||
"radius": 10.0,
|
||||
"screen": None,
|
||||
"opacity_function": inverse_quadratic(1, 2, 1),
|
||||
"max_opacity_ambient": AMBIENT_FULL,
|
||||
"max_opacity_spotlight": SPOTLIGHT_FULL,
|
||||
"camera_mob": None
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
|
||||
self.add(self.source_point)
|
||||
|
||||
self.lighthouse = Lighthouse()
|
||||
self.ambient_light = AmbientLight(
|
||||
source_point=VectorizedPoint(location=self.get_source_point()),
|
||||
color=self.color,
|
||||
num_levels=self.num_levels,
|
||||
radius=self.radius,
|
||||
opacity_function=self.opacity_function,
|
||||
max_opacity=self.max_opacity_ambient
|
||||
)
|
||||
if self.has_screen():
|
||||
self.spotlight = Spotlight(
|
||||
source_point=VectorizedPoint(location=self.get_source_point()),
|
||||
color=self.color,
|
||||
num_levels=self.num_levels,
|
||||
radius=self.radius,
|
||||
screen=self.screen,
|
||||
opacity_function=self.opacity_function,
|
||||
max_opacity=self.max_opacity_spotlight,
|
||||
camera_mob=self.camera_mob
|
||||
)
|
||||
else:
|
||||
self.spotlight = Spotlight()
|
||||
|
||||
self.shadow = VMobject(fill_color=SHADOW_COLOR,
|
||||
fill_opacity=1.0, stroke_color=BLACK)
|
||||
self.lighthouse.next_to(self.get_source_point(), DOWN, buff=0)
|
||||
self.ambient_light.move_source_to(self.get_source_point())
|
||||
|
||||
if self.has_screen():
|
||||
self.spotlight.move_source_to(self.get_source_point())
|
||||
self.update_shadow()
|
||||
|
||||
self.add(self.ambient_light, self.spotlight,
|
||||
self.lighthouse, self.shadow)
|
||||
|
||||
def has_screen(self):
|
||||
if self.screen is None:
|
||||
return False
|
||||
elif self.screen.get_num_points() == 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def dim_ambient(self):
|
||||
self.set_max_opacity_ambient(AMBIENT_DIMMED)
|
||||
|
||||
def set_max_opacity_ambient(self, new_opacity):
|
||||
self.max_opacity_ambient = new_opacity
|
||||
self.ambient_light.dimming(new_opacity)
|
||||
|
||||
def dim_spotlight(self):
|
||||
self.set_max_opacity_spotlight(SPOTLIGHT_DIMMED)
|
||||
|
||||
def set_max_opacity_spotlight(self, new_opacity):
|
||||
self.max_opacity_spotlight = new_opacity
|
||||
self.spotlight.dimming(new_opacity)
|
||||
|
||||
def set_camera_mob(self, new_cam_mob):
|
||||
self.camera_mob = new_cam_mob
|
||||
self.spotlight.camera_mob = new_cam_mob
|
||||
|
||||
def set_screen(self, new_screen):
|
||||
if self.has_screen():
|
||||
self.spotlight.screen = new_screen
|
||||
else:
|
||||
# Note: See below
|
||||
index = self.submobjects.index(self.spotlight)
|
||||
# camera_mob = self.spotlight.camera_mob
|
||||
self.remove(self.spotlight)
|
||||
self.spotlight = Spotlight(
|
||||
source_point=VectorizedPoint(location=self.get_source_point()),
|
||||
color=self.color,
|
||||
num_levels=self.num_levels,
|
||||
radius=self.radius,
|
||||
screen=new_screen,
|
||||
camera_mob=self.camera_mob,
|
||||
opacity_function=self.opacity_function,
|
||||
max_opacity=self.max_opacity_spotlight,
|
||||
)
|
||||
self.spotlight.move_source_to(self.get_source_point())
|
||||
|
||||
# Note: This line will make spotlight show up at the end
|
||||
# of the submojects list, which can make it show up on
|
||||
# top of the shadow. To make it show up in the
|
||||
# same spot, you could try the following line,
|
||||
# where "index" is what I defined above:
|
||||
self.submobjects.insert(index, self.spotlight)
|
||||
# self.add(self.spotlight)
|
||||
|
||||
# in any case
|
||||
self.screen = new_screen
|
||||
|
||||
def move_source_to(self, point):
|
||||
apoint = np.array(point)
|
||||
v = apoint - self.get_source_point()
|
||||
# Note: As discussed, things stand to behave better if source
|
||||
# point is a submobject, so that it automatically interpolates
|
||||
# during an animation, and other updates can be defined wrt
|
||||
# that source point's location
|
||||
self.source_point.set_location(apoint)
|
||||
# self.lighthouse.next_to(apoint,DOWN,buff = 0)
|
||||
# self.ambient_light.move_source_to(apoint)
|
||||
self.lighthouse.shift(v)
|
||||
# self.ambient_light.shift(v)
|
||||
self.ambient_light.move_source_to(apoint)
|
||||
if self.has_screen():
|
||||
self.spotlight.move_source_to(apoint)
|
||||
self.update()
|
||||
return self
|
||||
|
||||
def change_spotlight_opacity_function(self, new_of):
|
||||
self.spotlight.change_opacity_function(new_of)
|
||||
|
||||
def set_radius(self, new_radius):
|
||||
self.radius = new_radius
|
||||
self.ambient_light.radius = new_radius
|
||||
self.spotlight.radius = new_radius
|
||||
|
||||
def update(self):
|
||||
self.update_lighthouse()
|
||||
self.update_ambient()
|
||||
self.spotlight.update_sectors()
|
||||
self.update_shadow()
|
||||
|
||||
def update_lighthouse(self):
|
||||
self.lighthouse.move_to(self.get_source_point())
|
||||
# new_lh = Lighthouse()
|
||||
# new_lh.move_to(ORIGIN)
|
||||
# new_lh.apply_matrix(self.rotation_matrix())
|
||||
# new_lh.shift(self.get_source_point())
|
||||
# self.lighthouse.submobjects = new_lh.submobjects
|
||||
|
||||
def update_ambient(self):
|
||||
new_ambient_light = AmbientLight(
|
||||
source_point=VectorizedPoint(location=ORIGIN),
|
||||
color=self.color,
|
||||
num_levels=self.num_levels,
|
||||
radius=self.radius,
|
||||
opacity_function=self.opacity_function,
|
||||
max_opacity=self.max_opacity_ambient
|
||||
)
|
||||
new_ambient_light.apply_matrix(self.rotation_matrix())
|
||||
new_ambient_light.move_source_to(self.get_source_point())
|
||||
self.ambient_light.set_submobjects(new_ambient_light.submobjects)
|
||||
|
||||
def get_source_point(self):
|
||||
return self.source_point.get_location()
|
||||
|
||||
def rotation_matrix(self):
|
||||
|
||||
if self.camera_mob is None:
|
||||
return np.eye(3)
|
||||
|
||||
phi = self.camera_mob.get_center()[0]
|
||||
theta = self.camera_mob.get_center()[1]
|
||||
|
||||
R1 = np.array([
|
||||
[1, 0, 0],
|
||||
[0, np.cos(phi), -np.sin(phi)],
|
||||
[0, np.sin(phi), np.cos(phi)]
|
||||
])
|
||||
|
||||
R2 = np.array([
|
||||
[np.cos(theta + TAU / 4), -np.sin(theta + TAU / 4), 0],
|
||||
[np.sin(theta + TAU / 4), np.cos(theta + TAU / 4), 0],
|
||||
[0, 0, 1]
|
||||
])
|
||||
|
||||
R = np.dot(R2, R1)
|
||||
return R
|
||||
|
||||
def update_shadow(self):
|
||||
point = self.get_source_point()
|
||||
projected_screen_points = []
|
||||
if not self.has_screen():
|
||||
return
|
||||
for point in self.screen.get_anchors():
|
||||
projected_screen_points.append(self.spotlight.project(point))
|
||||
|
||||
projected_source = project_along_vector(
|
||||
self.get_source_point(), self.spotlight.projection_direction())
|
||||
|
||||
projected_point_cloud_3d = np.append(
|
||||
projected_screen_points,
|
||||
np.reshape(projected_source, (1, 3)),
|
||||
axis=0
|
||||
)
|
||||
# z_to_vector(self.spotlight.projection_direction())
|
||||
rotation_matrix = self.rotation_matrix()
|
||||
back_rotation_matrix = rotation_matrix.T # i. e. its inverse
|
||||
|
||||
rotated_point_cloud_3d = np.dot(
|
||||
projected_point_cloud_3d, back_rotation_matrix.T)
|
||||
# these points now should all have z = 0
|
||||
|
||||
point_cloud_2d = rotated_point_cloud_3d[:, :2]
|
||||
# now we can compute the convex hull
|
||||
hull_2d = ConvexHull(point_cloud_2d) # guaranteed to run ccw
|
||||
hull = []
|
||||
|
||||
# we also need the projected source point
|
||||
source_point_2d = np.dot(self.spotlight.project(
|
||||
self.get_source_point()), back_rotation_matrix.T)[:2]
|
||||
|
||||
index = 0
|
||||
for point in point_cloud_2d[hull_2d.vertices]:
|
||||
if np.all(np.abs(point - source_point_2d) < 1.0e-6):
|
||||
source_index = index
|
||||
index += 1
|
||||
continue
|
||||
point_3d = np.array([point[0], point[1], 0])
|
||||
hull.append(point_3d)
|
||||
index += 1
|
||||
|
||||
hull_mobject = VMobject()
|
||||
hull_mobject.set_points_as_corners(hull)
|
||||
hull_mobject.apply_matrix(rotation_matrix)
|
||||
|
||||
anchors = hull_mobject.get_anchors()
|
||||
|
||||
# add two control points for the outer cone
|
||||
if np.size(anchors) == 0:
|
||||
self.shadow.resize_points(0)
|
||||
return
|
||||
|
||||
ray1 = anchors[source_index - 1] - projected_source
|
||||
ray1 = ray1 / get_norm(ray1) * 100
|
||||
|
||||
ray2 = anchors[source_index] - projected_source
|
||||
ray2 = ray2 / get_norm(ray2) * 100
|
||||
outpoint1 = anchors[source_index - 1] + ray1
|
||||
outpoint2 = anchors[source_index] + ray2
|
||||
|
||||
new_anchors = anchors[:source_index]
|
||||
new_anchors = np.append(new_anchors, np.array(
|
||||
[outpoint1, outpoint2]), axis=0)
|
||||
new_anchors = np.append(new_anchors, anchors[source_index:], axis=0)
|
||||
self.shadow.set_points_as_corners(new_anchors)
|
||||
|
||||
# shift it closer to the camera so it is in front of the spotlight
|
||||
self.shadow.mark_paths_closed = True
|
||||
|
||||
|
||||
# Redefining what was once a ContinualAnimation class
|
||||
# as a function
|
||||
def ScreenTracker(light_source):
|
||||
light_source.add_updater(lambda m: m.update())
|
||||
return light_source
|
||||
@@ -1,141 +0,0 @@
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.creation import ShowCreation
|
||||
from manimlib.animation.fading import FadeOut
|
||||
from manimlib.animation.transform import ApplyMethod
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.matrix import Matrix
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class NumericalMatrixMultiplication(Scene):
|
||||
CONFIG = {
|
||||
"left_matrix": [[1, 2], [3, 4]],
|
||||
"right_matrix": [[5, 6], [7, 8]],
|
||||
"use_parens": True,
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
left_string_matrix, right_string_matrix = [
|
||||
np.array(matrix).astype("string")
|
||||
for matrix in (self.left_matrix, self.right_matrix)
|
||||
]
|
||||
if right_string_matrix.shape[0] != left_string_matrix.shape[1]:
|
||||
raise Exception("Incompatible shapes for matrix multiplication")
|
||||
|
||||
left = Matrix(left_string_matrix)
|
||||
right = Matrix(right_string_matrix)
|
||||
result = self.get_result_matrix(
|
||||
left_string_matrix, right_string_matrix
|
||||
)
|
||||
|
||||
self.organize_matrices(left, right, result)
|
||||
self.animate_product(left, right, result)
|
||||
|
||||
def get_result_matrix(self, left, right):
|
||||
(m, k), n = left.shape, right.shape[1]
|
||||
mob_matrix = np.array([VGroup()]).repeat(m * n).reshape((m, n))
|
||||
for a in range(m):
|
||||
for b in range(n):
|
||||
template = "(%s)(%s)" if self.use_parens else "%s%s"
|
||||
parts = [
|
||||
prefix + template % (left[a][c], right[c][b])
|
||||
for c in range(k)
|
||||
for prefix in ["" if c == 0 else "+"]
|
||||
]
|
||||
mob_matrix[a][b] = Tex(parts, next_to_buff=0.1)
|
||||
return Matrix(mob_matrix)
|
||||
|
||||
def add_lines(self, left, right):
|
||||
line_kwargs = {
|
||||
"color": BLUE,
|
||||
"stroke_width": 2,
|
||||
}
|
||||
left_rows = [
|
||||
VGroup(*row) for row in left.get_mob_matrix()
|
||||
]
|
||||
h_lines = VGroup()
|
||||
for row in left_rows[:-1]:
|
||||
h_line = Line(row.get_left(), row.get_right(), **line_kwargs)
|
||||
h_line.next_to(row, DOWN, buff=left.v_buff / 2.)
|
||||
h_lines.add(h_line)
|
||||
|
||||
right_cols = [
|
||||
VGroup(*col) for col in np.transpose(right.get_mob_matrix())
|
||||
]
|
||||
v_lines = VGroup()
|
||||
for col in right_cols[:-1]:
|
||||
v_line = Line(col.get_top(), col.get_bottom(), **line_kwargs)
|
||||
v_line.next_to(col, RIGHT, buff=right.h_buff / 2.)
|
||||
v_lines.add(v_line)
|
||||
|
||||
self.play(ShowCreation(h_lines))
|
||||
self.play(ShowCreation(v_lines))
|
||||
self.wait()
|
||||
self.show_frame()
|
||||
|
||||
def organize_matrices(self, left, right, result):
|
||||
equals = Tex("=")
|
||||
everything = VGroup(left, right, equals, result)
|
||||
everything.arrange()
|
||||
everything.set_width(FRAME_WIDTH - 1)
|
||||
self.add(everything)
|
||||
|
||||
def animate_product(self, left, right, result):
|
||||
l_matrix = left.get_mob_matrix()
|
||||
r_matrix = right.get_mob_matrix()
|
||||
result_matrix = result.get_mob_matrix()
|
||||
circle = Circle(
|
||||
radius=l_matrix[0][0].get_height(),
|
||||
color=GREEN
|
||||
)
|
||||
circles = VGroup(*[
|
||||
entry.get_point_mobject()
|
||||
for entry in (l_matrix[0][0], r_matrix[0][0])
|
||||
])
|
||||
(m, k), n = l_matrix.shape, r_matrix.shape[1]
|
||||
for mob in result_matrix.flatten():
|
||||
mob.set_color(BLACK)
|
||||
lagging_anims = []
|
||||
for a in range(m):
|
||||
for b in range(n):
|
||||
for c in range(k):
|
||||
l_matrix[a][c].set_color(YELLOW)
|
||||
r_matrix[c][b].set_color(YELLOW)
|
||||
for c in range(k):
|
||||
start_parts = VGroup(
|
||||
l_matrix[a][c].copy(),
|
||||
r_matrix[c][b].copy()
|
||||
)
|
||||
result_entry = result_matrix[a][b].split()[c]
|
||||
|
||||
new_circles = VGroup(*[
|
||||
circle.copy().shift(part.get_center())
|
||||
for part in start_parts.split()
|
||||
])
|
||||
self.play(Transform(circles, new_circles))
|
||||
self.play(
|
||||
Transform(
|
||||
start_parts,
|
||||
result_entry.copy().set_color(YELLOW),
|
||||
path_arc=-np.pi / 2,
|
||||
lag_ratio=0,
|
||||
),
|
||||
*lagging_anims
|
||||
)
|
||||
result_entry.set_color(YELLOW)
|
||||
self.remove(start_parts)
|
||||
lagging_anims = [
|
||||
ApplyMethod(result_entry.set_color, WHITE)
|
||||
]
|
||||
|
||||
for c in range(k):
|
||||
l_matrix[a][c].set_color(WHITE)
|
||||
r_matrix[c][b].set_color(WHITE)
|
||||
self.play(FadeOut(circles), *lagging_anims)
|
||||
self.wait()
|
||||
@@ -1,66 +0,0 @@
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class ReconfigurableScene(Scene):
|
||||
"""
|
||||
Note, this seems to no longer work as intented.
|
||||
"""
|
||||
CONFIG = {
|
||||
"allow_recursion": True,
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
self.states = []
|
||||
self.num_recursions = 0
|
||||
|
||||
def transition_to_alt_config(
|
||||
self,
|
||||
return_to_original_configuration=True,
|
||||
transformation_kwargs=None,
|
||||
**new_config
|
||||
):
|
||||
if transformation_kwargs is None:
|
||||
transformation_kwargs = {}
|
||||
original_state = self.get_state()
|
||||
state_copy = original_state.copy()
|
||||
self.states.append(state_copy)
|
||||
if not self.allow_recursion:
|
||||
return
|
||||
alt_scene = self.__class__(
|
||||
skip_animations=True,
|
||||
allow_recursion=False,
|
||||
**new_config
|
||||
)
|
||||
alt_state = alt_scene.states[len(self.states) - 1]
|
||||
|
||||
if return_to_original_configuration:
|
||||
self.clear()
|
||||
self.transition_between_states(
|
||||
state_copy, alt_state,
|
||||
**transformation_kwargs
|
||||
)
|
||||
self.transition_between_states(
|
||||
state_copy, original_state,
|
||||
**transformation_kwargs
|
||||
)
|
||||
self.clear()
|
||||
self.add(*original_state)
|
||||
else:
|
||||
self.transition_between_states(
|
||||
original_state, alt_state,
|
||||
**transformation_kwargs
|
||||
)
|
||||
self.__dict__.update(new_config)
|
||||
|
||||
def get_state(self):
|
||||
# Want to return a mobject that maintains the most
|
||||
# structure. The way to do that is to extract only
|
||||
# those that aren't inside another.
|
||||
return Mobject(*self.get_top_level_mobjects())
|
||||
|
||||
def transition_between_states(self, start_state, target_state, **kwargs):
|
||||
self.play(Transform(start_state, target_state, **kwargs))
|
||||
self.wait()
|
||||
@@ -1,107 +0,0 @@
|
||||
from copy import deepcopy
|
||||
import itertools as it
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import adjacent_pairs
|
||||
|
||||
# Warning: This is all now pretty deprecated, and should not be expected to work
|
||||
|
||||
|
||||
class Region(Mobject):
|
||||
CONFIG = {
|
||||
"display_mode": "region"
|
||||
}
|
||||
|
||||
def __init__(self, condition=(lambda x, y: True), **kwargs):
|
||||
"""
|
||||
Condition must be a function which takes in two real
|
||||
arrays (representing x and y values of space respectively)
|
||||
and return a boolean array. This can essentially look like
|
||||
a function from R^2 to {True, False}, but & and | must be
|
||||
used in place of "and" and "or"
|
||||
"""
|
||||
Mobject.__init__(self, **kwargs)
|
||||
self.condition = condition
|
||||
|
||||
def _combine(self, region, op):
|
||||
self.condition = lambda x, y: op(
|
||||
self.condition(x, y),
|
||||
region.condition(x, y)
|
||||
)
|
||||
|
||||
def union(self, region):
|
||||
self._combine(region, lambda bg1, bg2: bg1 | bg2)
|
||||
return self
|
||||
|
||||
def intersect(self, region):
|
||||
self._combine(region, lambda bg1, bg2: bg1 & bg2)
|
||||
return self
|
||||
|
||||
def complement(self):
|
||||
self.bool_grid = ~self.bool_grid
|
||||
return self
|
||||
|
||||
|
||||
class HalfPlane(Region):
|
||||
def __init__(self, point_pair, upper_left=True, *args, **kwargs):
|
||||
"""
|
||||
point_pair of the form [(x_0, y_0,...), (x_1, y_1,...)]
|
||||
|
||||
Pf upper_left is True, the side of the region will be
|
||||
everything on the upper left side of the line through
|
||||
the point pair
|
||||
"""
|
||||
if not upper_left:
|
||||
point_pair = list(point_pair)
|
||||
point_pair.reverse()
|
||||
(x0, y0), (x1, y1) = point_pair[0][:2], point_pair[1][:2]
|
||||
|
||||
def condition(x, y):
|
||||
return (x1 - x0) * (y - y0) > (y1 - y0) * (x - x0)
|
||||
Region.__init__(self, condition, *args, **kwargs)
|
||||
|
||||
|
||||
def region_from_line_boundary(*lines, **kwargs):
|
||||
reg = Region(**kwargs)
|
||||
for line in lines:
|
||||
reg.intersect(HalfPlane(line, **kwargs))
|
||||
return reg
|
||||
|
||||
|
||||
def region_from_polygon_vertices(*vertices, **kwargs):
|
||||
return region_from_line_boundary(*adjacent_pairs(vertices), **kwargs)
|
||||
|
||||
|
||||
def plane_partition(*lines, **kwargs):
|
||||
"""
|
||||
A 'line' is a pair of points [(x0, y0,...), (x1, y1,...)]
|
||||
|
||||
Returns the list of regions of the plane cut out by
|
||||
these lines
|
||||
"""
|
||||
result = []
|
||||
half_planes = [HalfPlane(line, **kwargs) for line in lines]
|
||||
complements = [deepcopy(hp).complement() for hp in half_planes]
|
||||
num_lines = len(lines)
|
||||
for bool_list in it.product(*[[True, False]] * num_lines):
|
||||
reg = Region(**kwargs)
|
||||
for i in range(num_lines):
|
||||
if bool_list[i]:
|
||||
reg.intersect(half_planes[i])
|
||||
else:
|
||||
reg.intersect(complements[i])
|
||||
if reg.bool_grid.any():
|
||||
result.append(reg)
|
||||
return result
|
||||
|
||||
|
||||
def plane_partition_from_points(*points, **kwargs):
|
||||
"""
|
||||
Returns list of regions cut out by the complete graph
|
||||
with points from the argument as vertices.
|
||||
|
||||
Each point comes in the form (x, y)
|
||||
"""
|
||||
lines = [[p1, p2] for (p1, p2) in it.combinations(points, 2)]
|
||||
return plane_partition(*lines, **kwargs)
|
||||
638
manimlib/scene/interactive_scene.py
Normal file
638
manimlib/scene/interactive_scene.py
Normal file
@@ -0,0 +1,638 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
import numpy as np
|
||||
import pyperclip
|
||||
from IPython.core.getipython import get_ipython
|
||||
from pyglet.window import key as PygletWindowKeys
|
||||
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
|
||||
from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF
|
||||
from manimlib.constants import PI
|
||||
from manimlib.constants import DEG
|
||||
from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import Square
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.dot_cloud import DotCloud
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VHighlight
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.scene.scene import SceneState
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.tex_file_writing import LatexError
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
|
||||
SELECT_KEY = manim_config.key_bindings.select
|
||||
UNSELECT_KEY = manim_config.key_bindings.unselect
|
||||
GRAB_KEY = manim_config.key_bindings.grab
|
||||
X_GRAB_KEY = manim_config.key_bindings.x_grab
|
||||
Y_GRAB_KEY = manim_config.key_bindings.y_grab
|
||||
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
|
||||
RESIZE_KEY = manim_config.key_bindings.resize # TODO
|
||||
COLOR_KEY = manim_config.key_bindings.color
|
||||
INFORMATION_KEY = manim_config.key_bindings.information
|
||||
CURSOR_KEY = manim_config.key_bindings.cursor
|
||||
|
||||
# For keyboard interactions
|
||||
|
||||
ARROW_SYMBOLS: list[int] = [
|
||||
PygletWindowKeys.LEFT,
|
||||
PygletWindowKeys.UP,
|
||||
PygletWindowKeys.RIGHT,
|
||||
PygletWindowKeys.DOWN,
|
||||
]
|
||||
|
||||
ALL_MODIFIERS = PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_SHIFT
|
||||
|
||||
# Note, a lot of the functionality here is still buggy and very much a work in progress.
|
||||
|
||||
|
||||
class InteractiveScene(Scene):
|
||||
"""
|
||||
To select mobjects on screen, hold ctrl and move the mouse to highlight a region,
|
||||
or just tap ctrl to select the mobject under the cursor.
|
||||
|
||||
Pressing command + t will toggle between modes where you either select top level
|
||||
mobjects part of the scene, or low level pieces.
|
||||
|
||||
Hold 'g' to grab the selection and move it around
|
||||
Hold 'h' to drag it constrained in the horizontal direction
|
||||
Hold 'v' to drag it constrained in the vertical direction
|
||||
Hold 't' to resize selection, adding 'shift' to resize with respect to a corner
|
||||
|
||||
Command + 'c' copies the ids of selections to clipboard
|
||||
Command + 'v' will paste either:
|
||||
- The copied mobject
|
||||
- A Tex mobject based on copied LaTeX
|
||||
- A Text mobject based on copied Text
|
||||
Command + 'z' restores selection back to its original state
|
||||
Command + 's' saves the selected mobjects to file
|
||||
"""
|
||||
corner_dot_config = dict(
|
||||
color=WHITE,
|
||||
radius=0.05,
|
||||
glow_factor=2.0,
|
||||
)
|
||||
selection_rectangle_stroke_color = WHITE
|
||||
selection_rectangle_stroke_width = 1.0
|
||||
palette_colors = MANIM_COLORS
|
||||
selection_nudge_size = 0.05
|
||||
cursor_location_config = dict(
|
||||
font_size=24,
|
||||
fill_color=GREY_C,
|
||||
num_decimal_places=3,
|
||||
)
|
||||
time_label_config = dict(
|
||||
font_size=24,
|
||||
fill_color=GREY_C,
|
||||
num_decimal_places=1,
|
||||
)
|
||||
crosshair_width = 0.2
|
||||
crosshair_style = dict(
|
||||
stroke_color=GREY_A,
|
||||
stroke_width=[3, 0, 3],
|
||||
)
|
||||
|
||||
def setup(self):
|
||||
self.selection = Group()
|
||||
self.selection_highlight = self.get_selection_highlight()
|
||||
self.selection_rectangle = self.get_selection_rectangle()
|
||||
self.crosshair = self.get_crosshair()
|
||||
self.information_label = self.get_information_label()
|
||||
self.color_palette = self.get_color_palette()
|
||||
self.unselectables = [
|
||||
self.selection,
|
||||
self.selection_highlight,
|
||||
self.selection_rectangle,
|
||||
self.crosshair,
|
||||
self.information_label,
|
||||
self.camera.frame
|
||||
]
|
||||
self.select_top_level_mobs = True
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
self.is_selecting = False
|
||||
self.is_grabbing = False
|
||||
|
||||
self.add(self.selection_highlight)
|
||||
|
||||
def get_selection_rectangle(self):
|
||||
rect = Rectangle(
|
||||
stroke_color=self.selection_rectangle_stroke_color,
|
||||
stroke_width=self.selection_rectangle_stroke_width,
|
||||
)
|
||||
rect.fix_in_frame()
|
||||
rect.fixed_corner = ORIGIN
|
||||
rect.add_updater(self.update_selection_rectangle)
|
||||
return rect
|
||||
|
||||
def update_selection_rectangle(self, rect: Rectangle):
|
||||
p1 = rect.fixed_corner
|
||||
p2 = self.frame.to_fixed_frame_point(self.mouse_point.get_center())
|
||||
rect.set_points_as_corners([
|
||||
p1, np.array([p2[0], p1[1], 0]),
|
||||
p2, np.array([p1[0], p2[1], 0]),
|
||||
p1,
|
||||
])
|
||||
return rect
|
||||
|
||||
def get_selection_highlight(self):
|
||||
result = Group()
|
||||
result.tracked_mobjects = []
|
||||
result.add_updater(self.update_selection_highlight)
|
||||
return result
|
||||
|
||||
def update_selection_highlight(self, highlight: Mobject):
|
||||
if set(highlight.tracked_mobjects) == set(self.selection):
|
||||
return
|
||||
|
||||
# Otherwise, refresh contents of highlight
|
||||
highlight.tracked_mobjects = list(self.selection)
|
||||
highlight.set_submobjects([
|
||||
self.get_highlight(mob)
|
||||
for mob in self.selection
|
||||
])
|
||||
try:
|
||||
index = min((
|
||||
i for i, mob in enumerate(self.mobjects)
|
||||
for sm in self.selection
|
||||
if sm in mob.get_family()
|
||||
))
|
||||
self.mobjects.remove(highlight)
|
||||
self.mobjects.insert(index - 1, highlight)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def get_crosshair(self):
|
||||
lines = VMobject().replicate(2)
|
||||
lines[0].set_points([LEFT, ORIGIN, RIGHT])
|
||||
lines[1].set_points([UP, ORIGIN, DOWN])
|
||||
crosshair = VGroup(*lines)
|
||||
|
||||
crosshair.set_width(self.crosshair_width)
|
||||
crosshair.set_style(**self.crosshair_style)
|
||||
crosshair.set_animating_status(True)
|
||||
crosshair.fix_in_frame()
|
||||
return crosshair
|
||||
|
||||
def get_color_palette(self):
|
||||
palette = VGroup(*(
|
||||
Square(fill_color=color, fill_opacity=1, side_length=1)
|
||||
for color in self.palette_colors
|
||||
))
|
||||
palette.set_stroke(width=0)
|
||||
palette.arrange(RIGHT, buff=0.5)
|
||||
palette.set_width(FRAME_WIDTH - 0.5)
|
||||
palette.to_edge(DOWN, buff=SMALL_BUFF)
|
||||
palette.fix_in_frame()
|
||||
return palette
|
||||
|
||||
def get_information_label(self):
|
||||
loc_label = VGroup(*(
|
||||
DecimalNumber(**self.cursor_location_config)
|
||||
for n in range(3)
|
||||
))
|
||||
|
||||
def update_coords(loc_label):
|
||||
for mob, coord in zip(loc_label, self.mouse_point.get_location()):
|
||||
mob.set_value(coord)
|
||||
loc_label.arrange(RIGHT, buff=loc_label.get_height())
|
||||
loc_label.to_corner(DR, buff=SMALL_BUFF)
|
||||
loc_label.fix_in_frame()
|
||||
return loc_label
|
||||
|
||||
loc_label.add_updater(update_coords)
|
||||
|
||||
time_label = DecimalNumber(0, **self.time_label_config)
|
||||
time_label.to_corner(DL, buff=SMALL_BUFF)
|
||||
time_label.fix_in_frame()
|
||||
time_label.add_updater(lambda m, dt: m.increment_value(dt))
|
||||
|
||||
return VGroup(loc_label, time_label)
|
||||
|
||||
# Overrides
|
||||
def get_state(self):
|
||||
return SceneState(self, ignore=[
|
||||
self.selection_highlight,
|
||||
self.selection_rectangle,
|
||||
self.crosshair,
|
||||
])
|
||||
|
||||
def restore_state(self, scene_state: SceneState):
|
||||
super().restore_state(scene_state)
|
||||
self.mobjects.insert(0, self.selection_highlight)
|
||||
|
||||
def add(self, *mobjects: Mobject):
|
||||
super().add(*mobjects)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
def remove(self, *mobjects: Mobject):
|
||||
super().remove(*mobjects)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
# Related to selection
|
||||
|
||||
def toggle_selection_mode(self):
|
||||
self.select_top_level_mobs = not self.select_top_level_mobs
|
||||
self.refresh_selection_scope()
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
def get_selection_search_set(self) -> list[Mobject]:
|
||||
return self.selection_search_set
|
||||
|
||||
def regenerate_selection_search_set(self):
|
||||
selectable = list(filter(
|
||||
lambda m: m not in self.unselectables,
|
||||
self.mobjects
|
||||
))
|
||||
if self.select_top_level_mobs:
|
||||
self.selection_search_set = selectable
|
||||
else:
|
||||
self.selection_search_set = [
|
||||
submob
|
||||
for mob in selectable
|
||||
for submob in mob.family_members_with_points()
|
||||
]
|
||||
|
||||
def refresh_selection_scope(self):
|
||||
curr = list(self.selection)
|
||||
if self.select_top_level_mobs:
|
||||
self.selection.set_submobjects([
|
||||
mob
|
||||
for mob in self.mobjects
|
||||
if any(sm in mob.get_family() for sm in curr)
|
||||
])
|
||||
self.selection.refresh_bounding_box(recurse_down=True)
|
||||
else:
|
||||
self.selection.set_submobjects(
|
||||
extract_mobject_family_members(
|
||||
curr, exclude_pointless=True,
|
||||
)
|
||||
)
|
||||
|
||||
def get_corner_dots(self, mobject: Mobject) -> Mobject:
|
||||
dots = DotCloud(**self.corner_dot_config)
|
||||
radius = float(self.corner_dot_config["radius"])
|
||||
if mobject.get_depth() < 1e-2:
|
||||
vects = [DL, UL, UR, DR]
|
||||
else:
|
||||
vects = np.array(list(it.product(*3 * [[-1, 1]])))
|
||||
dots.add_updater(lambda d: d.set_points([
|
||||
mobject.get_corner(v) + v * radius
|
||||
for v in vects
|
||||
]))
|
||||
return dots
|
||||
|
||||
def get_highlight(self, mobject: Mobject) -> Mobject:
|
||||
if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs:
|
||||
length = max([mobject.get_height(), mobject.get_width()])
|
||||
result = VHighlight(
|
||||
mobject,
|
||||
max_stroke_addition=min([50 * length, 10]),
|
||||
)
|
||||
result.add_updater(lambda m: m.replace(mobject, stretch=True))
|
||||
return result
|
||||
elif isinstance(mobject, DotCloud):
|
||||
return Mobject()
|
||||
else:
|
||||
return self.get_corner_dots(mobject)
|
||||
|
||||
def add_to_selection(self, *mobjects: Mobject):
|
||||
mobs = list(filter(
|
||||
lambda m: m not in self.unselectables and m not in self.selection,
|
||||
mobjects
|
||||
))
|
||||
if len(mobs) == 0:
|
||||
return
|
||||
self.selection.add(*mobs)
|
||||
for mob in mobs:
|
||||
mob.set_animating_status(True)
|
||||
|
||||
def toggle_from_selection(self, *mobjects: Mobject):
|
||||
for mob in mobjects:
|
||||
if mob in self.selection:
|
||||
self.selection.remove(mob)
|
||||
mob.set_animating_status(False)
|
||||
mob.refresh_bounding_box()
|
||||
else:
|
||||
self.add_to_selection(mob)
|
||||
|
||||
def clear_selection(self):
|
||||
for mob in self.selection:
|
||||
mob.set_animating_status(False)
|
||||
mob.refresh_bounding_box()
|
||||
self.selection.set_submobjects([])
|
||||
|
||||
def disable_interaction(self, *mobjects: Mobject):
|
||||
for mob in mobjects:
|
||||
for sm in mob.get_family():
|
||||
self.unselectables.append(sm)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
def enable_interaction(self, *mobjects: Mobject):
|
||||
for mob in mobjects:
|
||||
for sm in mob.get_family():
|
||||
if sm in self.unselectables:
|
||||
self.unselectables.remove(sm)
|
||||
|
||||
# Functions for keyboard actions
|
||||
|
||||
def copy_selection(self):
|
||||
names = []
|
||||
shell = get_ipython()
|
||||
for mob in self.selection:
|
||||
name = str(id(mob))
|
||||
if shell is None:
|
||||
continue
|
||||
for key, value in shell.user_ns.items():
|
||||
if mob is value:
|
||||
name = key
|
||||
names.append(name)
|
||||
pyperclip.copy(", ".join(names))
|
||||
|
||||
def paste_selection(self):
|
||||
clipboard_str = pyperclip.paste()
|
||||
# Try pasting a mobject
|
||||
try:
|
||||
ids = map(int, clipboard_str.split(","))
|
||||
mobs = map(self.id_to_mobject, ids)
|
||||
mob_copies = [m.copy() for m in mobs if m is not None]
|
||||
self.clear_selection()
|
||||
self.play(*(
|
||||
FadeIn(mc, run_time=0.5, scale=1.5)
|
||||
for mc in mob_copies
|
||||
))
|
||||
self.add_to_selection(*mob_copies)
|
||||
return
|
||||
except ValueError:
|
||||
pass
|
||||
# Otherwise, treat as tex or text
|
||||
if set("\\^=+").intersection(clipboard_str): # Proxy to text for LaTeX
|
||||
try:
|
||||
new_mob = Tex(clipboard_str)
|
||||
except LatexError:
|
||||
return
|
||||
else:
|
||||
new_mob = Text(clipboard_str)
|
||||
self.clear_selection()
|
||||
self.add(new_mob)
|
||||
self.add_to_selection(new_mob)
|
||||
|
||||
def delete_selection(self):
|
||||
self.remove(*self.selection)
|
||||
self.clear_selection()
|
||||
|
||||
def enable_selection(self):
|
||||
self.is_selecting = True
|
||||
self.add(self.selection_rectangle)
|
||||
self.selection_rectangle.fixed_corner = self.frame.to_fixed_frame_point(
|
||||
self.mouse_point.get_center()
|
||||
)
|
||||
|
||||
def gather_new_selection(self):
|
||||
self.is_selecting = False
|
||||
if self.selection_rectangle in self.mobjects:
|
||||
self.remove(self.selection_rectangle)
|
||||
additions = []
|
||||
for mob in reversed(self.get_selection_search_set()):
|
||||
if self.selection_rectangle.is_touching(mob):
|
||||
additions.append(mob)
|
||||
if self.selection_rectangle.get_arc_length() < 1e-2:
|
||||
break
|
||||
self.toggle_from_selection(*additions)
|
||||
|
||||
def prepare_grab(self):
|
||||
mp = self.mouse_point.get_center()
|
||||
self.mouse_to_selection = mp - self.selection.get_center()
|
||||
self.is_grabbing = True
|
||||
|
||||
def prepare_resizing(self, about_corner=False):
|
||||
center = self.selection.get_center()
|
||||
mp = self.mouse_point.get_center()
|
||||
if about_corner:
|
||||
self.scale_about_point = self.selection.get_corner(center - mp)
|
||||
else:
|
||||
self.scale_about_point = center
|
||||
self.scale_ref_vect = mp - self.scale_about_point
|
||||
self.scale_ref_width = self.selection.get_width()
|
||||
self.scale_ref_height = self.selection.get_height()
|
||||
|
||||
def toggle_color_palette(self):
|
||||
if len(self.selection) == 0:
|
||||
return
|
||||
if self.color_palette not in self.mobjects:
|
||||
self.save_state()
|
||||
self.add(self.color_palette)
|
||||
else:
|
||||
self.remove(self.color_palette)
|
||||
|
||||
def display_information(self, show=True):
|
||||
if show:
|
||||
self.add(self.information_label)
|
||||
else:
|
||||
self.remove(self.information_label)
|
||||
|
||||
def group_selection(self):
|
||||
group = self.get_group(*self.selection)
|
||||
self.add(group)
|
||||
self.clear_selection()
|
||||
self.add_to_selection(group)
|
||||
|
||||
def ungroup_selection(self):
|
||||
pieces = []
|
||||
for mob in list(self.selection):
|
||||
self.remove(mob)
|
||||
pieces.extend(list(mob))
|
||||
self.clear_selection()
|
||||
self.add(*pieces)
|
||||
self.add_to_selection(*pieces)
|
||||
|
||||
def nudge_selection(self, vect: np.ndarray, large: bool = False):
|
||||
nudge = self.selection_nudge_size
|
||||
if large:
|
||||
nudge *= 10
|
||||
self.selection.shift(nudge * vect)
|
||||
|
||||
# Key actions
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
super().on_key_press(symbol, modifiers)
|
||||
char = chr(symbol)
|
||||
if char == SELECT_KEY and (modifiers & ALL_MODIFIERS) == 0:
|
||||
self.enable_selection()
|
||||
if char == UNSELECT_KEY:
|
||||
self.clear_selection()
|
||||
elif char in GRAB_KEYS and (modifiers & ALL_MODIFIERS) == 0:
|
||||
self.prepare_grab()
|
||||
elif char == RESIZE_KEY and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||
self.prepare_resizing(about_corner=((modifiers & PygletWindowKeys.MOD_SHIFT) > 0))
|
||||
elif symbol == PygletWindowKeys.LSHIFT:
|
||||
if self.window.is_key_pressed(ord("t")):
|
||||
self.prepare_resizing(about_corner=True)
|
||||
elif char == COLOR_KEY and (modifiers & ALL_MODIFIERS) == 0:
|
||||
self.toggle_color_palette()
|
||||
elif char == INFORMATION_KEY and (modifiers & ALL_MODIFIERS) == 0:
|
||||
self.display_information()
|
||||
elif char == "c" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.copy_selection()
|
||||
elif char == "v" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.paste_selection()
|
||||
elif char == "x" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.copy_selection()
|
||||
self.delete_selection()
|
||||
elif symbol == PygletWindowKeys.BACKSPACE:
|
||||
self.delete_selection()
|
||||
elif char == "a" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.clear_selection()
|
||||
self.add_to_selection(*self.mobjects)
|
||||
elif char == "g" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.group_selection()
|
||||
elif char == "g" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_SHIFT)):
|
||||
self.ungroup_selection()
|
||||
elif char == "t" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||
self.toggle_selection_mode()
|
||||
elif char == "d" and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||
self.copy_frame_positioning()
|
||||
elif char == "c" and (modifiers & PygletWindowKeys.MOD_SHIFT):
|
||||
self.copy_cursor_position()
|
||||
elif symbol in ARROW_SYMBOLS:
|
||||
self.nudge_selection(
|
||||
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
|
||||
large=(modifiers & PygletWindowKeys.MOD_SHIFT),
|
||||
)
|
||||
# Adding crosshair
|
||||
if char == CURSOR_KEY:
|
||||
if self.crosshair in self.mobjects:
|
||||
self.remove(self.crosshair)
|
||||
else:
|
||||
self.add(self.crosshair)
|
||||
if char == SELECT_KEY:
|
||||
self.add(self.crosshair)
|
||||
|
||||
# Conditions for saving state
|
||||
if char in [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY, RESIZE_KEY]:
|
||||
self.save_state()
|
||||
|
||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||
super().on_key_release(symbol, modifiers)
|
||||
if chr(symbol) == SELECT_KEY:
|
||||
self.gather_new_selection()
|
||||
if chr(symbol) in GRAB_KEYS:
|
||||
self.is_grabbing = False
|
||||
elif chr(symbol) == INFORMATION_KEY:
|
||||
self.display_information(False)
|
||||
elif symbol == PygletWindowKeys.LSHIFT and self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
self.prepare_resizing(about_corner=False)
|
||||
|
||||
# Mouse actions
|
||||
def handle_grabbing(self, point: Vect3):
|
||||
diff = point - self.mouse_to_selection
|
||||
if self.window.is_key_pressed(ord(GRAB_KEY)):
|
||||
self.selection.move_to(diff)
|
||||
elif self.window.is_key_pressed(ord(X_GRAB_KEY)):
|
||||
self.selection.set_x(diff[0])
|
||||
elif self.window.is_key_pressed(ord(Y_GRAB_KEY)):
|
||||
self.selection.set_y(diff[1])
|
||||
|
||||
def handle_resizing(self, point: Vect3):
|
||||
if not hasattr(self, "scale_about_point"):
|
||||
return
|
||||
vect = point - self.scale_about_point
|
||||
if self.window.is_key_pressed(PygletWindowKeys.LCTRL):
|
||||
for i in (0, 1):
|
||||
scalar = vect[i] / self.scale_ref_vect[i]
|
||||
self.selection.rescale_to_fit(
|
||||
scalar * [self.scale_ref_width, self.scale_ref_height][i],
|
||||
dim=i,
|
||||
about_point=self.scale_about_point,
|
||||
stretch=True,
|
||||
)
|
||||
else:
|
||||
scalar = get_norm(vect) / get_norm(self.scale_ref_vect)
|
||||
self.selection.set_width(
|
||||
scalar * self.scale_ref_width,
|
||||
about_point=self.scale_about_point
|
||||
)
|
||||
|
||||
def handle_sweeping_selection(self, point: Vect3):
|
||||
mob = self.point_to_mobject(
|
||||
point,
|
||||
search_set=self.get_selection_search_set(),
|
||||
buff=SMALL_BUFF
|
||||
)
|
||||
if mob is not None:
|
||||
self.add_to_selection(mob)
|
||||
|
||||
def choose_color(self, point: Vect3):
|
||||
# Search through all mobject on the screen, not just the palette
|
||||
to_search = [
|
||||
sm
|
||||
for mobject in self.mobjects
|
||||
for sm in mobject.family_members_with_points()
|
||||
if mobject not in self.unselectables
|
||||
]
|
||||
mob = self.point_to_mobject(point, to_search)
|
||||
if mob is not None:
|
||||
self.selection.set_color(mob.get_color())
|
||||
self.remove(self.color_palette)
|
||||
|
||||
def on_mouse_motion(self, point: Vect3, d_point: Vect3) -> None:
|
||||
super().on_mouse_motion(point, d_point)
|
||||
self.crosshair.move_to(self.frame.to_fixed_frame_point(point))
|
||||
if self.is_grabbing:
|
||||
self.handle_grabbing(point)
|
||||
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
self.handle_resizing(point)
|
||||
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(PygletWindowKeys.LSHIFT):
|
||||
self.handle_sweeping_selection(point)
|
||||
|
||||
def on_mouse_drag(
|
||||
self,
|
||||
point: Vect3,
|
||||
d_point: Vect3,
|
||||
buttons: int,
|
||||
modifiers: int
|
||||
) -> None:
|
||||
super().on_mouse_drag(point, d_point, buttons, modifiers)
|
||||
self.crosshair.move_to(self.frame.to_fixed_frame_point(point))
|
||||
|
||||
def on_mouse_release(self, point: Vect3, button: int, mods: int) -> None:
|
||||
super().on_mouse_release(point, button, mods)
|
||||
if self.color_palette in self.mobjects:
|
||||
self.choose_color(point)
|
||||
else:
|
||||
self.clear_selection()
|
||||
|
||||
# Copying code to recreate state
|
||||
def copy_frame_positioning(self):
|
||||
frame = self.frame
|
||||
center = frame.get_center()
|
||||
height = frame.get_height()
|
||||
angles = frame.get_euler_angles()
|
||||
|
||||
call = f"reorient("
|
||||
theta, phi, gamma = (angles / DEG).astype(int)
|
||||
call += f"{theta}, {phi}, {gamma}"
|
||||
if any(center != 0):
|
||||
call += f", {tuple(np.round(center, 2))}"
|
||||
if height != FRAME_HEIGHT:
|
||||
call += ", {:.2f}".format(height)
|
||||
call += ")"
|
||||
pyperclip.copy(call)
|
||||
|
||||
def copy_cursor_position(self):
|
||||
pyperclip.copy(str(tuple(self.mouse_point.get_center().round(2))))
|
||||
@@ -1,143 +0,0 @@
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.transform import MoveToTarget
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.animation.update import UpdateFromFunc
|
||||
from manimlib.constants import *
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.mobject.probability import SampleSpace
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
|
||||
|
||||
class SampleSpaceScene(Scene):
|
||||
def get_sample_space(self, **config):
|
||||
self.sample_space = SampleSpace(**config)
|
||||
return self.sample_space
|
||||
|
||||
def add_sample_space(self, **config):
|
||||
self.add(self.get_sample_space(**config))
|
||||
|
||||
def get_division_change_animations(
|
||||
self, sample_space, parts, p_list,
|
||||
dimension=1,
|
||||
new_label_kwargs=None,
|
||||
**kwargs
|
||||
):
|
||||
if new_label_kwargs is None:
|
||||
new_label_kwargs = {}
|
||||
anims = []
|
||||
p_list = sample_space.complete_p_list(p_list)
|
||||
space_copy = sample_space.copy()
|
||||
|
||||
vect = DOWN if dimension == 1 else RIGHT
|
||||
parts.generate_target()
|
||||
for part, p in zip(parts.target, p_list):
|
||||
part.replace(space_copy, stretch=True)
|
||||
part.stretch(p, dimension)
|
||||
parts.target.arrange(vect, buff=0)
|
||||
parts.target.move_to(space_copy)
|
||||
anims.append(MoveToTarget(parts))
|
||||
if hasattr(parts, "labels") and parts.labels is not None:
|
||||
label_kwargs = parts.label_kwargs
|
||||
label_kwargs.update(new_label_kwargs)
|
||||
new_braces, new_labels = sample_space.get_subdivision_braces_and_labels(
|
||||
parts.target, **label_kwargs
|
||||
)
|
||||
anims += [
|
||||
Transform(parts.braces, new_braces),
|
||||
Transform(parts.labels, new_labels),
|
||||
]
|
||||
return anims
|
||||
|
||||
def get_horizontal_division_change_animations(self, p_list, **kwargs):
|
||||
assert(hasattr(self.sample_space, "horizontal_parts"))
|
||||
return self.get_division_change_animations(
|
||||
self.sample_space, self.sample_space.horizontal_parts, p_list,
|
||||
dimension=1,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_vertical_division_change_animations(self, p_list, **kwargs):
|
||||
assert(hasattr(self.sample_space, "vertical_parts"))
|
||||
return self.get_division_change_animations(
|
||||
self.sample_space, self.sample_space.vertical_parts, p_list,
|
||||
dimension=0,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def get_conditional_change_anims(
|
||||
self, sub_sample_space_index, value, post_rects=None,
|
||||
**kwargs
|
||||
):
|
||||
parts = self.sample_space.horizontal_parts
|
||||
sub_sample_space = parts[sub_sample_space_index]
|
||||
anims = self.get_division_change_animations(
|
||||
sub_sample_space, sub_sample_space.vertical_parts, value,
|
||||
dimension=0,
|
||||
**kwargs
|
||||
)
|
||||
if post_rects is not None:
|
||||
anims += self.get_posterior_rectangle_change_anims(post_rects)
|
||||
return anims
|
||||
|
||||
def get_top_conditional_change_anims(self, *args, **kwargs):
|
||||
return self.get_conditional_change_anims(0, *args, **kwargs)
|
||||
|
||||
def get_bottom_conditional_change_anims(self, *args, **kwargs):
|
||||
return self.get_conditional_change_anims(1, *args, **kwargs)
|
||||
|
||||
def get_prior_rectangles(self):
|
||||
return VGroup(*[
|
||||
self.sample_space.horizontal_parts[i].vertical_parts[0]
|
||||
for i in range(2)
|
||||
])
|
||||
|
||||
def get_posterior_rectangles(self, buff=MED_LARGE_BUFF):
|
||||
prior_rects = self.get_prior_rectangles()
|
||||
areas = [
|
||||
rect.get_width() * rect.get_height()
|
||||
for rect in prior_rects
|
||||
]
|
||||
total_area = sum(areas)
|
||||
total_height = prior_rects.get_height()
|
||||
|
||||
post_rects = prior_rects.copy()
|
||||
for rect, area in zip(post_rects, areas):
|
||||
rect.stretch_to_fit_height(total_height * area / total_area)
|
||||
rect.stretch_to_fit_width(
|
||||
area / rect.get_height()
|
||||
)
|
||||
post_rects.arrange(DOWN, buff=0)
|
||||
post_rects.next_to(
|
||||
self.sample_space, RIGHT, buff
|
||||
)
|
||||
return post_rects
|
||||
|
||||
def get_posterior_rectangle_braces_and_labels(
|
||||
self, post_rects, labels, direction=RIGHT, **kwargs
|
||||
):
|
||||
return self.sample_space.get_subdivision_braces_and_labels(
|
||||
post_rects, labels, direction, **kwargs
|
||||
)
|
||||
|
||||
def update_posterior_braces(self, post_rects):
|
||||
braces = post_rects.braces
|
||||
labels = post_rects.labels
|
||||
for rect, brace, label in zip(post_rects, braces, labels):
|
||||
brace.stretch_to_fit_height(rect.get_height())
|
||||
brace.next_to(rect, RIGHT, SMALL_BUFF)
|
||||
label.next_to(brace, RIGHT, SMALL_BUFF)
|
||||
|
||||
def get_posterior_rectangle_change_anims(self, post_rects):
|
||||
def update_rects(rects):
|
||||
new_rects = self.get_posterior_rectangles()
|
||||
Transform(rects, new_rects).update(1)
|
||||
if hasattr(rects, "braces"):
|
||||
self.update_posterior_braces(rects)
|
||||
return rects
|
||||
|
||||
anims = [UpdateFromFunc(post_rects, update_rects)]
|
||||
if hasattr(post_rects, "braces"):
|
||||
anims += list(map(Animation, [
|
||||
post_rects.labels, post_rects.braces
|
||||
]))
|
||||
return anims
|
||||
File diff suppressed because it is too large
Load Diff
200
manimlib/scene/scene_embed.py
Normal file
200
manimlib/scene/scene_embed.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import pyperclip
|
||||
import traceback
|
||||
|
||||
from IPython.terminal import pt_inputhooks
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
|
||||
from manimlib.animation.fading import VFadeInThenOut
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.constants import RED
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.frame import FullScreenRectangle
|
||||
from manimlib.module_loader import ModuleLoader
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class InteractiveSceneEmbed:
|
||||
def __init__(self, scene: Scene):
|
||||
self.scene = scene
|
||||
self.checkpoint_manager = CheckpointManager()
|
||||
|
||||
self.shell = self.get_ipython_shell_for_embedded_scene()
|
||||
self.enable_gui()
|
||||
self.ensure_frame_update_post_cell()
|
||||
self.ensure_flash_on_error()
|
||||
if manim_config.embed.autoreload:
|
||||
self.auto_reload()
|
||||
|
||||
def launch(self):
|
||||
self.shell()
|
||||
|
||||
def get_ipython_shell_for_embedded_scene(self) -> InteractiveShellEmbed:
|
||||
"""
|
||||
Create embedded IPython terminal configured to have access to
|
||||
the local namespace of the caller
|
||||
"""
|
||||
# Triple back should take us to the context in a user's scene definition
|
||||
# which is calling "self.embed"
|
||||
caller_frame = inspect.currentframe().f_back.f_back.f_back
|
||||
|
||||
# Update the module's namespace to include local variables
|
||||
module = ModuleLoader.get_module(caller_frame.f_globals["__file__"])
|
||||
module.__dict__.update(caller_frame.f_locals)
|
||||
module.__dict__.update(self.get_shortcuts())
|
||||
exception_mode = manim_config.embed.exception_mode
|
||||
|
||||
return InteractiveShellEmbed(
|
||||
user_module=module,
|
||||
display_banner=False,
|
||||
xmode=exception_mode
|
||||
)
|
||||
|
||||
def get_shortcuts(self):
|
||||
"""
|
||||
A few custom shortcuts useful to have in the interactive shell namespace
|
||||
"""
|
||||
scene = self.scene
|
||||
return dict(
|
||||
play=scene.play,
|
||||
wait=scene.wait,
|
||||
add=scene.add,
|
||||
remove=scene.remove,
|
||||
clear=scene.clear,
|
||||
focus=scene.focus,
|
||||
save_state=scene.save_state,
|
||||
undo=scene.undo,
|
||||
redo=scene.redo,
|
||||
i2g=scene.i2g,
|
||||
i2m=scene.i2m,
|
||||
checkpoint_paste=self.checkpoint_paste,
|
||||
clear_checkpoints=self.checkpoint_manager.clear_checkpoints,
|
||||
reload=self.reload_scene # Defined below
|
||||
)
|
||||
|
||||
def enable_gui(self):
|
||||
"""Enables gui interactions during the embed"""
|
||||
def inputhook(context):
|
||||
while not context.input_is_ready():
|
||||
if not self.scene.is_window_closing():
|
||||
self.scene.update_frame(dt=0)
|
||||
if self.scene.is_window_closing():
|
||||
self.shell.ask_exit()
|
||||
|
||||
pt_inputhooks.register("manim", inputhook)
|
||||
self.shell.enable_gui("manim")
|
||||
|
||||
def ensure_frame_update_post_cell(self):
|
||||
"""Ensure the scene updates its frame after each ipython cell"""
|
||||
def post_cell_func(*args, **kwargs):
|
||||
if not self.scene.is_window_closing():
|
||||
self.scene.update_frame(dt=0, force_draw=True)
|
||||
|
||||
self.shell.events.register("post_run_cell", post_cell_func)
|
||||
|
||||
def ensure_flash_on_error(self):
|
||||
"""Flash border, and potentially play sound, on exceptions"""
|
||||
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
|
||||
# Show the error don't just swallow it
|
||||
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
|
||||
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
|
||||
rect.fix_in_frame()
|
||||
self.scene.play(VFadeInThenOut(rect, run_time=0.5))
|
||||
|
||||
self.shell.set_custom_exc((Exception,), custom_exc)
|
||||
|
||||
def reload_scene(self, embed_line: int | None = None) -> None:
|
||||
"""
|
||||
Reloads the scene just like the `manimgl` command would do with the
|
||||
same arguments that were provided for the initial startup. This allows
|
||||
for quick iteration during scene development since we don't have to exit
|
||||
the IPython kernel and re-run the `manimgl` command again. The GUI stays
|
||||
open during the reload.
|
||||
|
||||
If `embed_line` is provided, the scene will be reloaded at that line
|
||||
number. This corresponds to the `linemarker` param of the
|
||||
`extract_scene.insert_embed_line_to_module()` method.
|
||||
|
||||
Before reload, the scene is cleared and the entire state is reset, such
|
||||
that we can start from a clean slate. This is taken care of by the
|
||||
run_scenes function in __main__.py, which will catch the error raised by the
|
||||
`exit_raise` magic command that we invoke here.
|
||||
|
||||
Note that we cannot define a custom exception class for this error,
|
||||
since the IPython kernel will swallow any exception. While we can catch
|
||||
such an exception in our custom exception handler registered with the
|
||||
`set_custom_exc` method, we cannot break out of the IPython shell by
|
||||
this means.
|
||||
"""
|
||||
# Update the global run configuration.
|
||||
run_config = manim_config.run
|
||||
run_config.is_reload = True
|
||||
if embed_line:
|
||||
run_config.embed_line = embed_line
|
||||
|
||||
print("Reloading...")
|
||||
self.shell.run_line_magic("exit_raise", "")
|
||||
|
||||
def auto_reload(self):
|
||||
"""Enables IPython autoreload for automatic reloading of modules."""
|
||||
self.shell.magic("load_ext autoreload")
|
||||
self.shell.magic("autoreload all")
|
||||
|
||||
def checkpoint_paste(
|
||||
self,
|
||||
skip: bool = False,
|
||||
record: bool = False,
|
||||
progress_bar: bool = True
|
||||
):
|
||||
with self.scene.temp_config_change(skip, record, progress_bar):
|
||||
self.checkpoint_manager.checkpoint_paste(self.shell, self.scene)
|
||||
|
||||
|
||||
class CheckpointManager:
|
||||
def __init__(self):
|
||||
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
|
||||
|
||||
def checkpoint_paste(self, shell, scene):
|
||||
"""
|
||||
Used during interactive development to run (or re-run)
|
||||
a block of scene code.
|
||||
|
||||
If the copied selection starts with a comment, this will
|
||||
revert to the state of the scene the first time this function
|
||||
was called on a block of code starting with that comment.
|
||||
"""
|
||||
code_string = pyperclip.paste()
|
||||
checkpoint_key = self.get_leading_comment(code_string)
|
||||
self.handle_checkpoint_key(scene, checkpoint_key)
|
||||
shell.run_cell(code_string)
|
||||
|
||||
@staticmethod
|
||||
def get_leading_comment(code_string: str) -> str:
|
||||
leading_line = code_string.partition("\n")[0].lstrip()
|
||||
if leading_line.startswith("#"):
|
||||
return leading_line
|
||||
return ""
|
||||
|
||||
def handle_checkpoint_key(self, scene, key: str):
|
||||
if not key:
|
||||
return
|
||||
elif key in self.checkpoint_states:
|
||||
# Revert to checkpoint
|
||||
scene.restore_state(self.checkpoint_states[key])
|
||||
|
||||
# Clear out any saved states that show up later
|
||||
all_keys = list(self.checkpoint_states.keys())
|
||||
index = all_keys.index(key)
|
||||
for later_key in all_keys[index + 1:]:
|
||||
self.checkpoint_states.pop(later_key)
|
||||
else:
|
||||
self.checkpoint_states[key] = scene.get_state()
|
||||
|
||||
def clear_checkpoints(self):
|
||||
self.checkpoint_states = dict()
|
||||
@@ -1,81 +1,110 @@
|
||||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess as sp
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
|
||||
from manimlib.constants import FFMPEG_BIN
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
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
|
||||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
from tqdm.auto import tqdm as ProgressDisplay
|
||||
from pathlib import Path
|
||||
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.file_ops import guarantee_existence
|
||||
from manimlib.utils.sounds import get_full_sound_file_path
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL.Image import Image
|
||||
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class SceneFileWriter(object):
|
||||
CONFIG = {
|
||||
"write_to_movie": False,
|
||||
"break_into_partial_movies": False,
|
||||
# TODO, save_pngs is doing nothing
|
||||
"save_pngs": False,
|
||||
"png_mode": "RGBA",
|
||||
"save_last_frame": False,
|
||||
"movie_file_extension": ".mp4",
|
||||
# Should the path of output files mirror the directory
|
||||
# structure of the module holding the scene?
|
||||
"mirror_module_path": False,
|
||||
# What python file is generating this scene
|
||||
"input_file_path": "",
|
||||
def __init__(
|
||||
self,
|
||||
scene: Scene,
|
||||
write_to_movie: bool = False,
|
||||
subdivide_output: bool = False,
|
||||
png_mode: str = "RGBA",
|
||||
save_last_frame: bool = False,
|
||||
movie_file_extension: str = ".mp4",
|
||||
# Where should this be written
|
||||
"output_directory": None,
|
||||
"file_name": None,
|
||||
"open_file_upon_completion": False,
|
||||
"show_file_location_upon_completion": False,
|
||||
"quiet": False,
|
||||
"total_frames": 0,
|
||||
"progress_description_len": 60,
|
||||
}
|
||||
output_directory: str = ".",
|
||||
file_name: str | None = None,
|
||||
open_file_upon_completion: bool = False,
|
||||
show_file_location_upon_completion: bool = False,
|
||||
quiet: bool = False,
|
||||
total_frames: int = 0,
|
||||
progress_description_len: int = 40,
|
||||
# Name of the binary used for ffmpeg
|
||||
ffmpeg_bin: str = "ffmpeg",
|
||||
video_codec: str = "libx264",
|
||||
pixel_format: str = "yuv420p",
|
||||
saturation: float = 1.0,
|
||||
gamma: float = 1.0,
|
||||
):
|
||||
self.scene: Scene = scene
|
||||
self.write_to_movie = write_to_movie
|
||||
self.subdivide_output = subdivide_output
|
||||
self.png_mode = png_mode
|
||||
self.save_last_frame = save_last_frame
|
||||
self.movie_file_extension = movie_file_extension
|
||||
self.output_directory = output_directory
|
||||
self.file_name = file_name
|
||||
self.open_file_upon_completion = open_file_upon_completion
|
||||
self.show_file_location_upon_completion = show_file_location_upon_completion
|
||||
self.quiet = quiet
|
||||
self.total_frames = total_frames
|
||||
self.progress_description_len = progress_description_len
|
||||
self.ffmpeg_bin = ffmpeg_bin
|
||||
self.video_codec = video_codec
|
||||
self.pixel_format = pixel_format
|
||||
self.saturation = saturation
|
||||
self.gamma = gamma
|
||||
|
||||
# State during file writing
|
||||
self.writing_process: sp.Popen | None = None
|
||||
self.progress_display: ProgressDisplay | None = None
|
||||
self.ended_with_interrupt: bool = False
|
||||
|
||||
def __init__(self, scene, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.scene = scene
|
||||
self.writing_process = None
|
||||
self.has_progress_display = False
|
||||
self.init_output_directories()
|
||||
self.init_audio()
|
||||
|
||||
# Output directories and files
|
||||
def init_output_directories(self):
|
||||
out_dir = self.output_directory
|
||||
if self.mirror_module_path:
|
||||
module_dir = self.get_default_module_directory()
|
||||
out_dir = os.path.join(out_dir, module_dir)
|
||||
|
||||
scene_name = self.file_name or self.get_default_scene_name()
|
||||
def init_output_directories(self) -> None:
|
||||
if self.save_last_frame:
|
||||
image_dir = guarantee_existence(os.path.join(out_dir, "images"))
|
||||
image_file = add_extension_if_not_present(scene_name, ".png")
|
||||
self.image_file_path = os.path.join(image_dir, image_file)
|
||||
self.image_file_path = self.init_image_file_path()
|
||||
if self.write_to_movie:
|
||||
movie_dir = guarantee_existence(os.path.join(out_dir, "videos"))
|
||||
movie_file = add_extension_if_not_present(scene_name, self.movie_file_extension)
|
||||
self.movie_file_path = os.path.join(movie_dir, movie_file)
|
||||
if self.break_into_partial_movies:
|
||||
self.partial_movie_directory = guarantee_existence(os.path.join(
|
||||
movie_dir, "partial_movie_files", scene_name,
|
||||
))
|
||||
self.movie_file_path = self.init_movie_file_path()
|
||||
if self.subdivide_output:
|
||||
self.partial_movie_directory = self.init_partial_movie_directory()
|
||||
|
||||
def get_default_module_directory(self):
|
||||
path, _ = os.path.splitext(self.input_file_path)
|
||||
if path.startswith("_"):
|
||||
path = path[1:]
|
||||
return path
|
||||
def init_image_file_path(self) -> Path:
|
||||
return self.get_output_file_rootname().with_suffix(".png")
|
||||
|
||||
def get_default_scene_name(self):
|
||||
def init_movie_file_path(self) -> Path:
|
||||
return self.get_output_file_rootname().with_suffix(self.movie_file_extension)
|
||||
|
||||
def init_partial_movie_directory(self):
|
||||
return guarantee_existence(self.get_output_file_rootname())
|
||||
|
||||
def get_output_file_rootname(self) -> Path:
|
||||
return Path(
|
||||
guarantee_existence(self.output_directory),
|
||||
self.get_output_file_name()
|
||||
)
|
||||
|
||||
def get_output_file_name(self) -> str:
|
||||
if self.file_name:
|
||||
return self.file_name
|
||||
# Otherwise, use the name of the scene, potentially
|
||||
# appending animation numbers
|
||||
name = str(self.scene)
|
||||
saan = self.scene.start_at_animation_number
|
||||
eaan = self.scene.end_at_animation_number
|
||||
@@ -85,40 +114,30 @@ class SceneFileWriter(object):
|
||||
name += f"_{eaan}"
|
||||
return name
|
||||
|
||||
def get_resolution_directory(self):
|
||||
pixel_height = self.scene.camera.pixel_height
|
||||
frame_rate = self.scene.camera.frame_rate
|
||||
return "{}p{}".format(
|
||||
pixel_height, frame_rate
|
||||
)
|
||||
|
||||
# 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):
|
||||
result = os.path.join(
|
||||
self.partial_movie_directory,
|
||||
"{:05}{}".format(
|
||||
self.scene.num_plays,
|
||||
self.movie_file_extension,
|
||||
)
|
||||
)
|
||||
return result
|
||||
def get_next_partial_movie_path(self) -> str:
|
||||
result = Path(self.partial_movie_directory, f"{self.scene.num_plays:05}")
|
||||
return result.with_suffix(self.movie_file_extension)
|
||||
|
||||
def get_movie_file_path(self):
|
||||
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()
|
||||
@@ -142,161 +161,146 @@ 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):
|
||||
if not self.break_into_partial_movies and self.write_to_movie:
|
||||
def begin(self) -> None:
|
||||
if not self.subdivide_output and self.write_to_movie:
|
||||
self.open_movie_pipe(self.get_movie_file_path())
|
||||
|
||||
def begin_animation(self):
|
||||
if self.break_into_partial_movies and self.write_to_movie:
|
||||
def begin_animation(self) -> None:
|
||||
if self.subdivide_output and self.write_to_movie:
|
||||
self.open_movie_pipe(self.get_next_partial_movie_path())
|
||||
|
||||
def end_animation(self):
|
||||
if self.break_into_partial_movies and self.write_to_movie:
|
||||
def end_animation(self) -> None:
|
||||
if self.subdivide_output and self.write_to_movie:
|
||||
self.close_movie_pipe()
|
||||
|
||||
def finish(self):
|
||||
if self.write_to_movie:
|
||||
if self.break_into_partial_movies:
|
||||
self.combine_movie_files()
|
||||
else:
|
||||
self.close_movie_pipe()
|
||||
def finish(self) -> None:
|
||||
if not self.subdivide_output and self.write_to_movie:
|
||||
self.close_movie_pipe()
|
||||
if self.includes_sound:
|
||||
self.add_sound_to_video()
|
||||
self.print_file_ready_message(self.get_movie_file_path())
|
||||
if self.save_last_frame:
|
||||
self.scene.update_frame(ignore_skipping=True)
|
||||
self.scene.update_frame(force_draw=True)
|
||||
self.save_final_image(self.scene.get_image())
|
||||
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
|
||||
|
||||
fps = self.scene.camera.frame_rate
|
||||
fps = self.scene.camera.fps
|
||||
width, height = self.scene.camera.get_pixel_shape()
|
||||
|
||||
vf_arg = 'vflip'
|
||||
vf_arg += f',eq=saturation={self.saturation}:gamma={self.gamma}'
|
||||
|
||||
command = [
|
||||
FFMPEG_BIN,
|
||||
self.ffmpeg_bin,
|
||||
'-y', # overwrite output file if it exists
|
||||
'-f', 'rawvideo',
|
||||
'-s', f'{width}x{height}', # size of one frame
|
||||
'-pix_fmt', 'rgba',
|
||||
'-r', str(fps), # frames per second
|
||||
'-i', '-', # The input comes from a pipe
|
||||
'-vf', 'vflip',
|
||||
'-an', # Tells FFMPEG not to expect any audio
|
||||
'-vf', vf_arg,
|
||||
'-an', # Tells ffmpeg not to expect any audio
|
||||
'-loglevel', 'error',
|
||||
]
|
||||
if self.movie_file_extension == ".mov":
|
||||
# This is if the background of the exported
|
||||
# video should be transparent.
|
||||
command += [
|
||||
'-vcodec', 'qtrle',
|
||||
]
|
||||
elif self.movie_file_extension == ".gif":
|
||||
command += []
|
||||
else:
|
||||
command += [
|
||||
'-vcodec', 'libx264',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
]
|
||||
if self.video_codec:
|
||||
command += ['-vcodec', self.video_codec]
|
||||
if self.pixel_format:
|
||||
command += ['-pix_fmt', self.pixel_format]
|
||||
command += [self.temp_file_path]
|
||||
self.writing_process = sp.Popen(command, stdin=sp.PIPE)
|
||||
|
||||
if self.total_frames > 0:
|
||||
if not self.quiet:
|
||||
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
|
||||
self.set_progress_display_description()
|
||||
|
||||
def use_fast_encoding(self):
|
||||
self.video_codec = "libx264rgb"
|
||||
self.pixel_format = "rgb32"
|
||||
|
||||
def get_insert_file_path(self, index: int) -> Path:
|
||||
movie_path = Path(self.get_movie_file_path())
|
||||
scene_name = movie_path.stem
|
||||
insert_dir = Path(movie_path.parent, "inserts")
|
||||
guarantee_existence(insert_dir)
|
||||
return Path(insert_dir, f"{scene_name}_{index}").with_suffix(self.movie_file_extension)
|
||||
|
||||
def begin_insert(self):
|
||||
# Begin writing process
|
||||
self.write_to_movie = True
|
||||
self.init_output_directories()
|
||||
index = 0
|
||||
while (insert_path := self.get_insert_file_path(index)).exists():
|
||||
index += 1
|
||||
self.inserted_file_path = insert_path
|
||||
self.open_movie_pipe(self.inserted_file_path)
|
||||
|
||||
def end_insert(self):
|
||||
self.close_movie_pipe()
|
||||
self.write_to_movie = False
|
||||
self.print_file_ready_message(self.inserted_file_path)
|
||||
|
||||
def has_progress_display(self):
|
||||
return self.progress_display is not None
|
||||
|
||||
def set_progress_display_description(self, file: str = "", sub_desc: str = "") -> None:
|
||||
if self.progress_display is None:
|
||||
return
|
||||
|
||||
def set_progress_display_subdescription(self, sub_desc):
|
||||
desc_len = self.progress_description_len
|
||||
file = os.path.split(self.get_movie_file_path())[1]
|
||||
full_desc = f"Rendering {file} ({sub_desc})"
|
||||
if not file:
|
||||
file = os.path.split(self.get_movie_file_path())[1]
|
||||
full_desc = f"{file} {sub_desc}"
|
||||
if len(full_desc) > desc_len:
|
||||
full_desc = full_desc[:desc_len - 4] + "...)"
|
||||
full_desc = full_desc[:desc_len - 3] + "..."
|
||||
else:
|
||||
full_desc += " " * (desc_len - len(full_desc))
|
||||
self.progress_display.set_description(full_desc)
|
||||
|
||||
def write_frame(self, camera):
|
||||
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:
|
||||
if self.progress_display is not None:
|
||||
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:
|
||||
if self.progress_display is not None:
|
||||
self.progress_display.close()
|
||||
shutil.move(self.temp_file_path, self.final_file_path)
|
||||
|
||||
def combine_movie_files(self):
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.movie_file_extension,
|
||||
}
|
||||
if self.scene.start_at_animation_number is not None:
|
||||
kwargs["min_index"] = self.scene.start_at_animation_number
|
||||
if self.scene.end_at_animation_number is not None:
|
||||
kwargs["max_index"] = self.scene.end_at_animation_number
|
||||
if not self.ended_with_interrupt:
|
||||
shutil.move(self.temp_file_path, self.final_file_path)
|
||||
else:
|
||||
kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1
|
||||
partial_movie_files = get_sorted_integer_files(
|
||||
self.partial_movie_directory,
|
||||
**kwargs
|
||||
)
|
||||
if len(partial_movie_files) == 0:
|
||||
log.warning("No animations in this scene")
|
||||
return
|
||||
self.movie_file_path = self.temp_file_path
|
||||
|
||||
# Write a file partial_file_list.txt containing all
|
||||
# partial movie files
|
||||
file_list = os.path.join(
|
||||
self.partial_movie_directory,
|
||||
"partial_movie_file_list.txt"
|
||||
)
|
||||
with open(file_list, 'w') as fp:
|
||||
for pf_path in partial_movie_files:
|
||||
if os.name == 'nt':
|
||||
pf_path = pf_path.replace('\\', '/')
|
||||
fp.write(f"file \'{pf_path}\'\n")
|
||||
|
||||
movie_file_path = self.get_movie_file_path()
|
||||
commands = [
|
||||
FFMPEG_BIN,
|
||||
'-y', # overwrite output file if it exists
|
||||
'-f', 'concat',
|
||||
'-safe', '0',
|
||||
'-i', file_list,
|
||||
'-loglevel', 'error',
|
||||
'-c', 'copy',
|
||||
movie_file_path
|
||||
]
|
||||
if not self.includes_sound:
|
||||
commands.insert(-1, '-an')
|
||||
|
||||
combine_process = sp.Popen(commands)
|
||||
combine_process.wait()
|
||||
|
||||
def add_sound_to_video(self):
|
||||
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"
|
||||
@@ -308,7 +312,7 @@ class SceneFileWriter(object):
|
||||
)
|
||||
temp_file_path = stem + "_temp" + ext
|
||||
commands = [
|
||||
"ffmpeg",
|
||||
self.ffmpeg_bin,
|
||||
"-i", movie_file_path,
|
||||
"-i", sound_file_path,
|
||||
'-y', # overwrite output file if it exists
|
||||
@@ -327,22 +331,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):
|
||||
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")
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class ThreeDScene(Scene):
|
||||
CONFIG = {
|
||||
"camera_config": {
|
||||
"samples": 4,
|
||||
}
|
||||
}
|
||||
|
||||
def begin_ambient_camera_rotation(self, rate=0.02):
|
||||
pass # TODO
|
||||
|
||||
def stop_ambient_camera_rotation(self):
|
||||
pass # TODO
|
||||
|
||||
def move_camera(self,
|
||||
phi=None,
|
||||
theta=None,
|
||||
distance=None,
|
||||
gamma=None,
|
||||
frame_center=None,
|
||||
**kwargs):
|
||||
pass # TODO
|
||||
@@ -1,512 +0,0 @@
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.creation import ShowCreation
|
||||
from manimlib.animation.creation import Write
|
||||
from manimlib.animation.fading import FadeOut
|
||||
from manimlib.animation.growing import GrowArrow
|
||||
from manimlib.animation.transform import ApplyFunction
|
||||
from manimlib.animation.transform import ApplyPointwiseFunction
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.coordinate_systems import Axes
|
||||
from manimlib.mobject.coordinate_systems import NumberPlane
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import Vector
|
||||
from manimlib.mobject.matrix import Matrix
|
||||
from manimlib.mobject.matrix import VECTOR_LABEL_SCALE_FACTOR
|
||||
from manimlib.mobject.matrix import vector_coordinate_label
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
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 manimlib.scene.scene import Scene
|
||||
from manimlib.utils.rate_functions import rush_from
|
||||
from manimlib.utils.rate_functions import rush_into
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
X_COLOR = GREEN_C
|
||||
Y_COLOR = RED_C
|
||||
Z_COLOR = BLUE_D
|
||||
|
||||
|
||||
# TODO: Much of this scene type seems dependent on the coordinate system chosen.
|
||||
# That is, being centered at the origin with grid units corresponding to the
|
||||
# arbitrary space units. Change it!
|
||||
#
|
||||
# Also, methods I would have thought of as getters, like coords_to_vector, are
|
||||
# actually doing a lot of animating.
|
||||
class VectorScene(Scene):
|
||||
CONFIG = {
|
||||
"basis_vector_stroke_width": 6
|
||||
}
|
||||
|
||||
def add_plane(self, animate=False, **kwargs):
|
||||
plane = NumberPlane(**kwargs)
|
||||
if animate:
|
||||
self.play(ShowCreation(plane, lag_ratio=0.5))
|
||||
self.add(plane)
|
||||
return plane
|
||||
|
||||
def add_axes(self, animate=False, color=WHITE, **kwargs):
|
||||
axes = Axes(color=color, tick_frequency=1)
|
||||
if animate:
|
||||
self.play(ShowCreation(axes))
|
||||
self.add(axes)
|
||||
return axes
|
||||
|
||||
def lock_in_faded_grid(self, dimness=0.7, axes_dimness=0.5):
|
||||
plane = self.add_plane()
|
||||
axes = plane.get_axes()
|
||||
plane.fade(dimness)
|
||||
axes.set_color(WHITE)
|
||||
axes.fade(axes_dimness)
|
||||
self.add(axes)
|
||||
self.freeze_background()
|
||||
|
||||
def get_vector(self, numerical_vector, **kwargs):
|
||||
return Arrow(
|
||||
self.plane.coords_to_point(0, 0),
|
||||
self.plane.coords_to_point(*numerical_vector[:2]),
|
||||
buff=0,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def add_vector(self, vector, color=YELLOW, animate=True, **kwargs):
|
||||
if not isinstance(vector, Arrow):
|
||||
vector = Vector(vector, color=color, **kwargs)
|
||||
if animate:
|
||||
self.play(GrowArrow(vector))
|
||||
self.add(vector)
|
||||
return vector
|
||||
|
||||
def write_vector_coordinates(self, vector, **kwargs):
|
||||
coords = vector_coordinate_label(vector, **kwargs)
|
||||
self.play(Write(coords))
|
||||
return coords
|
||||
|
||||
def get_basis_vectors(self, i_hat_color=X_COLOR, j_hat_color=Y_COLOR):
|
||||
return VGroup(*[
|
||||
Vector(
|
||||
vect,
|
||||
color=color,
|
||||
stroke_width=self.basis_vector_stroke_width
|
||||
)
|
||||
for vect, color in [
|
||||
([1, 0], i_hat_color),
|
||||
([0, 1], j_hat_color)
|
||||
]
|
||||
])
|
||||
|
||||
def get_basis_vector_labels(self, **kwargs):
|
||||
i_hat, j_hat = self.get_basis_vectors()
|
||||
return VGroup(*[
|
||||
self.get_vector_label(
|
||||
vect, label, color=color,
|
||||
label_scale_factor=1,
|
||||
**kwargs
|
||||
)
|
||||
for vect, label, color in [
|
||||
(i_hat, "\\hat{\\imath}", X_COLOR),
|
||||
(j_hat, "\\hat{\\jmath}", Y_COLOR),
|
||||
]
|
||||
])
|
||||
|
||||
def get_vector_label(self, vector, label,
|
||||
at_tip=False,
|
||||
direction="left",
|
||||
rotate=False,
|
||||
color=None,
|
||||
label_scale_factor=VECTOR_LABEL_SCALE_FACTOR):
|
||||
if not isinstance(label, Tex):
|
||||
if len(label) == 1:
|
||||
label = "\\vec{\\textbf{%s}}" % label
|
||||
label = Tex(label)
|
||||
if color is None:
|
||||
color = vector.get_color()
|
||||
label.set_color(color)
|
||||
label.scale(label_scale_factor)
|
||||
label.add_background_rectangle()
|
||||
|
||||
if at_tip:
|
||||
vect = vector.get_vector()
|
||||
vect /= get_norm(vect)
|
||||
label.next_to(vector.get_end(), vect, buff=SMALL_BUFF)
|
||||
else:
|
||||
angle = vector.get_angle()
|
||||
if not rotate:
|
||||
label.rotate(-angle, about_point=ORIGIN)
|
||||
if direction == "left":
|
||||
label.shift(-label.get_bottom() + 0.1 * UP)
|
||||
else:
|
||||
label.shift(-label.get_top() + 0.1 * DOWN)
|
||||
label.rotate(angle, about_point=ORIGIN)
|
||||
label.shift((vector.get_end() - vector.get_start()) / 2)
|
||||
return label
|
||||
|
||||
def label_vector(self, vector, label, animate=True, **kwargs):
|
||||
label = self.get_vector_label(vector, label, **kwargs)
|
||||
if animate:
|
||||
self.play(Write(label, run_time=1))
|
||||
self.add(label)
|
||||
return label
|
||||
|
||||
def position_x_coordinate(self, x_coord, x_line, vector):
|
||||
x_coord.next_to(x_line, -np.sign(vector[1]) * UP)
|
||||
x_coord.set_color(X_COLOR)
|
||||
return x_coord
|
||||
|
||||
def position_y_coordinate(self, y_coord, y_line, vector):
|
||||
y_coord.next_to(y_line, np.sign(vector[0]) * RIGHT)
|
||||
y_coord.set_color(Y_COLOR)
|
||||
return y_coord
|
||||
|
||||
def coords_to_vector(self, vector, coords_start=2 * RIGHT + 2 * UP, clean_up=True):
|
||||
starting_mobjects = list(self.mobjects)
|
||||
array = Matrix(vector)
|
||||
array.shift(coords_start)
|
||||
arrow = Vector(vector)
|
||||
x_line = Line(ORIGIN, vector[0] * RIGHT)
|
||||
y_line = Line(x_line.get_end(), arrow.get_end())
|
||||
x_line.set_color(X_COLOR)
|
||||
y_line.set_color(Y_COLOR)
|
||||
x_coord, y_coord = array.get_mob_matrix().flatten()
|
||||
|
||||
self.play(Write(array, run_time=1))
|
||||
self.wait()
|
||||
self.play(ApplyFunction(
|
||||
lambda x: self.position_x_coordinate(x, x_line, vector),
|
||||
x_coord
|
||||
))
|
||||
self.play(ShowCreation(x_line))
|
||||
self.play(
|
||||
ApplyFunction(
|
||||
lambda y: self.position_y_coordinate(y, y_line, vector),
|
||||
y_coord
|
||||
),
|
||||
FadeOut(array.get_brackets())
|
||||
)
|
||||
y_coord, brackets = self.get_mobjects_from_last_animation()
|
||||
self.play(ShowCreation(y_line))
|
||||
self.play(ShowCreation(arrow))
|
||||
self.wait()
|
||||
if clean_up:
|
||||
self.clear()
|
||||
self.add(*starting_mobjects)
|
||||
|
||||
def vector_to_coords(self, vector, integer_labels=True, clean_up=True):
|
||||
starting_mobjects = list(self.mobjects)
|
||||
show_creation = False
|
||||
if isinstance(vector, Arrow):
|
||||
arrow = vector
|
||||
vector = arrow.get_end()[:2]
|
||||
else:
|
||||
arrow = Vector(vector)
|
||||
show_creation = True
|
||||
array = vector_coordinate_label(arrow, integer_labels=integer_labels)
|
||||
x_line = Line(ORIGIN, vector[0] * RIGHT)
|
||||
y_line = Line(x_line.get_end(), arrow.get_end())
|
||||
x_line.set_color(X_COLOR)
|
||||
y_line.set_color(Y_COLOR)
|
||||
x_coord, y_coord = array.get_mob_matrix().flatten()
|
||||
x_coord_start = self.position_x_coordinate(
|
||||
x_coord.copy(), x_line, vector
|
||||
)
|
||||
y_coord_start = self.position_y_coordinate(
|
||||
y_coord.copy(), y_line, vector
|
||||
)
|
||||
brackets = array.get_brackets()
|
||||
|
||||
if show_creation:
|
||||
self.play(ShowCreation(arrow))
|
||||
self.play(
|
||||
ShowCreation(x_line),
|
||||
Write(x_coord_start),
|
||||
run_time=1
|
||||
)
|
||||
self.play(
|
||||
ShowCreation(y_line),
|
||||
Write(y_coord_start),
|
||||
run_time=1
|
||||
)
|
||||
self.wait()
|
||||
self.play(
|
||||
Transform(x_coord_start, x_coord, lag_ratio=0),
|
||||
Transform(y_coord_start, y_coord, lag_ratio=0),
|
||||
Write(brackets, run_time=1),
|
||||
)
|
||||
self.wait()
|
||||
|
||||
self.remove(x_coord_start, y_coord_start, brackets)
|
||||
self.add(array)
|
||||
if clean_up:
|
||||
self.clear()
|
||||
self.add(*starting_mobjects)
|
||||
return array, x_line, y_line
|
||||
|
||||
def show_ghost_movement(self, vector):
|
||||
if isinstance(vector, Arrow):
|
||||
vector = vector.get_end() - vector.get_start()
|
||||
elif len(vector) == 2:
|
||||
vector = np.append(np.array(vector), 0.0)
|
||||
x_max = int(FRAME_X_RADIUS + abs(vector[0]))
|
||||
y_max = int(FRAME_Y_RADIUS + abs(vector[1]))
|
||||
dots = VMobject(*[
|
||||
Dot(x * RIGHT + y * UP)
|
||||
for x in range(-x_max, x_max)
|
||||
for y in range(-y_max, y_max)
|
||||
])
|
||||
dots.set_fill(BLACK, opacity=0)
|
||||
dots_halfway = dots.copy().shift(vector / 2).set_fill(WHITE, 1)
|
||||
dots_end = dots.copy().shift(vector)
|
||||
|
||||
self.play(Transform(
|
||||
dots, dots_halfway, rate_func=rush_into
|
||||
))
|
||||
self.play(Transform(
|
||||
dots, dots_end, rate_func=rush_from
|
||||
))
|
||||
self.remove(dots)
|
||||
|
||||
|
||||
class LinearTransformationScene(VectorScene):
|
||||
CONFIG = {
|
||||
"include_background_plane": True,
|
||||
"include_foreground_plane": True,
|
||||
"foreground_plane_kwargs": {
|
||||
"x_max": FRAME_WIDTH / 2,
|
||||
"x_min": -FRAME_WIDTH / 2,
|
||||
"y_max": FRAME_WIDTH / 2,
|
||||
"y_min": -FRAME_WIDTH / 2,
|
||||
"faded_line_ratio": 0
|
||||
},
|
||||
"background_plane_kwargs": {
|
||||
"color": GREY,
|
||||
"axis_config": {
|
||||
"stroke_color": GREY_B,
|
||||
},
|
||||
"axis_config": {
|
||||
"color": GREY,
|
||||
},
|
||||
"background_line_style": {
|
||||
"stroke_color": GREY,
|
||||
"stroke_width": 1,
|
||||
},
|
||||
},
|
||||
"show_coordinates": False,
|
||||
"show_basis_vectors": True,
|
||||
"basis_vector_stroke_width": 6,
|
||||
"i_hat_color": X_COLOR,
|
||||
"j_hat_color": Y_COLOR,
|
||||
"leave_ghost_vectors": False,
|
||||
"t_matrix": [[3, 0], [1, 2]],
|
||||
}
|
||||
|
||||
def setup(self):
|
||||
# The has_already_setup attr is to not break all the old Scenes
|
||||
if hasattr(self, "has_already_setup"):
|
||||
return
|
||||
self.has_already_setup = True
|
||||
self.background_mobjects = []
|
||||
self.foreground_mobjects = []
|
||||
self.transformable_mobjects = []
|
||||
self.moving_vectors = []
|
||||
self.transformable_labels = []
|
||||
self.moving_mobjects = []
|
||||
|
||||
self.t_matrix = np.array(self.t_matrix)
|
||||
self.background_plane = NumberPlane(
|
||||
**self.background_plane_kwargs
|
||||
)
|
||||
|
||||
if self.show_coordinates:
|
||||
self.background_plane.add_coordinates()
|
||||
if self.include_background_plane:
|
||||
self.add_background_mobject(self.background_plane)
|
||||
if self.include_foreground_plane:
|
||||
self.plane = NumberPlane(**self.foreground_plane_kwargs)
|
||||
self.add_transformable_mobject(self.plane)
|
||||
if self.show_basis_vectors:
|
||||
self.basis_vectors = self.get_basis_vectors(
|
||||
i_hat_color=self.i_hat_color,
|
||||
j_hat_color=self.j_hat_color,
|
||||
)
|
||||
self.moving_vectors += list(self.basis_vectors)
|
||||
self.i_hat, self.j_hat = self.basis_vectors
|
||||
self.add(self.basis_vectors)
|
||||
|
||||
def add_special_mobjects(self, mob_list, *mobs_to_add):
|
||||
for mobject in mobs_to_add:
|
||||
if mobject not in mob_list:
|
||||
mob_list.append(mobject)
|
||||
self.add(mobject)
|
||||
|
||||
def add_background_mobject(self, *mobjects):
|
||||
self.add_special_mobjects(self.background_mobjects, *mobjects)
|
||||
|
||||
# TODO, this conflicts with Scene.add_fore
|
||||
def add_foreground_mobject(self, *mobjects):
|
||||
self.add_special_mobjects(self.foreground_mobjects, *mobjects)
|
||||
|
||||
def add_transformable_mobject(self, *mobjects):
|
||||
self.add_special_mobjects(self.transformable_mobjects, *mobjects)
|
||||
|
||||
def add_moving_mobject(self, mobject, target_mobject=None):
|
||||
mobject.target = target_mobject
|
||||
self.add_special_mobjects(self.moving_mobjects, mobject)
|
||||
|
||||
def get_unit_square(self, color=YELLOW, opacity=0.3, stroke_width=3):
|
||||
square = self.square = Rectangle(
|
||||
color=color,
|
||||
width=self.plane.get_x_unit_size(),
|
||||
height=self.plane.get_y_unit_size(),
|
||||
stroke_color=color,
|
||||
stroke_width=stroke_width,
|
||||
fill_color=color,
|
||||
fill_opacity=opacity
|
||||
)
|
||||
square.move_to(self.plane.coords_to_point(0, 0), DL)
|
||||
return square
|
||||
|
||||
def add_unit_square(self, animate=False, **kwargs):
|
||||
square = self.get_unit_square(**kwargs)
|
||||
if animate:
|
||||
self.play(
|
||||
DrawBorderThenFill(square),
|
||||
Animation(Group(*self.moving_vectors))
|
||||
)
|
||||
self.add_transformable_mobject(square)
|
||||
self.bring_to_front(*self.moving_vectors)
|
||||
self.square = square
|
||||
return self
|
||||
|
||||
def add_vector(self, vector, color=YELLOW, **kwargs):
|
||||
vector = VectorScene.add_vector(
|
||||
self, vector, color=color, **kwargs
|
||||
)
|
||||
self.moving_vectors.append(vector)
|
||||
return vector
|
||||
|
||||
def write_vector_coordinates(self, vector, **kwargs):
|
||||
coords = VectorScene.write_vector_coordinates(self, vector, **kwargs)
|
||||
self.add_foreground_mobject(coords)
|
||||
return coords
|
||||
|
||||
def add_transformable_label(
|
||||
self, vector, label,
|
||||
transformation_name="L",
|
||||
new_label=None,
|
||||
**kwargs):
|
||||
label_mob = self.label_vector(vector, label, **kwargs)
|
||||
if new_label:
|
||||
label_mob.target_text = new_label
|
||||
else:
|
||||
label_mob.target_text = "%s(%s)" % (
|
||||
transformation_name,
|
||||
label_mob.get_tex()
|
||||
)
|
||||
label_mob.vector = vector
|
||||
label_mob.kwargs = kwargs
|
||||
if "animate" in label_mob.kwargs:
|
||||
label_mob.kwargs.pop("animate")
|
||||
self.transformable_labels.append(label_mob)
|
||||
return label_mob
|
||||
|
||||
def add_title(self, title, scale_factor=1.5, animate=False):
|
||||
if not isinstance(title, Mobject):
|
||||
title = TexText(title).scale(scale_factor)
|
||||
title.to_edge(UP)
|
||||
title.add_background_rectangle()
|
||||
if animate:
|
||||
self.play(Write(title))
|
||||
self.add_foreground_mobject(title)
|
||||
self.title = title
|
||||
return self
|
||||
|
||||
def get_matrix_transformation(self, matrix):
|
||||
return self.get_transposed_matrix_transformation(np.array(matrix).T)
|
||||
|
||||
def get_transposed_matrix_transformation(self, transposed_matrix):
|
||||
transposed_matrix = np.array(transposed_matrix)
|
||||
if transposed_matrix.shape == (2, 2):
|
||||
new_matrix = np.identity(3)
|
||||
new_matrix[:2, :2] = transposed_matrix
|
||||
transposed_matrix = new_matrix
|
||||
elif transposed_matrix.shape != (3, 3):
|
||||
raise Exception("Matrix has bad dimensions")
|
||||
return lambda point: np.dot(point, transposed_matrix)
|
||||
|
||||
def get_piece_movement(self, pieces):
|
||||
start = VGroup(*pieces)
|
||||
target = VGroup(*[mob.target for mob in pieces])
|
||||
if self.leave_ghost_vectors:
|
||||
self.add(start.copy().fade(0.7))
|
||||
return Transform(start, target, lag_ratio=0)
|
||||
|
||||
def get_moving_mobject_movement(self, func):
|
||||
for m in self.moving_mobjects:
|
||||
if m.target is None:
|
||||
m.target = m.copy()
|
||||
target_point = func(m.get_center())
|
||||
m.target.move_to(target_point)
|
||||
return self.get_piece_movement(self.moving_mobjects)
|
||||
|
||||
def get_vector_movement(self, func):
|
||||
for v in self.moving_vectors:
|
||||
v.target = Vector(func(v.get_end()), color=v.get_color())
|
||||
norm = get_norm(v.target.get_end())
|
||||
if norm < 0.1:
|
||||
v.target.get_tip().scale(norm)
|
||||
return self.get_piece_movement(self.moving_vectors)
|
||||
|
||||
def get_transformable_label_movement(self):
|
||||
for l in self.transformable_labels:
|
||||
l.target = self.get_vector_label(
|
||||
l.vector.target, l.target_text, **l.kwargs
|
||||
)
|
||||
return self.get_piece_movement(self.transformable_labels)
|
||||
|
||||
def apply_matrix(self, matrix, **kwargs):
|
||||
self.apply_transposed_matrix(np.array(matrix).T, **kwargs)
|
||||
|
||||
def apply_inverse(self, matrix, **kwargs):
|
||||
self.apply_matrix(np.linalg.inv(matrix), **kwargs)
|
||||
|
||||
def apply_transposed_matrix(self, transposed_matrix, **kwargs):
|
||||
func = self.get_transposed_matrix_transformation(transposed_matrix)
|
||||
if "path_arc" not in kwargs:
|
||||
net_rotation = np.mean([
|
||||
angle_of_vector(func(RIGHT)),
|
||||
angle_of_vector(func(UP)) - np.pi / 2
|
||||
])
|
||||
kwargs["path_arc"] = net_rotation
|
||||
self.apply_function(func, **kwargs)
|
||||
|
||||
def apply_inverse_transpose(self, t_matrix, **kwargs):
|
||||
t_inv = np.linalg.inv(np.array(t_matrix).T).T
|
||||
self.apply_transposed_matrix(t_inv, **kwargs)
|
||||
|
||||
def apply_nonlinear_transformation(self, function, **kwargs):
|
||||
self.plane.prepare_for_nonlinear_transform()
|
||||
self.apply_function(function, **kwargs)
|
||||
|
||||
def apply_function(self, function, added_anims=[], **kwargs):
|
||||
if "run_time" not in kwargs:
|
||||
kwargs["run_time"] = 3
|
||||
anims = [
|
||||
ApplyPointwiseFunction(function, t_mob)
|
||||
for t_mob in self.transformable_mobjects
|
||||
] + [
|
||||
self.get_vector_movement(function),
|
||||
self.get_transformable_label_movement(),
|
||||
self.get_moving_mobject_movement(function),
|
||||
] + [
|
||||
Animation(f_mob)
|
||||
for f_mob in self.foreground_mobjects
|
||||
] + added_anims
|
||||
self.play(*anims, **kwargs)
|
||||
@@ -1,11 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
|
||||
import OpenGL.GL as gl
|
||||
import moderngl
|
||||
import numpy as np
|
||||
import copy
|
||||
from functools import lru_cache
|
||||
|
||||
from manimlib.utils.directories import get_shader_dir
|
||||
from manimlib.utils.file_ops import find_file
|
||||
from manimlib.config import parse_cli
|
||||
from manimlib.config import manim_config
|
||||
from manimlib.utils.shaders import get_shader_code_from_file
|
||||
from manimlib.utils.shaders import get_shader_program
|
||||
from manimlib.utils.shaders import image_path_to_texture
|
||||
from manimlib.utils.shaders import set_program_uniform
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, Tuple, Iterable
|
||||
from manimlib.typing import UniformDict
|
||||
from moderngl.vertex_array import VertexArray
|
||||
from moderngl.framebuffer import Framebuffer
|
||||
|
||||
# Mobjects that should be rendered with
|
||||
# the same shader will be organized and
|
||||
@@ -15,151 +32,449 @@ 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,
|
||||
ctx: moderngl.context.Context,
|
||||
vert_data: np.ndarray,
|
||||
shader_folder: Optional[str] = None,
|
||||
mobject_uniforms: Optional[UniformDict] = None, # A dictionary mapping names of uniform variables
|
||||
texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures.
|
||||
depth_test: bool = False,
|
||||
render_primitive: int = moderngl.TRIANGLE_STRIP,
|
||||
code_replacements: dict[str, str] = dict(),
|
||||
):
|
||||
self.ctx = ctx
|
||||
self.vert_data = vert_data
|
||||
self.vert_indices = vert_indices
|
||||
self.vert_attributes = vert_data.dtype.names
|
||||
self.shader_folder = shader_folder
|
||||
self.uniforms = uniforms or dict()
|
||||
self.texture_paths = texture_paths or dict()
|
||||
self.depth_test = depth_test
|
||||
self.render_primitive = str(render_primitive)
|
||||
self.render_primitive = render_primitive
|
||||
self.texture_paths = texture_paths or dict()
|
||||
|
||||
self.program_uniform_mirror: UniformDict = dict()
|
||||
self.bind_to_mobject_uniforms(mobject_uniforms or dict())
|
||||
|
||||
self.init_program_code()
|
||||
for old, new in code_replacements.items():
|
||||
self.replace_code(old, new)
|
||||
self.init_program()
|
||||
self.init_textures()
|
||||
self.init_vertex_objects()
|
||||
self.refresh_id()
|
||||
|
||||
def copy(self):
|
||||
result = copy.copy(self)
|
||||
result.vert_data = np.array(self.vert_data)
|
||||
if result.vert_indices is not None:
|
||||
result.vert_indices = np.array(self.vert_indices)
|
||||
if self.uniforms:
|
||||
result.uniforms = dict(self.uniforms)
|
||||
if self.texture_paths:
|
||||
result.texture_paths = dict(self.texture_paths)
|
||||
return result
|
||||
def __deepcopy__(self, memo):
|
||||
# Don't allow deepcopies, e.g. if the mobject with this ShaderWrapper as an
|
||||
# attribute gets copies. Returning None means the parent object with this ShaderWrapper
|
||||
# as an attribute should smoothly handle this case.
|
||||
return None
|
||||
|
||||
def is_valid(self):
|
||||
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):
|
||||
return self.id
|
||||
|
||||
def get_program_id(self):
|
||||
return self.program_id
|
||||
|
||||
def create_id(self):
|
||||
# A unique id for a shader
|
||||
return "|".join(map(str, [
|
||||
self.program_id,
|
||||
self.uniforms,
|
||||
self.texture_paths,
|
||||
self.depth_test,
|
||||
self.render_primitive,
|
||||
]))
|
||||
|
||||
def refresh_id(self):
|
||||
self.program_id = self.create_program_id()
|
||||
self.id = self.create_id()
|
||||
|
||||
def create_program_id(self):
|
||||
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):
|
||||
return self.program_code
|
||||
def init_program(self):
|
||||
if not self.shader_folder:
|
||||
self.program = None
|
||||
self.vert_format = None
|
||||
self.programs = []
|
||||
return
|
||||
self.program = get_shader_program(self.ctx, **self.program_code)
|
||||
self.vert_format = moderngl.detect_format(self.program, self.vert_attributes)
|
||||
self.programs = [self.program]
|
||||
|
||||
def replace_code(self, old, new):
|
||||
def init_textures(self):
|
||||
self.texture_names_to_ids = dict()
|
||||
self.textures = []
|
||||
for name, path in self.texture_paths.items():
|
||||
self.add_texture(name, image_path_to_texture(path, self.ctx))
|
||||
|
||||
def init_vertex_objects(self):
|
||||
self.vbo = None
|
||||
self.vaos = []
|
||||
|
||||
def add_texture(self, name: str, texture: moderngl.Texture):
|
||||
max_units = self.ctx.info['GL_MAX_TEXTURE_IMAGE_UNITS']
|
||||
if len(self.textures) >= max_units:
|
||||
raise ValueError(f"Unable to use more than {max_units} textures for a program")
|
||||
# The position in the list determines its id
|
||||
self.texture_names_to_ids[name] = len(self.textures)
|
||||
self.textures.append(texture)
|
||||
|
||||
def bind_to_mobject_uniforms(self, mobject_uniforms: UniformDict):
|
||||
self.mobject_uniforms = mobject_uniforms
|
||||
|
||||
def get_id(self) -> int:
|
||||
return self.id
|
||||
|
||||
def refresh_id(self) -> None:
|
||||
self.id = hash("".join(map(str, [
|
||||
"".join(map(str, self.program_code.values())),
|
||||
self.mobject_uniforms,
|
||||
self.depth_test,
|
||||
self.render_primitive,
|
||||
self.texture_paths,
|
||||
])))
|
||||
|
||||
def replace_code(self, old: str, new: str) -> None:
|
||||
code_map = self.program_code
|
||||
for (name, code) in code_map.items():
|
||||
for name in code_map:
|
||||
if code_map[name] is None:
|
||||
continue
|
||||
code_map[name] = re.sub(old, new, code_map[name])
|
||||
self.init_program()
|
||||
self.refresh_id()
|
||||
|
||||
def combine_with(self, *shader_wrappers):
|
||||
# Assume they are of the same type
|
||||
if len(shader_wrappers) == 0:
|
||||
return
|
||||
if self.vert_indices is not None:
|
||||
num_verts = len(self.vert_data)
|
||||
indices_list = [self.vert_indices]
|
||||
data_list = [self.vert_data]
|
||||
for sw in shader_wrappers:
|
||||
indices_list.append(sw.vert_indices + num_verts)
|
||||
data_list.append(sw.vert_data)
|
||||
num_verts += len(sw.vert_data)
|
||||
self.vert_indices = np.hstack(indices_list)
|
||||
self.vert_data = np.hstack(data_list)
|
||||
# Changing context
|
||||
def use_clip_plane(self):
|
||||
if "clip_plane" not in self.mobject_uniforms:
|
||||
return False
|
||||
return any(self.mobject_uniforms["clip_plane"])
|
||||
|
||||
def set_ctx_depth_test(self, enable: bool = True) -> None:
|
||||
if enable:
|
||||
self.ctx.enable(moderngl.DEPTH_TEST)
|
||||
else:
|
||||
self.vert_data = np.hstack([self.vert_data, *[sw.vert_data for sw in shader_wrappers]])
|
||||
return self
|
||||
self.ctx.disable(moderngl.DEPTH_TEST)
|
||||
|
||||
def set_ctx_clip_plane(self, enable: bool = True) -> None:
|
||||
if enable:
|
||||
gl.glEnable(gl.GL_CLIP_DISTANCE0)
|
||||
|
||||
# Adding data
|
||||
|
||||
def read_in(self, data_list: Iterable[np.ndarray]):
|
||||
total_len = sum(map(len, data_list))
|
||||
if total_len == 0:
|
||||
if self.vbo is not None:
|
||||
self.vbo.clear()
|
||||
return
|
||||
|
||||
# If possible, read concatenated data into existing list
|
||||
if len(self.vert_data) != total_len:
|
||||
self.vert_data = np.concatenate(data_list)
|
||||
else:
|
||||
np.concatenate(data_list, out=self.vert_data)
|
||||
|
||||
# Either create new vbo, or read data into it
|
||||
total_size = self.vert_data.itemsize * total_len
|
||||
if self.vbo is not None and self.vbo.size != total_size:
|
||||
self.release() # This sets vbo to be None
|
||||
if self.vbo is None:
|
||||
self.vbo = self.ctx.buffer(self.vert_data)
|
||||
self.generate_vaos()
|
||||
else:
|
||||
self.vbo.write(self.vert_data)
|
||||
|
||||
def generate_vaos(self):
|
||||
# Vertex array object
|
||||
self.vaos = [
|
||||
self.ctx.vertex_array(
|
||||
program=program,
|
||||
content=[(self.vbo, self.vert_format, *self.vert_attributes)],
|
||||
mode=self.render_primitive,
|
||||
)
|
||||
for program in self.programs
|
||||
]
|
||||
|
||||
# Related to data and rendering
|
||||
def pre_render(self):
|
||||
self.set_ctx_depth_test(self.depth_test)
|
||||
self.set_ctx_clip_plane(self.use_clip_plane())
|
||||
for tid, texture in enumerate(self.textures):
|
||||
texture.use(tid)
|
||||
|
||||
def render(self):
|
||||
for vao in self.vaos:
|
||||
vao.render()
|
||||
|
||||
def update_program_uniforms(self, camera_uniforms: UniformDict):
|
||||
for program in self.programs:
|
||||
if program is None:
|
||||
continue
|
||||
for uniforms in [self.mobject_uniforms, camera_uniforms, self.texture_names_to_ids]:
|
||||
for name, value in uniforms.items():
|
||||
set_program_uniform(program, name, value)
|
||||
|
||||
def release(self):
|
||||
for obj in (self.vbo, *self.vaos):
|
||||
if obj is not None:
|
||||
obj.release()
|
||||
self.init_vertex_objects()
|
||||
|
||||
def release_textures(self):
|
||||
for texture in self.textures:
|
||||
texture.release()
|
||||
del texture
|
||||
self.textures = []
|
||||
self.texture_names_to_ids = dict()
|
||||
|
||||
|
||||
# For caching
|
||||
filename_to_code_map = {}
|
||||
|
||||
|
||||
def get_shader_code_from_file(filename):
|
||||
if not filename:
|
||||
return None
|
||||
if filename in filename_to_code_map:
|
||||
return filename_to_code_map[filename]
|
||||
|
||||
try:
|
||||
filepath = find_file(
|
||||
filename,
|
||||
directories=[get_shader_dir(), "/"],
|
||||
extensions=[],
|
||||
class VShaderWrapper(ShaderWrapper):
|
||||
def __init__(
|
||||
self,
|
||||
ctx: moderngl.context.Context,
|
||||
vert_data: np.ndarray,
|
||||
shader_folder: Optional[str] = None,
|
||||
mobject_uniforms: Optional[UniformDict] = None, # A dictionary mapping names of uniform variables
|
||||
texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures.
|
||||
depth_test: bool = False,
|
||||
render_primitive: int = moderngl.TRIANGLES,
|
||||
code_replacements: dict[str, str] = dict(),
|
||||
stroke_behind: bool = False,
|
||||
):
|
||||
self.stroke_behind = stroke_behind
|
||||
super().__init__(
|
||||
ctx=ctx,
|
||||
vert_data=vert_data,
|
||||
shader_folder=shader_folder,
|
||||
mobject_uniforms=mobject_uniforms,
|
||||
texture_paths=texture_paths,
|
||||
depth_test=depth_test,
|
||||
render_primitive=render_primitive,
|
||||
code_replacements=code_replacements,
|
||||
)
|
||||
except IOError:
|
||||
return None
|
||||
self.fill_canvas = VShaderWrapper.get_fill_canvas(self.ctx)
|
||||
self.add_texture('Texture', self.fill_canvas[0].color_attachments[0])
|
||||
self.add_texture('DepthTexture', self.fill_canvas[2].color_attachments[0])
|
||||
|
||||
with open(filepath, "r") as f:
|
||||
result = f.read()
|
||||
def init_program_code(self) -> None:
|
||||
self.program_code = {
|
||||
f"{vtype}_{name}": get_shader_code_from_file(
|
||||
os.path.join("quadratic_bezier", f"{vtype}", f"{name}.glsl")
|
||||
)
|
||||
for vtype in ["stroke", "fill", "depth"]
|
||||
for name in ["vert", "geom", "frag"]
|
||||
}
|
||||
|
||||
# To share functionality between shaders, some functions are read in
|
||||
# from other files an inserted into the relevant strings before
|
||||
# passing to ctx.program for compiling
|
||||
# Replace "#INSERT " lines with relevant code
|
||||
insertions = re.findall(r"^#INSERT .*\.glsl$", result, flags=re.MULTILINE)
|
||||
for line in insertions:
|
||||
inserted_code = get_shader_code_from_file(
|
||||
os.path.join("inserts", line.replace("#INSERT ", ""))
|
||||
def init_program(self):
|
||||
self.stroke_program = get_shader_program(
|
||||
self.ctx,
|
||||
vertex_shader=self.program_code["stroke_vert"],
|
||||
geometry_shader=self.program_code["stroke_geom"],
|
||||
fragment_shader=self.program_code["stroke_frag"],
|
||||
)
|
||||
result = result.replace(line, inserted_code)
|
||||
filename_to_code_map[filename] = result
|
||||
return result
|
||||
self.fill_program = get_shader_program(
|
||||
self.ctx,
|
||||
vertex_shader=self.program_code["fill_vert"],
|
||||
geometry_shader=self.program_code["fill_geom"],
|
||||
fragment_shader=self.program_code["fill_frag"],
|
||||
)
|
||||
self.fill_border_program = get_shader_program(
|
||||
self.ctx,
|
||||
vertex_shader=self.program_code["stroke_vert"],
|
||||
geometry_shader=self.program_code["stroke_geom"],
|
||||
fragment_shader=self.program_code["stroke_frag"].replace(
|
||||
"// MODIFY FRAG COLOR",
|
||||
"frag_color.a *= 0.95; frag_color.rgb *= frag_color.a;",
|
||||
)
|
||||
)
|
||||
self.fill_depth_program = get_shader_program(
|
||||
self.ctx,
|
||||
vertex_shader=self.program_code["depth_vert"],
|
||||
geometry_shader=self.program_code["depth_geom"],
|
||||
fragment_shader=self.program_code["depth_frag"],
|
||||
)
|
||||
self.programs = [self.stroke_program, self.fill_program, self.fill_border_program, self.fill_depth_program]
|
||||
|
||||
# Full vert format looks like this (total of 4x23 = 92 bytes):
|
||||
# point 3
|
||||
# stroke_rgba 4
|
||||
# stroke_width 1
|
||||
# joint_angle 1
|
||||
# fill_rgba 4
|
||||
# base_normal 3
|
||||
# fill_border_width 1
|
||||
self.stroke_vert_format = '3f 4f 1f 1f 16x 3f 4x'
|
||||
self.stroke_vert_attributes = ['point', 'stroke_rgba', 'stroke_width', 'joint_angle', 'unit_normal']
|
||||
|
||||
def get_colormap_code(rgb_list):
|
||||
data = ",".join(
|
||||
"vec3({}, {}, {})".format(*rgb)
|
||||
for rgb in rgb_list
|
||||
)
|
||||
return f"vec3[{len(rgb_list)}]({data})"
|
||||
self.fill_vert_format = '3f 24x 4f 3f 4x'
|
||||
self.fill_vert_attributes = ['point', 'fill_rgba', 'base_normal']
|
||||
|
||||
self.fill_border_vert_format = '3f 20x 1f 4f 3f 1f'
|
||||
self.fill_border_vert_attributes = ['point', 'joint_angle', 'stroke_rgba', 'unit_normal', 'stroke_width']
|
||||
|
||||
self.fill_depth_vert_format = '3f 40x 3f 4x'
|
||||
self.fill_depth_vert_attributes = ['point', 'base_normal']
|
||||
|
||||
def init_vertex_objects(self):
|
||||
self.vbo = None
|
||||
self.stroke_vao = None
|
||||
self.fill_vao = None
|
||||
self.fill_border_vao = None
|
||||
self.vaos = []
|
||||
|
||||
def generate_vaos(self):
|
||||
self.stroke_vao = self.ctx.vertex_array(
|
||||
program=self.stroke_program,
|
||||
content=[(self.vbo, self.stroke_vert_format, *self.stroke_vert_attributes)],
|
||||
mode=self.render_primitive,
|
||||
)
|
||||
self.fill_vao = self.ctx.vertex_array(
|
||||
program=self.fill_program,
|
||||
content=[(self.vbo, self.fill_vert_format, *self.fill_vert_attributes)],
|
||||
mode=self.render_primitive,
|
||||
)
|
||||
self.fill_border_vao = self.ctx.vertex_array(
|
||||
program=self.fill_border_program,
|
||||
content=[(self.vbo, self.fill_border_vert_format, *self.fill_border_vert_attributes)],
|
||||
mode=self.render_primitive,
|
||||
)
|
||||
self.fill_depth_vao = self.ctx.vertex_array(
|
||||
program=self.fill_depth_program,
|
||||
content=[(self.vbo, self.fill_depth_vert_format, *self.fill_depth_vert_attributes)],
|
||||
mode=self.render_primitive,
|
||||
)
|
||||
self.vaos = [self.stroke_vao, self.fill_vao, self.fill_border_vao, self.fill_depth_vao]
|
||||
|
||||
def set_backstroke(self, value: bool = True):
|
||||
self.stroke_behind = value
|
||||
|
||||
def refresh_id(self):
|
||||
super().refresh_id()
|
||||
self.id = hash(str(self.id) + str(self.stroke_behind))
|
||||
|
||||
# Rendering
|
||||
def render_stroke(self):
|
||||
if self.stroke_vao is None:
|
||||
return
|
||||
self.stroke_vao.render()
|
||||
|
||||
def render_fill(self):
|
||||
if self.fill_vao is None:
|
||||
return
|
||||
|
||||
original_fbo = self.ctx.fbo
|
||||
fill_tx_fbo, fill_tx_vao, depth_tx_fbo = self.fill_canvas
|
||||
|
||||
# Render to a separate texture, due to strange alpha compositing
|
||||
# for the blended winding calculation
|
||||
fill_tx_fbo.clear()
|
||||
fill_tx_fbo.use()
|
||||
|
||||
# Be sure not to apply depth test while rendering fill
|
||||
# but set it back to where it was after
|
||||
apply_depth_test = bool(gl.glGetBooleanv(gl.GL_DEPTH_TEST))
|
||||
self.ctx.disable(moderngl.DEPTH_TEST)
|
||||
|
||||
# With this blend function, the effect of blending alpha a with
|
||||
# -a / (1 - a) cancels out, so we can cancel positively and negatively
|
||||
# oriented triangles
|
||||
gl.glBlendFuncSeparate(
|
||||
gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA,
|
||||
gl.GL_ONE_MINUS_DST_ALPHA, gl.GL_ONE
|
||||
)
|
||||
self.fill_vao.render()
|
||||
|
||||
if apply_depth_test:
|
||||
self.ctx.enable(moderngl.DEPTH_TEST)
|
||||
depth_tx_fbo.clear(1.0)
|
||||
depth_tx_fbo.use()
|
||||
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE)
|
||||
gl.glBlendEquation(gl.GL_MIN)
|
||||
self.fill_depth_vao.render()
|
||||
|
||||
# Now add border, just taking the max alpha
|
||||
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE)
|
||||
gl.glBlendEquation(gl.GL_MAX)
|
||||
self.fill_border_vao.render()
|
||||
|
||||
# Take the texture we were just drawing to, and render it to
|
||||
# the main scene. Account for how alphas have been premultiplied
|
||||
original_fbo.use()
|
||||
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
||||
gl.glBlendEquation(gl.GL_FUNC_ADD)
|
||||
fill_tx_vao.render()
|
||||
|
||||
# Return to original blending state
|
||||
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
|
||||
|
||||
# Static method returning one shared value across all VShaderWrappers
|
||||
@lru_cache
|
||||
@staticmethod
|
||||
def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Framebuffer]:
|
||||
"""
|
||||
Because VMobjects with fill are rendered in a funny way, using
|
||||
alpha blending to effectively compute the winding number around
|
||||
each pixel, they need to be rendered to a separate texture, which
|
||||
is then composited onto the ordinary frame buffer.
|
||||
|
||||
This returns a texture, loaded into a frame buffer, and a vao
|
||||
which can display that texture as a simple quad onto a screen,
|
||||
along with the rgb value which is meant to be discarded.
|
||||
"""
|
||||
size = manim_config.camera.resolution
|
||||
double_size = (2 * size[0], 2 * size[1])
|
||||
|
||||
# Important to make sure dtype is floating point (not fixed point)
|
||||
# so that alpha values can be negative and are not clipped
|
||||
fill_texture = ctx.texture(size=double_size, components=4, dtype='f2')
|
||||
# Use another one to keep track of depth
|
||||
depth_texture = ctx.texture(size=size, components=1, dtype='f4')
|
||||
|
||||
fill_texture_fbo = ctx.framebuffer(fill_texture)
|
||||
depth_texture_fbo = ctx.framebuffer(depth_texture)
|
||||
|
||||
simple_vert = '''
|
||||
#version 330
|
||||
|
||||
in vec2 texcoord;
|
||||
out vec2 uv;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4((2.0 * texcoord - 1.0), 0.0, 1.0);
|
||||
uv = texcoord;
|
||||
}
|
||||
'''
|
||||
alpha_adjust_frag = '''
|
||||
#version 330
|
||||
|
||||
uniform sampler2D Texture;
|
||||
uniform sampler2D DepthTexture;
|
||||
|
||||
in vec2 uv;
|
||||
out vec4 color;
|
||||
|
||||
void main() {
|
||||
color = texture(Texture, uv);
|
||||
if(color.a == 0) discard;
|
||||
|
||||
if(color.a < 0){
|
||||
color.a = -color.a / (1.0 - color.a);
|
||||
color.rgb *= (color.a - 1);
|
||||
}
|
||||
|
||||
// Counteract scaling in fill frag
|
||||
color *= 1.06;
|
||||
|
||||
gl_FragDepth = texture(DepthTexture, uv)[0];
|
||||
}
|
||||
'''
|
||||
fill_program = ctx.program(
|
||||
vertex_shader=simple_vert,
|
||||
fragment_shader=alpha_adjust_frag,
|
||||
)
|
||||
|
||||
verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
|
||||
simple_vbo = ctx.buffer(verts.astype('f4').tobytes())
|
||||
fill_texture_vao = ctx.simple_vertex_array(
|
||||
fill_program, simple_vbo, 'texcoord',
|
||||
mode=moderngl.TRIANGLE_STRIP
|
||||
)
|
||||
|
||||
return (fill_texture_fbo, fill_texture_vao, depth_texture_fbo)
|
||||
|
||||
def render(self):
|
||||
if self.stroke_behind:
|
||||
self.render_stroke()
|
||||
self.render_fill()
|
||||
else:
|
||||
self.render_fill()
|
||||
self.render_stroke()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#version 330
|
||||
|
||||
#INSERT camera_uniform_declarations.glsl
|
||||
|
||||
uniform sampler2D Texture;
|
||||
|
||||
in vec3 point;
|
||||
@@ -12,11 +10,10 @@ out vec2 v_im_coords;
|
||||
out float v_opacity;
|
||||
|
||||
// Analog of import for manim only
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT position_point_into_frame.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
v_im_coords = im_coords;
|
||||
v_opacity = opacity;
|
||||
gl_Position = get_gl_Position(position_point_into_frame(point));
|
||||
emit_gl_Position(point);
|
||||
}
|
||||
@@ -4,4 +4,4 @@ There seems to be no analog to #include in C++ for OpenGL shaders. While there
|
||||
|
||||
with the code from one of the files in this folder.
|
||||
|
||||
The functions in this file often include reference to uniforms which are assumed to be part of the surrounding context into which they are inserted.
|
||||
The functions in this file may include declarations of uniforms, so one should not re-declare those in the surrounding context.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
uniform vec2 frame_shape;
|
||||
uniform float anti_alias_width;
|
||||
uniform vec3 camera_offset;
|
||||
uniform mat3 camera_rotation;
|
||||
uniform float is_fixed_in_frame;
|
||||
uniform float focal_distance;
|
||||
21
manimlib/shaders/inserts/emit_gl_Position.glsl
Normal file
21
manimlib/shaders/inserts/emit_gl_Position.glsl
Normal file
@@ -0,0 +1,21 @@
|
||||
uniform float is_fixed_in_frame;
|
||||
uniform mat4 view;
|
||||
uniform float focal_distance;
|
||||
uniform vec3 frame_rescale_factors;
|
||||
uniform vec4 clip_plane;
|
||||
|
||||
void emit_gl_Position(vec3 point){
|
||||
vec4 result = vec4(point, 1.0);
|
||||
// This allow for smooth transitions between objects fixed and unfixed from frame
|
||||
result = mix(view * result, result, is_fixed_in_frame);
|
||||
// Essentially a projection matrix
|
||||
result.xyz *= frame_rescale_factors;
|
||||
result.w = 1.0 - result.z;
|
||||
// Flip and scale to prevent premature clipping
|
||||
result.z *= -0.1;
|
||||
gl_Position = result;
|
||||
|
||||
if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){
|
||||
gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
uniform vec3 light_position;
|
||||
uniform vec3 camera_position;
|
||||
uniform vec3 shading;
|
||||
|
||||
vec3 float_to_color(float value, float min_val, float max_val, vec3[9] colormap_data){
|
||||
float alpha = clamp((value - min_val) / (max_val - min_val), 0.0, 1.0);
|
||||
int disc_alpha = min(int(alpha * 8), 7);
|
||||
@@ -9,30 +13,22 @@ vec3 float_to_color(float value, float min_val, float max_val, vec3[9] colormap_
|
||||
}
|
||||
|
||||
|
||||
vec4 add_light(vec4 color,
|
||||
vec3 point,
|
||||
vec3 unit_normal,
|
||||
vec3 light_coords,
|
||||
vec3 cam_coords,
|
||||
float reflectiveness,
|
||||
float gloss,
|
||||
float shadow){
|
||||
if(reflectiveness == 0.0 && gloss == 0.0 && shadow == 0.0) return color;
|
||||
vec4 add_light(vec4 color, vec3 point, vec3 unit_normal){
|
||||
if(shading == vec3(0.0)) return color;
|
||||
|
||||
float reflectiveness = shading.x;
|
||||
float gloss = shading.y;
|
||||
float shadow = shading.z;
|
||||
|
||||
vec4 result = color;
|
||||
// Assume everything has already been rotated such that camera is in the z-direction
|
||||
// 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;
|
||||
vec3 to_camera = normalize(camera_position - point);
|
||||
vec3 to_light = normalize(light_position - point);
|
||||
|
||||
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);
|
||||
vec3 light_reflection = reflect(-to_light, unit_normal);
|
||||
float light_to_cam = dot(light_reflection, to_camera);
|
||||
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));
|
||||
bright_factor += shine;
|
||||
@@ -40,29 +36,18 @@ vec4 add_light(vec4 color,
|
||||
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);
|
||||
result.rgb = mix(
|
||||
result.rgb,
|
||||
vec3(0.0),
|
||||
max(-light_to_normal, 0) * 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){
|
||||
vec4 finalize_color(vec4 color, vec3 point, vec3 unit_normal){
|
||||
///// 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, cam_coords,
|
||||
reflectiveness, gloss, shadow
|
||||
);
|
||||
return add_light(color, point, unit_normal);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Assumes the following uniforms exist in the surrounding context:
|
||||
// uniform vec2 frame_shape;
|
||||
// uniform float focal_distance;
|
||||
// uniform float is_fixed_in_frame;
|
||||
|
||||
const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0);
|
||||
|
||||
float perspective_scale_factor(float z, float focal_distance){
|
||||
return max(0.0, focal_distance / (focal_distance - z));
|
||||
}
|
||||
|
||||
|
||||
vec4 get_gl_Position(vec3 point){
|
||||
vec4 result = vec4(point, 1.0);
|
||||
if(!bool(is_fixed_in_frame)){
|
||||
result.x *= 2.0 / frame_shape.x;
|
||||
result.y *= 2.0 / frame_shape.y;
|
||||
float psf = perspective_scale_factor(result.z, focal_distance);
|
||||
if (psf > 0){
|
||||
result.xy *= psf;
|
||||
// TODO, what's the better way to do this?
|
||||
// This is to keep vertices too far out of frame from getting cut.
|
||||
result.z *= 0.01;
|
||||
}
|
||||
} else{
|
||||
result.x *= 2.0 / DEFAULT_FRAME_SHAPE.x;
|
||||
result.y *= 2.0 / DEFAULT_FRAME_SHAPE.y;
|
||||
}
|
||||
result.z *= -1;
|
||||
return result;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Assumes the following uniforms exist in the surrounding context:
|
||||
// uniform vec3 camera_offset;
|
||||
// uniform mat3 camera_rotation;
|
||||
|
||||
vec3 get_rotated_surface_unit_normal_vector(vec3 point, vec3 du_point, vec3 dv_point){
|
||||
vec3 cp = cross(
|
||||
(du_point - point),
|
||||
(dv_point - point)
|
||||
);
|
||||
if(length(cp) == 0){
|
||||
// Instead choose a normal to just dv_point - point in the direction of point
|
||||
vec3 v2 = dv_point - point;
|
||||
cp = cross(cross(v2, point), v2);
|
||||
}
|
||||
return normalize(rotate_point_into_frame(cp));
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
vec3 get_unit_normal(in vec3[3] points){
|
||||
vec3 get_unit_normal(vec3 p0, vec3 p1, vec3 p2){
|
||||
float tol = 1e-6;
|
||||
vec3 v1 = normalize(points[1] - points[0]);
|
||||
vec3 v2 = normalize(points[2] - points[0]);
|
||||
vec3 v1 = normalize(p1 - p0);
|
||||
vec3 v2 = normalize(p2 - p0);
|
||||
vec3 cp = cross(v1, v2);
|
||||
float cp_norm = length(cp);
|
||||
if(cp_norm < tol){
|
||||
// Three points form a line, so find a normal vector
|
||||
// to that line in the plane shared with the z-axis
|
||||
vec3 k_hat = vec3(0.0, 0.0, 1.0);
|
||||
vec3 new_cp = cross(cross(v2, k_hat), v2);
|
||||
float new_cp_norm = length(new_cp);
|
||||
if(new_cp_norm < tol){
|
||||
// We only come here if all three points line up
|
||||
// on the z-axis.
|
||||
return vec3(0.0, -1.0, 0.0);
|
||||
// return k_hat;
|
||||
}
|
||||
return new_cp / new_cp_norm;
|
||||
}
|
||||
return cp / cp_norm;
|
||||
|
||||
if(cp_norm > tol) return cp / cp_norm;
|
||||
|
||||
// Otherwise, three pionts form a line, so find
|
||||
// a normal vector to that line in the plane shared
|
||||
// with the z-axis
|
||||
vec3 comb = v1 + v2;
|
||||
cp = cross(cross(comb, vec3(0.0, 0.0, 1.0)), comb);
|
||||
cp_norm = length(cp);
|
||||
if(cp_norm > tol) return cp / cp_norm;
|
||||
|
||||
// Otherwise, the points line up with the z-axis.
|
||||
return vec3(0.0, -1.0, 0.0);
|
||||
}
|
||||
104
manimlib/shaders/inserts/get_xyz_to_uv.glsl
Normal file
104
manimlib/shaders/inserts/get_xyz_to_uv.glsl
Normal file
@@ -0,0 +1,104 @@
|
||||
vec2 xs_on_clean_parabola(vec3 b0, vec3 b1, vec3 b2){
|
||||
/*
|
||||
Given three control points for a quadratic bezier,
|
||||
this returns the two values (x0, x2) such that the
|
||||
section of the parabola y = x^2 between those values
|
||||
is isometric to the given quadratic bezier.
|
||||
|
||||
Adapated from https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
|
||||
*/
|
||||
vec3 dd = 2 * b1 - b0 - b2;
|
||||
|
||||
float u0 = dot(b1 - b0, dd);
|
||||
float u2 = dot(b2 - b1, dd);
|
||||
float cp = length(cross(b2 - b0, dd));
|
||||
|
||||
return vec2(u0 / cp, u2 / cp);
|
||||
}
|
||||
|
||||
|
||||
mat4 map_triangles(vec3 src0, vec3 src1, vec3 src2, vec3 dst0, vec3 dst1, vec3 dst2){
|
||||
/*
|
||||
Return an affine transform which maps the triangle (src0, src1, src2)
|
||||
onto the triangle (dst0, dst1, dst2)
|
||||
*/
|
||||
mat4 src_mat = mat4(
|
||||
src0, 1.0,
|
||||
src1, 1.0,
|
||||
src2, 1.0,
|
||||
vec4(1.0)
|
||||
);
|
||||
mat4 dst_mat = mat4(
|
||||
dst0, 1.0,
|
||||
dst1, 1.0,
|
||||
dst2, 1.0,
|
||||
vec4(1.0)
|
||||
);
|
||||
return dst_mat * inverse(src_mat);
|
||||
}
|
||||
|
||||
|
||||
mat4 rotation(vec3 axis, float cos_angle){
|
||||
float c = cos_angle;
|
||||
float s = sqrt(1 - c * c); // Sine of the angle
|
||||
float oc = 1.0 - c;
|
||||
float ax = axis.x;
|
||||
float ay = axis.y;
|
||||
float az = axis.z;
|
||||
|
||||
return mat4(
|
||||
oc * ax * ax + c, oc * ax * ay + az * s, oc * az * ax - ay * s, 0.0,
|
||||
oc * ax * ay - az * s, oc * ay * ay + c, oc * ay * az + ax * s, 0.0,
|
||||
oc * az * ax + ay * s, oc * ay * az - ax * s, oc * az * az + c, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
mat4 map_onto_x_axis(vec3 src0, vec3 src1){
|
||||
mat4 shift = mat4(1.0);
|
||||
shift[3].xyz = -src0;
|
||||
|
||||
// Find rotation matrix between unit vectors in each direction
|
||||
vec3 vect = normalize(src1 - src0);
|
||||
// No rotation needed
|
||||
if(vect.x > 1 - 1e-6) return shift;
|
||||
|
||||
// Equivalent to cross(vect, vec3(1, 0, 0))
|
||||
vec3 axis = normalize(vec3(0.0, vect.z, -vect.y));
|
||||
mat4 rotate = rotation(axis, vect.x);
|
||||
return rotate * shift;
|
||||
}
|
||||
|
||||
|
||||
mat4 get_xyz_to_uv(
|
||||
vec3 b0, vec3 b1, vec3 b2,
|
||||
float threshold,
|
||||
out bool exceeds_threshold
|
||||
){
|
||||
/*
|
||||
Populates the matrix `result` with an affine transformation which maps a set of
|
||||
quadratic bezier controls points into a new coordinate system such that the bezier
|
||||
curve coincides with y = x^2.
|
||||
|
||||
If the x-range under this part of the curve exceeds `threshold`, this returns false
|
||||
and populates result a matrix mapping b0 and b2 onto the x-axis
|
||||
*/
|
||||
vec2 xs = xs_on_clean_parabola(b0, b1, b2);
|
||||
float x0 = xs[0];
|
||||
float x1 = 0.5 * (xs[0] + xs[1]);
|
||||
float x2 = xs[1];
|
||||
// Portions of the parabola y = x^2 where abs(x) exceeds
|
||||
// this value are treated as straight lines.
|
||||
exceeds_threshold = (min(x0, x2) > threshold || max(x0, x2) < -threshold);
|
||||
if(exceeds_threshold){
|
||||
return map_onto_x_axis(b0, b2);
|
||||
}
|
||||
// This triangle on the xy plane should be isometric
|
||||
// to (b0, b1, b2), and it should define a quadratic
|
||||
// bezier segment aligned with y = x^2
|
||||
vec3 dst0 = vec3(x0, x0 * x0, 0.0);
|
||||
vec3 dst1 = vec3(x1, x0 * x2, 0.0);
|
||||
vec3 dst2 = vec3(x2, x2 * x2, 0.0);
|
||||
return map_triangles(b0, b1, b2, dst0, dst1, dst2);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Assumes the following uniforms exist in the surrounding context:
|
||||
// uniform float is_fixed_in_frame;
|
||||
// uniform vec3 camera_offset;
|
||||
// uniform mat3 camera_rotation;
|
||||
|
||||
vec3 rotate_point_into_frame(vec3 point){
|
||||
if(bool(is_fixed_in_frame)){
|
||||
return point;
|
||||
}
|
||||
return camera_rotation * point;
|
||||
}
|
||||
|
||||
|
||||
vec3 position_point_into_frame(vec3 point){
|
||||
if(bool(is_fixed_in_frame)){
|
||||
return point;
|
||||
}
|
||||
return rotate_point_into_frame(point - camera_offset);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user