mirror of
https://github.com/3b1b/manim.git
synced 2026-01-13 00:18:05 -05:00
Compare commits
234 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b61f1473a5 | ||
|
|
e3d5b49a55 | ||
|
|
4d6a0db1e1 | ||
|
|
0af46e149d | ||
|
|
896b011d76 | ||
|
|
3adaf8e325 | ||
|
|
8762177df5 | ||
|
|
a1d51474ea | ||
|
|
83841ae415 | ||
|
|
b81f244c3c | ||
|
|
7023548ec6 | ||
|
|
758f329a06 | ||
|
|
8f1dfabff0 | ||
|
|
7fa01d5de8 | ||
|
|
0de303d5e0 | ||
|
|
155839bde9 | ||
|
|
3a1e5e1bcf | ||
|
|
264f7b1172 | ||
|
|
85e90a1488 | ||
|
|
f8e6e7df3c | ||
|
|
5dd7cce67f | ||
|
|
f21a4a4696 | ||
|
|
98b0d266d2 | ||
|
|
6821a7c20e | ||
|
|
00f72da493 | ||
|
|
744916507c | ||
|
|
88d863c1d7 | ||
|
|
d7dcc9d76f | ||
|
|
4631508b7d | ||
|
|
8803088121 | ||
|
|
1d466cb299 | ||
|
|
5a1f00b1cb | ||
|
|
17d31045b2 | ||
|
|
950466c1da | ||
|
|
62151e52f1 | ||
|
|
b4ce0b910c | ||
|
|
9dd1f47dab | ||
|
|
49743daf32 | ||
|
|
ba23fbe71e | ||
|
|
ee1594a3cb | ||
|
|
e9afb0ee33 | ||
|
|
8b1715379d | ||
|
|
2501fac32f | ||
|
|
1aec0462ec | ||
|
|
83c70a59d8 | ||
|
|
9b8a6e7ff8 | ||
|
|
758f2ec236 | ||
|
|
d9cac38618 | ||
|
|
e8ebfa312b | ||
|
|
dae24891fa | ||
|
|
a4f9de1ca1 | ||
|
|
697028cd4c | ||
|
|
c84acc0023 | ||
|
|
b1d869cd11 | ||
|
|
13a5f6d6ff | ||
|
|
e3f87d835b | ||
|
|
7ffab788b7 | ||
|
|
bcd09906be | ||
|
|
407c53f97c | ||
|
|
eea3c6b294 | ||
|
|
d2182b9112 | ||
|
|
fbc329d7ce | ||
|
|
25045143a1 | ||
|
|
e899604a2d | ||
|
|
0b898a5594 | ||
|
|
ee2f68cd49 | ||
|
|
2cce4ccdd7 | ||
|
|
f3ecebee43 | ||
|
|
e764da3c3a | ||
|
|
fbbea47d11 | ||
|
|
781a9934fd | ||
|
|
a7173142bf | ||
|
|
0e78027186 | ||
|
|
82bd02d21f | ||
|
|
d065e1973d | ||
|
|
7070777408 | ||
|
|
5c2a9f2129 | ||
|
|
1b695e1c19 | ||
|
|
da1cc44d90 | ||
|
|
3bbb759112 | ||
|
|
41c6cbcb59 | ||
|
|
5930e6a176 | ||
|
|
8f3ff91165 | ||
|
|
b12677bc1a | ||
|
|
cdec64e3f1 | ||
|
|
2dc8bc9b9c | ||
|
|
94f0bf557a | ||
|
|
e20690b7c1 | ||
|
|
2c7689ed9e | ||
|
|
c73d507c76 | ||
|
|
317a5d6226 | ||
|
|
4339f97c56 | ||
|
|
81c3ae3037 | ||
|
|
61b04079f5 | ||
|
|
5a0e5a16ea | ||
|
|
f0b5181694 | ||
|
|
185782a2e7 | ||
|
|
8ab95ebe9d | ||
|
|
77159eea2e | ||
|
|
6766e459f2 | ||
|
|
01f4ef3e5d | ||
|
|
b531c82bc4 | ||
|
|
5d942d5ac0 | ||
|
|
b285ca7c22 | ||
|
|
82540edae9 | ||
|
|
f9a6fa7036 | ||
|
|
4eabaecfc8 | ||
|
|
b881e55fca | ||
|
|
f1c50640a3 | ||
|
|
deb1311e48 | ||
|
|
82fa6ab125 | ||
|
|
4d91ff3f2f | ||
|
|
b6f9da87d0 | ||
|
|
c60e97ebf9 | ||
|
|
b1ed16e81a | ||
|
|
c94ebaa260 | ||
|
|
030fb52018 | ||
|
|
487f582302 | ||
|
|
6d0c55d2ba | ||
|
|
c82f60e29e | ||
|
|
c03279d626 | ||
|
|
7b72fa8ca1 | ||
|
|
8b454fbe93 | ||
|
|
f77e25ff86 | ||
|
|
872ef67cf7 | ||
|
|
305ca72ebe | ||
|
|
4d81d3678b | ||
|
|
55e968e174 | ||
|
|
97d1609849 | ||
|
|
e10f850d0d | ||
|
|
b8584fe5ab | ||
|
|
b6c23a09e9 | ||
|
|
0e574882b3 | ||
|
|
bee3470856 | ||
|
|
ed3d44120c | ||
|
|
4466cfe727 | ||
|
|
e9aba0b92c | ||
|
|
6cdbe0d67a | ||
|
|
7732d2f0ee | ||
|
|
f77482c864 | ||
|
|
23ebbb2af1 | ||
|
|
14fbed76da | ||
|
|
e10a752c00 | ||
|
|
fde82e09c0 | ||
|
|
cfd362aa56 | ||
|
|
329d2c6eae | ||
|
|
f22a341e84 | ||
|
|
2d115a2c90 | ||
|
|
c726eb7a18 | ||
|
|
33fa76dfac | ||
|
|
0021880fba | ||
|
|
ed99427a3b | ||
|
|
7425057d9f | ||
|
|
ef5253f1bc | ||
|
|
fbccb1ebf3 | ||
|
|
f626a1a1e2 | ||
|
|
c1242d2dd5 | ||
|
|
719c81d72b | ||
|
|
ed1fc4d5f9 | ||
|
|
3822b00bec | ||
|
|
2753beb7bb | ||
|
|
a4afbfd739 | ||
|
|
5f878a2c1a | ||
|
|
9483f26a3b | ||
|
|
b4132e3d5e | ||
|
|
e9b404406d | ||
|
|
b543cc0e32 | ||
|
|
d45ea28dc1 | ||
|
|
788775e419 | ||
|
|
1bca0e63e9 | ||
|
|
54ad3550ef | ||
|
|
d19b386415 | ||
|
|
e359f520bc | ||
|
|
696fc85ff7 | ||
|
|
add1daf500 | ||
|
|
242e4a3471 | ||
|
|
9e563ae3b4 | ||
|
|
da909c0df8 | ||
|
|
0239e12d8a | ||
|
|
0fd8fdc3ca | ||
|
|
762f1abef7 | ||
|
|
17c2772b84 | ||
|
|
0d2d1b5c03 | ||
|
|
4ce123be44 | ||
|
|
d5a88d0fa4 | ||
|
|
3b146636b4 | ||
|
|
b24ba19dec | ||
|
|
0dc096bf57 | ||
|
|
e40a2935b1 | ||
|
|
f84b8a66fe | ||
|
|
952a598e3b | ||
|
|
c635f19f2a | ||
|
|
8645894255 | ||
|
|
e712951f2d | ||
|
|
1b24074369 | ||
|
|
acba13f499 | ||
|
|
61aec6051a | ||
|
|
9a78d13212 | ||
|
|
0787c4f362 | ||
|
|
0b7b3f4f31 | ||
|
|
7356a36fa7 | ||
|
|
f3e3a7c56f | ||
|
|
a35dd5a3cb | ||
|
|
7b4199c674 | ||
|
|
d8378d8157 | ||
|
|
8647a6429d | ||
|
|
3bb8f3f042 | ||
|
|
56df15453f | ||
|
|
d50717a3fc | ||
|
|
ca9b70e218 | ||
|
|
25c5aa2c65 | ||
|
|
c08ea4e645 | ||
|
|
573d630e5b | ||
|
|
6d72893382 | ||
|
|
40290ada83 | ||
|
|
bd356daa99 | ||
|
|
b667db2d31 | ||
|
|
f92211b352 | ||
|
|
1c2b52a128 | ||
|
|
eb315daeda | ||
|
|
e151334675 | ||
|
|
bbeba108bc | ||
|
|
77ce17679c | ||
|
|
7fa2654d8a | ||
|
|
ec620fa849 | ||
|
|
da53a6f808 | ||
|
|
1e621e8278 | ||
|
|
31119b630e | ||
|
|
b0fd520382 | ||
|
|
c1e14ef5b6 | ||
|
|
e9470b6bde | ||
|
|
5c0a1e4b76 | ||
|
|
121e6215f8 | ||
|
|
3d5642f3d7 |
@@ -1,4 +1,4 @@
|
||||
from manimlib.imports import *
|
||||
from manimlib import *
|
||||
|
||||
class SquareToCircle(Scene):
|
||||
def construct(self):
|
||||
|
||||
@@ -1,10 +1,128 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Unreleased
|
||||
----------
|
||||
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``
|
||||
|
||||
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``
|
||||
|
||||
- ``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
|
||||
|
||||
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
|
||||
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added dependency on python packages `skia-pathops <https://github.com/fonttools/skia-pathops>`__
|
||||
|
||||
v1.2.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- `#1592 <https://github.com/3b1b/manim/pull/1592>`__: Fixed ``put_start_and_end_on`` in 3D
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Fixed ``DecimalNumber``'s scaling issue
|
||||
- `56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__: Fixed bug with common range array used for all coordinate systems
|
||||
- `8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__: Fixed ``CoordinateSystem`` init bug
|
||||
- `0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__: Fixed bug for single-valued ``ValueTracker``
|
||||
- `54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__: Fixed bug with SVG rectangles
|
||||
- `d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__: Fixed ``DotCloud.set_radii``
|
||||
- `b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__: Temporarily fixed bug for ``PMobject`` array resizing
|
||||
- `5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__: Fixed ``match_style``
|
||||
- `719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__: Fixed negative ``path_arc`` case
|
||||
- `c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__: Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis``
|
||||
- `7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__: Fixed ``ComplexPlane`` -i display bug
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `#1598 <https://github.com/3b1b/manim/pull/1598>`__: Supported the elliptical arc command ``A`` for ``SVGMobject``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Added ``FlashyFadeIn``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Save triangulation
|
||||
- `#1625 <https://github.com/3b1b/manim/pull/1625>`__: Added new ``Code`` mobject
|
||||
- `#1637 <https://github.com/3b1b/manim/pull/1637>`__: Add warnings and use rich to display log
|
||||
- `bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__: Added ``VCube``
|
||||
- `6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__: Supported ``ValueTracker`` to track vectors
|
||||
- `3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__: Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject``
|
||||
- `a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__: Added ``TracgTail``
|
||||
- `acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__: Added ``Scene.point_to_mobject``
|
||||
- `f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__: Added poly_fractal shader
|
||||
- `b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__: Added kwargs to ``TipableVMobject.set_length``
|
||||
- `17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__: Added ``Mobject.replicate``
|
||||
- `33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__: Added mandelbrot_fractal shader
|
||||
- `f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__: Saved state before each embed
|
||||
- `e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__: Allowed releasing of Textures
|
||||
- `14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__: Consolidated and renamed newton_fractal shader
|
||||
- `6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__: Hade ``ImageMoject`` remember the filepath to the Image
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Changed back to simpler ``Mobject.scale`` implementation
|
||||
- `b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__: Simplified ``Square``
|
||||
- `40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__: Removed unused parameter ``triangulation_locked``
|
||||
- `8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__: Reimplemented ``Arrow``
|
||||
- `d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__: Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default
|
||||
- `7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__: Refactored to call ``_handle_scale_side_effects`` after scaling takes place
|
||||
- `7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__: Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end``
|
||||
- `0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__: Made sure framerate is 30 for previewed scenes
|
||||
- `c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__: Pushed ``pixel_coords_to_space_coords`` to ``Window``
|
||||
- `d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__: Refactored to pass tuples and not arrays to uniforms
|
||||
- `9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__: Refactored to copy uniform arrays in ``Mobject.copy``
|
||||
- `ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__: Added ``bounding_box`` as exceptional key to point_cloud mobject
|
||||
- `329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__: Made sure stroke width is always a float
|
||||
|
||||
No changes
|
||||
|
||||
v1.1.0
|
||||
-------
|
||||
|
||||
@@ -83,22 +83,3 @@ Its value is a dictionary, passed in as ``kwargs`` when initializing the ``Camer
|
||||
to modify the value of the properties of the ``Camera`` class.
|
||||
|
||||
So the nesting of the ``CONFIG`` dictionary **essentially** passes in the value as ``kwargs``.
|
||||
|
||||
Common usage
|
||||
------------
|
||||
|
||||
When writing a class by yourself, you can add attributes or modify the attributes
|
||||
of the parent class through ``CONFIG``.
|
||||
|
||||
The most commonly used is to modify the properties of the camera when writing a ``Scene``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class YourScene(Scene):
|
||||
CONFIG = {
|
||||
"camera_config": {
|
||||
"background_color": WHITE,
|
||||
},
|
||||
}
|
||||
|
||||
For example, the above dictionary will change the background color to white, etc.
|
||||
@@ -52,7 +52,8 @@ flag abbr function
|
||||
``--finder`` Show the output file in finder
|
||||
``--config`` Guide for automatic configuration
|
||||
``--file_name FILE_NAME`` Name for the movie or image file
|
||||
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passin two comma separated values, e.g. "3,6", it will end the rendering at the second value.
|
||||
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value.
|
||||
``--embed LINENO`` ``-e`` Takes a line number as an argument, and results in the scene being called as if the line ``self.embed()`` was inserted into the scene code at that line number
|
||||
``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080"
|
||||
``--frame_rate FRAME_RATE`` Frame rate, as an integer
|
||||
``--color COLOR`` ``-c`` Background color
|
||||
|
||||
@@ -187,7 +187,7 @@ TexTransformExample
|
||||
# Passing in muliple arguments to Tex will result
|
||||
# in the same expression as if those arguments had
|
||||
# been joined together, except that the submobject
|
||||
# heirarchy of the resulting mobject ensure that the
|
||||
# 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
|
||||
@@ -335,7 +335,7 @@ UpdatersExample
|
||||
# If the argument itself might change, you can use f_always,
|
||||
# for which the arguments following the initial Mobject method
|
||||
# should be functions returning arguments to that method.
|
||||
# The following line ensures thst decimal.set_value(square.get_y())
|
||||
# The following line ensures that decimal.set_value(square.get_y())
|
||||
# is called every frame
|
||||
f_always(number.set_value, square.get_width)
|
||||
# You could also write the following equivalent line
|
||||
|
||||
@@ -6,7 +6,7 @@ Manim's documentation
|
||||
Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as seen in the videos
|
||||
at `3Blue1Brown <https://www.3blue1brown.com/>`_.
|
||||
|
||||
And here is a Chinese version of this documentation: https://docs.manim.org.cn/shaders
|
||||
And here is a Chinese version of this documentation: https://docs.manim.org.cn/
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -2,7 +2,7 @@ from manimlib import *
|
||||
import numpy as np
|
||||
|
||||
# To watch one of these scenes, run the following:
|
||||
# python -m manim example_scenes.py SquareToCircle
|
||||
# manimgl example_scenes.py OpeningManimExample
|
||||
# Use -s to skip to the end and just save the final frame
|
||||
# Use -w to write the animation to a file
|
||||
# Use -o to write it to a file and open it once done
|
||||
@@ -164,7 +164,7 @@ class TexTransformExample(Scene):
|
||||
# Passing in muliple arguments to Tex will result
|
||||
# in the same expression as if those arguments had
|
||||
# been joined together, except that the submobject
|
||||
# heirarchy of the resulting mobject ensure that the
|
||||
# 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
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import pkg_resources
|
||||
|
||||
__version__ = pkg_resources.get_distribution("manimgl").version
|
||||
|
||||
from manimlib.constants import *
|
||||
|
||||
from manimlib.animation.animation import *
|
||||
@@ -18,6 +22,7 @@ from manimlib.camera.camera import *
|
||||
|
||||
from manimlib.window import *
|
||||
|
||||
from manimlib.mobject.boolean_ops import *
|
||||
from manimlib.mobject.coordinate_systems import *
|
||||
from manimlib.mobject.changing import *
|
||||
from manimlib.mobject.frame import *
|
||||
@@ -32,6 +37,7 @@ 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.svg_mobject import *
|
||||
from manimlib.mobject.svg.tex_mobject import *
|
||||
from manimlib.mobject.svg.text_mobject import *
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
import manimlib.config
|
||||
import manimlib.logger
|
||||
import manimlib.extract_scene
|
||||
import manimlib.utils.init_config
|
||||
from manimlib import __version__
|
||||
|
||||
|
||||
def main():
|
||||
print(f"ManimGL \033[32mv{__version__}\033[0m")
|
||||
|
||||
args = manimlib.config.parse_cli()
|
||||
if args.version and args.file is None:
|
||||
return
|
||||
if args.log_level:
|
||||
manimlib.logger.log.setLevel(args.log_level)
|
||||
|
||||
if args.config:
|
||||
manimlib.utils.init_config.init_customization()
|
||||
@@ -16,5 +24,6 @@ def main():
|
||||
for scene in scenes:
|
||||
scene.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -174,16 +174,12 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
|
||||
"int_func": np.ceil,
|
||||
}
|
||||
|
||||
def __init__(self, group, **kwargs):
|
||||
new_group = Group(*group)
|
||||
super().__init__(new_group, **kwargs)
|
||||
|
||||
def update_submobject_list(self, index):
|
||||
# N = len(self.all_submobs)
|
||||
if index == 0:
|
||||
self.mobject.set_submobjects([])
|
||||
else:
|
||||
self.mobject.set_submobjects(self.all_submobs[index - 1])
|
||||
self.mobject.set_submobjects([self.all_submobs[index - 1]])
|
||||
|
||||
|
||||
# TODO, this is broken...
|
||||
|
||||
@@ -9,6 +9,7 @@ from manimlib.animation.composition import Succession
|
||||
from manimlib.animation.creation import ShowCreation
|
||||
from manimlib.animation.creation import ShowPartial
|
||||
from manimlib.animation.fading import FadeOut
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.animation.transform import Transform
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.geometry import Circle
|
||||
@@ -21,6 +22,8 @@ from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import there_and_back
|
||||
from manimlib.utils.rate_functions import wiggle
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.rate_functions import squish_rate_func
|
||||
|
||||
|
||||
class FocusOn(Transform):
|
||||
@@ -221,6 +224,8 @@ class FlashAround(VShowPassingFlash):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
path = self.get_path(mobject)
|
||||
if mobject.is_fixed_in_frame:
|
||||
path.fix_in_frame()
|
||||
path.insert_n_curves(self.n_inserted_curves)
|
||||
path.set_points(path.get_points_without_null_curves())
|
||||
path.set_stroke(self.color, self.stroke_width)
|
||||
@@ -361,3 +366,22 @@ class TurnInsideOut(Transform):
|
||||
|
||||
def create_target(self):
|
||||
return self.mobject.copy().reverse_points()
|
||||
|
||||
|
||||
class FlashyFadeIn(AnimationGroup):
|
||||
CONFIG = {
|
||||
"fade_lag": 0,
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, stroke_width=2, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
outline = vmobject.copy()
|
||||
outline.set_fill(opacity=0)
|
||||
outline.set_stroke(width=stroke_width, opacity=1)
|
||||
|
||||
rate_func = kwargs.get("rate_func", smooth)
|
||||
super().__init__(
|
||||
FadeIn(vmobject, rate_func=squish_rate_func(rate_func, self.fade_lag, 1)),
|
||||
VShowPassingFlash(outline, time_width=1),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ class Transform(Animation):
|
||||
self.check_target_mobject_validity()
|
||||
# Use a copy of target_mobject for the align_data_and_family
|
||||
# call so that the actual target_mobject stays
|
||||
# preserved, since calling allign_data will potentailly
|
||||
# preserved, since calling allign_data will potentially
|
||||
# change the structure of both arguments
|
||||
self.target_copy = self.target_mobject.copy()
|
||||
self.mobject.align_data_and_family(self.target_copy)
|
||||
@@ -161,7 +161,7 @@ class ApplyMethod(Transform):
|
||||
method is a method of Mobject, *args are arguments for
|
||||
that method. Key word arguments should be passed in
|
||||
as the last arg, as a dict, since **kwargs is for
|
||||
configuration of the transform itslef
|
||||
configuration of the transform itself
|
||||
|
||||
Relies on the fact that mobject methods return the mobject
|
||||
"""
|
||||
|
||||
@@ -68,10 +68,10 @@ class TransformMatchingParts(AnimationGroup):
|
||||
anims.append(FadeTransformPieces(fade_source, fade_target, **kwargs))
|
||||
else:
|
||||
anims.append(FadeOutToPoint(
|
||||
fade_source, fade_target.get_center(), **kwargs
|
||||
fade_source, target_mobject.get_center(), **kwargs
|
||||
))
|
||||
anims.append(FadeInFromPoint(
|
||||
fade_target.copy(), fade_source.get_center(), **kwargs
|
||||
fade_target.copy(), mobject.get_center(), **kwargs
|
||||
))
|
||||
|
||||
super().__init__(*anims)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import moderngl
|
||||
import math
|
||||
from colour import Color
|
||||
import OpenGL.GL as gl
|
||||
|
||||
@@ -67,7 +68,7 @@ class CameraFrame(Mobject):
|
||||
added_rot_T = rotation_matrix_transpose(angle, axis)
|
||||
new_rot_T = np.dot(curr_rot_T, added_rot_T)
|
||||
Fz = new_rot_T[2]
|
||||
phi = np.arccos(Fz[2])
|
||||
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),
|
||||
@@ -121,6 +122,15 @@ class CameraFrame(Mobject):
|
||||
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())
|
||||
|
||||
@@ -139,6 +149,16 @@ class CameraFrame(Mobject):
|
||||
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()
|
||||
@@ -194,20 +214,30 @@ class Camera(object):
|
||||
fbo = self.get_fbo(ctx, 0)
|
||||
else:
|
||||
fbo = ctx.detect_framebuffer()
|
||||
self.ctx = ctx
|
||||
self.fbo = fbo
|
||||
self.set_ctx_blending()
|
||||
|
||||
# For multisample antialiasing
|
||||
fbo_msaa = self.get_fbo(ctx, self.samples)
|
||||
fbo_msaa.use()
|
||||
self.fbo_msaa = fbo_msaa
|
||||
|
||||
ctx.enable(moderngl.BLEND)
|
||||
ctx.blend_func = (
|
||||
def set_ctx_blending(self, enable=True):
|
||||
if enable:
|
||||
self.ctx.enable(moderngl.BLEND)
|
||||
else:
|
||||
self.ctx.disable(moderngl.BLEND)
|
||||
self.ctx.blend_func = (
|
||||
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
|
||||
moderngl.ONE, moderngl.ONE
|
||||
# moderngl.ONE, moderngl.ONE
|
||||
)
|
||||
|
||||
self.ctx = ctx
|
||||
self.fbo = fbo
|
||||
self.fbo_msaa = fbo_msaa
|
||||
def set_ctx_depth_test(self, enable=True):
|
||||
if enable:
|
||||
self.ctx.enable(moderngl.DEPTH_TEST)
|
||||
else:
|
||||
self.ctx.disable(moderngl.DEPTH_TEST)
|
||||
|
||||
def init_light_source(self):
|
||||
self.light_source = Point(self.light_source_position)
|
||||
@@ -297,6 +327,9 @@ class Camera(object):
|
||||
def get_frame_center(self):
|
||||
return self.frame.get_center()
|
||||
|
||||
def get_location(self):
|
||||
return self.frame.get_implied_camera_location()
|
||||
|
||||
def resize_frame_shape(self, fixed_dimension=0):
|
||||
"""
|
||||
Changes frame_shape to match the aspect ratio
|
||||
@@ -316,17 +349,6 @@ class Camera(object):
|
||||
self.frame.set_height(frame_height)
|
||||
self.frame.set_width(frame_width)
|
||||
|
||||
def pixel_coords_to_space_coords(self, px, py, relative=False):
|
||||
pw, ph = self.fbo.size
|
||||
fw, fh = self.get_frame_shape()
|
||||
fc = self.get_frame_center()
|
||||
if relative:
|
||||
return 2 * np.array([px / pw, py / ph, 0])
|
||||
else:
|
||||
# Only scale wrt one axis
|
||||
scale = fh / ph
|
||||
return fc + scale * np.array([(px - pw / 2), (py - ph / 2), 0])
|
||||
|
||||
# Rendering
|
||||
def capture(self, *mobjects, **kwargs):
|
||||
self.refresh_perspective_uniforms()
|
||||
@@ -338,17 +360,11 @@ class Camera(object):
|
||||
shader_wrapper = render_group["shader_wrapper"]
|
||||
shader_program = render_group["prog"]
|
||||
self.set_shader_uniforms(shader_program, shader_wrapper)
|
||||
self.update_depth_test(shader_wrapper)
|
||||
self.set_ctx_depth_test(shader_wrapper.depth_test)
|
||||
render_group["vao"].render(int(shader_wrapper.render_primitive))
|
||||
if render_group["single_use"]:
|
||||
self.release_render_group(render_group)
|
||||
|
||||
def update_depth_test(self, shader_wrapper):
|
||||
if shader_wrapper.depth_test:
|
||||
self.ctx.enable(moderngl.DEPTH_TEST)
|
||||
else:
|
||||
self.ctx.disable(moderngl.DEPTH_TEST)
|
||||
|
||||
def get_render_group_list(self, mobject):
|
||||
try:
|
||||
return self.static_mobject_to_render_group_list[id(mobject)]
|
||||
@@ -421,8 +437,10 @@ class Camera(object):
|
||||
for name, path in shader_wrapper.texture_paths.items():
|
||||
tid = self.get_texture_id(path)
|
||||
shader[name].value = tid
|
||||
for name, value in it.chain(shader_wrapper.uniforms.items(), self.perspective_uniforms.items()):
|
||||
for name, value in it.chain(self.perspective_uniforms.items(), shader_wrapper.uniforms.items()):
|
||||
try:
|
||||
if isinstance(value, np.ndarray):
|
||||
value = tuple(value)
|
||||
shader[name].value = value
|
||||
except KeyError:
|
||||
pass
|
||||
@@ -436,25 +454,32 @@ class Camera(object):
|
||||
anti_alias_width = self.anti_alias_width / (ph / fh)
|
||||
# Orient light
|
||||
rotation = frame.get_inverse_camera_rotation_matrix()
|
||||
light_pos = self.light_source.get_location()
|
||||
light_pos = np.dot(rotation, light_pos)
|
||||
offset = frame.get_center()
|
||||
light_pos = np.dot(
|
||||
rotation, self.light_source.get_location() + offset
|
||||
)
|
||||
cam_pos = self.frame.get_implied_camera_location() # TODO
|
||||
|
||||
self.perspective_uniforms = {
|
||||
"frame_shape": frame.get_shape(),
|
||||
"anti_alias_width": anti_alias_width,
|
||||
"camera_center": tuple(frame.get_center()),
|
||||
"camera_offset": tuple(offset),
|
||||
"camera_rotation": tuple(np.array(rotation).T.flatten()),
|
||||
"camera_position": tuple(cam_pos),
|
||||
"light_source_position": tuple(light_pos),
|
||||
"focal_distance": frame.get_focal_distance(),
|
||||
}
|
||||
|
||||
def init_textures(self):
|
||||
self.path_to_texture_id = {}
|
||||
self.n_textures = 0
|
||||
self.path_to_texture = {}
|
||||
|
||||
def get_texture_id(self, path):
|
||||
if path not in self.path_to_texture_id:
|
||||
# A way to increase tid's sequentially
|
||||
tid = len(self.path_to_texture_id)
|
||||
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,
|
||||
@@ -462,12 +487,19 @@ class Camera(object):
|
||||
data=im.tobytes(),
|
||||
)
|
||||
texture.use(location=tid)
|
||||
self.path_to_texture_id[path] = tid
|
||||
return self.path_to_texture_id[path]
|
||||
self.path_to_texture[path] = (tid, texture)
|
||||
return self.path_to_texture[path][0]
|
||||
|
||||
def release_texture(self, path):
|
||||
tid_and_texture = self.path_to_texture.pop(path, None)
|
||||
if tid_and_texture:
|
||||
tid_and_texture[1].release()
|
||||
return self
|
||||
|
||||
|
||||
# Mostly just defined so old scenes don't break
|
||||
class ThreeDCamera(Camera):
|
||||
CONFIG = {
|
||||
"samples": 4,
|
||||
"anti_alias_width": 0,
|
||||
}
|
||||
|
||||
@@ -5,10 +5,15 @@ import importlib
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from contextlib import contextmanager
|
||||
from screeninfo import get_monitors
|
||||
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.init_config import init_customization
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
__config_file__ = "custom_config.yml"
|
||||
|
||||
|
||||
def parse_cli():
|
||||
@@ -111,6 +116,12 @@ def parse_cli():
|
||||
"in two comma separated values, e.g. \"3,6\", it will end"
|
||||
"the rendering at the second value",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e", "--embed", metavar="LINENO",
|
||||
help="Takes a line number as an argument, and results"
|
||||
"in the scene being called as if the line `self.embed()`"
|
||||
"was inserted into the scene code at that line number."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--resolution",
|
||||
help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"",
|
||||
@@ -136,10 +147,19 @@ def parse_cli():
|
||||
"--config_file",
|
||||
help="Path to the custom configuration file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--version",
|
||||
action="store_true",
|
||||
help="Display the version of manimgl"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
help="Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
except argparse.ArgumentError as err:
|
||||
print(str(err))
|
||||
log.error(str(err))
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
@@ -152,14 +172,30 @@ def get_manim_dir():
|
||||
def get_module(file_name):
|
||||
if file_name is None:
|
||||
return None
|
||||
else:
|
||||
module_name = file_name.replace(os.sep, ".").replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
module_name = file_name.replace(os.sep, ".").replace(".py", "")
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@contextmanager
|
||||
def insert_embed_line(file_name, lineno):
|
||||
with open(file_name, 'r') as fp:
|
||||
lines = fp.readlines()
|
||||
line = lines[lineno - 1]
|
||||
n_spaces = len(line) - len(line.lstrip())
|
||||
lines.insert(lineno, " " * n_spaces + "self.embed()\n")
|
||||
|
||||
alt_file = file_name.replace(".py", "_inserted_embed.py")
|
||||
with open(alt_file, 'w') as fp:
|
||||
fp.writelines(lines)
|
||||
|
||||
try:
|
||||
yield alt_file
|
||||
finally:
|
||||
os.remove(alt_file)
|
||||
|
||||
__config_file__ = "custom_config.yml"
|
||||
|
||||
def get_custom_config():
|
||||
global __config_file__
|
||||
@@ -185,36 +221,49 @@ def get_custom_config():
|
||||
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):
|
||||
print(f"Can't find {args.config_file}.")
|
||||
log.error(f"Can't find {args.config_file}.")
|
||||
if sys.platform == 'win32':
|
||||
print(f"Copying default configuration file to {args.config_file}...")
|
||||
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"]:
|
||||
print(f"Copying default configuration file to {args.config_file}...")
|
||||
log.info(f"Copying default configuration file to {args.config_file}...")
|
||||
os.system(f"cp default_config.yml {args.config_file}")
|
||||
else:
|
||||
print("Please create the configuration file manually.")
|
||||
print("Read configuration from default_config.yml.")
|
||||
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__)):
|
||||
print("There is no configuration file detected. Initial configuration:\n")
|
||||
log.info("There is no configuration file detected. Switch to the config file initializer:")
|
||||
init_customization()
|
||||
|
||||
elif not os.path.exists(__config_file__):
|
||||
print(f"Warning: Using the default configuration file, which you can modify in {global_defaults_file}")
|
||||
print(f"If you want to create a local configuration file, you can create a file named {__config_file__}, or run manimgl --config")
|
||||
log.info(f"Using the default configuration file, which you can modify in `{global_defaults_file}`")
|
||||
log.info(
|
||||
"If you want to create a local configuration file, you can create a file named"
|
||||
f" `{__config_file__}`, or run `manimgl --config`"
|
||||
)
|
||||
|
||||
custom_config = get_custom_config()
|
||||
check_temporary_storage(custom_config)
|
||||
|
||||
write_file = any([args.write_file, args.open, args.finder])
|
||||
if args.transparent:
|
||||
@@ -241,16 +290,22 @@ def get_configuration(args):
|
||||
"quiet": args.quiet,
|
||||
}
|
||||
|
||||
module = get_module(args.file)
|
||||
if args.embed is None:
|
||||
module = get_module(args.file)
|
||||
else:
|
||||
with insert_embed_line(args.file, int(args.embed)) as alt_file:
|
||||
module = get_module(alt_file)
|
||||
|
||||
config = {
|
||||
"module": module,
|
||||
"scene_names": args.scene_names,
|
||||
"file_writer_config": file_writer_config,
|
||||
"quiet": args.quiet or args.write_all,
|
||||
"write_all": args.write_all,
|
||||
"skip_animations": args.skip_animations,
|
||||
"start_at_animation_number": args.start_at_animation_number,
|
||||
"preview": not write_file,
|
||||
"end_at_animation_number": None,
|
||||
"preview": not write_file,
|
||||
"leave_progress_bars": args.leave_progress_bars,
|
||||
}
|
||||
|
||||
@@ -263,7 +318,7 @@ def get_configuration(args):
|
||||
mon_index = custom_config["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
window_width = monitor.width
|
||||
if not args.full_screen:
|
||||
if not (args.full_screen or custom_config["full_screen"]):
|
||||
window_width //= 2
|
||||
window_height = window_width * 9 // 16
|
||||
config["window_config"] = {
|
||||
@@ -280,10 +335,6 @@ def get_configuration(args):
|
||||
else:
|
||||
config["start_at_animation_number"] = int(stan)
|
||||
|
||||
config["skip_animations"] = any([
|
||||
args.skip_animations,
|
||||
args.start_at_animation_number,
|
||||
])
|
||||
return config
|
||||
|
||||
|
||||
@@ -319,9 +370,9 @@ def get_camera_configuration(args, custom_config):
|
||||
try:
|
||||
bg_color = args.color or custom_config["style"]["background_color"]
|
||||
camera_config["background_color"] = colour.Color(bg_color)
|
||||
except AttributeError as err:
|
||||
print("Please use a valid color")
|
||||
print(err)
|
||||
except ValueError as err:
|
||||
log.error("Please use a valid color")
|
||||
log.error(err)
|
||||
sys.exit(2)
|
||||
|
||||
# If rendering a transparent image/move, make sure the
|
||||
|
||||
@@ -34,6 +34,7 @@ style:
|
||||
# the window on the monitor, e.g. "960,540"
|
||||
window_position: UR
|
||||
window_monitor: 0
|
||||
full_screen: False
|
||||
# If break_into_partial_movies is set to True, then many small
|
||||
# files will be written corresponding to each Scene.play and
|
||||
# Scene.wait call, and these files will then be combined
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import inspect
|
||||
import sys
|
||||
import logging
|
||||
import copy
|
||||
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
class BlankScene(Scene):
|
||||
@@ -38,11 +39,14 @@ def prompt_user_for_choice(scene_classes):
|
||||
"\nScene Name or Number: "
|
||||
)
|
||||
return [
|
||||
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str)-1]
|
||||
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str) - 1]
|
||||
for split_str in user_input.replace(" ", "").split(",")
|
||||
]
|
||||
except IndexError:
|
||||
log.error("Invalid scene number")
|
||||
sys.exit(2)
|
||||
except KeyError:
|
||||
logging.log(logging.ERROR, "Invalid scene")
|
||||
log.error("Invalid scene name")
|
||||
sys.exit(2)
|
||||
except EOFError:
|
||||
sys.exit(1)
|
||||
@@ -64,6 +68,26 @@ def get_scene_config(config):
|
||||
])
|
||||
|
||||
|
||||
def compute_total_frames(scene_class, scene_config):
|
||||
"""
|
||||
When a scene is being written to file, a copy of the scene is run with
|
||||
skip_animations set to true so as to count how many frames it will require.
|
||||
This allows for a total progress bar on rendering, and also allows runtime
|
||||
errors to be exposed preemptively for long running scenes. The final frame
|
||||
is saved by default, so that one can more quickly check that the last frame
|
||||
looks as expected.
|
||||
"""
|
||||
pre_config = copy.deepcopy(scene_config)
|
||||
pre_config["file_writer_config"]["write_to_movie"] = False
|
||||
pre_config["file_writer_config"]["save_last_frame"] = True
|
||||
pre_config["file_writer_config"]["quiet"] = True
|
||||
pre_config["skip_animations"] = True
|
||||
pre_scene = scene_class(**pre_config)
|
||||
pre_scene.run()
|
||||
total_time = pre_scene.time - pre_scene.skip_time
|
||||
return int(total_time * scene_config["camera_config"]["frame_rate"])
|
||||
|
||||
|
||||
def get_scenes_to_render(scene_classes, scene_config, config):
|
||||
if config["write_all"]:
|
||||
return [sc(**scene_config) for sc in scene_classes]
|
||||
@@ -73,15 +97,15 @@ def get_scenes_to_render(scene_classes, scene_config, config):
|
||||
found = False
|
||||
for scene_class in scene_classes:
|
||||
if scene_class.__name__ == scene_name:
|
||||
fw_config = scene_config["file_writer_config"]
|
||||
if fw_config["write_to_movie"]:
|
||||
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
|
||||
scene = scene_class(**scene_config)
|
||||
result.append(scene)
|
||||
found = True
|
||||
break
|
||||
if not found and (scene_name != ""):
|
||||
logging.log(
|
||||
logging.ERROR,
|
||||
f"No scene named {scene_name} found",
|
||||
)
|
||||
log.error(f"No scene named {scene_name} found")
|
||||
if result:
|
||||
return result
|
||||
if len(scene_classes) == 1:
|
||||
|
||||
13
manimlib/logger.py
Normal file
13
manimlib/logger.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import logging
|
||||
from rich.logging import RichHandler
|
||||
|
||||
__all__ = ["log"]
|
||||
|
||||
|
||||
FORMAT = "%(message)s"
|
||||
logging.basicConfig(
|
||||
level=logging.WARNING, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||
)
|
||||
|
||||
log = logging.getLogger("manimgl")
|
||||
log.setLevel("DEBUG")
|
||||
116
manimlib/mobject/boolean_ops.py
Normal file
116
manimlib/mobject/boolean_ops.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import numpy as np
|
||||
import pathops
|
||||
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
# Boolean operations between 2D mobjects
|
||||
# Borrowed from from https://github.com/ManimCommunity/manim/
|
||||
|
||||
def _convert_vmobject_to_skia_path(vmobject):
|
||||
path = pathops.Path()
|
||||
subpaths = vmobject.get_subpaths_from_points(vmobject.get_all_points())
|
||||
for subpath in subpaths:
|
||||
quads = vmobject.get_bezier_tuples_from_points(subpath)
|
||||
start = subpath[0]
|
||||
path.moveTo(*start[:2])
|
||||
for p0, p1, p2 in quads:
|
||||
path.quadTo(*p1[:2], *p2[:2])
|
||||
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
return path
|
||||
|
||||
|
||||
def _convert_skia_path_to_vmobject(path, vmobject):
|
||||
PathVerb = pathops.PathVerb
|
||||
current_path_start = np.array([0.0, 0.0, 0.0])
|
||||
for path_verb, points in path:
|
||||
if path_verb == PathVerb.CLOSE:
|
||||
vmobject.add_line_to(current_path_start)
|
||||
else:
|
||||
points = np.hstack((np.array(points), np.zeros((len(points), 1))))
|
||||
if path_verb == PathVerb.MOVE:
|
||||
for point in points:
|
||||
current_path_start = point
|
||||
vmobject.start_new_path(point)
|
||||
elif path_verb == PathVerb.CUBIC:
|
||||
vmobject.add_cubic_bezier_curve_to(*points)
|
||||
elif path_verb == PathVerb.LINE:
|
||||
vmobject.add_line_to(points[0])
|
||||
elif path_verb == PathVerb.QUAD:
|
||||
vmobject.add_quadratic_bezier_curve_to(*points)
|
||||
else:
|
||||
raise Exception(f"Unsupported: {path_verb}")
|
||||
return vmobject
|
||||
|
||||
|
||||
class Union(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Union.")
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
paths = [
|
||||
_convert_vmobject_to_skia_path(vmobject)
|
||||
for vmobject in vmobjects
|
||||
]
|
||||
pathops.union(paths, outpen.getPen())
|
||||
_convert_skia_path_to_vmobject(outpen, self)
|
||||
|
||||
|
||||
class Difference(VMobject):
|
||||
def __init__(self, subject, clip, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
pathops.difference(
|
||||
[_convert_vmobject_to_skia_path(subject)],
|
||||
[_convert_vmobject_to_skia_path(clip)],
|
||||
outpen.getPen(),
|
||||
)
|
||||
_convert_skia_path_to_vmobject(outpen, self)
|
||||
|
||||
|
||||
class Intersection(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Intersection.")
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
pathops.intersection(
|
||||
[_convert_vmobject_to_skia_path(vmobjects[0])],
|
||||
[_convert_vmobject_to_skia_path(vmobjects[1])],
|
||||
outpen.getPen(),
|
||||
)
|
||||
new_outpen = outpen
|
||||
for _i in range(2, len(vmobjects)):
|
||||
new_outpen = pathops.Path()
|
||||
pathops.intersection(
|
||||
[outpen],
|
||||
[_convert_vmobject_to_skia_path(vmobjects[_i])],
|
||||
new_outpen.getPen(),
|
||||
)
|
||||
outpen = new_outpen
|
||||
_convert_skia_path_to_vmobject(outpen, self)
|
||||
|
||||
|
||||
class Exclusion(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Exclusion.")
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
pathops.xor(
|
||||
[_convert_vmobject_to_skia_path(vmobjects[0])],
|
||||
[_convert_vmobject_to_skia_path(vmobjects[1])],
|
||||
outpen.getPen(),
|
||||
)
|
||||
new_outpen = outpen
|
||||
for _i in range(2, len(vmobjects)):
|
||||
new_outpen = pathops.Path()
|
||||
pathops.xor(
|
||||
[outpen],
|
||||
[_convert_vmobject_to_skia_path(vmobjects[_i])],
|
||||
new_outpen.getPen(),
|
||||
)
|
||||
outpen = new_outpen
|
||||
_convert_skia_path_to_vmobject(outpen, self)
|
||||
@@ -1,8 +1,13 @@
|
||||
from manimlib.constants import *
|
||||
import numpy as np
|
||||
from manimlib.constants import BLUE_D
|
||||
from manimlib.constants import BLUE_B
|
||||
from manimlib.constants import BLUE_E
|
||||
from manimlib.constants import GREY_BROWN
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
|
||||
class AnimatedBoundary(VGroup):
|
||||
@@ -74,25 +79,63 @@ class TracedPath(VMobject):
|
||||
CONFIG = {
|
||||
"stroke_width": 2,
|
||||
"stroke_color": WHITE,
|
||||
"min_distance_to_new_point": 0.1,
|
||||
"time_traced": np.inf,
|
||||
"fill_opacity": 0,
|
||||
"time_per_anchor": 1 / 15,
|
||||
}
|
||||
|
||||
def __init__(self, traced_point_func, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.add_updater(lambda m: m.update_path())
|
||||
self.time = 0
|
||||
self.traced_points = []
|
||||
self.add_updater(lambda m, dt: m.update_path(dt))
|
||||
|
||||
def update_path(self):
|
||||
new_point = self.traced_point_func()
|
||||
if not self.has_points():
|
||||
self.start_new_path(new_point)
|
||||
self.add_line_to(new_point)
|
||||
def update_path(self, dt):
|
||||
if dt == 0:
|
||||
return self
|
||||
point = self.traced_point_func().copy()
|
||||
self.traced_points.append(point)
|
||||
|
||||
if self.time_traced < np.inf:
|
||||
n_relevant_points = int(self.time_traced / dt + 0.5)
|
||||
# n_anchors = int(self.time_traced / self.time_per_anchor)
|
||||
n_tps = len(self.traced_points)
|
||||
if n_tps < n_relevant_points:
|
||||
points = self.traced_points + [point] * (n_relevant_points - n_tps)
|
||||
else:
|
||||
points = self.traced_points[n_tps - n_relevant_points:]
|
||||
# points = [
|
||||
# self.traced_points[max(n_tps - int(alpha * n_relevant_points) - 1, 0)]
|
||||
# for alpha in np.linspace(1, 0, n_anchors)
|
||||
# ]
|
||||
# Every now and then refresh the list
|
||||
if n_tps > 10 * n_relevant_points:
|
||||
self.traced_points = self.traced_points[-n_relevant_points:]
|
||||
else:
|
||||
# Set the end to be the new point
|
||||
self.get_points()[-1] = new_point
|
||||
# sparseness = max(int(self.time_per_anchor / dt), 1)
|
||||
# points = self.traced_points[::sparseness]
|
||||
# points[-1] = self.traced_points[-1]
|
||||
points = self.traced_points
|
||||
|
||||
# Second to last point
|
||||
nppcc = self.n_points_per_curve
|
||||
dist = get_norm(new_point - self.get_points()[-nppcc])
|
||||
if dist >= self.min_distance_to_new_point:
|
||||
self.add_line_to(new_point)
|
||||
if points:
|
||||
self.set_points_smoothly(points)
|
||||
|
||||
self.time += dt
|
||||
return self
|
||||
|
||||
|
||||
class TracingTail(TracedPath):
|
||||
CONFIG = {
|
||||
"stroke_width": (0, 3),
|
||||
"stroke_opacity": (0, 1),
|
||||
"stroke_color": WHITE,
|
||||
"time_traced": 1.0,
|
||||
}
|
||||
|
||||
def __init__(self, mobject_or_func, **kwargs):
|
||||
if isinstance(mobject_or_func, Mobject):
|
||||
func = mobject_or_func.get_center
|
||||
else:
|
||||
func = mobject_or_func
|
||||
super().__init__(func, **kwargs)
|
||||
|
||||
@@ -10,6 +10,7 @@ from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.number_line import NumberLine
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.simple_functions import binary_search
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
@@ -25,13 +26,18 @@ class CoordinateSystem():
|
||||
"""
|
||||
CONFIG = {
|
||||
"dimension": 2,
|
||||
"x_range": np.array([-8.0, 8.0, 1.0]),
|
||||
"y_range": np.array([-4.0, 4.0, 1.0]),
|
||||
"width": None,
|
||||
"height": None,
|
||||
"default_x_range": [-8.0, 8.0, 1.0],
|
||||
"default_y_range": [-4.0, 4.0, 1.0],
|
||||
"width": FRAME_WIDTH,
|
||||
"height": FRAME_HEIGHT,
|
||||
"num_sampled_graph_points_per_tick": 20,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.x_range = np.array(self.default_x_range)
|
||||
self.y_range = np.array(self.default_y_range)
|
||||
|
||||
def coords_to_point(self, *coords):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
@@ -127,6 +133,7 @@ class CoordinateSystem():
|
||||
**kwargs
|
||||
)
|
||||
graph.underlying_function = function
|
||||
graph.x_range = x_range
|
||||
return graph
|
||||
|
||||
def get_parametric_curve(self, function, **kwargs):
|
||||
@@ -144,14 +151,14 @@ class CoordinateSystem():
|
||||
else:
|
||||
alpha = binary_search(
|
||||
function=lambda a: self.point_to_coords(
|
||||
graph.point_from_proportion(a)
|
||||
graph.quick_point_from_proportion(a)
|
||||
)[0],
|
||||
target=x,
|
||||
lower_bound=self.x_range[0],
|
||||
upper_bound=self.x_range[1],
|
||||
)
|
||||
if alpha is not None:
|
||||
return graph.point_from_proportion(alpha)
|
||||
return graph.quick_point_from_proportion(alpha)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -282,7 +289,9 @@ class Axes(VGroup, CoordinateSystem):
|
||||
x_range=None,
|
||||
y_range=None,
|
||||
**kwargs):
|
||||
super().__init__(**kwargs)
|
||||
CoordinateSystem.__init__(self, **kwargs)
|
||||
VGroup.__init__(self, **kwargs)
|
||||
|
||||
if x_range is not None:
|
||||
self.x_range[:len(x_range)] = x_range
|
||||
if y_range is not None:
|
||||
@@ -441,7 +450,7 @@ class NumberPlane(Axes):
|
||||
return lines1, lines2
|
||||
|
||||
def get_lines_parallel_to_axis(self, axis1, axis2):
|
||||
freq = axis1.x_step
|
||||
freq = axis2.x_step
|
||||
ratio = self.faded_line_ratio
|
||||
line = Line(axis1.get_start(), axis1.get_end())
|
||||
dense_freq = (1 + ratio)
|
||||
@@ -501,15 +510,15 @@ class ComplexPlane(NumberPlane):
|
||||
def p2n(self, point):
|
||||
return self.point_to_number(point)
|
||||
|
||||
def get_default_coordinate_values(self):
|
||||
def get_default_coordinate_values(self, skip_first=True):
|
||||
x_numbers = self.get_x_axis().get_tick_range()[1:]
|
||||
y_numbers = self.get_y_axis().get_tick_range()[1:]
|
||||
y_numbers = [complex(0, y) for y in y_numbers if y != 0]
|
||||
return [*x_numbers, *y_numbers]
|
||||
|
||||
def add_coordinate_labels(self, numbers=None, **kwargs):
|
||||
def add_coordinate_labels(self, numbers=None, skip_first=True, **kwargs):
|
||||
if numbers is None:
|
||||
numbers = self.get_default_coordinate_values()
|
||||
numbers = self.get_default_coordinate_values(skip_first)
|
||||
|
||||
self.coordinate_labels = VGroup()
|
||||
for number in numbers:
|
||||
@@ -522,6 +531,15 @@ class ComplexPlane(NumberPlane):
|
||||
axis = self.get_x_axis()
|
||||
value = z.real
|
||||
number_mob = axis.get_number_mobject(value, **kwargs)
|
||||
# For i and -i, remove the "1"
|
||||
if z.imag == 1:
|
||||
number_mob.remove(number_mob[0])
|
||||
if z.imag == -1:
|
||||
number_mob.remove(number_mob[1])
|
||||
number_mob[0].next_to(
|
||||
number_mob[1], LEFT,
|
||||
buff=number_mob[0].get_width() / 4
|
||||
)
|
||||
self.coordinate_labels.add(number_mob)
|
||||
self.add(self.coordinate_labels)
|
||||
return self
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import numpy as np
|
||||
import math
|
||||
import numbers
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -27,6 +29,7 @@ DEFAULT_ARROW_TIP_LENGTH = 0.35
|
||||
DEFAULT_ARROW_TIP_WIDTH = 0.35
|
||||
|
||||
|
||||
# Deprecate?
|
||||
class TipableVMobject(VMobject):
|
||||
"""
|
||||
Meant for shared functionality between Arc and Line.
|
||||
@@ -302,6 +305,9 @@ class Circle(Arc):
|
||||
(angle - start_angle) / TAU
|
||||
)
|
||||
|
||||
def get_radius(self):
|
||||
return get_norm(self.get_start() - self.get_center())
|
||||
|
||||
|
||||
class Dot(Circle):
|
||||
CONFIG = {
|
||||
@@ -404,32 +410,38 @@ class Line(TipableVMobject):
|
||||
self.set_points_by_ends(self.start, self.end, self.buff, self.path_arc)
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
if path_arc:
|
||||
self.set_points(Arc.create_quadratic_bezier_points(path_arc))
|
||||
self.put_start_and_end_on(start, end)
|
||||
else:
|
||||
vect = end - start
|
||||
dist = get_norm(vect)
|
||||
if np.isclose(dist, 0):
|
||||
self.set_points_as_corners([start, end])
|
||||
self.account_for_buff(self.buff)
|
||||
return self
|
||||
if path_arc:
|
||||
neg = path_arc < 0
|
||||
if neg:
|
||||
path_arc = -path_arc
|
||||
start, end = end, start
|
||||
radius = (dist / 2) / math.sin(path_arc / 2)
|
||||
alpha = (PI - path_arc) / 2
|
||||
center = start + radius * normalize(rotate_vector(end - start, alpha))
|
||||
|
||||
raw_arc_points = Arc.create_quadratic_bezier_points(
|
||||
angle=path_arc - 2 * buff / radius,
|
||||
start_angle=angle_of_vector(start - center) + buff / radius,
|
||||
)
|
||||
if neg:
|
||||
raw_arc_points = raw_arc_points[::-1]
|
||||
self.set_points(center + radius * raw_arc_points)
|
||||
else:
|
||||
if buff > 0 and dist > 0:
|
||||
start = start + vect * (buff / dist)
|
||||
end = end - vect * (buff / dist)
|
||||
self.set_points_as_corners([start, end])
|
||||
return self
|
||||
|
||||
def set_path_arc(self, new_value):
|
||||
self.path_arc = new_value
|
||||
self.init_points()
|
||||
|
||||
def account_for_buff(self, buff):
|
||||
if buff == 0:
|
||||
return
|
||||
#
|
||||
if self.path_arc == 0:
|
||||
length = self.get_length()
|
||||
else:
|
||||
length = self.get_arc_length()
|
||||
#
|
||||
if length < 2 * buff:
|
||||
return
|
||||
buff_prop = buff / length
|
||||
self.pointwise_become_partial(self, buff_prop, 1 - buff_prop)
|
||||
return self
|
||||
|
||||
def set_start_and_end_attrs(self, start, end):
|
||||
# If either start or end are Mobjects, this
|
||||
# gives their centers
|
||||
@@ -439,8 +451,8 @@ class Line(TipableVMobject):
|
||||
# Now that we know the direction between them,
|
||||
# we can find the appropriate boundary point from
|
||||
# start and end, if they're mobjects
|
||||
self.start = self.pointify(start, vect) + self.buff * vect
|
||||
self.end = self.pointify(end, -vect) - self.buff * vect
|
||||
self.start = self.pointify(start, vect)
|
||||
self.end = self.pointify(end, -vect)
|
||||
|
||||
def pointify(self, mob_or_point, direction=None):
|
||||
"""
|
||||
@@ -461,8 +473,10 @@ class Line(TipableVMobject):
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
curr_start, curr_end = self.get_start_and_end()
|
||||
if (curr_start == curr_end).all():
|
||||
self.set_points_by_ends(start, end, self.path_arc)
|
||||
if np.isclose(curr_start, curr_end).all():
|
||||
# Handle null lines more gracefully
|
||||
self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
|
||||
return self
|
||||
return super().put_start_and_end_on(start, end)
|
||||
|
||||
def get_vector(self):
|
||||
@@ -494,8 +508,9 @@ class Line(TipableVMobject):
|
||||
)
|
||||
return self
|
||||
|
||||
def set_length(self, length):
|
||||
self.scale(length / self.get_length())
|
||||
def set_length(self, length, **kwargs):
|
||||
self.scale(length / self.get_length(), **kwargs)
|
||||
return self
|
||||
|
||||
|
||||
class DashedLine(Line):
|
||||
@@ -578,6 +593,80 @@ class Elbow(VMobject):
|
||||
|
||||
|
||||
class Arrow(Line):
|
||||
CONFIG = {
|
||||
"stroke_color": GREY_A,
|
||||
"stroke_width": 5,
|
||||
"tip_width_ratio": 4,
|
||||
"width_to_tip_len": 0.0075,
|
||||
"max_tip_length_to_length_ratio": 0.3,
|
||||
"max_width_to_length_ratio": 10,
|
||||
"buff": 0.25,
|
||||
}
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
super().set_points_by_ends(start, end, buff, path_arc)
|
||||
self.insert_tip_anchor()
|
||||
return self
|
||||
|
||||
def init_colors(self):
|
||||
super().init_colors()
|
||||
self.create_tip_with_stroke_width()
|
||||
|
||||
def get_arc_length(self):
|
||||
# Push up into Line?
|
||||
arc_len = get_norm(self.get_vector())
|
||||
if self.path_arc > 0:
|
||||
arc_len *= self.path_arc / (2 * math.sin(self.path_arc / 2))
|
||||
return arc_len
|
||||
|
||||
def insert_tip_anchor(self):
|
||||
prev_end = self.get_end()
|
||||
arc_len = self.get_arc_length()
|
||||
tip_len = self.get_stroke_width() * self.width_to_tip_len * self.tip_width_ratio
|
||||
if tip_len >= self.max_tip_length_to_length_ratio * arc_len:
|
||||
alpha = self.max_tip_length_to_length_ratio
|
||||
else:
|
||||
alpha = tip_len / arc_len
|
||||
self.pointwise_become_partial(self, 0, 1 - alpha)
|
||||
self.add_line_to(prev_end)
|
||||
return self
|
||||
|
||||
def create_tip_with_stroke_width(self):
|
||||
width = min(
|
||||
self.max_stroke_width,
|
||||
self.max_width_to_length_ratio * self.get_length(),
|
||||
)
|
||||
widths_array = np.full(self.get_num_points(), width)
|
||||
nppc = self.n_points_per_curve
|
||||
if len(widths_array) > nppc:
|
||||
widths_array[-nppc:] = [
|
||||
a * self.tip_width_ratio * width
|
||||
for a in np.linspace(1, 0, nppc)
|
||||
]
|
||||
self.set_stroke(width=widths_array)
|
||||
return self
|
||||
|
||||
def reset_tip(self):
|
||||
self.set_points_by_ends(
|
||||
self.get_start(),
|
||||
self.get_end(),
|
||||
path_arc=self.path_arc,
|
||||
)
|
||||
self.create_tip_with_stroke_width()
|
||||
return self
|
||||
|
||||
def set_stroke(self, color=None, width=None, *args, **kwargs):
|
||||
super().set_stroke(color=color, width=width, *args, **kwargs)
|
||||
if isinstance(width, numbers.Number):
|
||||
self.max_stroke_width = width
|
||||
self.reset_tip()
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor):
|
||||
return self.reset_tip()
|
||||
|
||||
|
||||
class FillArrow(Line):
|
||||
CONFIG = {
|
||||
"fill_color": GREY_A,
|
||||
"fill_opacity": 1,
|
||||
@@ -842,16 +931,8 @@ class Rectangle(Polygon):
|
||||
|
||||
|
||||
class Square(Rectangle):
|
||||
CONFIG = {
|
||||
"side_length": 2.0,
|
||||
}
|
||||
|
||||
def __init__(self, side_length=None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
|
||||
if side_length is None:
|
||||
side_length = self.side_length
|
||||
|
||||
def __init__(self, side_length=2.0, **kwargs):
|
||||
self.side_length = side_length
|
||||
super().__init__(side_length, side_length, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class Matrix(VMobject):
|
||||
|
||||
def __init__(self, matrix, **kwargs):
|
||||
"""
|
||||
Matrix can either either include numbres, tex_strings,
|
||||
Matrix can either include numbers, tex_strings,
|
||||
or mobjects
|
||||
"""
|
||||
VMobject.__init__(self, **kwargs)
|
||||
|
||||
@@ -43,10 +43,13 @@ class Mobject(object):
|
||||
"opacity": 1,
|
||||
"dim": 3, # TODO, get rid of this
|
||||
# Lighting parameters
|
||||
# Positive gloss up to 1 makes it reflect the light.
|
||||
"gloss": 0.0,
|
||||
# Positive shadow up to 1 makes a side opposite the light darker
|
||||
# ...
|
||||
# Larger reflectiveness makes things brighter when facing the light
|
||||
"reflectiveness": 0.0,
|
||||
# Larger shadow makes faces opposite the light darker
|
||||
"shadow": 0.0,
|
||||
# Makes parts bright where light gets reflected toward the camera
|
||||
"gloss": 0.0,
|
||||
# For shaders
|
||||
"shader_folder": "",
|
||||
"render_primitive": moderngl.TRIANGLE_STRIP,
|
||||
@@ -82,6 +85,14 @@ class Mobject(object):
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def __add__(self, other: 'Mobject') -> 'Mobject':
|
||||
assert(isinstance(other, Mobject))
|
||||
return self.get_group_class()(self, other)
|
||||
|
||||
def __mul__(self, other: 'int') -> 'Mobject':
|
||||
assert(isinstance(other, int))
|
||||
return self.replicate(other)
|
||||
|
||||
def init_data(self):
|
||||
self.data = {
|
||||
"points": np.zeros((0, 3)),
|
||||
@@ -94,6 +105,7 @@ class Mobject(object):
|
||||
"is_fixed_in_frame": float(self.is_fixed_in_frame),
|
||||
"gloss": self.gloss,
|
||||
"shadow": self.shadow,
|
||||
"reflectiveness": self.reflectiveness,
|
||||
}
|
||||
|
||||
def init_colors(self):
|
||||
@@ -145,6 +157,7 @@ class Mobject(object):
|
||||
for mob in self.get_family():
|
||||
for key in mob.data:
|
||||
mob.data[key] = mob.data[key][::-1]
|
||||
self.refresh_unit_normal()
|
||||
return self
|
||||
|
||||
def apply_points_function(self, func, about_point=None, about_edge=ORIGIN, works_on_bounding_box=False):
|
||||
@@ -300,6 +313,11 @@ class Mobject(object):
|
||||
self.assemble_family()
|
||||
return self
|
||||
|
||||
def insert_submobject(self, index, new_submob):
|
||||
self.submobjects.insert(index, new_submob)
|
||||
self.assemble_family()
|
||||
return self
|
||||
|
||||
def set_submobjects(self, submobject_list):
|
||||
self.remove(*self.submobjects)
|
||||
self.add(*submobject_list)
|
||||
@@ -365,14 +383,17 @@ class Mobject(object):
|
||||
self.center()
|
||||
return self
|
||||
|
||||
def replicate(self, n):
|
||||
return self.get_group_class()(
|
||||
*(self.copy() for x in range(n))
|
||||
)
|
||||
|
||||
def get_grid(self, n_rows, n_cols, height=None, **kwargs):
|
||||
"""
|
||||
Returns a new mobject containing multiple copies of this one
|
||||
arranged in a grid
|
||||
"""
|
||||
grid = self.get_group_class()(
|
||||
*(self.copy() for n in range(n_rows * n_cols))
|
||||
)
|
||||
grid = self.replicate(n_rows * n_cols)
|
||||
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
|
||||
if height is not None:
|
||||
grid.set_height(height)
|
||||
@@ -383,6 +404,7 @@ class Mobject(object):
|
||||
self.submobjects.sort(key=submob_func)
|
||||
else:
|
||||
self.submobjects.sort(key=lambda m: point_to_num_func(m.get_center()))
|
||||
self.assemble_family()
|
||||
return self
|
||||
|
||||
def shuffle(self, recurse=False):
|
||||
@@ -390,6 +412,7 @@ class Mobject(object):
|
||||
for submob in self.submobjects:
|
||||
submob.shuffle(recurse=True)
|
||||
random.shuffle(self.submobjects)
|
||||
self.assemble_family()
|
||||
return self
|
||||
|
||||
# Copying
|
||||
@@ -408,8 +431,10 @@ class Mobject(object):
|
||||
for key in self.data:
|
||||
copy_mobject.data[key] = self.data[key].copy()
|
||||
|
||||
# TODO, are uniforms ever numpy arrays?
|
||||
copy_mobject.uniforms = dict(self.uniforms)
|
||||
for key in self.uniforms:
|
||||
if isinstance(self.uniforms[key], np.ndarray):
|
||||
copy_mobject.uniforms[key] = self.uniforms[key].copy()
|
||||
|
||||
copy_mobject.submobjects = []
|
||||
copy_mobject.add(*[sm.copy() for sm in self.submobjects])
|
||||
@@ -561,7 +586,7 @@ class Mobject(object):
|
||||
)
|
||||
return self
|
||||
|
||||
def scale(self, scale_factor, min_scale_factor=1e-8, **kwargs):
|
||||
def scale(self, scale_factor, min_scale_factor=1e-8, about_point=None, about_edge=ORIGIN):
|
||||
"""
|
||||
Default behavior is to scale about the center of the mobject.
|
||||
The argument about_edge can be a vector, indicating which side of
|
||||
@@ -574,11 +599,19 @@ class Mobject(object):
|
||||
scale_factor = max(scale_factor, min_scale_factor)
|
||||
self.apply_points_function(
|
||||
lambda points: scale_factor * points,
|
||||
about_point=about_point,
|
||||
about_edge=about_edge,
|
||||
works_on_bounding_box=True,
|
||||
**kwargs
|
||||
)
|
||||
for mob in self.get_family():
|
||||
mob._handle_scale_side_effects(scale_factor)
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor):
|
||||
# In case subclasses, such as DecimalNumber, need to make
|
||||
# any other changes when the size gets altered
|
||||
pass
|
||||
|
||||
def stretch(self, factor, dim, **kwargs):
|
||||
def func(points):
|
||||
points[:, dim] *= factor
|
||||
@@ -754,7 +787,7 @@ class Mobject(object):
|
||||
return self.rescale_to_fit(height, 1, stretch=True, **kwargs)
|
||||
|
||||
def stretch_to_fit_depth(self, depth, **kwargs):
|
||||
return self.rescale_to_fit(depth, 1, stretch=True, **kwargs)
|
||||
return self.rescale_to_fit(depth, 2, stretch=True, **kwargs)
|
||||
|
||||
def set_width(self, width, stretch=False, **kwargs):
|
||||
return self.rescale_to_fit(width, 0, stretch=stretch, **kwargs)
|
||||
@@ -765,6 +798,21 @@ class Mobject(object):
|
||||
def set_depth(self, depth, stretch=False, **kwargs):
|
||||
return self.rescale_to_fit(depth, 2, stretch=stretch, **kwargs)
|
||||
|
||||
def set_max_width(self, max_width, **kwargs):
|
||||
if self.get_width() > max_width:
|
||||
self.set_width(max_width, **kwargs)
|
||||
return self
|
||||
|
||||
def set_max_height(self, max_height, **kwargs):
|
||||
if self.get_height() > max_height:
|
||||
self.set_height(max_height, **kwargs)
|
||||
return self
|
||||
|
||||
def set_max_depth(self, max_depth, **kwargs):
|
||||
if self.get_depth() > max_depth:
|
||||
self.set_depth(max_depth, **kwargs)
|
||||
return self
|
||||
|
||||
def set_coord(self, value, dim, direction=ORIGIN):
|
||||
curr = self.get_coord(dim, direction)
|
||||
shift_vect = np.zeros(self.dim)
|
||||
@@ -823,7 +871,6 @@ class Mobject(object):
|
||||
return self
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
# TODO, this doesn't currently work in 3d
|
||||
curr_start, curr_end = self.get_start_and_end()
|
||||
curr_vect = curr_end - curr_start
|
||||
if np.all(curr_vect == 0):
|
||||
@@ -835,9 +882,12 @@ class Mobject(object):
|
||||
)
|
||||
self.rotate(
|
||||
angle_of_vector(target_vect) - angle_of_vector(curr_vect),
|
||||
about_point=curr_start
|
||||
)
|
||||
self.shift(start - curr_start)
|
||||
self.rotate(
|
||||
np.arctan2(curr_vect[2], get_norm(curr_vect[:2])) - np.arctan2(target_vect[2], get_norm(target_vect[:2])),
|
||||
axis=np.array([-target_vect[1], target_vect[0], 0]),
|
||||
)
|
||||
self.shift(start - self.get_start())
|
||||
return self
|
||||
|
||||
# Color functions
|
||||
@@ -936,12 +986,12 @@ class Mobject(object):
|
||||
def fade(self, darkness=0.5, recurse=True):
|
||||
self.set_opacity(1.0 - darkness, recurse=recurse)
|
||||
|
||||
def get_gloss(self):
|
||||
return self.uniforms["gloss"]
|
||||
def get_reflectiveness(self):
|
||||
return self.uniforms["reflectiveness"]
|
||||
|
||||
def set_gloss(self, gloss, recurse=True):
|
||||
def set_reflectiveness(self, reflectiveness, recurse=True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.uniforms["gloss"] = gloss
|
||||
mob.uniforms["reflectiveness"] = reflectiveness
|
||||
return self
|
||||
|
||||
def get_shadow(self):
|
||||
@@ -952,6 +1002,14 @@ class Mobject(object):
|
||||
mob.uniforms["shadow"] = shadow
|
||||
return self
|
||||
|
||||
def get_gloss(self):
|
||||
return self.uniforms["gloss"]
|
||||
|
||||
def set_gloss(self, gloss, recurse=True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.uniforms["gloss"] = gloss
|
||||
return self
|
||||
|
||||
# Background rectangle
|
||||
|
||||
def add_background_rectangle(self, color=None, opacity=0.75, **kwargs):
|
||||
@@ -1063,14 +1121,16 @@ class Mobject(object):
|
||||
|
||||
def get_start(self):
|
||||
self.throw_error_if_no_points()
|
||||
return np.array(self.get_points()[0])
|
||||
return self.get_points()[0].copy()
|
||||
|
||||
def get_end(self):
|
||||
self.throw_error_if_no_points()
|
||||
return np.array(self.get_points()[-1])
|
||||
return self.get_points()[-1].copy()
|
||||
|
||||
def get_start_and_end(self):
|
||||
return self.get_start(), self.get_end()
|
||||
self.throw_error_if_no_points()
|
||||
points = self.get_points()
|
||||
return (points[0].copy(), points[-1].copy())
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
points = self.get_points()
|
||||
@@ -1331,11 +1391,13 @@ class Mobject(object):
|
||||
@affects_shader_info_id
|
||||
def fix_in_frame(self):
|
||||
self.uniforms["is_fixed_in_frame"] = 1.0
|
||||
self.is_fixed_in_frame = True
|
||||
return self
|
||||
|
||||
@affects_shader_info_id
|
||||
def unfix_from_frame(self):
|
||||
self.uniforms["is_fixed_in_frame"] = 0.0
|
||||
self.is_fixed_in_frame = False
|
||||
return self
|
||||
|
||||
@affects_shader_info_id
|
||||
@@ -1436,7 +1498,7 @@ class Mobject(object):
|
||||
return result
|
||||
|
||||
def check_data_alignment(self, array, data_key):
|
||||
# Makes sure that self.data[key] can be brodcast into
|
||||
# Makes sure that self.data[key] can be broadcast into
|
||||
# the given array, meaning its length has to be either 1
|
||||
# or the length of the array
|
||||
d_len = len(self.data[data_key])
|
||||
@@ -1578,6 +1640,10 @@ class Group(Mobject):
|
||||
Mobject.__init__(self, **kwargs)
|
||||
self.add(*mobjects)
|
||||
|
||||
def __add__(self, other: 'Mobject' or 'Group'):
|
||||
assert(isinstance(other, Mobject))
|
||||
return self.add(other)
|
||||
|
||||
|
||||
class Point(Mobject):
|
||||
CONFIG = {
|
||||
|
||||
@@ -23,7 +23,7 @@ def always(method, *args, **kwargs):
|
||||
def f_always(method, *arg_generators, **kwargs):
|
||||
"""
|
||||
More functional version of always, where instead
|
||||
of taking in args, it takes in functions which ouput
|
||||
of taking in args, it takes in functions which output
|
||||
the relevant arguments.
|
||||
"""
|
||||
assert_is_mobject_method(method)
|
||||
|
||||
@@ -6,7 +6,6 @@ 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.simple_functions import fdiv
|
||||
from manimlib.utils.space_ops import normalize
|
||||
|
||||
|
||||
class NumberLine(Line):
|
||||
@@ -83,7 +82,7 @@ class NumberLine(Line):
|
||||
ticks = VGroup()
|
||||
for x in self.get_tick_range():
|
||||
size = self.tick_size
|
||||
if x in self.numbers_with_elongated_ticks:
|
||||
if np.isclose(self.numbers_with_elongated_ticks, x).any():
|
||||
size *= self.longer_tick_multiple
|
||||
ticks.add(self.get_tick(x, size))
|
||||
self.add(ticks)
|
||||
@@ -106,11 +105,13 @@ class NumberLine(Line):
|
||||
return interpolate(self.get_start(), self.get_end(), alpha)
|
||||
|
||||
def point_to_number(self, point):
|
||||
start, end = self.get_start_and_end()
|
||||
unit_vect = normalize(end - start)
|
||||
points = self.get_points()
|
||||
start = points[0]
|
||||
end = points[-1]
|
||||
vect = end - start
|
||||
proportion = fdiv(
|
||||
np.dot(point - start, unit_vect),
|
||||
np.dot(end - start, unit_vect),
|
||||
np.dot(point - start, vect),
|
||||
np.dot(end - start, vect),
|
||||
)
|
||||
return interpolate(self.x_min, self.x_max, proportion)
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ class DecimalNumber(VMobject):
|
||||
|
||||
# Add non-numerical bits
|
||||
if self.show_ellipsis:
|
||||
self.add(self.string_to_mob("..."))
|
||||
dots = self.string_to_mob("...")
|
||||
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
|
||||
self.add(dots)
|
||||
if self.unit is not None:
|
||||
self.unit_sign = self.string_to_mob(self.unit, SingleStringTex)
|
||||
self.add(self.unit_sign)
|
||||
@@ -128,17 +130,15 @@ class DecimalNumber(VMobject):
|
||||
|
||||
def set_value(self, number):
|
||||
move_to_point = self.get_edge_center(self.edge_to_fix)
|
||||
old_submobjects = self.submobjects
|
||||
old_submobjects = list(self.submobjects)
|
||||
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)
|
||||
return self
|
||||
|
||||
def scale(self, scale_factor, **kwargs):
|
||||
super().scale(scale_factor, **kwargs)
|
||||
def _handle_scale_side_effects(self, scale_factor):
|
||||
self.data["font_size"] *= scale_factor
|
||||
return self
|
||||
|
||||
def get_value(self):
|
||||
return self.number
|
||||
|
||||
@@ -47,7 +47,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||
fill_opacity=None,
|
||||
family=True
|
||||
):
|
||||
# Unchangable style, except for fill_opacity
|
||||
# Unchangeable style, except for fill_opacity
|
||||
VMobject.set_style_data(
|
||||
self,
|
||||
stroke_color=BLACK,
|
||||
|
||||
@@ -200,12 +200,11 @@ class Laptop(VGroup):
|
||||
|
||||
class VideoIcon(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name": "video_icon",
|
||||
"width": FRAME_WIDTH / 12.,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
SVGMobject.__init__(self, **kwargs)
|
||||
super().__init__(file_name="video_icon", **kwargs)
|
||||
self.center()
|
||||
self.set_width(self.width)
|
||||
self.set_stroke(color=WHITE, width=0)
|
||||
|
||||
521
manimlib/mobject/svg/mtex_mobject.py
Normal file
521
manimlib/mobject/svg/mtex_mobject.py
Normal file
@@ -0,0 +1,521 @@
|
||||
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,
|
||||
}
|
||||
@@ -11,6 +11,7 @@ from manimlib.constants import DEFAULT_STROKE_WIDTH
|
||||
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import DEGREES, PI
|
||||
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
@@ -21,6 +22,7 @@ from manimlib.utils.color import *
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.directories import get_mobject_data_dir
|
||||
from manimlib.utils.images import get_full_vector_image_path
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
|
||||
def string_to_numbers(num_string):
|
||||
@@ -86,10 +88,10 @@ class SVGMobject(VMobject):
|
||||
elif element.tagName == 'style':
|
||||
pass # TODO, handle style
|
||||
elif element.tagName in ['g', 'svg', 'symbol']:
|
||||
result += it.chain(*[
|
||||
result += it.chain(*(
|
||||
self.get_mobjects_from(child)
|
||||
for child in element.childNodes
|
||||
])
|
||||
))
|
||||
elif element.tagName == 'path':
|
||||
result.append(self.path_string_to_mobject(
|
||||
element.getAttribute('d')
|
||||
@@ -182,8 +184,9 @@ class SVGMobject(VMobject):
|
||||
corner_radius = rect_element.getAttribute("rx")
|
||||
|
||||
# input preprocessing
|
||||
fill_opacity = 1
|
||||
if fill_color in ["", "none", "#FFF", "#FFFFFF"] or Color(fill_color) == Color(WHITE):
|
||||
opacity = 0
|
||||
fill_opacity = 0
|
||||
fill_color = BLACK # shdn't be necessary but avoids error msgs
|
||||
if fill_color in ["#000", "#000000"]:
|
||||
fill_color = WHITE
|
||||
@@ -211,7 +214,7 @@ class SVGMobject(VMobject):
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=opacity
|
||||
fill_opacity=fill_opacity
|
||||
)
|
||||
else:
|
||||
mob = RoundedRectangle(
|
||||
@@ -319,7 +322,7 @@ class SVGMobject(VMobject):
|
||||
|
||||
class VMobjectFromSVGPathstring(VMobject):
|
||||
CONFIG = {
|
||||
"long_lines": True,
|
||||
"long_lines": False,
|
||||
"should_subdivide_sharp_curves": False,
|
||||
"should_remove_null_curves": False,
|
||||
}
|
||||
@@ -339,11 +342,10 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
|
||||
if os.path.exists(points_filepath) and os.path.exists(tris_filepath):
|
||||
self.set_points(np.load(points_filepath))
|
||||
self.triangulation = np.load(tris_filepath)
|
||||
self.needs_new_triangulation = False
|
||||
else:
|
||||
self.relative_point = np.array(ORIGIN)
|
||||
for command, coord_string in self.get_commands_and_coord_strings():
|
||||
new_points = self.string_to_points(command, coord_string)
|
||||
self.handle_command(command, new_points)
|
||||
self.handle_commands()
|
||||
if self.should_subdivide_sharp_curves:
|
||||
# For a healthy triangulation later
|
||||
self.subdivide_sharp_curves()
|
||||
@@ -354,6 +356,7 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
self.stretch(-1, 1, about_point=ORIGIN)
|
||||
# Save to a file for future use
|
||||
np.save(points_filepath, self.get_points())
|
||||
np.save(tris_filepath, self.get_triangulation())
|
||||
|
||||
def get_commands_and_coord_strings(self):
|
||||
all_commands = list(self.get_command_to_function_map().keys())
|
||||
@@ -364,43 +367,141 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
re.split(pattern, self.path_string)[1:]
|
||||
)
|
||||
|
||||
def handle_command(self, command, new_points):
|
||||
if command.islower():
|
||||
# Treat it as a relative command
|
||||
new_points += self.relative_point
|
||||
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
|
||||
|
||||
func, n_points = self.command_to_function(command)
|
||||
func(*new_points[:n_points])
|
||||
leftover_points = new_points[n_points:]
|
||||
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))
|
||||
|
||||
# Recursively handle the rest of the points
|
||||
if len(leftover_points) > 0:
|
||||
if command.upper() == "M":
|
||||
# Treat following points as relative line coordinates
|
||||
command = "l"
|
||||
if command.islower():
|
||||
leftover_points -= self.relative_point
|
||||
self.relative_point = self.get_last_point()
|
||||
self.handle_command(command, leftover_points)
|
||||
else:
|
||||
# Command is over, reset for future relative commands
|
||||
self.relative_point = self.get_last_point()
|
||||
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]
|
||||
|
||||
def string_to_points(self, command, coord_string):
|
||||
numbers = string_to_numbers(coord_string)
|
||||
if command.upper() in ["H", "V"]:
|
||||
i = {"H": 0, "V": 1}[command.upper()]
|
||||
xy = np.zeros((len(numbers), 2))
|
||||
xy[:, i] = numbers
|
||||
if command.isupper():
|
||||
xy[:, 1 - i] = self.relative_point[1 - i]
|
||||
elif command.upper() == "A":
|
||||
raise Exception("Not implemented")
|
||||
else:
|
||||
xy = np.array(numbers).reshape((len(numbers) // 2, 2))
|
||||
result = np.zeros((xy.shape[0], self.dim))
|
||||
result[:, :2] = xy
|
||||
return result
|
||||
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()]
|
||||
@@ -408,19 +509,19 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
def get_command_to_function_map(self):
|
||||
"""
|
||||
Associates svg command to VMobject function, and
|
||||
the number of arguments it takes in
|
||||
the types of arguments it takes in
|
||||
"""
|
||||
return {
|
||||
"M": (self.start_new_path, 1),
|
||||
"L": (self.add_line_to, 1),
|
||||
"H": (self.add_line_to, 1),
|
||||
"V": (self.add_line_to, 1),
|
||||
"C": (self.add_cubic_bezier_curve_to, 3),
|
||||
"S": (self.add_smooth_cubic_curve_to, 2),
|
||||
"Q": (self.add_quadratic_bezier_curve_to, 2),
|
||||
"T": (self.add_smooth_curve_to, 1),
|
||||
"A": (self.add_quadratic_bezier_curve_to, 2), # TODO
|
||||
"Z": (self.close_path, 0),
|
||||
"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 get_original_path_string(self):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from functools import reduce
|
||||
import operator as op
|
||||
import re
|
||||
import itertools as it
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
@@ -130,15 +129,18 @@ class SingleStringTex(VMobject):
|
||||
|
||||
def balance_braces(self, tex):
|
||||
"""
|
||||
Makes Tex resiliant to unmatched { at start
|
||||
Makes Tex resiliant to unmatched braces
|
||||
"""
|
||||
num_lefts, num_rights = [tex.count(char) for char in "{}"]
|
||||
while num_rights > num_lefts:
|
||||
tex = "{" + tex
|
||||
num_lefts += 1
|
||||
while num_lefts > num_rights:
|
||||
tex = tex + "}"
|
||||
num_rights += 1
|
||||
num_unclosed_brackets = 0
|
||||
for 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):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
@@ -7,6 +6,9 @@ import typing
|
||||
import warnings
|
||||
import xml.etree.ElementTree as ET
|
||||
import functools
|
||||
import pygments
|
||||
import pygments.lexers
|
||||
import pygments.styles
|
||||
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
@@ -22,7 +24,8 @@ from manimlib.utils.directories import get_downloads_dir, get_text_dir
|
||||
from manimpango import PangoUtils, TextSetting, MarkupUtils
|
||||
|
||||
TEXT_MOB_SCALE_FACTOR = 0.0076
|
||||
DEFAULT_LINE_SPACING_SCALE = 0.3
|
||||
DEFAULT_LINE_SPACING_SCALE = 0.6
|
||||
|
||||
|
||||
class Text(SVGMobject):
|
||||
CONFIG = {
|
||||
@@ -47,9 +50,9 @@ class Text(SVGMobject):
|
||||
"disable_ligatures": True,
|
||||
}
|
||||
|
||||
def __init__(self, text, **config):
|
||||
self.full2short(config)
|
||||
digest_config(self, config)
|
||||
def __init__(self, text, **kwargs):
|
||||
self.full2short(kwargs)
|
||||
digest_config(self, kwargs)
|
||||
if self.size:
|
||||
warnings.warn(
|
||||
"self.size has been deprecated and will "
|
||||
@@ -68,7 +71,7 @@ class Text(SVGMobject):
|
||||
file_name = self.text2svg()
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
self.remove_empty_path(file_name)
|
||||
SVGMobject.__init__(self, file_name, **config)
|
||||
SVGMobject.__init__(self, file_name, **kwargs)
|
||||
self.text = text
|
||||
if self.disable_ligatures:
|
||||
self.apply_space_chars()
|
||||
@@ -145,13 +148,13 @@ class Text(SVGMobject):
|
||||
|
||||
def set_color_by_t2c(self, t2c=None):
|
||||
t2c = t2c if t2c else self.t2c
|
||||
for word, color in list(t2c.items()):
|
||||
for word, color in t2c.items():
|
||||
for start, end in self.find_indexes(word):
|
||||
self[start:end].set_color(color)
|
||||
|
||||
def set_color_by_t2g(self, t2g=None):
|
||||
t2g = t2g if t2g else self.t2g
|
||||
for word, gradient in list(t2g.items()):
|
||||
for word, gradient in t2g.items():
|
||||
for start, end in self.find_indexes(word):
|
||||
self[start:end].set_color_by_gradient(*gradient)
|
||||
|
||||
@@ -165,49 +168,62 @@ class Text(SVGMobject):
|
||||
return hasher.hexdigest()[:16]
|
||||
|
||||
def text2settings(self):
|
||||
"""
|
||||
Substrings specified in t2f, t2s, t2w can occupy each other.
|
||||
For each category of style, a stack following first-in-last-out is constructed,
|
||||
and the last value in each stack takes effect.
|
||||
"""
|
||||
settings = []
|
||||
t2x = [self.t2f, self.t2s, self.t2w]
|
||||
for i in range(len(t2x)):
|
||||
fsw = [self.font, self.slant, self.weight]
|
||||
if t2x[i]:
|
||||
for word, x in list(t2x[i].items()):
|
||||
for start, end in self.find_indexes(word):
|
||||
fsw[i] = x
|
||||
settings.append(TextSetting(start, end, *fsw))
|
||||
self.line_num = 0
|
||||
def add_text_settings(start, end, style_stacks):
|
||||
if start == end:
|
||||
return
|
||||
breakdown_indices = [start, *[
|
||||
i + start + 1 for i, char in enumerate(self.text[start:end]) if char == "\n"
|
||||
], end]
|
||||
style = [stack[-1] for stack in style_stacks]
|
||||
for atom_start, atom_end in zip(breakdown_indices[:-1], breakdown_indices[1:]):
|
||||
if atom_start < atom_end:
|
||||
settings.append(TextSetting(atom_start, atom_end, *style, self.line_num))
|
||||
self.line_num += 1
|
||||
self.line_num -= 1
|
||||
|
||||
# Set All text settings(default font slant weight)
|
||||
fsw = [self.font, self.slant, self.weight]
|
||||
settings.sort(key=lambda setting: setting.start)
|
||||
temp_settings = settings.copy()
|
||||
start = 0
|
||||
for setting in settings:
|
||||
if setting.start != start:
|
||||
temp_settings.append(TextSetting(start, setting.start, *fsw))
|
||||
start = setting.end
|
||||
if start != len(self.text):
|
||||
temp_settings.append(TextSetting(start, len(self.text), *fsw))
|
||||
settings = sorted(temp_settings, key=lambda setting: setting.start)
|
||||
# Set all the default and specified values.
|
||||
len_text = len(self.text)
|
||||
t2x_items = sorted([
|
||||
*[
|
||||
(0, len_text, t2x_index, value)
|
||||
for t2x_index, value in enumerate([self.font, self.slant, self.weight])
|
||||
],
|
||||
*[
|
||||
(start, end, t2x_index, value)
|
||||
for t2x_index, t2x in enumerate([self.t2f, self.t2s, self.t2w])
|
||||
for word, value in t2x.items()
|
||||
for start, end in self.find_indexes(word)
|
||||
]
|
||||
], key=lambda item: item[0])
|
||||
|
||||
if re.search(r'\n', self.text):
|
||||
line_num = 0
|
||||
for start, end in self.find_indexes('\n'):
|
||||
for setting in settings:
|
||||
if setting.line_num == -1:
|
||||
setting.line_num = line_num
|
||||
if start < setting.end:
|
||||
line_num += 1
|
||||
new_setting = copy.copy(setting)
|
||||
setting.end = end
|
||||
new_setting.start = end
|
||||
new_setting.line_num = line_num
|
||||
settings.append(new_setting)
|
||||
settings.sort(key=lambda setting: setting.start)
|
||||
break
|
||||
|
||||
for setting in settings:
|
||||
if setting.line_num == -1:
|
||||
setting.line_num = 0
|
||||
# Break down ranges and construct settings separately.
|
||||
active_items = []
|
||||
style_stacks = [[] for _ in range(3)]
|
||||
for item, next_start in zip(t2x_items, [*[item[0] for item in t2x_items[1:]], len_text]):
|
||||
active_items.append(item)
|
||||
start, end, t2x_index, value = item
|
||||
style_stacks[t2x_index].append(value)
|
||||
halting_items = sorted(filter(
|
||||
lambda item: item[1] <= next_start,
|
||||
active_items
|
||||
), key=lambda item: item[1])
|
||||
atom_start = start
|
||||
for halting_item in halting_items:
|
||||
active_items.remove(halting_item)
|
||||
_, atom_end, t2x_index, _ = halting_item
|
||||
add_text_settings(atom_start, atom_end, style_stacks)
|
||||
style_stacks[t2x_index].pop()
|
||||
atom_start = atom_end
|
||||
add_text_settings(atom_start, next_start, style_stacks)
|
||||
|
||||
del self.line_num
|
||||
return settings
|
||||
|
||||
def text2svg(self):
|
||||
@@ -257,6 +273,7 @@ class MarkupText(SVGMobject):
|
||||
"gradient": None,
|
||||
"disable_ligatures": True,
|
||||
}
|
||||
|
||||
def __init__(self, text, **config):
|
||||
digest_config(self, config)
|
||||
self.text = f'<span>{text}</span>'
|
||||
@@ -303,6 +320,7 @@ class MarkupText(SVGMobject):
|
||||
# anti-aliasing
|
||||
if self.height is None:
|
||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||
|
||||
def text2hash(self):
|
||||
"""Generates ``sha256`` hash for file name."""
|
||||
settings = (
|
||||
@@ -349,7 +367,6 @@ class MarkupText(SVGMobject):
|
||||
**extra_kwargs
|
||||
)
|
||||
|
||||
|
||||
def _parse_color(self, col):
|
||||
"""Parse color given in ``<color>`` or ``<gradient>`` tags."""
|
||||
if re.match("#[0-9a-f]{6}", col):
|
||||
@@ -472,6 +489,63 @@ class MarkupText(SVGMobject):
|
||||
def __repr__(self):
|
||||
return f"MarkupText({repr(self.original_text)})"
|
||||
|
||||
|
||||
class Code(Text):
|
||||
CONFIG = {
|
||||
"font": "Consolas",
|
||||
"font_size": 24,
|
||||
"lsh": 1.0,
|
||||
"language": "python",
|
||||
# Visit https://pygments.org/demo/ to have a preview of more styles.
|
||||
"code_style": "monokai",
|
||||
# If not None, then each character will cover a space of equal width.
|
||||
"char_width": None
|
||||
}
|
||||
|
||||
def __init__(self, code, **kwargs):
|
||||
self.full2short(kwargs)
|
||||
digest_config(self, kwargs)
|
||||
code = code.lstrip("\n") # avoid mismatches of character indices
|
||||
lexer = pygments.lexers.get_lexer_by_name(self.language)
|
||||
tokens_generator = pygments.lex(code, lexer)
|
||||
styles_dict = dict(pygments.styles.get_style_by_name(self.code_style))
|
||||
default_color_hex = styles_dict[pygments.token.Text]["color"]
|
||||
if not default_color_hex:
|
||||
default_color_hex = self.color[1:]
|
||||
start_index = 0
|
||||
t2c = {}
|
||||
t2s = {}
|
||||
t2w = {}
|
||||
for pair in tokens_generator:
|
||||
ttype, token = pair
|
||||
end_index = start_index + len(token)
|
||||
range_str = f"[{start_index}:{end_index}]"
|
||||
style_dict = styles_dict[ttype]
|
||||
t2c[range_str] = "#" + (style_dict["color"] or default_color_hex)
|
||||
t2s[range_str] = ITALIC if style_dict["italic"] else NORMAL
|
||||
t2w[range_str] = BOLD if style_dict["bold"] else NORMAL
|
||||
start_index = end_index
|
||||
t2c.update(self.t2c)
|
||||
t2s.update(self.t2s)
|
||||
t2w.update(self.t2w)
|
||||
kwargs["t2c"] = t2c
|
||||
kwargs["t2s"] = t2s
|
||||
kwargs["t2w"] = t2w
|
||||
Text.__init__(self, code, **kwargs)
|
||||
if self.char_width is not None:
|
||||
self.set_monospace(self.char_width)
|
||||
|
||||
def set_monospace(self, char_width):
|
||||
current_char_index = 0
|
||||
for i, char in enumerate(self.text):
|
||||
if char == "\n":
|
||||
current_char_index = 0
|
||||
continue
|
||||
self[i].set_x(current_char_index * char_width)
|
||||
current_char_index += 1
|
||||
self.center()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def register_font(font_file: typing.Union[str, Path]):
|
||||
"""Temporarily add a font file to Pango's search path.
|
||||
|
||||
@@ -5,16 +5,21 @@ from manimlib.mobject.types.surface import Surface
|
||||
from manimlib.mobject.types.surface import SGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.geometry import Square
|
||||
from manimlib.mobject.geometry import Polygon
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.space_ops import z_to_vector
|
||||
from manimlib.utils.space_ops import compass_directions
|
||||
|
||||
|
||||
class SurfaceMesh(VGroup):
|
||||
CONFIG = {
|
||||
"resolution": (21, 21),
|
||||
"resolution": (21, 11),
|
||||
"stroke_width": 1,
|
||||
"normal_nudge": 1e-3,
|
||||
"normal_nudge": 1e-2,
|
||||
"depth_test": True,
|
||||
"flat_stroke": False,
|
||||
}
|
||||
@@ -30,8 +35,11 @@ class SurfaceMesh(VGroup):
|
||||
|
||||
full_nu, full_nv = uv_surface.resolution
|
||||
part_nu, part_nv = self.resolution
|
||||
u_indices = np.linspace(0, full_nu, part_nu).astype(int)
|
||||
v_indices = np.linspace(0, full_nv, part_nv).astype(int)
|
||||
# 'indices' are treated as floats. Later, there will be
|
||||
# an interpolation between the floor and ceiling of these
|
||||
# indices
|
||||
u_indices = np.linspace(0, full_nu - 1, part_nu)
|
||||
v_indices = np.linspace(0, full_nv - 1, part_nv)
|
||||
|
||||
points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points()
|
||||
normals = uv_surface.get_unit_normals()
|
||||
@@ -40,12 +48,21 @@ class SurfaceMesh(VGroup):
|
||||
|
||||
for ui in u_indices:
|
||||
path = VMobject()
|
||||
full_ui = full_nv * ui
|
||||
path.set_points_smoothly(nudged_points[full_ui:full_ui + full_nv])
|
||||
low_ui = full_nv * int(math.floor(ui))
|
||||
high_ui = full_nv * int(math.ceil(ui))
|
||||
path.set_points_smoothly(interpolate(
|
||||
nudged_points[low_ui:low_ui + full_nv],
|
||||
nudged_points[high_ui:high_ui + full_nv],
|
||||
ui % 1
|
||||
))
|
||||
self.add(path)
|
||||
for vi in v_indices:
|
||||
path = VMobject()
|
||||
path.set_points_smoothly(nudged_points[vi::full_nv])
|
||||
path.set_points_smoothly(interpolate(
|
||||
nudged_points[int(math.floor(vi))::full_nv],
|
||||
nudged_points[int(math.ceil(vi))::full_nv],
|
||||
vi % 1
|
||||
))
|
||||
self.add(path)
|
||||
|
||||
|
||||
@@ -161,15 +178,96 @@ class Cube(SGroup):
|
||||
"gloss": 0.5,
|
||||
"square_resolution": (2, 2),
|
||||
"side_length": 2,
|
||||
"square_class": Square3D,
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
for vect in [OUT, RIGHT, UP, LEFT, DOWN, IN]:
|
||||
face = Square3D(resolution=self.square_resolution)
|
||||
face.shift(OUT)
|
||||
face.apply_matrix(z_to_vector(vect))
|
||||
self.add(face)
|
||||
self.set_height(self.side_length)
|
||||
face = Square3D(
|
||||
resolution=self.square_resolution,
|
||||
side_length=self.side_length,
|
||||
)
|
||||
self.add(*self.square_to_cube_faces(face))
|
||||
|
||||
@staticmethod
|
||||
def square_to_cube_faces(square):
|
||||
radius = square.get_height() / 2
|
||||
square.move_to(radius * OUT)
|
||||
result = [square]
|
||||
result.extend([
|
||||
square.copy().rotate(PI / 2, axis=vect, about_point=ORIGIN)
|
||||
for vect in compass_directions(4)
|
||||
])
|
||||
result.append(square.copy().rotate(PI, RIGHT, about_point=ORIGIN))
|
||||
return result
|
||||
|
||||
def _get_face(self):
|
||||
return Square3D(resolution=self.square_resolution)
|
||||
|
||||
|
||||
class VCube(VGroup):
|
||||
CONFIG = {
|
||||
"fill_color": BLUE_D,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
"gloss": 0.5,
|
||||
"shadow": 0.5,
|
||||
}
|
||||
|
||||
def __init__(self, side_length=2, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
face = Square(side_length=side_length)
|
||||
face.get_triangulation()
|
||||
self.add(*Cube.square_to_cube_faces(face))
|
||||
self.init_colors()
|
||||
self.apply_depth_test()
|
||||
self.refresh_unit_normal()
|
||||
|
||||
|
||||
class Dodecahedron(VGroup):
|
||||
CONFIG = {
|
||||
"fill_color": BLUE_E,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 1,
|
||||
"reflectiveness": 0.2,
|
||||
"gloss": 0.3,
|
||||
"shadow": 0.2,
|
||||
"depth_test": True,
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
# Star by creating two of the pentagons, meeting
|
||||
# back to back on the positive x-axis
|
||||
phi = (1 + math.sqrt(5)) / 2
|
||||
x, y, z = np.identity(3)
|
||||
pentagon1 = Polygon(
|
||||
[phi, 1 / phi, 0],
|
||||
[1, 1, 1],
|
||||
[1 / phi, 0, phi],
|
||||
[1, -1, 1],
|
||||
[phi, -1 / phi, 0],
|
||||
)
|
||||
pentagon2 = pentagon1.copy().stretch(-1, 2, about_point=ORIGIN)
|
||||
pentagon2.reverse_points()
|
||||
x_pair = VGroup(pentagon1, pentagon2)
|
||||
z_pair = x_pair.copy().apply_matrix(np.array([z, -x, -y]).T)
|
||||
y_pair = x_pair.copy().apply_matrix(np.array([y, z, x]).T)
|
||||
|
||||
self.add(*x_pair, *y_pair, *z_pair)
|
||||
for pentagon in list(self):
|
||||
pc = pentagon.copy()
|
||||
pc.apply_function(lambda p: -p)
|
||||
pc.reverse_points()
|
||||
self.add(pc)
|
||||
|
||||
# # Rotate those two pentagons by all the axis permuations to fill
|
||||
# # out the dodecahedron
|
||||
# Id = np.identity(3)
|
||||
# for i in range(3):
|
||||
# perm = [j % 3 for j in range(i, i + 3)]
|
||||
# for b in [1, -1]:
|
||||
# matrix = b * np.array([Id[0][perm], Id[1][perm], Id[2][perm]])
|
||||
# self.add(pentagon1.copy().apply_matrix(matrix, about_point=ORIGIN))
|
||||
# self.add(pentagon2.copy().apply_matrix(matrix, about_point=ORIGIN))
|
||||
|
||||
|
||||
class Prism(Cube):
|
||||
|
||||
@@ -2,12 +2,14 @@ import numpy as np
|
||||
import moderngl
|
||||
|
||||
from manimlib.constants import GREY_C
|
||||
from manimlib.constants import YELLOW
|
||||
from manimlib.constants import ORIGIN
|
||||
from manimlib.mobject.types.point_cloud_mobject import PMobject
|
||||
from manimlib.utils.iterables import resize_preserving_order
|
||||
|
||||
|
||||
DEFAULT_DOT_RADIUS = 0.05
|
||||
DEFAULT_GLOW_DOT_RADIUS = 0.2
|
||||
DEFAULT_GRID_HEIGHT = 6
|
||||
DEFAULT_BUFF_RATIO = 0.5
|
||||
|
||||
@@ -17,6 +19,7 @@ class DotCloud(PMobject):
|
||||
"color": GREY_C,
|
||||
"opacity": 1,
|
||||
"radius": DEFAULT_DOT_RADIUS,
|
||||
"glow_factor": 0,
|
||||
"shader_folder": "true_dot",
|
||||
"render_primitive": moderngl.POINTS,
|
||||
"shader_dtype": [
|
||||
@@ -36,6 +39,10 @@ class DotCloud(PMobject):
|
||||
self.data["radii"] = np.zeros((1, 1))
|
||||
self.set_radius(self.radius)
|
||||
|
||||
def init_uniforms(self):
|
||||
super().init_uniforms()
|
||||
self.uniforms["glow_factor"] = self.glow_factor
|
||||
|
||||
def to_grid(self, n_rows, n_cols, n_layers=1,
|
||||
buff_ratio=None,
|
||||
h_buff_ratio=1.0,
|
||||
@@ -68,7 +75,9 @@ class DotCloud(PMobject):
|
||||
return self
|
||||
|
||||
def set_radii(self, radii):
|
||||
self.data["radii"][:] = resize_preserving_order(radii, len(self.data["radii"]))
|
||||
n_points = len(self.get_points())
|
||||
radii = np.array(radii).reshape((len(radii), 1))
|
||||
self.data["radii"] = resize_preserving_order(radii, n_points)
|
||||
self.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
@@ -83,6 +92,12 @@ class DotCloud(PMobject):
|
||||
def get_radius(self):
|
||||
return self.get_radii().max()
|
||||
|
||||
def set_glow_factor(self, glow_factor):
|
||||
self.uniforms["glow_factor"] = glow_factor
|
||||
|
||||
def get_glow_factor(self):
|
||||
return self.uniforms["glow_factor"]
|
||||
|
||||
def compute_bounding_box(self):
|
||||
bb = super().compute_bounding_box()
|
||||
radius = self.get_radius()
|
||||
@@ -96,8 +111,8 @@ class DotCloud(PMobject):
|
||||
self.set_radii(scale_factor * self.get_radii())
|
||||
return self
|
||||
|
||||
def make_3d(self, gloss=0.5, shadow=0.2):
|
||||
self.set_gloss(gloss)
|
||||
def make_3d(self, reflectiveness=0.5, shadow=0.2):
|
||||
self.set_reflectiveness(reflectiveness)
|
||||
self.set_shadow(shadow)
|
||||
self.apply_depth_test()
|
||||
return self
|
||||
@@ -110,5 +125,13 @@ class DotCloud(PMobject):
|
||||
|
||||
|
||||
class TrueDot(DotCloud):
|
||||
def __init__(self, center=ORIGIN, radius=DEFAULT_DOT_RADIUS, **kwargs):
|
||||
super().__init__(points=[center], radius=radius, **kwargs)
|
||||
def __init__(self, center=ORIGIN, **kwargs):
|
||||
super().__init__(points=[center], **kwargs)
|
||||
|
||||
|
||||
class GlowDot(TrueDot):
|
||||
CONFIG = {
|
||||
"glow_factor": 2,
|
||||
"radius": DEFAULT_GLOW_DOT_RADIUS,
|
||||
"color": YELLOW,
|
||||
}
|
||||
|
||||
@@ -22,10 +22,13 @@ class ImageMobject(Mobject):
|
||||
}
|
||||
|
||||
def __init__(self, filename, **kwargs):
|
||||
path = get_full_raster_image_path(filename)
|
||||
self.set_image_path(get_full_raster_image_path(filename))
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def set_image_path(self, path):
|
||||
self.path = path
|
||||
self.image = Image.open(path)
|
||||
self.texture_paths = {"Texture": path}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_data(self):
|
||||
self.data = {
|
||||
|
||||
@@ -14,10 +14,17 @@ class PMobject(Mobject):
|
||||
def resize_points(self, size, resize_func=resize_array):
|
||||
# TODO
|
||||
for key in self.data:
|
||||
if key == "bounding_box":
|
||||
continue
|
||||
if len(self.data[key]) != size:
|
||||
self.data[key] = resize_array(self.data[key], size)
|
||||
return self
|
||||
|
||||
def set_points(self, points):
|
||||
super().set_points(points)
|
||||
self.resize_points(len(points))
|
||||
return self
|
||||
|
||||
def add_points(self, points, rgbas=None, color=None, opacity=None):
|
||||
"""
|
||||
points must be a Nx3 numpy array, as must rgbas if it is not None
|
||||
@@ -54,6 +61,8 @@ class PMobject(Mobject):
|
||||
for mob in self.family_members_with_points():
|
||||
to_keep = ~np.apply_along_axis(condition, 1, mob.get_points())
|
||||
for key in mob.data:
|
||||
if key == "bounding_box":
|
||||
continue
|
||||
mob.data[key] = mob.data[key][to_keep]
|
||||
return self
|
||||
|
||||
@@ -85,7 +94,9 @@ class PMobject(Mobject):
|
||||
lower_index = int(a * pmobject.get_num_points())
|
||||
upper_index = int(b * pmobject.get_num_points())
|
||||
for key in self.data:
|
||||
self.data[key] = pmobject.data[key][lower_index:upper_index]
|
||||
if key == "bounding_box":
|
||||
continue
|
||||
self.data[key] = pmobject.data[key][lower_index:upper_index].copy()
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ class Surface(Mobject):
|
||||
"resolution": (101, 101),
|
||||
"color": GREY,
|
||||
"opacity": 1.0,
|
||||
"gloss": 0.3,
|
||||
"reflectiveness": 0.3,
|
||||
"gloss": 0.1,
|
||||
"shadow": 0.4,
|
||||
"prefered_creation_axis": 1,
|
||||
# For du and dv steps. Much smaller and numerical error
|
||||
@@ -161,6 +162,11 @@ class Surface(Mobject):
|
||||
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()
|
||||
))
|
||||
|
||||
# For shaders
|
||||
def get_shader_data(self):
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
|
||||
@@ -12,6 +12,7 @@ from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points
|
||||
from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points
|
||||
from manimlib.utils.bezier import get_quadratic_approximation_of_cubic
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import inverse_interpolate
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.bezier import partial_quadratic_bezier_points
|
||||
from manimlib.utils.color import rgb_to_hex
|
||||
@@ -54,7 +55,6 @@ class VMobject(Mobject):
|
||||
"joint_type": "auto",
|
||||
"flat_stroke": False,
|
||||
"render_primitive": moderngl.TRIANGLES,
|
||||
"triangulation_locked": False,
|
||||
"fill_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
('unit_normal', np.float32, (3,)),
|
||||
@@ -75,7 +75,6 @@ class VMobject(Mobject):
|
||||
self.needs_new_triangulation = True
|
||||
self.triangulation = np.zeros(0, dtype='i4')
|
||||
super().__init__(**kwargs)
|
||||
self.refresh_unit_normal()
|
||||
|
||||
def get_group_class(self):
|
||||
return VGroup
|
||||
@@ -128,7 +127,7 @@ class VMobject(Mobject):
|
||||
if isinstance(width, np.ndarray):
|
||||
arr = width.reshape((len(width), 1))
|
||||
else:
|
||||
arr = np.array([[w] for w in listify(width)])
|
||||
arr = np.array([[w] for w in listify(width)], dtype=float)
|
||||
mob.data['stroke_width'] = arr
|
||||
|
||||
if background is not None:
|
||||
@@ -136,6 +135,10 @@ class VMobject(Mobject):
|
||||
mob.draw_stroke_behind_fill = background
|
||||
return self
|
||||
|
||||
def set_backstroke(self, color=BLACK, width=3, background=True):
|
||||
self.set_stroke(color, width, background=background)
|
||||
return self
|
||||
|
||||
def align_stroke_width_data_to_points(self, recurse=True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["stroke_width"] = resize_with_interpolation(
|
||||
@@ -150,6 +153,8 @@ class VMobject(Mobject):
|
||||
stroke_opacity=None,
|
||||
stroke_rgba=None,
|
||||
stroke_width=None,
|
||||
stroke_background=True,
|
||||
reflectiveness=None,
|
||||
gloss=None,
|
||||
shadow=None,
|
||||
recurse=True):
|
||||
@@ -164,15 +169,21 @@ class VMobject(Mobject):
|
||||
|
||||
if stroke_rgba is not None:
|
||||
self.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba))
|
||||
self.set_stroke(width=stroke_width)
|
||||
self.set_stroke(
|
||||
width=stroke_width,
|
||||
background=stroke_background,
|
||||
)
|
||||
else:
|
||||
self.set_stroke(
|
||||
color=stroke_color,
|
||||
width=stroke_width,
|
||||
opacity=stroke_opacity,
|
||||
recurse=recurse,
|
||||
background=stroke_background,
|
||||
)
|
||||
|
||||
if reflectiveness is not None:
|
||||
self.set_reflectiveness(reflectiveness, recurse=recurse)
|
||||
if gloss is not None:
|
||||
self.set_gloss(gloss, recurse=recurse)
|
||||
if shadow is not None:
|
||||
@@ -181,9 +192,11 @@ class VMobject(Mobject):
|
||||
|
||||
def get_style(self):
|
||||
return {
|
||||
"fill_rgba": self.data['fill_rgba'],
|
||||
"stroke_rgba": self.data['stroke_rgba'],
|
||||
"stroke_width": self.data['stroke_width'],
|
||||
"fill_rgba": self.data['fill_rgba'].copy(),
|
||||
"stroke_rgba": self.data['stroke_rgba'].copy(),
|
||||
"stroke_width": self.data['stroke_width'].copy(),
|
||||
"stroke_background": self.draw_stroke_behind_fill,
|
||||
"reflectiveness": self.get_reflectiveness(),
|
||||
"gloss": self.get_gloss(),
|
||||
"shadow": self.get_shadow(),
|
||||
}
|
||||
@@ -213,16 +226,17 @@ class VMobject(Mobject):
|
||||
return self
|
||||
|
||||
def fade(self, darkness=0.5, recurse=True):
|
||||
factor = 1.0 - darkness
|
||||
self.set_fill(
|
||||
opacity=factor * self.get_fill_opacity(),
|
||||
recurse=False,
|
||||
)
|
||||
self.set_stroke(
|
||||
opacity=factor * self.get_stroke_opacity(),
|
||||
recurse=False,
|
||||
)
|
||||
super().fade(darkness, recurse)
|
||||
mobs = self.get_family() if recurse else [self]
|
||||
for mob in mobs:
|
||||
factor = 1.0 - darkness
|
||||
mob.set_fill(
|
||||
opacity=factor * mob.get_fill_opacity(),
|
||||
recurse=False,
|
||||
)
|
||||
mob.set_stroke(
|
||||
opacity=factor * mob.get_stroke_opacity(),
|
||||
recurse=False,
|
||||
)
|
||||
return self
|
||||
|
||||
def get_fill_colors(self):
|
||||
@@ -272,9 +286,9 @@ class VMobject(Mobject):
|
||||
return self.get_stroke_opacities()[0]
|
||||
|
||||
def get_color(self):
|
||||
if self.has_stroke():
|
||||
return self.get_stroke_color()
|
||||
return self.get_fill_color()
|
||||
if self.has_fill():
|
||||
return self.get_fill_color()
|
||||
return self.get_stroke_color()
|
||||
|
||||
def has_stroke(self):
|
||||
return self.get_stroke_widths().any() and self.get_stroke_opacities().any()
|
||||
@@ -424,7 +438,10 @@ class VMobject(Mobject):
|
||||
|
||||
def set_points_smoothly(self, points, true_smooth=False):
|
||||
self.set_points_as_corners(points)
|
||||
self.make_smooth()
|
||||
if true_smooth:
|
||||
self.make_smooth()
|
||||
else:
|
||||
self.make_approximately_smooth()
|
||||
return self
|
||||
|
||||
def change_anchor_mode(self, mode):
|
||||
@@ -496,10 +513,10 @@ class VMobject(Mobject):
|
||||
nppc = self.n_points_per_curve
|
||||
remainder = len(points) % nppc
|
||||
points = points[:len(points) - remainder]
|
||||
return [
|
||||
return (
|
||||
points[i:i + nppc]
|
||||
for i in range(0, len(points), nppc)
|
||||
]
|
||||
)
|
||||
|
||||
def get_bezier_tuples(self):
|
||||
return self.get_bezier_tuples_from_points(self.get_points())
|
||||
@@ -535,12 +552,35 @@ class VMobject(Mobject):
|
||||
def get_num_curves(self):
|
||||
return self.get_num_points() // self.n_points_per_curve
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
def quick_point_from_proportion(self, alpha):
|
||||
# Assumes all curves have the same length, so is inaccurate
|
||||
num_curves = self.get_num_curves()
|
||||
n, residue = integer_interpolate(0, num_curves, alpha)
|
||||
curve_func = self.get_nth_curve_function(n)
|
||||
return curve_func(residue)
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
if alpha <= 0:
|
||||
return self.get_start()
|
||||
elif alpha >= 1:
|
||||
return self.get_end()
|
||||
|
||||
partials = [0]
|
||||
for tup in self.get_bezier_tuples():
|
||||
# Approximate length with straight line from start to end
|
||||
arclen = get_norm(tup[0] - tup[-1])
|
||||
partials.append(partials[-1] + arclen)
|
||||
full = partials[-1]
|
||||
if full == 0:
|
||||
return self.get_start()
|
||||
# First index where the partial lenth is more alpha times the full length
|
||||
i = next(
|
||||
(i for i, x in enumerate(partials) if x >= full * alpha),
|
||||
len(partials) # Default
|
||||
)
|
||||
residue = inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha)
|
||||
return self.get_nth_curve_function(i - 1)(residue)
|
||||
|
||||
def get_anchors_and_handles(self):
|
||||
"""
|
||||
returns anchors1, handles, anchors2,
|
||||
@@ -621,17 +661,19 @@ class VMobject(Mobject):
|
||||
area_vect = self.get_area_vector()
|
||||
area = get_norm(area_vect)
|
||||
if area > 0:
|
||||
return area_vect / area
|
||||
normal = area_vect / area
|
||||
else:
|
||||
points = self.get_points()
|
||||
return get_unit_normal(
|
||||
normal = get_unit_normal(
|
||||
points[1] - points[0],
|
||||
points[2] - points[1],
|
||||
)
|
||||
self.data["unit_normal"][:] = normal
|
||||
return normal
|
||||
|
||||
def refresh_unit_normal(self):
|
||||
for mob in self.get_family():
|
||||
mob.data["unit_normal"][:] = mob.get_unit_normal(recompute=True)
|
||||
mob.get_unit_normal(recompute=True)
|
||||
return self
|
||||
|
||||
# Alignment
|
||||
@@ -693,7 +735,7 @@ class VMobject(Mobject):
|
||||
if len(points) == 1:
|
||||
return np.repeat(points, nppc * n, 0)
|
||||
|
||||
bezier_groups = self.get_bezier_tuples_from_points(points)
|
||||
bezier_groups = list(self.get_bezier_tuples_from_points(points))
|
||||
norms = np.array([
|
||||
get_norm(bg[nppc - 1] - bg[0])
|
||||
for bg in bezier_groups
|
||||
@@ -789,7 +831,7 @@ class VMobject(Mobject):
|
||||
# how to send the points as to the vertex shader.
|
||||
# First triangles come directly from the points
|
||||
if normal_vector is None:
|
||||
normal_vector = self.get_unit_normal()
|
||||
normal_vector = self.get_unit_normal(recompute=True)
|
||||
|
||||
if not self.needs_new_triangulation:
|
||||
return self.triangulation
|
||||
@@ -845,7 +887,7 @@ class VMobject(Mobject):
|
||||
def triggers_refreshed_triangulation(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
old_points = self.get_points()
|
||||
old_points = self.get_points().copy()
|
||||
func(self, *args, **kwargs)
|
||||
if not np.all(self.get_points() == old_points):
|
||||
self.refresh_unit_normal()
|
||||
@@ -991,6 +1033,10 @@ class VGroup(VMobject):
|
||||
super().__init__(**kwargs)
|
||||
self.add(*vmobjects)
|
||||
|
||||
def __add__(self: 'VGroup', other: 'VMobject' or 'VGroup'):
|
||||
assert(isinstance(other, VMobject))
|
||||
return self.add(other)
|
||||
|
||||
|
||||
class VectorizedPoint(Point, VMobject):
|
||||
CONFIG = {
|
||||
@@ -1002,7 +1048,8 @@ class VectorizedPoint(Point, VMobject):
|
||||
}
|
||||
|
||||
def __init__(self, location=ORIGIN, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
Point.__init__(self, **kwargs)
|
||||
VMobject.__init__(self, **kwargs)
|
||||
self.set_points(np.array([location]))
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import numpy as np
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import listify
|
||||
|
||||
|
||||
class ValueTracker(Mobject):
|
||||
@@ -15,18 +16,25 @@ class ValueTracker(Mobject):
|
||||
}
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
self.value = value
|
||||
super().__init__(**kwargs)
|
||||
self.set_value(value)
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
self.data["value"] = np.zeros((1, 1), dtype=self.value_type)
|
||||
self.data["value"] = np.array(
|
||||
listify(self.value),
|
||||
ndmin=2,
|
||||
dtype=self.value_type,
|
||||
)
|
||||
|
||||
def get_value(self):
|
||||
return self.data["value"][0, 0]
|
||||
result = self.data["value"][0, :]
|
||||
if len(result) == 1:
|
||||
return result[0]
|
||||
return result
|
||||
|
||||
def set_value(self, value):
|
||||
self.data["value"][0, 0] = value
|
||||
self.data["value"][0, :] = value
|
||||
return self
|
||||
|
||||
def increment_value(self, d_value):
|
||||
|
||||
@@ -333,7 +333,7 @@ class DiscreteGraphScene(Scene):
|
||||
x_coord_of = {root: 0}
|
||||
y_coord_of = {root: bottom}
|
||||
# width to allocate to a given node, computed as
|
||||
# the maxium number of decendents in a single generation,
|
||||
# the maximum number of decendents in a single generation,
|
||||
# minus 1, multiplied by x_sep
|
||||
width_of = {}
|
||||
for index in indices:
|
||||
|
||||
@@ -5,7 +5,7 @@ from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import adjacent_pairs
|
||||
|
||||
# Warning: This is all now pretty depricated, and should not be expected to work
|
||||
# Warning: This is all now pretty deprecated, and should not be expected to work
|
||||
|
||||
|
||||
class Region(Mobject):
|
||||
|
||||
@@ -2,7 +2,6 @@ import inspect
|
||||
import random
|
||||
import platform
|
||||
import itertools as it
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
@@ -11,16 +10,17 @@ import time
|
||||
|
||||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.animation.transform import MoveToTarget
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
from manimlib.utils.family_ops import restructure_list_to_exclude_certain_family_members
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
from manimlib.event_handler import EVENT_DISPATCHER
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
class Scene(object):
|
||||
@@ -45,6 +45,7 @@ class Scene(object):
|
||||
from manimlib.window import Window
|
||||
self.window = Window(scene=self, **self.window_config)
|
||||
self.camera_config["ctx"] = self.window.ctx
|
||||
self.camera_config["frame_rate"] = 30 # Where's that 30 from?
|
||||
else:
|
||||
self.window = None
|
||||
|
||||
@@ -55,6 +56,8 @@ class Scene(object):
|
||||
self.time = 0
|
||||
self.skip_time = 0
|
||||
self.original_skipping_status = self.skip_animations
|
||||
if self.start_at_animation_number is not None:
|
||||
self.skip_animations = True
|
||||
|
||||
# Items associated with interaction
|
||||
self.mouse_point = Point()
|
||||
@@ -100,10 +103,12 @@ class Scene(object):
|
||||
# If there is a window, enter a loop
|
||||
# which updates the frame while under
|
||||
# the hood calling the pyglet event loop
|
||||
log.info("Tips: You are now in the interactive mode. Now you can use the keyboard"
|
||||
" and the mouse to interact with the scene. Just press `q` if you want to quit.")
|
||||
self.quit_interaction = False
|
||||
self.lock_static_mobject_data()
|
||||
while not (self.window.is_closing or self.quit_interaction):
|
||||
self.update_frame()
|
||||
self.update_frame(1 / self.camera.frame_rate)
|
||||
if self.window.is_closing:
|
||||
self.window.destroy()
|
||||
if self.quit_interaction:
|
||||
@@ -118,16 +123,21 @@ class Scene(object):
|
||||
self.linger_after_completion = False
|
||||
self.update_frame()
|
||||
|
||||
# Save scene state at the point of embedding
|
||||
self.save_state()
|
||||
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
shell = InteractiveShellEmbed()
|
||||
# Have the frame update after each command
|
||||
shell.events.register('post_run_cell', lambda *a, **kw: self.update_frame())
|
||||
# Use the locals of the caller as the local namespace
|
||||
# once embeded, and add a few custom shortcuts
|
||||
# once embedded, and add a few custom shortcuts
|
||||
local_ns = inspect.currentframe().f_back.f_locals
|
||||
local_ns["touch"] = self.interact
|
||||
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
|
||||
local_ns[term] = getattr(self, term)
|
||||
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
||||
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
||||
shell(local_ns=local_ns, stack_depth=2)
|
||||
# End scene when exiting an embed.
|
||||
raise EndSceneEarlyException()
|
||||
@@ -176,6 +186,13 @@ class Scene(object):
|
||||
for mob in self.mobjects
|
||||
])
|
||||
|
||||
def has_time_based_updaters(self):
|
||||
return any([
|
||||
sm.has_time_based_updater()
|
||||
for mob in self.mobjects()
|
||||
for sm in mob.get_family()
|
||||
])
|
||||
|
||||
# Related to time
|
||||
def get_time(self):
|
||||
return self.time
|
||||
@@ -247,63 +264,70 @@ class Scene(object):
|
||||
def get_mobject_copies(self):
|
||||
return [m.copy() for m in self.mobjects]
|
||||
|
||||
def point_to_mobject(self, point, search_set=None, buff=0):
|
||||
"""
|
||||
E.g. if clicking on the scene, this returns the top layer mobject
|
||||
under a given point
|
||||
"""
|
||||
if search_set is None:
|
||||
search_set = self.mobjects
|
||||
for mobject in reversed(search_set):
|
||||
if mobject.is_point_touching(point, buff=buff):
|
||||
return mobject
|
||||
return None
|
||||
|
||||
# Related to skipping
|
||||
def update_skipping_status(self):
|
||||
if self.start_at_animation_number is not None:
|
||||
if self.num_plays == self.start_at_animation_number:
|
||||
self.stop_skipping()
|
||||
self.skip_time = self.time
|
||||
if not self.original_skipping_status:
|
||||
self.stop_skipping()
|
||||
if self.end_at_animation_number is not None:
|
||||
if self.num_plays >= self.end_at_animation_number:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
def stop_skipping(self):
|
||||
if self.skip_animations:
|
||||
self.skip_animations = False
|
||||
self.skip_time += self.time
|
||||
self.virtual_animation_start_time = self.time
|
||||
self.skip_animations = False
|
||||
|
||||
# Methods associated with running animations
|
||||
def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False):
|
||||
def get_time_progression(self, run_time, n_iterations=None, desc="", override_skip_animations=False):
|
||||
if self.skip_animations and not override_skip_animations:
|
||||
times = [run_time]
|
||||
return [run_time]
|
||||
else:
|
||||
step = 1 / self.camera.frame_rate
|
||||
times = np.arange(0, run_time, step)
|
||||
time_progression = ProgressDisplay(
|
||||
|
||||
if self.file_writer.has_progress_display:
|
||||
self.file_writer.set_progress_display_subdescription(desc)
|
||||
return times
|
||||
|
||||
return ProgressDisplay(
|
||||
times,
|
||||
total=n_iterations,
|
||||
leave=self.leave_progress_bars,
|
||||
ascii=True if platform.system() == 'Windows' else None
|
||||
ascii=True if platform.system() == 'Windows' else None,
|
||||
desc=desc,
|
||||
)
|
||||
return time_progression
|
||||
|
||||
def get_run_time(self, animations):
|
||||
return np.max([animation.run_time for animation in animations])
|
||||
|
||||
def get_animation_time_progression(self, animations):
|
||||
run_time = self.get_run_time(animations)
|
||||
time_progression = self.get_time_progression(run_time)
|
||||
time_progression.set_description("".join([
|
||||
f"Animation {self.num_plays}: {animations[0]}",
|
||||
", etc." if len(animations) > 1 else "",
|
||||
]))
|
||||
description = f"{self.num_plays} {animations[0]}"
|
||||
if len(animations) > 1:
|
||||
description += ", etc."
|
||||
time_progression = self.get_time_progression(run_time, desc=description)
|
||||
return time_progression
|
||||
|
||||
def get_wait_time_progression(self, duration, stop_condition):
|
||||
def get_wait_time_progression(self, duration, stop_condition=None):
|
||||
kw = {"desc": f"{self.num_plays} Waiting"}
|
||||
if stop_condition is not None:
|
||||
time_progression = self.get_time_progression(
|
||||
duration,
|
||||
n_iterations=-1, # So it doesn't show % progress
|
||||
override_skip_animations=True
|
||||
)
|
||||
time_progression.set_description(
|
||||
"Waiting for {}".format(stop_condition.__name__)
|
||||
)
|
||||
else:
|
||||
time_progression = self.get_time_progression(duration)
|
||||
time_progression.set_description(
|
||||
"Waiting {}".format(self.num_plays)
|
||||
)
|
||||
return time_progression
|
||||
kw["n_iterations"] = -1 # So it doesn't show % progress
|
||||
kw["override_skip_animations"] = True
|
||||
return self.get_time_progression(duration, **kw)
|
||||
|
||||
def anims_from_play_args(self, *args, **kwargs):
|
||||
"""
|
||||
@@ -443,10 +467,7 @@ class Scene(object):
|
||||
@handle_play_like_call
|
||||
def play(self, *args, **kwargs):
|
||||
if len(args) == 0:
|
||||
logging.log(
|
||||
logging.WARNING,
|
||||
"Called Scene.play with no animations"
|
||||
)
|
||||
log.warning("Called Scene.play with no animations")
|
||||
return
|
||||
animations = self.anims_from_play_args(*args, **kwargs)
|
||||
self.lock_static_mobject_data(*animations)
|
||||
@@ -458,27 +479,18 @@ class Scene(object):
|
||||
@handle_play_like_call
|
||||
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
if self.should_update_mobjects():
|
||||
self.lock_static_mobject_data()
|
||||
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||
last_t = 0
|
||||
for t in time_progression:
|
||||
dt = t - last_t
|
||||
last_t = t
|
||||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
if stop_condition is not None and stop_condition():
|
||||
time_progression.close()
|
||||
break
|
||||
self.unlock_mobject_data()
|
||||
elif self.skip_animations:
|
||||
# Do nothing
|
||||
return self
|
||||
else:
|
||||
self.update_frame(duration)
|
||||
n_frames = int(duration * self.camera.frame_rate)
|
||||
for n in range(n_frames):
|
||||
self.emit_frame()
|
||||
self.lock_static_mobject_data()
|
||||
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||
last_t = 0
|
||||
for t in time_progression:
|
||||
dt = t - last_t
|
||||
last_t = t
|
||||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
if stop_condition is not None and stop_condition():
|
||||
time_progression.close()
|
||||
break
|
||||
self.unlock_mobject_data()
|
||||
return self
|
||||
|
||||
def wait_until(self, stop_condition, max_time=60):
|
||||
@@ -586,7 +598,7 @@ class Scene(object):
|
||||
try:
|
||||
char = chr(symbol)
|
||||
except OverflowError:
|
||||
print(" Warning: The value of the pressed key is too large.")
|
||||
log.warning("The value of the pressed key is too large.")
|
||||
return
|
||||
|
||||
event_data = {"symbol": symbol, "modifiers": modifiers}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -12,6 +13,7 @@ from manimlib.utils.file_ops import guarantee_existence
|
||||
from manimlib.utils.file_ops import add_extension_if_not_present
|
||||
from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
from manimlib.utils.sounds import get_full_sound_file_path
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
class SceneFileWriter(object):
|
||||
@@ -34,12 +36,15 @@ class SceneFileWriter(object):
|
||||
"open_file_upon_completion": False,
|
||||
"show_file_location_upon_completion": False,
|
||||
"quiet": False,
|
||||
"total_frames": 0,
|
||||
"progress_description_len": 60,
|
||||
}
|
||||
|
||||
def __init__(self, scene, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.scene = scene
|
||||
self.writing_process = None
|
||||
self.has_progress_display = False
|
||||
self.init_output_directories()
|
||||
self.init_audio()
|
||||
|
||||
@@ -71,10 +76,14 @@ class SceneFileWriter(object):
|
||||
return path
|
||||
|
||||
def get_default_scene_name(self):
|
||||
if self.file_name is None:
|
||||
return self.scene.__class__.__name__
|
||||
else:
|
||||
return self.file_name
|
||||
name = str(self.scene)
|
||||
saan = self.scene.start_at_animation_number
|
||||
eaan = self.scene.end_at_animation_number
|
||||
if saan is not None:
|
||||
name += f"_{saan}"
|
||||
if eaan is not None:
|
||||
name += f"_{eaan}"
|
||||
return name
|
||||
|
||||
def get_resolution_directory(self):
|
||||
pixel_height = self.scene.camera.pixel_height
|
||||
@@ -183,7 +192,7 @@ class SceneFileWriter(object):
|
||||
'-s', f'{width}x{height}', # size of one frame
|
||||
'-pix_fmt', 'rgba',
|
||||
'-r', str(fps), # frames per second
|
||||
'-i', '-', # The imput comes from a pipe
|
||||
'-i', '-', # The input comes from a pipe
|
||||
'-vf', 'vflip',
|
||||
'-an', # Tells FFMPEG not to expect any audio
|
||||
'-loglevel', 'error',
|
||||
@@ -204,15 +213,39 @@ class SceneFileWriter(object):
|
||||
command += [self.temp_file_path]
|
||||
self.writing_process = sp.Popen(command, stdin=sp.PIPE)
|
||||
|
||||
if self.total_frames > 0:
|
||||
self.progress_display = ProgressDisplay(
|
||||
range(self.total_frames),
|
||||
# bar_format="{l_bar}{bar}|{n_fmt}/{total_fmt}",
|
||||
leave=False,
|
||||
ascii=True if platform.system() == 'Windows' else None,
|
||||
dynamic_ncols=True,
|
||||
)
|
||||
self.has_progress_display = True
|
||||
|
||||
def set_progress_display_subdescription(self, sub_desc):
|
||||
desc_len = self.progress_description_len
|
||||
file = os.path.split(self.get_movie_file_path())[1]
|
||||
full_desc = f"Rendering {file} ({sub_desc})"
|
||||
if len(full_desc) > desc_len:
|
||||
full_desc = full_desc[:desc_len - 4] + "...)"
|
||||
else:
|
||||
full_desc += " " * (desc_len - len(full_desc))
|
||||
self.progress_display.set_description(full_desc)
|
||||
|
||||
def write_frame(self, camera):
|
||||
if self.write_to_movie:
|
||||
raw_bytes = camera.get_raw_fbo_data()
|
||||
self.writing_process.stdin.write(raw_bytes)
|
||||
if self.has_progress_display:
|
||||
self.progress_display.update()
|
||||
|
||||
def close_movie_pipe(self):
|
||||
self.writing_process.stdin.close()
|
||||
self.writing_process.wait()
|
||||
self.writing_process.terminate()
|
||||
if self.has_progress_display:
|
||||
self.progress_display.close()
|
||||
shutil.move(self.temp_file_path, self.final_file_path)
|
||||
|
||||
def combine_movie_files(self):
|
||||
@@ -231,7 +264,7 @@ class SceneFileWriter(object):
|
||||
**kwargs
|
||||
)
|
||||
if len(partial_movie_files) == 0:
|
||||
print("No animations in this scene")
|
||||
log.warning("No animations in this scene")
|
||||
return
|
||||
|
||||
# Write a file partial_file_list.txt containing all
|
||||
@@ -300,7 +333,8 @@ class SceneFileWriter(object):
|
||||
self.print_file_ready_message(file_path)
|
||||
|
||||
def print_file_ready_message(self, file_path):
|
||||
print(f"\nFile ready at {file_path}\n")
|
||||
if not self.quiet:
|
||||
log.info(f"File ready at {file_path}")
|
||||
|
||||
def should_open_file(self):
|
||||
return any([
|
||||
|
||||
@@ -5,7 +5,6 @@ class ThreeDScene(Scene):
|
||||
CONFIG = {
|
||||
"camera_config": {
|
||||
"samples": 4,
|
||||
"anti_alias_width": 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
///// INSERT COLOR_MAP FUNCTION HERE /////
|
||||
|
||||
vec4 add_light(vec4 color,
|
||||
vec3 point,
|
||||
vec3 unit_normal,
|
||||
vec3 light_coords,
|
||||
float gloss,
|
||||
float shadow){
|
||||
///// INSERT COLOR FUNCTION HERE /////
|
||||
// The line above may be replaced by arbitrary code snippets, as per
|
||||
// the method Mobject.set_color_by_code
|
||||
if(gloss == 0.0 && shadow == 0.0) return color;
|
||||
|
||||
// TODO, do we actually want this? It effectively treats surfaces as two-sided
|
||||
if(unit_normal.z < 0){
|
||||
unit_normal *= -1;
|
||||
}
|
||||
|
||||
// TODO, read this in as a uniform?
|
||||
float camera_distance = 6;
|
||||
// Assume everything has already been rotated such that camera is in the z-direction
|
||||
vec3 to_camera = vec3(0, 0, camera_distance) - point;
|
||||
vec3 to_light = light_coords - point;
|
||||
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
|
||||
float dot_prod = dot(normalize(light_reflection), normalize(to_camera));
|
||||
float shine = gloss * exp(-3 * pow(1 - dot_prod, 2));
|
||||
float dp2 = dot(normalize(to_light), unit_normal);
|
||||
float darkening = mix(1, max(dp2, 0), shadow);
|
||||
return vec4(
|
||||
darkening * mix(color.rgb, vec3(1.0), shine),
|
||||
color.a
|
||||
);
|
||||
}
|
||||
|
||||
vec4 finalize_color(vec4 color,
|
||||
vec3 point,
|
||||
vec3 unit_normal,
|
||||
vec3 light_coords,
|
||||
float gloss,
|
||||
float shadow){
|
||||
// Put insertion here instead
|
||||
return add_light(color, point, unit_normal, light_coords, gloss, shadow);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
uniform vec2 frame_shape;
|
||||
uniform float anti_alias_width;
|
||||
uniform vec3 camera_center;
|
||||
uniform vec3 camera_offset;
|
||||
uniform mat3 camera_rotation;
|
||||
uniform float is_fixed_in_frame;
|
||||
uniform float focal_distance;
|
||||
15
manimlib/shaders/inserts/complex_functions.glsl
Normal file
15
manimlib/shaders/inserts/complex_functions.glsl
Normal file
@@ -0,0 +1,15 @@
|
||||
vec2 complex_mult(vec2 z, vec2 w){
|
||||
return vec2(z.x * w.x - z.y * w.y, z.x * w.y + z.y * w.x);
|
||||
}
|
||||
|
||||
vec2 complex_div(vec2 z, vec2 w){
|
||||
return complex_mult(z, vec2(w.x, -w.y)) / (w.x * w.x + w.y * w.y);
|
||||
}
|
||||
|
||||
vec2 complex_pow(vec2 z, int n){
|
||||
vec2 result = vec2(1.0, 0.0);
|
||||
for(int i = 0; i < n; i++){
|
||||
result = complex_mult(result, z);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -13,39 +13,56 @@ vec4 add_light(vec4 color,
|
||||
vec3 point,
|
||||
vec3 unit_normal,
|
||||
vec3 light_coords,
|
||||
vec3 cam_coords,
|
||||
float reflectiveness,
|
||||
float gloss,
|
||||
float shadow){
|
||||
if(gloss == 0.0 && shadow == 0.0) return color;
|
||||
if(reflectiveness == 0.0 && gloss == 0.0 && shadow == 0.0) return color;
|
||||
|
||||
float camera_distance = focal_distance;
|
||||
vec4 result = color;
|
||||
// Assume everything has already been rotated such that camera is in the z-direction
|
||||
vec3 to_camera = vec3(0, 0, camera_distance) - point;
|
||||
vec3 to_light = light_coords - point;
|
||||
// cam_coords = vec3(0, 0, focal_distance);
|
||||
vec3 to_camera = normalize(cam_coords - point);
|
||||
vec3 to_light = normalize(light_coords - point);
|
||||
|
||||
// TODO, do we actually want this? It effectively treats surfaces as two-sided
|
||||
if(dot(to_camera,unit_normal) < 0){
|
||||
unit_normal *= -1;
|
||||
}
|
||||
// Note, this effectively treats surfaces as two-sided
|
||||
// if(dot(to_camera, unit_normal) < 0) unit_normal *= -1;
|
||||
|
||||
float light_to_normal = dot(to_light, unit_normal);
|
||||
// When unit normal points towards light, brighten
|
||||
float bright_factor = max(light_to_normal, 0) * reflectiveness;
|
||||
// For glossy surface, add extra shine if light beam go towards camera
|
||||
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
|
||||
float dot_prod = dot(normalize(light_reflection), normalize(to_camera));
|
||||
float shine = gloss * exp(-3 * pow(1 - dot_prod, 2));
|
||||
float dp2 = dot(normalize(to_light), unit_normal);
|
||||
float darkening = mix(1, max(dp2, 0), shadow);
|
||||
return vec4(
|
||||
darkening * mix(color.rgb, vec3(1.0), shine),
|
||||
color.a
|
||||
);
|
||||
float light_to_cam = dot(light_reflection, to_camera);
|
||||
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));
|
||||
bright_factor += shine;
|
||||
|
||||
result.rgb = mix(result.rgb, vec3(1.0), bright_factor);
|
||||
if (light_to_normal < 0){
|
||||
// Darken
|
||||
result.rgb = mix(result.rgb, vec3(0.0), -light_to_normal * shadow);
|
||||
}
|
||||
// float darkening = mix(1, max(light_to_normal, 0), shadow);
|
||||
// return vec4(
|
||||
// darkening * mix(color.rgb, vec3(1.0), shine),
|
||||
// color.a
|
||||
// );
|
||||
return result;
|
||||
}
|
||||
|
||||
vec4 finalize_color(vec4 color,
|
||||
vec3 point,
|
||||
vec3 unit_normal,
|
||||
vec3 light_coords,
|
||||
vec3 cam_coords,
|
||||
float reflectiveness,
|
||||
float gloss,
|
||||
float shadow){
|
||||
///// INSERT COLOR FUNCTION HERE /////
|
||||
// The line above may be replaced by arbitrary code snippets, as per
|
||||
// the method Mobject.set_color_by_code
|
||||
return add_light(color, point, unit_normal, light_coords, gloss, shadow);
|
||||
return add_light(
|
||||
color, point, unit_normal, light_coords, cam_coords,
|
||||
reflectiveness, gloss, shadow
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Assumes the following uniforms exist in the surrounding context:
|
||||
// uniform vec3 camera_center;
|
||||
// uniform vec3 camera_offset;
|
||||
// uniform mat3 camera_rotation;
|
||||
|
||||
vec3 get_rotated_surface_unit_normal_vector(vec3 point, vec3 du_point, vec3 dv_point){
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Assumes the following uniforms exist in the surrounding context:
|
||||
// uniform float is_fixed_in_frame;
|
||||
// uniform vec3 camera_center;
|
||||
// uniform vec3 camera_offset;
|
||||
// uniform mat3 camera_rotation;
|
||||
|
||||
vec3 rotate_point_into_frame(vec3 point){
|
||||
@@ -15,5 +15,5 @@ vec3 position_point_into_frame(vec3 point){
|
||||
if(bool(is_fixed_in_frame)){
|
||||
return point;
|
||||
}
|
||||
return rotate_point_into_frame(point - camera_center);
|
||||
return rotate_point_into_frame(point - camera_offset);
|
||||
}
|
||||
|
||||
81
manimlib/shaders/mandelbrot_fractal/frag.glsl
Normal file
81
manimlib/shaders/mandelbrot_fractal/frag.glsl
Normal file
@@ -0,0 +1,81 @@
|
||||
#version 330
|
||||
|
||||
uniform vec3 light_source_position;
|
||||
uniform vec3 camera_position;
|
||||
uniform float reflectiveness;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
|
||||
uniform vec2 parameter;
|
||||
uniform float opacity;
|
||||
uniform float n_steps;
|
||||
uniform float mandelbrot;
|
||||
|
||||
uniform vec3 color0;
|
||||
uniform vec3 color1;
|
||||
uniform vec3 color2;
|
||||
uniform vec3 color3;
|
||||
uniform vec3 color4;
|
||||
uniform vec3 color5;
|
||||
uniform vec3 color6;
|
||||
uniform vec3 color7;
|
||||
uniform vec3 color8;
|
||||
|
||||
uniform vec2 frame_shape;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
#INSERT finalize_color.glsl
|
||||
#INSERT complex_functions.glsl
|
||||
|
||||
const int MAX_DEGREE = 5;
|
||||
|
||||
void main() {
|
||||
vec3 color_map[9] = vec3[9](
|
||||
color0, color1, color2, color3,
|
||||
color4, color5, color6, color7, color8
|
||||
);
|
||||
vec3 color;
|
||||
|
||||
vec2 z;
|
||||
vec2 c;
|
||||
|
||||
if(bool(mandelbrot)){
|
||||
c = xyz_coords.xy;
|
||||
z = vec2(0.0, 0.0);
|
||||
}else{
|
||||
c = parameter;
|
||||
z = xyz_coords.xy;
|
||||
}
|
||||
|
||||
float outer_bound = 2.0;
|
||||
bool stable = true;
|
||||
for(int n = 0; n < int(n_steps); n++){
|
||||
z = complex_mult(z, z) + c;
|
||||
if(length(z) > outer_bound){
|
||||
float float_n = float(n);
|
||||
float_n += log(outer_bound) / log(length(z));
|
||||
float_n += 0.5 * length(c);
|
||||
color = float_to_color(sqrt(float_n), 1.5, 8.0, color_map);
|
||||
stable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(stable){
|
||||
color = vec3(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
frag_color = finalize_color(
|
||||
vec4(color, opacity),
|
||||
xyz_coords,
|
||||
vec3(0.0, 0.0, 1.0),
|
||||
light_source_position,
|
||||
camera_position,
|
||||
reflectiveness,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
}
|
||||
17
manimlib/shaders/mandelbrot_fractal/vert.glsl
Normal file
17
manimlib/shaders/mandelbrot_fractal/vert.glsl
Normal file
@@ -0,0 +1,17 @@
|
||||
#version 330
|
||||
|
||||
#INSERT camera_uniform_declarations.glsl
|
||||
|
||||
in vec3 point;
|
||||
out vec3 xyz_coords;
|
||||
|
||||
uniform float scale_factor;
|
||||
uniform vec3 offset;
|
||||
|
||||
#INSERT position_point_into_frame.glsl
|
||||
#INSERT get_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
xyz_coords = (point - offset) / scale_factor;
|
||||
gl_Position = get_gl_Position(position_point_into_frame(point));
|
||||
}
|
||||
161
manimlib/shaders/newton_fractal/frag.glsl
Normal file
161
manimlib/shaders/newton_fractal/frag.glsl
Normal file
@@ -0,0 +1,161 @@
|
||||
#version 330
|
||||
|
||||
uniform vec3 light_source_position;
|
||||
uniform vec3 camera_position;
|
||||
uniform float reflectiveness;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
|
||||
uniform vec4 color0;
|
||||
uniform vec4 color1;
|
||||
uniform vec4 color2;
|
||||
uniform vec4 color3;
|
||||
uniform vec4 color4;
|
||||
|
||||
uniform vec2 coef0;
|
||||
uniform vec2 coef1;
|
||||
uniform vec2 coef2;
|
||||
uniform vec2 coef3;
|
||||
uniform vec2 coef4;
|
||||
uniform vec2 coef5;
|
||||
|
||||
uniform vec2 root0;
|
||||
uniform vec2 root1;
|
||||
uniform vec2 root2;
|
||||
uniform vec2 root3;
|
||||
uniform vec2 root4;
|
||||
|
||||
uniform float n_roots;
|
||||
uniform float n_steps;
|
||||
uniform float julia_highlight;
|
||||
uniform float saturation_factor;
|
||||
uniform float black_for_cycles;
|
||||
uniform float is_parameter_space;
|
||||
|
||||
uniform vec2 frame_shape;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
#INSERT finalize_color.glsl
|
||||
#INSERT complex_functions.glsl
|
||||
|
||||
const int MAX_DEGREE = 5;
|
||||
const float CLOSE_ENOUGH = 1e-3;
|
||||
|
||||
|
||||
vec2 poly(vec2 z, vec2[MAX_DEGREE + 1] coefs){
|
||||
vec2 result = vec2(0.0);
|
||||
for(int n = 0; n < int(n_roots) + 1; n++){
|
||||
result += complex_mult(coefs[n], complex_pow(z, n));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
vec2 dpoly(vec2 z, vec2[MAX_DEGREE + 1] coefs){
|
||||
vec2 result = vec2(0.0);
|
||||
for(int n = 1; n < int(n_roots) + 1; n++){
|
||||
result += n * complex_mult(coefs[n], complex_pow(z, n - 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
vec2 seek_root(vec2 z, vec2[MAX_DEGREE + 1] coefs, int max_steps, out float n_iters){
|
||||
float last_len;
|
||||
float curr_len;
|
||||
float threshold = CLOSE_ENOUGH;
|
||||
|
||||
for(int i = 0; i < max_steps; i++){
|
||||
last_len = curr_len;
|
||||
n_iters = float(i);
|
||||
vec2 step = complex_div(poly(z, coefs), dpoly(z, coefs));
|
||||
curr_len = length(step);
|
||||
if(curr_len < threshold){
|
||||
break;
|
||||
}
|
||||
z = z - step;
|
||||
}
|
||||
n_iters -= log(curr_len) / log(threshold);
|
||||
|
||||
return z;
|
||||
}
|
||||
|
||||
|
||||
void main() {
|
||||
vec2[MAX_DEGREE + 1] coefs = vec2[MAX_DEGREE + 1](coef0, coef1, coef2, coef3, coef4, coef5);
|
||||
vec2[MAX_DEGREE] roots = vec2[MAX_DEGREE](root0, root1, root2, root3, root4);
|
||||
vec4[MAX_DEGREE] colors = vec4[MAX_DEGREE](color0, color1, color2, color3, color4);
|
||||
|
||||
vec2 z = xyz_coords.xy;
|
||||
|
||||
if(is_parameter_space > 0){
|
||||
// In this case, pixel should correspond to one of the roots
|
||||
roots[2] = xyz_coords.xy;
|
||||
vec2 r0 = roots[0];
|
||||
vec2 r1 = roots[1];
|
||||
vec2 r2 = roots[2];
|
||||
|
||||
// It is assumed that the polynomial is cubid...
|
||||
coefs[0] = -complex_mult(complex_mult(r0, r1), r2);
|
||||
coefs[1] = complex_mult(r0, r1) + complex_mult(r0, r2) + complex_mult(r1, r2);
|
||||
coefs[2] = -(r0 + r1 + r2);
|
||||
coefs[3] = vec2(1.0, 0.0);
|
||||
|
||||
// Seed value is always center of the roots
|
||||
z = -coefs[2] / 3.0;
|
||||
}
|
||||
|
||||
float n_iters;
|
||||
vec2 found_root = seek_root(z, coefs, int(n_steps), n_iters);
|
||||
|
||||
vec4 color = vec4(0.0);
|
||||
float min_dist = 1e10;
|
||||
float dist;
|
||||
for(int i = 0; i < int(n_roots); i++){
|
||||
dist = distance(roots[i], found_root);
|
||||
if(dist < min_dist){
|
||||
min_dist = dist;
|
||||
color = colors[i];
|
||||
}
|
||||
}
|
||||
color *= 1.0 + (0.01 * saturation_factor) * (n_iters - 2 * saturation_factor);
|
||||
|
||||
if(black_for_cycles > 0 && min_dist > CLOSE_ENOUGH){
|
||||
color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
if(julia_highlight > 0.0){
|
||||
float radius = julia_highlight;
|
||||
vec2[4] samples = vec2[4](
|
||||
z + vec2(radius, 0.0),
|
||||
z + vec2(-radius, 0.0),
|
||||
z + vec2(0.0, radius),
|
||||
z + vec2(0.0, -radius)
|
||||
);
|
||||
for(int i = 0; i < 4; i++){
|
||||
for(int j = 0; j < n_steps; j++){
|
||||
vec2 z = samples[i];
|
||||
z = z - complex_div(poly(z, coefs), dpoly(z, coefs));
|
||||
samples[i] = z;
|
||||
}
|
||||
}
|
||||
float max_dist = 0.0;
|
||||
for(int i = 0; i < 4; i++){
|
||||
max_dist = max(max_dist, distance(samples[i], samples[(i + 1) % 4]));
|
||||
}
|
||||
color *= 1.0 * smoothstep(0, 0.1, max_dist);
|
||||
}
|
||||
|
||||
frag_color = finalize_color(
|
||||
color,
|
||||
xyz_coords,
|
||||
vec3(0.0, 0.0, 1.0),
|
||||
light_source_position,
|
||||
camera_position,
|
||||
reflectiveness,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
}
|
||||
17
manimlib/shaders/newton_fractal/vert.glsl
Normal file
17
manimlib/shaders/newton_fractal/vert.glsl
Normal file
@@ -0,0 +1,17 @@
|
||||
#version 330
|
||||
|
||||
#INSERT camera_uniform_declarations.glsl
|
||||
|
||||
in vec3 point;
|
||||
out vec3 xyz_coords;
|
||||
|
||||
uniform float scale_factor;
|
||||
uniform vec3 offset;
|
||||
|
||||
#INSERT position_point_into_frame.glsl
|
||||
#INSERT get_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
xyz_coords = (point - offset) / scale_factor;
|
||||
gl_Position = get_gl_Position(position_point_into_frame(point));
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
#INSERT camera_uniform_declarations.glsl
|
||||
|
||||
in vec4 color;
|
||||
in float fill_all; // Either 0 or 1e
|
||||
in float fill_all; // Either 0 or 1
|
||||
in float uv_anti_alias_width;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
|
||||
@@ -11,6 +11,8 @@ uniform float focal_distance;
|
||||
uniform float is_fixed_in_frame;
|
||||
// Needed for finalize_color
|
||||
uniform vec3 light_source_position;
|
||||
uniform vec3 camera_position;
|
||||
uniform float reflectiveness;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
|
||||
@@ -44,6 +46,8 @@ void emit_vertex_wrapper(vec3 point, int index){
|
||||
point,
|
||||
v_global_unit_normal[index],
|
||||
light_source_position,
|
||||
camera_position,
|
||||
reflectiveness,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
|
||||
@@ -13,7 +13,9 @@ uniform float flat_stroke;
|
||||
|
||||
//Needed for lighting
|
||||
uniform vec3 light_source_position;
|
||||
uniform vec3 camera_position;
|
||||
uniform float joint_type;
|
||||
uniform float reflectiveness;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
|
||||
@@ -259,6 +261,8 @@ void main() {
|
||||
xyz_coords,
|
||||
v_global_unit_normal[index_map[i]],
|
||||
light_source_position,
|
||||
camera_position,
|
||||
reflectiveness,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#version 330
|
||||
|
||||
uniform vec3 light_source_position;
|
||||
uniform vec3 camera_position;
|
||||
uniform float reflectiveness;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
@@ -19,6 +21,8 @@ void main() {
|
||||
xyz_coords,
|
||||
normalize(v_normal),
|
||||
light_source_position,
|
||||
camera_position,
|
||||
reflectiveness,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ uniform sampler2D LightTexture;
|
||||
uniform sampler2D DarkTexture;
|
||||
uniform float num_textures;
|
||||
uniform vec3 light_source_position;
|
||||
uniform vec3 camera_position;
|
||||
uniform float reflectiveness;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
@@ -36,6 +38,8 @@ void main() {
|
||||
xyz_coords,
|
||||
normalize(v_normal),
|
||||
light_source_position,
|
||||
camera_position,
|
||||
reflectiveness,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#version 330
|
||||
|
||||
uniform vec3 light_source_position;
|
||||
uniform vec3 camera_position;
|
||||
uniform float reflectiveness;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float anti_alias_width;
|
||||
uniform float focal_distance;
|
||||
uniform float glow_factor;
|
||||
|
||||
in vec4 color;
|
||||
in float radius;
|
||||
@@ -22,14 +25,23 @@ void main() {
|
||||
if (signed_dist > 0.5 * anti_alias_width){
|
||||
discard;
|
||||
}
|
||||
vec3 normal = vec3(diff / radius, sqrt(1 - (dist * dist) / (radius * radius)));
|
||||
frag_color = finalize_color(
|
||||
color,
|
||||
vec3(point.xy, 0.0),
|
||||
normal,
|
||||
light_source_position,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
frag_color = color;
|
||||
if(gloss > 0 || shadow > 0){
|
||||
vec3 normal = vec3(diff / radius, sqrt(1 - (dist * dist) / (radius * radius)));
|
||||
frag_color = finalize_color(
|
||||
frag_color,
|
||||
vec3(point.xy, 0.0),
|
||||
normal,
|
||||
light_source_position,
|
||||
camera_position,
|
||||
reflectiveness,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
}
|
||||
if(glow_factor > 0){
|
||||
frag_color.a *= pow(1 - dist / radius, glow_factor);
|
||||
}
|
||||
|
||||
frag_color.a *= smoothstep(0.5, -0.5, signed_dist / anti_alias_width);
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import numpy as np
|
||||
from manimlib.utils.simple_functions import choose
|
||||
from manimlib.utils.space_ops import find_intersection
|
||||
from manimlib.utils.space_ops import cross2d
|
||||
from manimlib.utils.space_ops import midpoint
|
||||
from manimlib.logger import log
|
||||
|
||||
CLOSED_THRESHOLD = 0.001
|
||||
|
||||
@@ -67,9 +69,9 @@ def interpolate(start, end, alpha):
|
||||
try:
|
||||
return (1 - alpha) * start + alpha * end
|
||||
except TypeError:
|
||||
print(type(start), start.dtype)
|
||||
print(type(end), start.dtype)
|
||||
print(alpha)
|
||||
log.debug(f"`start` parameter with type `{type(start)}` and dtype `{start.dtype}`")
|
||||
log.debug(f"`end` parameter with type `{type(end)}` and dtype `{end.dtype}`")
|
||||
log.debug(f"`alpha` parameter with value `{alpha}`")
|
||||
import sys
|
||||
sys.exit(2)
|
||||
|
||||
@@ -130,6 +132,8 @@ def get_smooth_quadratic_bezier_handle_points(points):
|
||||
another that would produce a parabola passing through P0, call it smooth_to_left,
|
||||
and use the midpoint between the two.
|
||||
"""
|
||||
if len(points) == 2:
|
||||
return midpoint(*points)
|
||||
smooth_to_right, smooth_to_left = [
|
||||
0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:]
|
||||
for ps in (points, points[::-1])
|
||||
@@ -157,7 +161,7 @@ def get_smooth_cubic_bezier_handle_points(points):
|
||||
l, u = 2, 1
|
||||
# diag is a representation of the matrix in diagonal form
|
||||
# See https://www.particleincell.com/2012/bezier-splines/
|
||||
# for how to arive at these equations
|
||||
# for how to arrive at these equations
|
||||
diag = np.zeros((l + u + 1, 2 * num_handles))
|
||||
diag[0, 1::2] = -1
|
||||
diag[0, 2::2] = 1
|
||||
|
||||
@@ -3,11 +3,12 @@ import time
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.mobject.numbers import Integer
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
def print_family(mobject, n_tabs=0):
|
||||
"""For debugging purposes"""
|
||||
print("\t" * n_tabs, mobject, id(mobject))
|
||||
log.debug("\t" * n_tabs + str(mobject) + " " + str(id(mobject)))
|
||||
for submob in mobject.submobjects:
|
||||
print_family(submob, n_tabs + 1)
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ def init_customization():
|
||||
}
|
||||
}
|
||||
|
||||
print("Initialize configuration")
|
||||
scope = input(" Please select the scope of the configuration [global/local]: ")
|
||||
if scope == "global":
|
||||
from manimlib.config import get_manim_dir
|
||||
|
||||
@@ -5,7 +5,7 @@ import numpy as np
|
||||
def remove_list_redundancies(l):
|
||||
"""
|
||||
Used instead of list(set(l)) to maintain order
|
||||
Keeps the last occurance of each element
|
||||
Keeps the last occurrence of each element
|
||||
"""
|
||||
reversed_result = []
|
||||
used = set()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
import operator as op
|
||||
from functools import reduce
|
||||
import math
|
||||
from mapbox_earcut import triangulate_float32 as earcut
|
||||
|
||||
@@ -9,10 +10,19 @@ from manimlib.constants import OUT
|
||||
from manimlib.constants import PI
|
||||
from manimlib.constants import TAU
|
||||
from manimlib.utils.iterables import adjacent_pairs
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
|
||||
def cross(v1, v2):
|
||||
return [
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0]
|
||||
]
|
||||
|
||||
|
||||
def get_norm(vect):
|
||||
return sum([x**2 for x in vect])**0.5
|
||||
return sum((x**2 for x in vect))**0.5
|
||||
|
||||
|
||||
# Quaternions
|
||||
@@ -146,6 +156,15 @@ def z_to_vector(vector):
|
||||
return rotation_matrix(angle, axis=axis)
|
||||
|
||||
|
||||
def rotation_between_vectors(v1, v2):
|
||||
if np.all(np.isclose(v1, v2)):
|
||||
return np.identity(3)
|
||||
return rotation_matrix(
|
||||
angle=angle_between_vectors(v1, v2),
|
||||
axis=normalize(np.cross(v1, v2))
|
||||
)
|
||||
|
||||
|
||||
def angle_of_vector(vector):
|
||||
"""
|
||||
Returns polar coordinate theta when vector is project on xy plane
|
||||
@@ -158,8 +177,7 @@ def angle_between_vectors(v1, v2):
|
||||
Returns the angle between two 3D vectors.
|
||||
This angle will always be btw 0 and pi
|
||||
"""
|
||||
diff = (angle_of_vector(v2) - angle_of_vector(v1)) % TAU
|
||||
return min(diff, TAU - diff)
|
||||
return math.acos(clip(np.dot(normalize(v1), normalize(v2)), -1, 1))
|
||||
|
||||
|
||||
def project_along_vector(point, vector):
|
||||
@@ -185,14 +203,6 @@ def normalize_along_axis(array, axis, fall_back=None):
|
||||
return array
|
||||
|
||||
|
||||
def cross(v1, v2):
|
||||
return np.array([
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0]
|
||||
])
|
||||
|
||||
|
||||
def get_unit_normal(v1, v2, tol=1e-6):
|
||||
v1 = normalize(v1)
|
||||
v2 = normalize(v2)
|
||||
@@ -376,8 +386,8 @@ def earclip_triangulation(verts, ring_ends):
|
||||
|
||||
# Points at the same position may cause problems
|
||||
for i in rings:
|
||||
verts[i[0]] += (verts[i[1]]-verts[i[0]]) * 1e-6
|
||||
verts[i[-1]] += (verts[i[-2]]-verts[i[-1]]) * 1e-6
|
||||
verts[i[0]] += (verts[i[1]] - verts[i[0]]) * 1e-6
|
||||
verts[i[-1]] += (verts[i[-2]] - verts[i[-1]]) * 1e-6
|
||||
|
||||
# First, we should know which rings are directly contained in it for each ring
|
||||
|
||||
@@ -393,9 +403,11 @@ def earclip_triangulation(verts, ring_ends):
|
||||
|
||||
def is_in_fast(ring_a, ring_b):
|
||||
# Whether a is in b
|
||||
return (left[ring_b] <= left[ring_a] <= right[ring_a] <= right[ring_b] and
|
||||
bottom[ring_b] <= bottom[ring_a] <= top[ring_a] <= top[ring_b] and
|
||||
is_in(verts[rings[ring_a][0]], ring_b))
|
||||
return reduce(op.and_, (
|
||||
left[ring_b] <= left[ring_a] <= right[ring_a] <= right[ring_b],
|
||||
bottom[ring_b] <= bottom[ring_a] <= top[ring_a] <= top[ring_b],
|
||||
is_in(verts[rings[ring_a][0]], ring_b)
|
||||
))
|
||||
|
||||
chilren = [[] for i in rings]
|
||||
for idx, i in enumerate(rings_sorted):
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
@@ -7,6 +6,7 @@ from contextlib import contextmanager
|
||||
from manimlib.utils.directories import get_tex_dir
|
||||
from manimlib.config import get_manim_dir
|
||||
from manimlib.config import get_custom_config
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
SAVED_TEX_CONFIG = {}
|
||||
@@ -87,15 +87,11 @@ def tex_to_dvi(tex_file):
|
||||
exit_code = os.system(" ".join(commands))
|
||||
if exit_code != 0:
|
||||
log_file = tex_file.replace(".tex", ".log")
|
||||
logging.log(
|
||||
logging.ERROR,
|
||||
"\n\n LaTeX Error! Not a worry, it happens to the best of us.\n"
|
||||
)
|
||||
log.error("LaTeX Error! Not a worry, it happens to the best of us.")
|
||||
with open(log_file, "r") as file:
|
||||
for line in file.readlines():
|
||||
if line.startswith("!"):
|
||||
print(line[1:])
|
||||
logging.log(logging.INFO, line)
|
||||
log.debug(f"The error could be: `{line[2:-1]}`")
|
||||
sys.exit(2)
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import numpy as np
|
||||
import moderngl_window as mglw
|
||||
from moderngl_window.context.pyglet.window import Window as PygletWindow
|
||||
from moderngl_window.timers.clock import Timer
|
||||
@@ -48,7 +49,7 @@ class Window(PygletWindow):
|
||||
return tuple(map(int, custom_position.split(",")))
|
||||
|
||||
# Alternatively, it might be specified with a string like
|
||||
# UR, OO, DL, etc. specifiying what corner it should go to
|
||||
# UR, OO, DL, etc. specifying what corner it should go to
|
||||
char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2}
|
||||
width_diff = monitor.width - window_width
|
||||
height_diff = monitor.height - window_height
|
||||
@@ -59,7 +60,17 @@ class Window(PygletWindow):
|
||||
|
||||
# Delegate event handling to scene
|
||||
def pixel_coords_to_space_coords(self, px, py, relative=False):
|
||||
return self.scene.camera.pixel_coords_to_space_coords(px, py, relative)
|
||||
pw, ph = self.size
|
||||
fw, fh = self.scene.camera.get_frame_shape()
|
||||
fc = self.scene.camera.get_frame_center()
|
||||
if relative:
|
||||
return np.array([px / pw, py / ph, 0])
|
||||
else:
|
||||
return np.array([
|
||||
fc[0] + px * fw / pw - fw / 2,
|
||||
fc[1] + py * fh / ph - fh / 2,
|
||||
0
|
||||
])
|
||||
|
||||
def on_mouse_motion(self, x, y, dx, dy):
|
||||
super().on_mouse_motion(x, y, dx, dy)
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["setuptools", "wheel"]
|
||||
@@ -9,8 +9,11 @@ mapbox-earcut
|
||||
matplotlib
|
||||
moderngl
|
||||
moderngl_window
|
||||
skia-pathops
|
||||
pydub
|
||||
pygments
|
||||
pyyaml
|
||||
rich
|
||||
screeninfo
|
||||
pyreadline; sys_platform == 'win32'
|
||||
validators
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = manimgl
|
||||
version = 1.1.0
|
||||
version = 1.3.0
|
||||
author = Grant Sanderson
|
||||
author_email= grant@3blue1brown.com
|
||||
description = Animation engine for explanatory math videos
|
||||
@@ -28,8 +28,11 @@ install_requires =
|
||||
matplotlib
|
||||
moderngl
|
||||
moderngl_window
|
||||
skia-pathops
|
||||
pydub
|
||||
pygments
|
||||
pyyaml
|
||||
rich
|
||||
screeninfo
|
||||
pyreadline; sys_platform == 'win32'
|
||||
validators
|
||||
|
||||
Reference in New Issue
Block a user