mirror of
https://github.com/3b1b/manim.git
synced 2026-01-13 00:18:05 -05:00
Compare commits
238 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4356c42e00 | ||
|
|
aea79be6cc | ||
|
|
8ef42fae24 | ||
|
|
6be6bd3075 | ||
|
|
a33eac7aa8 | ||
|
|
9d6a28bc29 | ||
|
|
06405d5758 | ||
|
|
46e356e791 | ||
|
|
97ca42d454 | ||
|
|
a4eee6f44c | ||
|
|
8cac16b452 | ||
|
|
719cd8cde3 | ||
|
|
0bb9216c14 | ||
|
|
6f9df8db26 | ||
|
|
3756605a45 | ||
|
|
0cab23b2ba | ||
|
|
aef02bfcf9 | ||
|
|
9d04e287d7 | ||
|
|
97c0f4857b | ||
|
|
7f9b0a7eac | ||
|
|
133724d29a | ||
|
|
559b96e7ce | ||
|
|
f29ef87bba | ||
|
|
fc1e916f42 | ||
|
|
b3b7d214ad | ||
|
|
602809758e | ||
|
|
f9351536e4 | ||
|
|
67f5b10626 | ||
|
|
baba6929df | ||
|
|
d6b20a7306 | ||
|
|
4c3ba7f674 | ||
|
|
3883f57bf8 | ||
|
|
d2e0811285 | ||
|
|
1e2a6ffb8a | ||
|
|
56e5696163 | ||
|
|
1ec00629a5 | ||
|
|
aa6335cd90 | ||
|
|
7093f7d02d | ||
|
|
fad9ed2df7 | ||
|
|
725155409b | ||
|
|
a6675eb043 | ||
|
|
5d2dcec307 | ||
|
|
f60dc7cd07 | ||
|
|
6c39cac62b | ||
|
|
2bd25a55fa | ||
|
|
0e4edfdd79 | ||
|
|
277256a407 | ||
|
|
831b7d455c | ||
|
|
1d14a23af9 | ||
|
|
dffa70ea15 | ||
|
|
31976063df | ||
|
|
aa135280ac | ||
|
|
f0160822ba | ||
|
|
48e07d1817 | ||
|
|
3ef5899a24 | ||
|
|
f895455264 | ||
|
|
3baa14103e | ||
|
|
c315300ff1 | ||
|
|
3b17d6d0eb | ||
|
|
8a29de5ef0 | ||
|
|
ecb729850a | ||
|
|
a770291053 | ||
|
|
27c666fab5 | ||
|
|
942a7e71b8 | ||
|
|
ebb75d1235 | ||
|
|
9af23415a2 | ||
|
|
19778e405a | ||
|
|
833e40c2d4 | ||
|
|
9df53b8a18 | ||
|
|
ff86b0e378 | ||
|
|
92adcd75d4 | ||
|
|
240f5020b4 | ||
|
|
e8205a5049 | ||
|
|
6c8dd14adc | ||
|
|
07f84e2676 | ||
|
|
8db1164ece | ||
|
|
790bf0a104 | ||
|
|
8205edcc4c | ||
|
|
05b3c9852e | ||
|
|
925f2e123f | ||
|
|
565763a2ff | ||
|
|
6a74c241b8 | ||
|
|
416cc8e6d5 | ||
|
|
d694aed452 | ||
|
|
11379283aa | ||
|
|
dd13559b11 | ||
|
|
1658438fef | ||
|
|
f4eb2724c5 | ||
|
|
33f720c73a | ||
|
|
bbb4fa155c | ||
|
|
2318c9e716 | ||
|
|
e80dd243f1 | ||
|
|
3ffe300f96 | ||
|
|
24e3caa072 | ||
|
|
9efd02c500 | ||
|
|
0a318486c5 | ||
|
|
919133c6bf | ||
|
|
066a2ed5dc | ||
|
|
09ced7ce9a | ||
|
|
505b229117 | ||
|
|
5aa8d15d85 | ||
|
|
7aa05572ab | ||
|
|
f1996f8479 | ||
|
|
37b63ca956 | ||
|
|
84fd657d9b | ||
|
|
b489490f41 | ||
|
|
bbf45f95c6 | ||
|
|
b61f1473a5 | ||
|
|
e3d5b49a55 | ||
|
|
4d6a0db1e1 | ||
|
|
0af46e149d | ||
|
|
896b011d76 | ||
|
|
3adaf8e325 | ||
|
|
8762177df5 | ||
|
|
a1d51474ea | ||
|
|
83841ae415 | ||
|
|
b81f244c3c | ||
|
|
7023548ec6 | ||
|
|
758f329a06 | ||
|
|
8f1dfabff0 | ||
|
|
7fa01d5de8 | ||
|
|
0de303d5e0 | ||
|
|
155839bde9 | ||
|
|
3a1e5e1bcf | ||
|
|
264f7b1172 | ||
|
|
85e90a1488 | ||
|
|
f8e6e7df3c | ||
|
|
5dd7cce67f | ||
|
|
f21a4a4696 | ||
|
|
98b0d266d2 | ||
|
|
6821a7c20e | ||
|
|
00f72da493 | ||
|
|
744916507c | ||
|
|
88d863c1d7 | ||
|
|
d7dcc9d76f | ||
|
|
4631508b7d | ||
|
|
8803088121 | ||
|
|
1d466cb299 | ||
|
|
5a1f00b1cb | ||
|
|
17d31045b2 | ||
|
|
950466c1da | ||
|
|
62151e52f1 | ||
|
|
b4ce0b910c | ||
|
|
9dd1f47dab | ||
|
|
49743daf32 | ||
|
|
ba23fbe71e | ||
|
|
ee1594a3cb | ||
|
|
e9afb0ee33 | ||
|
|
8b1715379d | ||
|
|
2501fac32f | ||
|
|
1aec0462ec | ||
|
|
83c70a59d8 | ||
|
|
9b8a6e7ff8 | ||
|
|
758f2ec236 | ||
|
|
d9cac38618 | ||
|
|
e8ebfa312b | ||
|
|
dae24891fa | ||
|
|
a4f9de1ca1 | ||
|
|
697028cd4c | ||
|
|
c84acc0023 | ||
|
|
b1d869cd11 | ||
|
|
13a5f6d6ff | ||
|
|
e3f87d835b | ||
|
|
7ffab788b7 | ||
|
|
bcd09906be | ||
|
|
407c53f97c | ||
|
|
eea3c6b294 | ||
|
|
d2182b9112 | ||
|
|
fbc329d7ce | ||
|
|
25045143a1 | ||
|
|
e899604a2d | ||
|
|
0b898a5594 | ||
|
|
ee2f68cd49 | ||
|
|
2cce4ccdd7 | ||
|
|
f3ecebee43 | ||
|
|
e764da3c3a | ||
|
|
fbbea47d11 | ||
|
|
781a9934fd | ||
|
|
a7173142bf | ||
|
|
0e78027186 | ||
|
|
82bd02d21f | ||
|
|
d065e1973d | ||
|
|
7070777408 | ||
|
|
5c2a9f2129 | ||
|
|
1b695e1c19 | ||
|
|
da1cc44d90 | ||
|
|
3bbb759112 | ||
|
|
41c6cbcb59 | ||
|
|
5930e6a176 | ||
|
|
8f3ff91165 | ||
|
|
b12677bc1a | ||
|
|
cdec64e3f1 | ||
|
|
2dc8bc9b9c | ||
|
|
94f0bf557a | ||
|
|
e20690b7c1 | ||
|
|
2c7689ed9e | ||
|
|
c73d507c76 | ||
|
|
317a5d6226 | ||
|
|
4339f97c56 | ||
|
|
81c3ae3037 | ||
|
|
61b04079f5 | ||
|
|
5a0e5a16ea | ||
|
|
f0b5181694 | ||
|
|
185782a2e7 | ||
|
|
8ab95ebe9d | ||
|
|
77159eea2e | ||
|
|
6766e459f2 | ||
|
|
01f4ef3e5d | ||
|
|
b531c82bc4 | ||
|
|
5d942d5ac0 | ||
|
|
b285ca7c22 | ||
|
|
82540edae9 | ||
|
|
f9a6fa7036 | ||
|
|
4eabaecfc8 | ||
|
|
b881e55fca | ||
|
|
f1c50640a3 | ||
|
|
deb1311e48 | ||
|
|
82fa6ab125 | ||
|
|
4d91ff3f2f | ||
|
|
b6f9da87d0 | ||
|
|
c60e97ebf9 | ||
|
|
b1ed16e81a | ||
|
|
c94ebaa260 | ||
|
|
030fb52018 | ||
|
|
487f582302 | ||
|
|
6d0c55d2ba | ||
|
|
c82f60e29e | ||
|
|
c03279d626 | ||
|
|
7b72fa8ca1 | ||
|
|
8b454fbe93 | ||
|
|
f77e25ff86 | ||
|
|
872ef67cf7 | ||
|
|
305ca72ebe | ||
|
|
4d81d3678b | ||
|
|
55e968e174 | ||
|
|
97d1609849 | ||
|
|
e10f850d0d | ||
|
|
b8584fe5ab |
13
.github/workflows/publish.yml
vendored
13
.github/workflows/publish.yml
vendored
@@ -8,6 +8,11 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ["py36", "py37", "py38", "py39", "py310"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -20,11 +25,13 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine build
|
||||
|
||||
- name: Build and publish
|
||||
|
||||
- name: Build wheels
|
||||
run: python setup.py bdist_wheel --python-tag ${{ matrix.python }}
|
||||
|
||||
- name: Upload wheels
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python -m build
|
||||
twine upload dist/*
|
||||
@@ -1,63 +1,210 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
v1.5.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Bug fix for the case of calling ``Write`` on a null object (`#1740 <https://github.com/3b1b/manim/pull/1740>`__)
|
||||
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
- Added ``TransformMatchingMTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
|
||||
- Added ``ImplicitFunction`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
|
||||
- Added ``Polyline`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
- Allowed ``Mobject.set_points`` to take in an empty list, and added ``Mobject.add_point`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/a64259158538eae6043566aaf3d3329ff4ac394b>`__)
|
||||
- Added ``Scene.refresh_locked_data`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/33d2894c167c577a15fdadbaf26488ff1f5bff87>`__)
|
||||
- Added presenter mode to scenes with ``-p`` option (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9a9cc8bdacb7541b7cd4a52ad705abc21f3e27fe>`__ and `#1742 <https://github.com/3b1b/manim/pull/1742>`__)
|
||||
- Allowed for an embed by hitting ``ctrl+shift+e`` during interaction (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9df12fcb7d8360e51cd7021d6877ca1a5c31835e>`__ and `#1746 <https://github.com/3b1b/manim/pull/1746>`__)
|
||||
- Added ``Mobject.set_min_width/height/depth`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2798d15591a0375ae6bb9135473e6f5328267323>`__)
|
||||
- Allowed ``Mobject.match_coord/x/y/z`` to take in a point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/29a4d3e82ba94c007c996b2d1d0f923941452698>`__)
|
||||
- Added ``text_config`` to ``DecimalNumber`` (`#1744 <https://github.com/3b1b/manim/pull/1744>`__)
|
||||
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Refactored ``MTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
|
||||
- Refactored ``SVGMobject`` with svgelements (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
- Made sure ``ParametricCurve`` has at least one point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2488b9e866fb1ecb842a27dd9f4956ec167e3dee>`__)
|
||||
- Set default to no tips on ``Axes`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/6c6d387a210756c38feca7d34838aa9ac99bb58a>`__)
|
||||
- Stopped displaying when writing tex string is happening (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/58e06e8f6b7c5059ff315d51fd0018fec5cfbb05>`__)
|
||||
- Reorganize inheriting order and refactor SVGMobject (`#1745 <https://github.com/3b1b/manim/pull/1745>`__)
|
||||
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
- Added dependency on ``isosurfaces`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
|
||||
- Removed dependency on ``argparse`` since it's a built-in module (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
|
||||
- Removed dependency on ``pyreadline`` (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
|
||||
- Removed dependency on ``cssselect2`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
- Added dependency on ``svgelements`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
|
||||
|
||||
v1.4.1
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Temporarily fixed boolean operations' bug (`#1724 <https://github.com/3b1b/manim/pull/1724>`__)
|
||||
- Import ``Iterable`` from ``collections.abc`` instead of ``collections`` which is deprecated since python 3.9 (`d2e0811 <https://github.com/3b1b/manim/commit/d2e0811285f7908e71a65e664fec88b1af1c6144>`__)
|
||||
|
||||
v1.4.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Temporarily fixed ``Lightbulb`` (`f1996f8 <https://github.com/3b1b/manim/pull/1697/commits/f1996f8479f9e33d626b3b66e9eb6995ce231d86>`__)
|
||||
- Fixed some bugs of ``SVGMobject`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
|
||||
- Fixed some bugs of SVG path string parser (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
|
||||
- Fixed some bugs of ``MTex`` (`#1720 <https://github.com/3b1b/manim/pull/1720>`__)
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
- Added option to add ticks on x-axis in ``BarChart`` (`#1694 <https://github.com/3b1b/manim/pull/1694>`__)
|
||||
- Added ``lable_buff`` config parameter for ``Brace`` (`#1704 <https://github.com/3b1b/manim/pull/1704>`__)
|
||||
- Added support for ``rotate skewX skewY`` transform in SVG (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
|
||||
- Added style support to ``SVGMobject`` (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
|
||||
- Added parser to <style> element of SVG (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
|
||||
- Added support for <line> element in ``SVGMobject`` (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Used ``FFMPEG_BIN`` instead of ``"ffmpeg"`` for sound incorporation (`5aa8d15 <https://github.com/3b1b/manim/pull/1697/commits/5aa8d15d85797f68a8f169ca69fd90d441a3abbe>`__)
|
||||
- Decorated ``CoordinateSystem.get_axes`` and ``.get_all_ranges`` as abstract method (`#1709 <https://github.com/3b1b/manim/pull/1709>`__)
|
||||
- Refactored SVG path string parser (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
|
||||
- Allowed ``Mobject.scale`` to receive iterable ``scale_factor`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
|
||||
- Refactored ``MTex`` (`#1716 <https://github.com/3b1b/manim/pull/1716>`__)
|
||||
- Improved config helper (``manimgl --config``) (`#1721 <https://github.com/3b1b/manim/pull/1721>`__)
|
||||
- Refactored ``MTex`` (`#1723 <https://github.com/3b1b/manim/pull/1723>`__)
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
- Added dependency on python package `cssselect2 <https://github.com/Kozea/cssselect2>`__ (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
|
||||
|
||||
|
||||
v1.3.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- Fixed ``Mobject.stretch_to_fit_depth`` (`#1653 <https://github.com/3b1b/manim/pull/1653>`__)
|
||||
- Fixed the bug of rotating camera (`#1655 <https://github.com/3b1b/manim/pull/1655>`__)
|
||||
- Fixed ``SurfaceMesh`` to be evenly spaced (`c73d507 <https://github.com/3b1b/manim/pull/1688/commits/c73d507c76af5c8602d4118bc7538ba04c03ebae>`__)
|
||||
- Fixed ``angle_between_vectors`` add ``rotation_between_vectors`` (`82bd02d <https://github.com/3b1b/manim/pull/1688/commits/82bd02d21fbd89b71baa21e077e143f440df9014>`__)
|
||||
- Fixed ``VMobject.fade`` (`a717314 <https://github.com/3b1b/manim/pull/1688/commits/a7173142bf93fd309def0cc10f3c56f5e6972332>`__)
|
||||
- Fixed ``angle_between_vectors`` (`fbc329d <https://github.com/3b1b/manim/pull/1688/commits/fbc329d7ce3b11821d47adf6052d932f7eff724a>`__)
|
||||
- Fixed bug in ``ShowSubmobjectsOneByOne`` (`bcd0990 <https://github.com/3b1b/manim/pull/1688/commits/bcd09906bea5eaaa5352e7bee8f3153f434cf606>`__)
|
||||
- Fixed bug in ``TransformMatchingParts`` (`7023548 <https://github.com/3b1b/manim/pull/1691/commits/7023548ec62c4adb2f371aab6a8c7f62deb7c33c>`__)
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Added CLI flag ``--log-level`` to specify log level (`e10f850 <https://github.com/3b1b/manim/commit/e10f850d0d9f971931cc85d44befe67dc842af6d>`__)
|
||||
- Added operations (``+`` and ``*``) for ``Mobject`` (`#1667 <https://github.com/3b1b/manim/pull/1667>`__)
|
||||
- Added 4 boolean operations for ``VMobject`` in ``manimlib/mobject/boolean_ops.py`` (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
|
||||
|
||||
- ``Union(*vmobjects, **kwargs)``
|
||||
- ``Difference(subject, clip, **kwargs)``
|
||||
- ``Intersection(*vmobjects, **kwargs)``
|
||||
- ``Exclusion(*vmobjects, **kwargs)``
|
||||
- Added reflectiveness (`81c3ae3 <https://github.com/3b1b/manim/pull/1688/commits/81c3ae30372e288dc772633dbd17def6e603753e>`__)
|
||||
- Enabled ``glow_factor`` on ``DotCloud`` (`2c7689e <https://github.com/3b1b/manim/pull/1688/commits/2c7689ed9e81229ce87c648f97f26267956c0bc9>`__)
|
||||
- Added option ``-e`` to insert embed line from the command line (`d065e19 <https://github.com/3b1b/manim/pull/1688/commits/d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772>`__)
|
||||
- Improved ``point_from_proportion`` to account for arc length (`0e78027 <https://github.com/3b1b/manim/pull/1688/commits/0e78027186a976f7e5fa8d586f586bf6e6baab8d>`__)
|
||||
- Added shortcut ``set_backstroke`` for setting black background stroke (`781a993 <https://github.com/3b1b/manim/pull/1688/commits/781a9934fda6ba11f22ba32e8ccddcb3ba78592e>`__)
|
||||
- Added ``Suface.always_sort_to_camera`` (`0b898a5 <https://github.com/3b1b/manim/pull/1688/commits/0b898a5594203668ed9cad38b490ab49ba233bd4>`__)
|
||||
- Added getter methods for specific euler angles (`e899604 <https://github.com/3b1b/manim/pull/1688/commits/e899604a2d05f78202fcb3b9824ec34647237eae>`__)
|
||||
- Hade ``rotation_between_vectors`` handle identical/similar vectors (`407c53f <https://github.com/3b1b/manim/pull/1688/commits/407c53f97c061bfd8a53beacd88af4c786f9e9ee>`__)
|
||||
- Added ``Mobject.insert_submobject`` method (`49743da <https://github.com/3b1b/manim/pull/1688/commits/49743daf3244bfa11a427040bdde8e2bb79589e8>`__)
|
||||
- Created single progress display for full scene render (`9dd1f47 <https://github.com/3b1b/manim/pull/1688/commits/9dd1f47dabca1580d6102e34e44574b0cba556e7>`__)
|
||||
- Added ``Circle.get_radius`` (`264f7b1 <https://github.com/3b1b/manim/pull/1691/commits/264f7b11726e9e736f0fe472f66e38539f74e848>`__)
|
||||
- Added ``Dodecahedron`` (`83841ae <https://github.com/3b1b/manim/pull/1691/commits/83841ae41568a9c9dff44cd163106c19a74ac281>`__)
|
||||
- Added ``GlowDot`` (`a1d5147 <https://github.com/3b1b/manim/pull/1691/commits/a1d51474ea1ce3b7aa3efbe4c5e221be70ee2f5b>`__)
|
||||
- Added ``MTex`` , see `#1678 <https://github.com/3b1b/manim/pull/1678>`__ for details (`#1678 <https://github.com/3b1b/manim/pull/1678>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
|
||||
- Refactored support for command ``A`` in path of SVG (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
|
||||
- Refactored ``SingleStringTex.balance_braces`` (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
|
||||
- Slight tweaks to how saturation_factor works on newton-fractal (`8b454fb <https://github.com/3b1b/manim/pull/1688/commits/8b454fbe9335a7011e947093230b07a74ba9c653>`__)
|
||||
- Made it possible to set full screen preview as a default (`317a5d6 <https://github.com/3b1b/manim/pull/1688/commits/317a5d6226475b6b54a78db7116c373ef84ea923>`__)
|
||||
- Used ``quick_point_from_proportion`` for graph points (`e764da3 <https://github.com/3b1b/manim/pull/1688/commits/e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc>`__)
|
||||
- Made sure ``Line.set_length`` returns self (`d2182b9 <https://github.com/3b1b/manim/pull/1688/commits/d2182b9112300558b6c074cefd685f97c10b3898>`__)
|
||||
- Better align ``SurfaceMesh`` to the corresponding surface polygons (`eea3c6b <https://github.com/3b1b/manim/pull/1688/commits/eea3c6b29438f9e9325329c4355e76b9f635e97a>`__)
|
||||
- Match ``fix_in_frame`` status for ``FlashAround`` mobject (`ee1594a <https://github.com/3b1b/manim/pull/1688/commits/ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3>`__)
|
||||
- Made sure ``Mobject.is_fixed_in_frame`` stays updated with uniforms (`ba23fbe <https://github.com/3b1b/manim/pull/1688/commits/ba23fbe71e4a038201cd7df1d200514ed1c13bc2>`__)
|
||||
- Made sure ``skip_animations`` and ``start_at_animation_number`` play well together (`98b0d26 <https://github.com/3b1b/manim/pull/1691/commits/98b0d266d2475926a606331923cca3dc1dea97ad>`__)
|
||||
- Updated progress display for full scene render (`f8e6e7d <https://github.com/3b1b/manim/pull/1691/commits/f8e6e7df3ceb6f3d845ced4b690a85b35e0b8d00>`__)
|
||||
- ``VectorizedPoint`` should call ``__init__`` for both super classes (`8f1dfab <https://github.com/3b1b/manim/pull/1691/commits/8f1dfabff04a8456f5c4df75b0f97d50b2755003>`__)
|
||||
- Used array copy when checking need for refreshing triangulation (`758f329 <https://github.com/3b1b/manim/pull/1691/commits/758f329a06a0c198b27a48c577575d94554305bf>`__)
|
||||
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__ (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
|
||||
|
||||
v1.2.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- `#1592 <https://github.com/3b1b/manim/pull/1592>`__: Fixed ``put_start_and_end_on`` in 3D
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Fixed ``DecimalNumber``'s scaling issue
|
||||
- `56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__: Fixed bug with common range array used for all coordinate systems
|
||||
- `8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__: Fixed ``CoordinateSystem`` init bug
|
||||
- `0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__: Fixed bug for single-valued ``ValueTracker``
|
||||
- `54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__: Fixed bug with SVG rectangles
|
||||
- `d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__: Fixed ``DotCloud.set_radii``
|
||||
- `b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__: Temporarily fixed bug for ``PMobject`` array resizing
|
||||
- `5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__: Fixed ``match_style``
|
||||
- `719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__: Fixed negative ``path_arc`` case
|
||||
- `c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__: Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis``
|
||||
- `7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__: Fixed ``ComplexPlane`` -i display bug
|
||||
- Fixed ``put_start_and_end_on`` in 3D (`#1592 <https://github.com/3b1b/manim/pull/1592>`__)
|
||||
- Fixed ``DecimalNumber``'s scaling issue (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
|
||||
- Fixed bug with common range array used for all coordinate systems (`56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__)
|
||||
- Fixed ``CoordinateSystem`` init bug (`8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__)
|
||||
- Fixed bug for single-valued ``ValueTracker`` (`0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__)
|
||||
- Fixed bug with SVG rectangles (`54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__)
|
||||
- Fixed ``DotCloud.set_radii`` (`d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__)
|
||||
- Temporarily fixed bug for ``PMobject`` array resizing (`b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__)
|
||||
- Fixed ``match_style`` (`5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__)
|
||||
- Fixed negative ``path_arc`` case (`719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__)
|
||||
- Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis`` (`c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__)
|
||||
- Fixed ``ComplexPlane`` -i display bug (`7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__)
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `#1598 <https://github.com/3b1b/manim/pull/1598>`__: Supported the elliptical arc command ``A`` for ``SVGMobject``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Added ``FlashyFadeIn``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Save triangulation
|
||||
- `#1625 <https://github.com/3b1b/manim/pull/1625>`__: Added new ``Code`` mobject
|
||||
- `#1637 <https://github.com/3b1b/manim/pull/1637>`__: Add warnings and use rich to display log
|
||||
- `bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__: Added ``VCube``
|
||||
- `6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__: Supported ``ValueTracker`` to track vectors
|
||||
- `3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__: Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject``
|
||||
- `a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__: Added ``TracgTail``
|
||||
- `acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__: Added ``Scene.point_to_mobject``
|
||||
- `f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__: Added poly_fractal shader
|
||||
- `b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__: Added kwargs to ``TipableVMobject.set_length``
|
||||
- `17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__: Added ``Mobject.replicate``
|
||||
- `33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__: Added mandelbrot_fractal shader
|
||||
- `f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__: Saved state before each embed
|
||||
- `e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__: Allowed releasing of Textures
|
||||
- `14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__: Consolidated and renamed newton_fractal shader
|
||||
- `6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__: Hade ``ImageMoject`` remember the filepath to the Image
|
||||
- Supported the elliptical arc command ``A`` for ``SVGMobject`` (`#1598 <https://github.com/3b1b/manim/pull/1598>`__)
|
||||
- Added ``FlashyFadeIn`` (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
|
||||
- Save triangulation (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
|
||||
- Added new ``Code`` mobject (`#1625 <https://github.com/3b1b/manim/pull/1625>`__)
|
||||
- Add warnings and use rich to display log (`#1637 <https://github.com/3b1b/manim/pull/1637>`__)
|
||||
- Added ``VCube`` (`bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__)
|
||||
- Supported ``ValueTracker`` to track vectors (`6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__)
|
||||
- Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject`` (`3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__)
|
||||
- Added ``TracgTail`` (`a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__)
|
||||
- Added ``Scene.point_to_mobject`` (`acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__)
|
||||
- Added poly_fractal shader (`f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__)
|
||||
- Added kwargs to ``TipableVMobject.set_length`` (`b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__)
|
||||
- Added ``Mobject.replicate`` (`17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__)
|
||||
- Added mandelbrot_fractal shader (`33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__)
|
||||
- Saved state before each embed (`f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__)
|
||||
- Allowed releasing of Textures (`e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__)
|
||||
- Consolidated and renamed newton_fractal shader (`14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__)
|
||||
- Hade ``ImageMoject`` remember the filepath to the Image (`6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Changed back to simpler ``Mobject.scale`` implementation
|
||||
- `b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__: Simplified ``Square``
|
||||
- `40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__: Removed unused parameter ``triangulation_locked``
|
||||
- `8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__: Reimplemented ``Arrow``
|
||||
- `d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__: Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default
|
||||
- `7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__: Refactored to call ``_handle_scale_side_effects`` after scaling takes place
|
||||
- `7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__: Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end``
|
||||
- `0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__: Made sure framerate is 30 for previewed scenes
|
||||
- `c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__: Pushed ``pixel_coords_to_space_coords`` to ``Window``
|
||||
- `d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__: Refactored to pass tuples and not arrays to uniforms
|
||||
- `9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__: Refactored to copy uniform arrays in ``Mobject.copy``
|
||||
- `ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__: Added ``bounding_box`` as exceptional key to point_cloud mobject
|
||||
- `329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__: Made sure stroke width is always a float
|
||||
- Changed back to simpler ``Mobject.scale`` implementation (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
|
||||
- Simplified ``Square`` (`b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__)
|
||||
- Removed unused parameter ``triangulation_locked`` (`40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__)
|
||||
- Reimplemented ``Arrow`` (`8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__)
|
||||
- Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default (`d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__)
|
||||
- Refactored to call ``_handle_scale_side_effects`` after scaling takes place (`7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__)
|
||||
- Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end`` (`7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__)
|
||||
- Made sure framerate is 30 for previewed scenes (`0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__)
|
||||
- Pushed ``pixel_coords_to_space_coords`` to ``Window`` (`c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__)
|
||||
- Refactored to pass tuples and not arrays to uniforms (`d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__)
|
||||
- Refactored to copy uniform arrays in ``Mobject.copy`` (`9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__)
|
||||
- Added ``bounding_box`` as exceptional key to point_cloud mobject (`ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__)
|
||||
- Made sure stroke width is always a float (`329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__)
|
||||
|
||||
|
||||
v1.1.0
|
||||
|
||||
@@ -83,22 +83,3 @@ Its value is a dictionary, passed in as ``kwargs`` when initializing the ``Camer
|
||||
to modify the value of the properties of the ``Camera`` class.
|
||||
|
||||
So the nesting of the ``CONFIG`` dictionary **essentially** passes in the value as ``kwargs``.
|
||||
|
||||
Common usage
|
||||
------------
|
||||
|
||||
When writing a class by yourself, you can add attributes or modify the attributes
|
||||
of the parent class through ``CONFIG``.
|
||||
|
||||
The most commonly used is to modify the properties of the camera when writing a ``Scene``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class YourScene(Scene):
|
||||
CONFIG = {
|
||||
"camera_config": {
|
||||
"background_color": WHITE,
|
||||
},
|
||||
}
|
||||
|
||||
For example, the above dictionary will change the background color to white, etc.
|
||||
@@ -43,6 +43,7 @@ flag abbr function
|
||||
``--hd`` Render at a 1080p quality
|
||||
``--uhd`` Render at a 4k quality
|
||||
``--full_screen`` ``-f`` Show window in full screen
|
||||
``--presenter_mode`` ``-p`` Scene will stay paused during wait calls until space bar or right arrow is hit, like a slide show
|
||||
``--save_pngs`` ``-g`` Save each frame as a png
|
||||
``--save_as_gif`` ``-i`` Save the video as gif
|
||||
``--transparent`` ``-t`` Render to a movie file with an alpha channel
|
||||
@@ -53,11 +54,12 @@ flag abbr function
|
||||
``--config`` Guide for automatic configuration
|
||||
``--file_name FILE_NAME`` Name for the movie or image file
|
||||
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value.
|
||||
``--embed LINENO`` ``-e`` Takes a line number as an argument, and results in the scene being called as if the line ``self.embed()`` was inserted into the scene code at that line number
|
||||
``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080"
|
||||
``--frame_rate FRAME_RATE`` Frame rate, as an integer
|
||||
``--color COLOR`` ``-c`` Background color
|
||||
``--leave_progress_bars`` Leave progress bars displayed in terminal
|
||||
``--video_dir VIDEO_DIR`` directory to write video
|
||||
``--video_dir VIDEO_DIR`` Directory to write video
|
||||
``--config_file CONFIG_FILE`` Path to the custom configuration file
|
||||
========================================================== ====== =================================================================================================================================================================================================
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -274,7 +274,7 @@ class UpdatersExample(Scene):
|
||||
square = Square()
|
||||
square.set_fill(BLUE_E, 1)
|
||||
|
||||
# On all all frames, the constructor Brace(square, UP) will
|
||||
# On all frames, the constructor Brace(square, UP) will
|
||||
# be called, and the mobject brace will set its data to match
|
||||
# that of the newly constructed object
|
||||
brace = always_redraw(Brace, square, UP)
|
||||
|
||||
@@ -22,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 *
|
||||
@@ -36,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 *
|
||||
@@ -66,4 +68,3 @@ from manimlib.utils.rate_functions import *
|
||||
from manimlib.utils.simple_functions import *
|
||||
from manimlib.utils.sounds import *
|
||||
from manimlib.utils.space_ops import *
|
||||
from manimlib.utils.strings import *
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
import manimlib.config
|
||||
import manimlib.logger
|
||||
import manimlib.extract_scene
|
||||
import manimlib.utils.init_config
|
||||
from manimlib import __version__
|
||||
@@ -9,8 +10,10 @@ def main():
|
||||
print(f"ManimGL \033[32mv{__version__}\033[0m")
|
||||
|
||||
args = manimlib.config.parse_cli()
|
||||
if args.version and args.file == None:
|
||||
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()
|
||||
@@ -21,5 +24,6 @@ def main():
|
||||
for scene in scenes:
|
||||
scene.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.composition import Succession
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import linear
|
||||
@@ -147,7 +146,7 @@ class Write(DrawBorderThenFill):
|
||||
else:
|
||||
self.run_time = 2
|
||||
if self.lag_ratio is None:
|
||||
self.lag_ratio = min(4.0 / length, 0.2)
|
||||
self.lag_ratio = min(4.0 / (length + 1.0), 0.2)
|
||||
|
||||
|
||||
class ShowIncreasingSubsets(Animation):
|
||||
@@ -174,16 +173,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...
|
||||
|
||||
@@ -224,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)
|
||||
|
||||
@@ -17,7 +17,6 @@ class Broadcast(LaggedStart):
|
||||
"remover": True,
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": 3,
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def __init__(self, focal_point, **kwargs):
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.animation.fading import FadeTransformPieces
|
||||
from manimlib.animation.fading import FadeInFromPoint
|
||||
from manimlib.animation.fading import FadeOutToPoint
|
||||
from manimlib.animation.transform import ReplacementTransform
|
||||
from manimlib.animation.transform import Transform
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.svg.mtex_mobject import MTex
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
|
||||
|
||||
class TransformMatchingParts(AnimationGroup):
|
||||
@@ -68,10 +72,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)
|
||||
@@ -139,3 +143,108 @@ class TransformMatchingTex(TransformMatchingParts):
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
return mobject.get_tex()
|
||||
|
||||
|
||||
class TransformMatchingMTex(AnimationGroup):
|
||||
CONFIG = {
|
||||
"key_map": dict(),
|
||||
}
|
||||
|
||||
def __init__(self, source_mobject, target_mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
assert isinstance(source_mobject, MTex)
|
||||
assert isinstance(target_mobject, MTex)
|
||||
anims = []
|
||||
rest_source_submobs = source_mobject.submobjects.copy()
|
||||
rest_target_submobs = target_mobject.submobjects.copy()
|
||||
|
||||
def add_anim_from(anim_class, func, source_attr, target_attr=None):
|
||||
if target_attr is None:
|
||||
target_attr = source_attr
|
||||
source_parts = func(source_mobject, source_attr)
|
||||
target_parts = func(target_mobject, target_attr)
|
||||
filtered_source_parts = [
|
||||
submob_part for submob_part in source_parts
|
||||
if all([
|
||||
submob in rest_source_submobs
|
||||
for submob in submob_part
|
||||
])
|
||||
]
|
||||
filtered_target_parts = [
|
||||
submob_part for submob_part in target_parts
|
||||
if all([
|
||||
submob in rest_target_submobs
|
||||
for submob in submob_part
|
||||
])
|
||||
]
|
||||
if not (filtered_source_parts and filtered_target_parts):
|
||||
return
|
||||
anims.append(anim_class(
|
||||
VGroup(*filtered_source_parts),
|
||||
VGroup(*filtered_target_parts),
|
||||
**kwargs
|
||||
))
|
||||
for submob in it.chain(*filtered_source_parts):
|
||||
rest_source_submobs.remove(submob)
|
||||
for submob in it.chain(*filtered_target_parts):
|
||||
rest_target_submobs.remove(submob)
|
||||
|
||||
def get_submobs_from_keys(mobject, keys):
|
||||
if not isinstance(keys, tuple):
|
||||
keys = (keys,)
|
||||
indices = []
|
||||
for key in keys:
|
||||
if isinstance(key, int):
|
||||
indices.append(key)
|
||||
elif isinstance(key, range):
|
||||
indices.extend(key)
|
||||
elif isinstance(key, str):
|
||||
all_parts = mobject.get_parts_by_tex(key)
|
||||
indices.extend(it.chain(*[
|
||||
mobject.indices_of_part(part) for part in all_parts
|
||||
]))
|
||||
else:
|
||||
raise TypeError(key)
|
||||
return VGroup(VGroup(*[
|
||||
mobject[i] for i in remove_list_redundancies(indices)
|
||||
]))
|
||||
|
||||
for source_key, target_key in self.key_map.items():
|
||||
add_anim_from(
|
||||
ReplacementTransform, get_submobs_from_keys,
|
||||
source_key, target_key
|
||||
)
|
||||
|
||||
common_specified_substrings = sorted(list(
|
||||
set(source_mobject.get_specified_substrings()).intersection(
|
||||
target_mobject.get_specified_substrings()
|
||||
)
|
||||
), key=len, reverse=True)
|
||||
for part_tex_string in common_specified_substrings:
|
||||
add_anim_from(
|
||||
FadeTransformPieces, MTex.get_parts_by_tex, part_tex_string
|
||||
)
|
||||
|
||||
common_submob_tex_strings = {
|
||||
source_submob.get_tex() for source_submob in source_mobject
|
||||
}.intersection({
|
||||
target_submob.get_tex() for target_submob in target_mobject
|
||||
})
|
||||
for tex_string in common_submob_tex_strings:
|
||||
add_anim_from(
|
||||
FadeTransformPieces,
|
||||
lambda mobject, attr: VGroup(*[
|
||||
VGroup(mob) for mob in mobject
|
||||
if mob.get_tex() == attr
|
||||
]),
|
||||
tex_string
|
||||
)
|
||||
|
||||
anims.append(FadeOutToPoint(
|
||||
VGroup(*rest_source_submobs), target_mobject.get_center(), **kwargs
|
||||
))
|
||||
anims.append(FadeInFromPoint(
|
||||
VGroup(*rest_target_submobs), source_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
|
||||
@@ -327,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)]
|
||||
@@ -410,7 +437,7 @@ 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)
|
||||
@@ -427,14 +454,18 @@ 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(),
|
||||
}
|
||||
@@ -445,6 +476,8 @@ class Camera(object):
|
||||
|
||||
def get_texture_id(self, path):
|
||||
if path not in self.path_to_texture:
|
||||
if self.n_textures == 15: # I have no clue why this is needed
|
||||
self.n_textures += 1
|
||||
tid = self.n_textures
|
||||
self.n_textures += 1
|
||||
im = Image.open(path).convert("RGBA")
|
||||
@@ -468,4 +501,5 @@ class Camera(object):
|
||||
class ThreeDCamera(Camera):
|
||||
CONFIG = {
|
||||
"samples": 4,
|
||||
"anti_alias_width": 0,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -12,6 +13,9 @@ from manimlib.utils.init_config import init_customization
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
__config_file__ = "custom_config.yml"
|
||||
|
||||
|
||||
def parse_cli():
|
||||
try:
|
||||
parser = argparse.ArgumentParser()
|
||||
@@ -19,7 +23,7 @@ def parse_cli():
|
||||
module_location.add_argument(
|
||||
"file",
|
||||
nargs="?",
|
||||
help="path to file holding the python code for the scene",
|
||||
help="Path to file holding the python code for the scene",
|
||||
)
|
||||
parser.add_argument(
|
||||
"scene_names",
|
||||
@@ -61,6 +65,12 @@ def parse_cli():
|
||||
action="store_true",
|
||||
help="Show window in full screen",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--presenter_mode",
|
||||
action="store_true",
|
||||
help="Scene will stay paused during wait calls until "
|
||||
"space bar or right arrow is hit, like a slide show"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-g", "--save_pngs",
|
||||
action="store_true",
|
||||
@@ -112,6 +122,12 @@ def parse_cli():
|
||||
"in two comma separated values, e.g. \"3,6\", it will end"
|
||||
"the rendering at the second value",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-e", "--embed", metavar="LINENO",
|
||||
help="Takes a line number as an argument, and results"
|
||||
"in the scene being called as if the line `self.embed()`"
|
||||
"was inserted into the scene code at that line number."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--resolution",
|
||||
help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"",
|
||||
@@ -142,6 +158,10 @@ def parse_cli():
|
||||
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:
|
||||
@@ -158,14 +178,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__
|
||||
@@ -193,9 +229,11 @@ def get_custom_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"
|
||||
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)")
|
||||
" to specify the `temporary_storage` in the config file (.yml)"
|
||||
)
|
||||
|
||||
|
||||
def get_configuration(args):
|
||||
@@ -225,8 +263,10 @@ def get_configuration(args):
|
||||
|
||||
elif not os.path.exists(__config_file__):
|
||||
log.info(f"Using the default configuration file, which you can modify in `{global_defaults_file}`")
|
||||
log.info("If you want to create a local configuration file, you can create a file named"
|
||||
f" `{__config_file__}`, or run `manimgl --config`")
|
||||
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)
|
||||
@@ -256,16 +296,23 @@ def get_configuration(args):
|
||||
"quiet": args.quiet,
|
||||
}
|
||||
|
||||
module = get_module(args.file)
|
||||
if args.embed is None:
|
||||
module = get_module(args.file)
|
||||
else:
|
||||
with insert_embed_line(args.file, int(args.embed)) as alt_file:
|
||||
module = get_module(alt_file)
|
||||
|
||||
config = {
|
||||
"module": module,
|
||||
"scene_names": args.scene_names,
|
||||
"file_writer_config": file_writer_config,
|
||||
"quiet": args.quiet or args.write_all,
|
||||
"write_all": args.write_all,
|
||||
"skip_animations": args.skip_animations,
|
||||
"start_at_animation_number": args.start_at_animation_number,
|
||||
"preview": not write_file,
|
||||
"end_at_animation_number": None,
|
||||
"preview": not write_file,
|
||||
"presenter_mode": args.presenter_mode,
|
||||
"leave_progress_bars": args.leave_progress_bars,
|
||||
}
|
||||
|
||||
@@ -278,7 +325,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"] = {
|
||||
@@ -295,10 +342,6 @@ def get_configuration(args):
|
||||
else:
|
||||
config["start_at_animation_number"] = int(stan)
|
||||
|
||||
config["skip_animations"] = any([
|
||||
args.skip_animations,
|
||||
args.start_at_animation_number,
|
||||
])
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
import inspect
|
||||
import sys
|
||||
import copy
|
||||
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.config import get_custom_config
|
||||
@@ -38,7 +39,7 @@ 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:
|
||||
@@ -63,10 +64,31 @@ def get_scene_config(config):
|
||||
"end_at_animation_number",
|
||||
"leave_progress_bars",
|
||||
"preview",
|
||||
"presenter_mode",
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
def compute_total_frames(scene_class, scene_config):
|
||||
"""
|
||||
When a scene is being written to file, a copy of the scene is run with
|
||||
skip_animations set to true so as to count how many frames it will require.
|
||||
This allows for a total progress bar on rendering, and also allows runtime
|
||||
errors to be exposed preemptively for long running scenes. The final frame
|
||||
is saved by default, so that one can more quickly check that the last frame
|
||||
looks as expected.
|
||||
"""
|
||||
pre_config = copy.deepcopy(scene_config)
|
||||
pre_config["file_writer_config"]["write_to_movie"] = False
|
||||
pre_config["file_writer_config"]["save_last_frame"] = True
|
||||
pre_config["file_writer_config"]["quiet"] = True
|
||||
pre_config["skip_animations"] = True
|
||||
pre_scene = scene_class(**pre_config)
|
||||
pre_scene.run()
|
||||
total_time = pre_scene.time - pre_scene.skip_time
|
||||
return int(total_time * scene_config["camera_config"]["frame_rate"])
|
||||
|
||||
|
||||
def get_scenes_to_render(scene_classes, scene_config, config):
|
||||
if config["write_all"]:
|
||||
return [sc(**scene_config) for sc in scene_classes]
|
||||
@@ -76,6 +98,9 @@ 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
|
||||
|
||||
@@ -6,7 +6,8 @@ __all__ = ["log"]
|
||||
|
||||
FORMAT = "%(message)s"
|
||||
logging.basicConfig(
|
||||
level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||
level=logging.WARNING, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||
)
|
||||
|
||||
log = logging.getLogger("rich")
|
||||
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.reverse_points()
|
||||
|
||||
|
||||
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,3 +1,4 @@
|
||||
from abc import abstractmethod
|
||||
import numpy as np
|
||||
import numbers
|
||||
|
||||
@@ -55,9 +56,11 @@ class CoordinateSystem():
|
||||
def get_origin(self):
|
||||
return self.c2p(*[0] * self.dimension)
|
||||
|
||||
@abstractmethod
|
||||
def get_axes(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
@abstractmethod
|
||||
def get_all_ranges(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
@@ -151,14 +154,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
|
||||
|
||||
@@ -274,7 +277,7 @@ class CoordinateSystem():
|
||||
class Axes(VGroup, CoordinateSystem):
|
||||
CONFIG = {
|
||||
"axis_config": {
|
||||
"include_tip": True,
|
||||
"include_tip": False,
|
||||
"numbers_to_exclude": [0],
|
||||
},
|
||||
"x_axis_config": {},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from isosurfaces import plot_isoline
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
@@ -42,6 +44,8 @@ class ParametricCurve(VMobject):
|
||||
self.add_points_as_corners(points[1:])
|
||||
if self.use_smoothing:
|
||||
self.make_approximately_smooth()
|
||||
if not self.has_points():
|
||||
self.set_points([self.t_func(t_min)])
|
||||
return self
|
||||
|
||||
|
||||
@@ -68,3 +72,40 @@ class FunctionGraph(ParametricCurve):
|
||||
|
||||
def get_point_from_function(self, x):
|
||||
return self.t_func(x)
|
||||
|
||||
|
||||
class ImplicitFunction(VMobject):
|
||||
CONFIG = {
|
||||
"x_range": [-FRAME_X_RADIUS, FRAME_X_RADIUS],
|
||||
"y_range": [-FRAME_Y_RADIUS, FRAME_Y_RADIUS],
|
||||
"min_depth": 5,
|
||||
"max_quads": 1500,
|
||||
"use_smoothing": True
|
||||
}
|
||||
|
||||
def __init__(self, func, x_range=None, y_range=None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.function = func
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
p_min, p_max = (
|
||||
np.array([self.x_range[0], self.y_range[0]]),
|
||||
np.array([self.x_range[1], self.y_range[1]]),
|
||||
)
|
||||
curves = plot_isoline(
|
||||
fn=lambda u: self.function(u[0], u[1]),
|
||||
pmin=p_min,
|
||||
pmax=p_max,
|
||||
min_depth=self.min_depth,
|
||||
max_quads=self.max_quads,
|
||||
) # returns a list of lists of 2D points
|
||||
curves = [
|
||||
np.pad(curve, [(0, 0), (0, 1)]) for curve in curves if curve != []
|
||||
] # add z coord as 0
|
||||
for curve in curves:
|
||||
self.start_new_path(curve[0])
|
||||
self.add_points_as_corners(curve[1:])
|
||||
if self.use_smoothing:
|
||||
self.make_smooth()
|
||||
return self
|
||||
|
||||
@@ -305,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 = {
|
||||
@@ -507,6 +510,7 @@ class Line(TipableVMobject):
|
||||
|
||||
def set_length(self, length, **kwargs):
|
||||
self.scale(length / self.get_length(), **kwargs)
|
||||
return self
|
||||
|
||||
|
||||
class DashedLine(Line):
|
||||
@@ -845,6 +849,11 @@ class Polygon(VMobject):
|
||||
return self
|
||||
|
||||
|
||||
class Polyline(Polygon):
|
||||
def init_points(self):
|
||||
self.set_points_as_corners(self.vertices)
|
||||
|
||||
|
||||
class RegularPolygon(Polygon):
|
||||
CONFIG = {
|
||||
"start_angle": None,
|
||||
|
||||
@@ -112,7 +112,7 @@ class Matrix(VMobject):
|
||||
"\\left[",
|
||||
"\\begin{array}{c}",
|
||||
*height * ["\\quad \\\\"],
|
||||
"\\end{array}"
|
||||
"\\end{array}",
|
||||
"\\right]",
|
||||
]))[0]
|
||||
bracket_pair.set_height(
|
||||
|
||||
@@ -4,6 +4,7 @@ import random
|
||||
import sys
|
||||
import moderngl
|
||||
from functools import wraps
|
||||
from collections.abc import Iterable
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -43,10 +44,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 +86,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 +106,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 +158,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 +314,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)
|
||||
@@ -386,6 +405,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):
|
||||
@@ -393,6 +413,7 @@ class Mobject(object):
|
||||
for submob in self.submobjects:
|
||||
submob.shuffle(recurse=True)
|
||||
random.shuffle(self.submobjects)
|
||||
self.assemble_family()
|
||||
return self
|
||||
|
||||
# Copying
|
||||
@@ -576,7 +597,10 @@ class Mobject(object):
|
||||
Otherwise, if about_point is given a value, scaling is done with
|
||||
respect to that point.
|
||||
"""
|
||||
scale_factor = max(scale_factor, min_scale_factor)
|
||||
if isinstance(scale_factor, Iterable):
|
||||
scale_factor = np.array(scale_factor).clip(min=min_scale_factor)
|
||||
else:
|
||||
scale_factor = max(scale_factor, min_scale_factor)
|
||||
self.apply_points_function(
|
||||
lambda points: scale_factor * points,
|
||||
about_point=about_point,
|
||||
@@ -767,7 +791,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)
|
||||
@@ -793,6 +817,21 @@ class Mobject(object):
|
||||
self.set_depth(max_depth, **kwargs)
|
||||
return self
|
||||
|
||||
def set_min_width(self, min_width, **kwargs):
|
||||
if self.get_width() < min_width:
|
||||
self.set_width(min_width, **kwargs)
|
||||
return self
|
||||
|
||||
def set_min_height(self, min_height, **kwargs):
|
||||
if self.get_height() < min_height:
|
||||
self.set_height(min_height, **kwargs)
|
||||
return self
|
||||
|
||||
def set_min_depth(self, min_depth, **kwargs):
|
||||
if self.get_depth() < min_depth:
|
||||
self.set_depth(min_depth, **kwargs)
|
||||
return self
|
||||
|
||||
def set_coord(self, value, dim, direction=ORIGIN):
|
||||
curr = self.get_coord(dim, direction)
|
||||
shift_vect = np.zeros(self.dim)
|
||||
@@ -966,12 +1005,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):
|
||||
@@ -982,6 +1021,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):
|
||||
@@ -1149,21 +1196,21 @@ class Mobject(object):
|
||||
def match_depth(self, mobject, **kwargs):
|
||||
return self.match_dim_size(mobject, 2, **kwargs)
|
||||
|
||||
def match_coord(self, mobject, dim, direction=ORIGIN):
|
||||
return self.set_coord(
|
||||
mobject.get_coord(dim, direction),
|
||||
dim=dim,
|
||||
direction=direction,
|
||||
)
|
||||
def match_coord(self, mobject_or_point, dim, direction=ORIGIN):
|
||||
if isinstance(mobject_or_point, Mobject):
|
||||
coord = mobject_or_point.get_coord(dim, direction)
|
||||
else:
|
||||
coord = mobject_or_point[dim]
|
||||
return self.set_coord(coord, dim=dim, direction=direction)
|
||||
|
||||
def match_x(self, mobject, direction=ORIGIN):
|
||||
return self.match_coord(mobject, 0, direction)
|
||||
def match_x(self, mobject_or_point, direction=ORIGIN):
|
||||
return self.match_coord(mobject_or_point, 0, direction)
|
||||
|
||||
def match_y(self, mobject, direction=ORIGIN):
|
||||
return self.match_coord(mobject, 1, direction)
|
||||
def match_y(self, mobject_or_point, direction=ORIGIN):
|
||||
return self.match_coord(mobject_or_point, 1, direction)
|
||||
|
||||
def match_z(self, mobject, direction=ORIGIN):
|
||||
return self.match_coord(mobject, 2, direction)
|
||||
def match_z(self, mobject_or_point, direction=ORIGIN):
|
||||
return self.match_coord(mobject_or_point, 2, direction)
|
||||
|
||||
def align_to(self, mobject_or_point, direction=ORIGIN):
|
||||
"""
|
||||
@@ -1363,11 +1410,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
|
||||
@@ -1610,6 +1659,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 = {
|
||||
|
||||
@@ -2,6 +2,7 @@ from manimlib.constants import *
|
||||
from manimlib.mobject.svg.tex_mobject import SingleStringTex
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.iterables import hash_obj
|
||||
|
||||
|
||||
string_to_mob_map = {}
|
||||
@@ -20,23 +21,23 @@ class DecimalNumber(VMobject):
|
||||
"include_background_rectangle": False,
|
||||
"edge_to_fix": LEFT,
|
||||
"font_size": 48,
|
||||
"text_config": {} # Do not pass in font_size here
|
||||
}
|
||||
|
||||
def __init__(self, number=0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_submobjects_from_number(number)
|
||||
self.init_colors()
|
||||
|
||||
def set_submobjects_from_number(self, number):
|
||||
self.number = number
|
||||
self.set_submobjects([])
|
||||
|
||||
string_to_mob_ = lambda s: self.string_to_mob(s, **self.text_config)
|
||||
num_string = self.get_num_string(number)
|
||||
self.add(*map(self.string_to_mob, num_string))
|
||||
self.add(*map(string_to_mob_, num_string))
|
||||
|
||||
# Add non-numerical bits
|
||||
if self.show_ellipsis:
|
||||
dots = self.string_to_mob("...")
|
||||
dots = string_to_mob_("...")
|
||||
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
|
||||
self.add(dots)
|
||||
if self.unit is not None:
|
||||
@@ -85,10 +86,10 @@ class DecimalNumber(VMobject):
|
||||
def get_font_size(self):
|
||||
return self.data["font_size"][0]
|
||||
|
||||
def string_to_mob(self, string, mob_class=Text):
|
||||
if string not in string_to_mob_map:
|
||||
string_to_mob_map[string] = mob_class(string, font_size=1)
|
||||
mob = string_to_mob_map[string].copy()
|
||||
def string_to_mob(self, string, mob_class=Text, **kwargs):
|
||||
if (string, hash_obj(kwargs)) not in string_to_mob_map:
|
||||
string_to_mob_map[(string, hash_obj(kwargs))] = mob_class(string, font_size=1, **kwargs)
|
||||
mob = string_to_mob_map[(string, hash_obj(kwargs))].copy()
|
||||
mob.scale(self.get_font_size())
|
||||
return mob
|
||||
|
||||
|
||||
@@ -149,7 +149,9 @@ class BarChart(VGroup):
|
||||
"height": 4,
|
||||
"width": 6,
|
||||
"n_ticks": 4,
|
||||
"include_x_ticks": False,
|
||||
"tick_width": 0.2,
|
||||
"tick_height": 0.15,
|
||||
"label_y_axis": True,
|
||||
"y_axis_label_height": 0.25,
|
||||
"max_value": 1,
|
||||
@@ -165,6 +167,7 @@ class BarChart(VGroup):
|
||||
if self.max_value is None:
|
||||
self.max_value = max(values)
|
||||
|
||||
self.n_ticks_x = len(values)
|
||||
self.add_axes()
|
||||
self.add_bars(values)
|
||||
self.center()
|
||||
@@ -172,31 +175,42 @@ class BarChart(VGroup):
|
||||
def add_axes(self):
|
||||
x_axis = Line(self.tick_width * LEFT / 2, self.width * RIGHT)
|
||||
y_axis = Line(MED_LARGE_BUFF * DOWN, self.height * UP)
|
||||
ticks = VGroup()
|
||||
y_ticks = VGroup()
|
||||
heights = np.linspace(0, self.height, self.n_ticks + 1)
|
||||
values = np.linspace(0, self.max_value, self.n_ticks + 1)
|
||||
for y, value in zip(heights, values):
|
||||
tick = Line(LEFT, RIGHT)
|
||||
tick.set_width(self.tick_width)
|
||||
tick.move_to(y * UP)
|
||||
ticks.add(tick)
|
||||
y_axis.add(ticks)
|
||||
y_tick = Line(LEFT, RIGHT)
|
||||
y_tick.set_width(self.tick_width)
|
||||
y_tick.move_to(y * UP)
|
||||
y_ticks.add(y_tick)
|
||||
y_axis.add(y_ticks)
|
||||
|
||||
if self.include_x_ticks == True:
|
||||
x_ticks = VGroup()
|
||||
widths = np.linspace(0, self.width, self.n_ticks_x + 1)
|
||||
label_values = np.linspace(0, len(self.bar_names), self.n_ticks_x + 1)
|
||||
for x, value in zip(widths, label_values):
|
||||
x_tick = Line(UP, DOWN)
|
||||
x_tick.set_height(self.tick_height)
|
||||
x_tick.move_to(x * RIGHT)
|
||||
x_ticks.add(x_tick)
|
||||
x_axis.add(x_ticks)
|
||||
|
||||
self.add(x_axis, y_axis)
|
||||
self.x_axis, self.y_axis = x_axis, y_axis
|
||||
|
||||
if self.label_y_axis:
|
||||
labels = VGroup()
|
||||
for tick, value in zip(ticks, values):
|
||||
for y_tick, value in zip(y_ticks, values):
|
||||
label = Tex(str(np.round(value, 2)))
|
||||
label.set_height(self.y_axis_label_height)
|
||||
label.next_to(tick, LEFT, SMALL_BUFF)
|
||||
label.next_to(y_tick, LEFT, SMALL_BUFF)
|
||||
labels.add(label)
|
||||
self.y_axis_labels = labels
|
||||
self.add(labels)
|
||||
|
||||
def add_bars(self, values):
|
||||
buff = float(self.width) / (2 * len(values) + 1)
|
||||
buff = float(self.width) / (2 * len(values))
|
||||
bars = VGroup()
|
||||
for i, value in enumerate(values):
|
||||
bar = Rectangle(
|
||||
@@ -205,7 +219,7 @@ class BarChart(VGroup):
|
||||
stroke_width=self.bar_stroke_width,
|
||||
fill_opacity=self.bar_fill_opacity,
|
||||
)
|
||||
bar.move_to((2 * i + 1) * buff * RIGHT, DOWN + LEFT)
|
||||
bar.move_to((2 * i + 0.5) * buff * RIGHT, DOWN + LEFT * 5)
|
||||
bars.add(bar)
|
||||
bars.set_color_by_gradient(*self.bar_colors)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import numpy as np
|
||||
import math
|
||||
import copy
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.constants import *
|
||||
@@ -88,6 +89,7 @@ class BraceLabel(VMobject):
|
||||
CONFIG = {
|
||||
"label_constructor": Tex,
|
||||
"label_scale": 1,
|
||||
"label_buff": DEFAULT_MOBJECT_TO_MOBJECT_BUFFER
|
||||
}
|
||||
|
||||
def __init__(self, obj, text, brace_direction=DOWN, **kwargs):
|
||||
@@ -104,7 +106,7 @@ class BraceLabel(VMobject):
|
||||
if self.label_scale != 1:
|
||||
self.label.scale(self.label_scale)
|
||||
|
||||
self.brace.put_at_tip(self.label)
|
||||
self.brace.put_at_tip(self.label, buff=self.label_buff)
|
||||
self.set_submobjects([self.brace, self.label])
|
||||
|
||||
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):
|
||||
|
||||
@@ -50,6 +50,7 @@ class Lightbulb(SVGMobject):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("lightbulb", **kwargs)
|
||||
self.insert_n_curves(25)
|
||||
|
||||
|
||||
class Speedometer(VMobject):
|
||||
@@ -200,12 +201,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)
|
||||
|
||||
603
manimlib/mobject/svg/mtex_mobject.py
Normal file
603
manimlib/mobject/svg/mtex_mobject.py
Normal file
@@ -0,0 +1,603 @@
|
||||
import itertools as it
|
||||
import re
|
||||
from types import MethodType
|
||||
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.color import color_to_int_rgb
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
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
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
|
||||
|
||||
def _get_neighbouring_pairs(iterable):
|
||||
return list(adjacent_pairs(iterable))[:-1]
|
||||
|
||||
|
||||
class _TexParser(object):
|
||||
def __init__(self, tex_string, additional_substrings):
|
||||
self.tex_string = tex_string
|
||||
self.whitespace_indices = self.get_whitespace_indices()
|
||||
self.backslash_indices = self.get_backslash_indices()
|
||||
self.script_indices = self.get_script_indices()
|
||||
self.brace_indices_dict = self.get_brace_indices_dict()
|
||||
self.tex_span_list = []
|
||||
self.script_span_to_char_dict = {}
|
||||
self.script_span_to_tex_span_dict = {}
|
||||
self.neighbouring_script_span_pairs = []
|
||||
self.specified_substrings = []
|
||||
self.add_tex_span((0, len(tex_string)))
|
||||
self.break_up_by_scripts()
|
||||
self.break_up_by_double_braces()
|
||||
self.break_up_by_additional_substrings(additional_substrings)
|
||||
self.tex_span_list.sort(key=lambda t: (t[0], -t[1]))
|
||||
self.specified_substrings = remove_list_redundancies(
|
||||
self.specified_substrings
|
||||
)
|
||||
self.containing_labels_dict = self.get_containing_labels_dict()
|
||||
|
||||
def add_tex_span(self, tex_span):
|
||||
if tex_span not in self.tex_span_list:
|
||||
self.tex_span_list.append(tex_span)
|
||||
|
||||
def get_whitespace_indices(self):
|
||||
return [
|
||||
match_obj.start()
|
||||
for match_obj in re.finditer(r"\s", self.tex_string)
|
||||
]
|
||||
|
||||
def get_backslash_indices(self):
|
||||
# Newlines (`\\`) don't count.
|
||||
return [
|
||||
match_obj.end() - 1
|
||||
for match_obj in re.finditer(r"\\+", self.tex_string)
|
||||
if len(match_obj.group()) % 2 == 1
|
||||
]
|
||||
|
||||
def filter_out_escaped_characters(self, indices):
|
||||
return list(filter(
|
||||
lambda index: index - 1 not in self.backslash_indices,
|
||||
indices
|
||||
))
|
||||
|
||||
def get_script_indices(self):
|
||||
return self.filter_out_escaped_characters([
|
||||
match_obj.start()
|
||||
for match_obj in re.finditer(r"[_^]", self.tex_string)
|
||||
])
|
||||
|
||||
def get_brace_indices_dict(self):
|
||||
tex_string = self.tex_string
|
||||
indices = self.filter_out_escaped_characters([
|
||||
match_obj.start()
|
||||
for match_obj in re.finditer(r"[{}]", tex_string)
|
||||
])
|
||||
result = {}
|
||||
left_brace_indices_stack = []
|
||||
for index in indices:
|
||||
if tex_string[index] == "{":
|
||||
left_brace_indices_stack.append(index)
|
||||
else:
|
||||
left_brace_index = left_brace_indices_stack.pop()
|
||||
result[left_brace_index] = index
|
||||
return result
|
||||
|
||||
def break_up_by_scripts(self):
|
||||
# Match subscripts & superscripts.
|
||||
tex_string = self.tex_string
|
||||
whitespace_indices = self.whitespace_indices
|
||||
brace_indices_dict = self.brace_indices_dict
|
||||
script_spans = []
|
||||
for script_index in self.script_indices:
|
||||
script_char = tex_string[script_index]
|
||||
extended_begin = script_index
|
||||
while extended_begin - 1 in whitespace_indices:
|
||||
extended_begin -= 1
|
||||
script_begin = script_index + 1
|
||||
while script_begin in whitespace_indices:
|
||||
script_begin += 1
|
||||
if script_begin in brace_indices_dict.keys():
|
||||
script_end = brace_indices_dict[script_begin] + 1
|
||||
else:
|
||||
pattern = re.compile(r"[a-zA-Z0-9]|\\[a-zA-Z]+")
|
||||
match_obj = pattern.match(tex_string, pos=script_begin)
|
||||
if not match_obj:
|
||||
script_name = {
|
||||
"_": "subscript",
|
||||
"^": "superscript"
|
||||
}[script_char]
|
||||
log.warning(
|
||||
f"Unclear {script_name} detected while parsing. "
|
||||
"Please use braces to clarify"
|
||||
)
|
||||
continue
|
||||
script_end = match_obj.end()
|
||||
tex_span = (script_begin, script_end)
|
||||
script_span = (extended_begin, script_end)
|
||||
script_spans.append(script_span)
|
||||
self.add_tex_span(tex_span)
|
||||
self.script_span_to_char_dict[script_span] = script_char
|
||||
self.script_span_to_tex_span_dict[script_span] = tex_span
|
||||
|
||||
if not script_spans:
|
||||
return
|
||||
|
||||
_, sorted_script_spans = zip(*sorted([
|
||||
(index, script_span)
|
||||
for script_span in script_spans
|
||||
for index in script_span
|
||||
]))
|
||||
for span_0, span_1 in _get_neighbouring_pairs(sorted_script_spans):
|
||||
if span_0[1] == span_1[0]:
|
||||
self.neighbouring_script_span_pairs.append((span_0, span_1))
|
||||
|
||||
def break_up_by_double_braces(self):
|
||||
# Match paired double braces (`{{...}}`).
|
||||
tex_string = self.tex_string
|
||||
reversed_indices_dict = dict(
|
||||
item[::-1] for item in self.brace_indices_dict.items()
|
||||
)
|
||||
skip = False
|
||||
for prev_right_index, right_index in _get_neighbouring_pairs(
|
||||
list(reversed_indices_dict.keys())
|
||||
):
|
||||
if skip:
|
||||
skip = False
|
||||
continue
|
||||
if right_index != prev_right_index + 1:
|
||||
continue
|
||||
left_index = reversed_indices_dict[right_index]
|
||||
prev_left_index = reversed_indices_dict[prev_right_index]
|
||||
if left_index != prev_left_index - 1:
|
||||
continue
|
||||
tex_span = (left_index, right_index + 1)
|
||||
self.add_tex_span(tex_span)
|
||||
self.specified_substrings.append(tex_string[slice(*tex_span)])
|
||||
skip = True
|
||||
|
||||
def break_up_by_additional_substrings(self, additional_substrings):
|
||||
stripped_substrings = sorted(remove_list_redundancies([
|
||||
string.strip()
|
||||
for string in additional_substrings
|
||||
]))
|
||||
if "" in stripped_substrings:
|
||||
stripped_substrings.remove("")
|
||||
|
||||
tex_string = self.tex_string
|
||||
all_tex_spans = []
|
||||
for string in stripped_substrings:
|
||||
match_objs = list(re.finditer(re.escape(string), tex_string))
|
||||
if not match_objs:
|
||||
continue
|
||||
self.specified_substrings.append(string)
|
||||
for match_obj in match_objs:
|
||||
all_tex_spans.append(match_obj.span())
|
||||
|
||||
former_script_spans_dict = dict([
|
||||
script_span_pair[0][::-1]
|
||||
for script_span_pair in self.neighbouring_script_span_pairs
|
||||
])
|
||||
for span_begin, span_end in all_tex_spans:
|
||||
# Deconstruct spans containing one out of two scripts.
|
||||
if span_end in former_script_spans_dict.keys():
|
||||
span_end = former_script_spans_dict[span_end]
|
||||
if span_begin >= span_end:
|
||||
continue
|
||||
self.add_tex_span((span_begin, span_end))
|
||||
|
||||
def get_containing_labels_dict(self):
|
||||
tex_span_list = self.tex_span_list
|
||||
result = {
|
||||
tex_span: []
|
||||
for tex_span in tex_span_list
|
||||
}
|
||||
overlapping_tex_span_pairs = []
|
||||
for index_0, span_0 in enumerate(tex_span_list):
|
||||
for index_1, span_1 in enumerate(tex_span_list[index_0:]):
|
||||
if span_0[1] <= span_1[0]:
|
||||
continue
|
||||
if span_0[1] < span_1[1]:
|
||||
overlapping_tex_span_pairs.append((span_0, span_1))
|
||||
result[span_0].append(index_0 + index_1)
|
||||
if overlapping_tex_span_pairs:
|
||||
tex_string = self.tex_string
|
||||
log.error("Partially overlapping substrings detected:")
|
||||
for tex_span_pair in overlapping_tex_span_pairs:
|
||||
log.error(", ".join(
|
||||
f"\"{tex_string[slice(*tex_span)]}\""
|
||||
for tex_span in tex_span_pair
|
||||
))
|
||||
raise ValueError
|
||||
return result
|
||||
|
||||
def get_labelled_tex_string(self):
|
||||
indices, _, flags, labels = zip(*sorted([
|
||||
(*tex_span[::(1, -1)[flag]], flag, label)
|
||||
for label, tex_span in enumerate(self.tex_span_list)
|
||||
for flag in range(2)
|
||||
], key=lambda t: (t[0], -t[2], -t[1])))
|
||||
command_pieces = [
|
||||
("{{" + self.get_color_command(label), "}}")[flag]
|
||||
for flag, label in zip(flags, labels)
|
||||
][1:-1]
|
||||
command_pieces.insert(0, "")
|
||||
string_pieces = [
|
||||
self.tex_string[slice(*tex_span)]
|
||||
for tex_span in _get_neighbouring_pairs(indices)
|
||||
]
|
||||
return "".join(it.chain(*zip(command_pieces, string_pieces)))
|
||||
|
||||
@staticmethod
|
||||
def get_color_command(label):
|
||||
rg, b = divmod(label, 256)
|
||||
r, g = divmod(rg, 256)
|
||||
return "".join([
|
||||
"\\color[RGB]",
|
||||
"{",
|
||||
",".join(map(str, (r, g, b))),
|
||||
"}"
|
||||
])
|
||||
|
||||
def get_sorted_submob_indices(self, submob_labels):
|
||||
def script_span_to_submob_range(script_span):
|
||||
tex_span = self.script_span_to_tex_span_dict[script_span]
|
||||
submob_indices = [
|
||||
index for index, label in enumerate(submob_labels)
|
||||
if label in self.containing_labels_dict[tex_span]
|
||||
]
|
||||
return range(submob_indices[0], submob_indices[-1] + 1)
|
||||
|
||||
filtered_script_span_pairs = filter(
|
||||
lambda script_span_pair: all([
|
||||
self.script_span_to_char_dict[script_span] == character
|
||||
for script_span, character in zip(script_span_pair, "_^")
|
||||
]),
|
||||
self.neighbouring_script_span_pairs
|
||||
)
|
||||
switch_range_pairs = sorted([
|
||||
tuple([
|
||||
script_span_to_submob_range(script_span)
|
||||
for script_span in script_span_pair
|
||||
])
|
||||
for script_span_pair in filtered_script_span_pairs
|
||||
], key=lambda t: (t[0].stop, -t[0].start))
|
||||
result = list(range(len(submob_labels)))
|
||||
for range_0, range_1 in switch_range_pairs:
|
||||
result = [
|
||||
*result[:range_1.start],
|
||||
*result[range_0.start:range_0.stop],
|
||||
*result[range_1.stop:range_0.start],
|
||||
*result[range_1.start:range_1.stop],
|
||||
*result[range_0.stop:]
|
||||
]
|
||||
return result
|
||||
|
||||
def get_submob_tex_strings(self, submob_labels):
|
||||
ordered_tex_spans = [
|
||||
self.tex_span_list[label] for label in submob_labels
|
||||
]
|
||||
ordered_containing_labels = [
|
||||
self.containing_labels_dict[tex_span]
|
||||
for tex_span in ordered_tex_spans
|
||||
]
|
||||
ordered_span_begins, ordered_span_ends = zip(*ordered_tex_spans)
|
||||
string_span_begins = [
|
||||
prev_end if prev_label in containing_labels else curr_begin
|
||||
for prev_end, prev_label, containing_labels, curr_begin in zip(
|
||||
ordered_span_ends[:-1], submob_labels[:-1],
|
||||
ordered_containing_labels[1:], ordered_span_begins[1:]
|
||||
)
|
||||
]
|
||||
string_span_begins.insert(0, ordered_span_begins[0])
|
||||
string_span_ends = [
|
||||
next_begin if next_label in containing_labels else curr_end
|
||||
for next_begin, next_label, containing_labels, curr_end in zip(
|
||||
ordered_span_begins[1:], submob_labels[1:],
|
||||
ordered_containing_labels[:-1], ordered_span_ends[:-1]
|
||||
)
|
||||
]
|
||||
string_span_ends.append(ordered_span_ends[-1])
|
||||
|
||||
tex_string = self.tex_string
|
||||
left_brace_indices = sorted(self.brace_indices_dict.keys())
|
||||
right_brace_indices = sorted(self.brace_indices_dict.values())
|
||||
ignored_indices = sorted(it.chain(
|
||||
self.whitespace_indices,
|
||||
left_brace_indices,
|
||||
right_brace_indices,
|
||||
self.script_indices
|
||||
))
|
||||
result = []
|
||||
for span_begin, span_end in zip(string_span_begins, string_span_ends):
|
||||
while span_begin in ignored_indices:
|
||||
span_begin += 1
|
||||
if span_begin >= span_end:
|
||||
result.append("")
|
||||
continue
|
||||
while span_end - 1 in ignored_indices:
|
||||
span_end -= 1
|
||||
unclosed_left_brace = 0
|
||||
unclosed_right_brace = 0
|
||||
for index in range(span_begin, span_end):
|
||||
if index in left_brace_indices:
|
||||
unclosed_left_brace += 1
|
||||
elif index in right_brace_indices:
|
||||
if unclosed_left_brace == 0:
|
||||
unclosed_right_brace += 1
|
||||
else:
|
||||
unclosed_left_brace -= 1
|
||||
result.append("".join([
|
||||
unclosed_right_brace * "{",
|
||||
tex_string[span_begin:span_end],
|
||||
unclosed_left_brace * "}"
|
||||
]))
|
||||
return result
|
||||
|
||||
def find_span_components_of_custom_span(self, custom_span):
|
||||
skipped_indices = sorted(it.chain(
|
||||
self.whitespace_indices,
|
||||
self.script_indices
|
||||
))
|
||||
tex_span_choices = sorted(filter(
|
||||
lambda tex_span: all([
|
||||
tex_span[0] >= custom_span[0],
|
||||
tex_span[1] <= custom_span[1]
|
||||
]),
|
||||
self.tex_span_list
|
||||
))
|
||||
# Choose spans that reach the farthest.
|
||||
tex_span_choices_dict = dict(tex_span_choices)
|
||||
|
||||
span_begin, span_end = custom_span
|
||||
result = []
|
||||
while span_begin != span_end:
|
||||
if span_begin not in tex_span_choices_dict.keys():
|
||||
if span_begin in skipped_indices:
|
||||
span_begin += 1
|
||||
continue
|
||||
return None
|
||||
next_begin = tex_span_choices_dict[span_begin]
|
||||
result.append((span_begin, next_begin))
|
||||
span_begin = next_begin
|
||||
return result
|
||||
|
||||
def get_containing_labels_by_tex_spans(self, tex_spans):
|
||||
return remove_list_redundancies(list(it.chain(*[
|
||||
self.containing_labels_dict[tex_span]
|
||||
for tex_span in tex_spans
|
||||
])))
|
||||
|
||||
def get_specified_substrings(self):
|
||||
return self.specified_substrings
|
||||
|
||||
def get_isolated_substrings(self):
|
||||
return remove_list_redundancies([
|
||||
self.tex_string[slice(*tex_span)]
|
||||
for tex_span in self.tex_span_list
|
||||
])
|
||||
|
||||
|
||||
class _TexSVG(SVGMobject):
|
||||
CONFIG = {
|
||||
"height": None,
|
||||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0,
|
||||
"path_string_config": {
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class MTex(_TexSVG):
|
||||
CONFIG = {
|
||||
"color": WHITE,
|
||||
"font_size": 48,
|
||||
"alignment": "\\centering",
|
||||
"tex_environment": "align*",
|
||||
"isolate": [],
|
||||
"tex_to_color_map": {},
|
||||
"use_plain_tex": False,
|
||||
}
|
||||
|
||||
def __init__(self, tex_string, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
tex_string = tex_string.strip()
|
||||
# Prevent from passing an empty string.
|
||||
if not tex_string:
|
||||
tex_string = "\\quad"
|
||||
self.tex_string = tex_string
|
||||
self.parser = _TexParser(
|
||||
self.tex_string,
|
||||
[*self.tex_to_color_map.keys(), *self.isolate]
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
||||
|
||||
@property
|
||||
def hash_seed(self):
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.tex_string,
|
||||
self.parser.specified_substrings,
|
||||
self.alignment,
|
||||
self.tex_environment,
|
||||
self.use_plain_tex
|
||||
)
|
||||
|
||||
def get_file_path(self):
|
||||
return self._get_file_path(self.use_plain_tex)
|
||||
|
||||
def _get_file_path(self, use_plain_tex):
|
||||
if use_plain_tex:
|
||||
tex_string = self.tex_string
|
||||
else:
|
||||
tex_string = self.parser.get_labelled_tex_string()
|
||||
|
||||
full_tex = self.get_tex_file_body(tex_string)
|
||||
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||
file_path = self.tex_to_svg_file_path(full_tex)
|
||||
return file_path
|
||||
|
||||
def get_tex_file_body(self, tex_string):
|
||||
if self.tex_environment:
|
||||
tex_string = "\n".join([
|
||||
f"\\begin{{{self.tex_environment}}}",
|
||||
tex_string,
|
||||
f"\\end{{{self.tex_environment}}}"
|
||||
])
|
||||
if self.alignment:
|
||||
tex_string = "\n".join([self.alignment, tex_string])
|
||||
|
||||
tex_config = get_tex_config()
|
||||
return tex_config["tex_body"].replace(
|
||||
tex_config["text_to_replace"],
|
||||
tex_string
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def tex_to_svg_file_path(tex_file_content):
|
||||
return tex_to_svg_file(tex_file_content)
|
||||
|
||||
def generate_mobject(self):
|
||||
super().generate_mobject()
|
||||
|
||||
if not self.use_plain_tex:
|
||||
labelled_svg_glyphs = self
|
||||
else:
|
||||
file_path = self._get_file_path(use_plain_tex=False)
|
||||
labelled_svg_glyphs = _TexSVG(file_path)
|
||||
|
||||
glyph_labels = [
|
||||
self.color_to_label(labelled_glyph.get_fill_color())
|
||||
for labelled_glyph in labelled_svg_glyphs
|
||||
]
|
||||
mob = self.build_mobject(self, glyph_labels)
|
||||
self.set_submobjects(mob.submobjects)
|
||||
|
||||
@staticmethod
|
||||
def color_to_label(color):
|
||||
r, g, b = color_to_int_rgb(color)
|
||||
rg = r * 256 + g
|
||||
return rg * 256 + b
|
||||
|
||||
def build_mobject(self, svg_glyphs, glyph_labels):
|
||||
if not svg_glyphs:
|
||||
return VGroup()
|
||||
|
||||
# Simply pack together adjacent mobjects with the same label.
|
||||
submobjects = []
|
||||
submob_labels = []
|
||||
new_glyphs = []
|
||||
current_glyph_label = glyph_labels[0]
|
||||
for glyph, label in zip(svg_glyphs, glyph_labels):
|
||||
if label == current_glyph_label:
|
||||
new_glyphs.append(glyph)
|
||||
else:
|
||||
submobject = VGroup(*new_glyphs)
|
||||
submob_labels.append(current_glyph_label)
|
||||
submobjects.append(submobject)
|
||||
new_glyphs = [glyph]
|
||||
current_glyph_label = label
|
||||
submobject = VGroup(*new_glyphs)
|
||||
submob_labels.append(current_glyph_label)
|
||||
submobjects.append(submobject)
|
||||
|
||||
indices = self.parser.get_sorted_submob_indices(submob_labels)
|
||||
rearranged_submobjects = [submobjects[index] for index in indices]
|
||||
rearranged_labels = [submob_labels[index] for index in indices]
|
||||
|
||||
submob_tex_strings = self.parser.get_submob_tex_strings(
|
||||
rearranged_labels
|
||||
)
|
||||
for submob, label, submob_tex in zip(
|
||||
rearranged_submobjects, rearranged_labels, submob_tex_strings
|
||||
):
|
||||
submob.submob_label = label
|
||||
submob.tex_string = submob_tex
|
||||
# Support `get_tex()` method here.
|
||||
submob.get_tex = MethodType(lambda inst: inst.tex_string, submob)
|
||||
return VGroup(*rearranged_submobjects)
|
||||
|
||||
def get_part_by_tex_spans(self, tex_spans):
|
||||
labels = self.parser.get_containing_labels_by_tex_spans(tex_spans)
|
||||
return VGroup(*filter(
|
||||
lambda submob: submob.submob_label in labels,
|
||||
self.submobjects
|
||||
))
|
||||
|
||||
def get_part_by_custom_span(self, custom_span):
|
||||
tex_spans = self.parser.find_span_components_of_custom_span(
|
||||
custom_span
|
||||
)
|
||||
if tex_spans is None:
|
||||
tex = self.tex_string[slice(*custom_span)]
|
||||
raise ValueError(f"Failed to match mobjects from tex: \"{tex}\"")
|
||||
return self.get_part_by_tex_spans(tex_spans)
|
||||
|
||||
def get_parts_by_tex(self, tex):
|
||||
return VGroup(*[
|
||||
self.get_part_by_custom_span(match_obj.span())
|
||||
for match_obj in re.finditer(
|
||||
re.escape(tex.strip()), 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 tex_to_color_map.items():
|
||||
self.set_color_by_tex(tex, color)
|
||||
return self
|
||||
|
||||
def indices_of_part(self, part):
|
||||
indices = [
|
||||
index for index, 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 get_tex(self):
|
||||
return self.tex_string
|
||||
|
||||
def get_submob_tex(self):
|
||||
return [
|
||||
submob.get_tex()
|
||||
for submob in self.submobjects
|
||||
]
|
||||
|
||||
def get_specified_substrings(self):
|
||||
return self.parser.get_specified_substrings()
|
||||
|
||||
def get_isolated_substrings(self):
|
||||
return self.parser.get_isolated_substrings()
|
||||
|
||||
|
||||
class MTexText(MTex):
|
||||
CONFIG = {
|
||||
"tex_environment": None,
|
||||
}
|
||||
@@ -1,38 +1,31 @@
|
||||
import itertools as it
|
||||
import re
|
||||
import string
|
||||
import warnings
|
||||
import os
|
||||
import hashlib
|
||||
import itertools as it
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from xml.dom import minidom
|
||||
|
||||
from manimlib.constants import DEFAULT_STROKE_WIDTH
|
||||
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import DEGREES, PI
|
||||
import svgelements as se
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Polygon
|
||||
from manimlib.mobject.geometry import Polyline
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.geometry import RoundedRectangle
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.color import *
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.directories import get_mobject_data_dir
|
||||
from manimlib.utils.images import get_full_vector_image_path
|
||||
from manimlib.utils.simple_functions import clip
|
||||
from manimlib.utils.iterables import hash_obj
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
def string_to_numbers(num_string):
|
||||
num_string = num_string.replace("-", ",-")
|
||||
num_string = num_string.replace("e,-", "e-")
|
||||
return [
|
||||
float(s)
|
||||
for s in re.split("[ ,]", num_string)
|
||||
if s != ""
|
||||
]
|
||||
SVG_HASH_TO_MOB_MAP = {}
|
||||
|
||||
|
||||
def _convert_point_to_3d(x, y):
|
||||
return np.array([x, y, 0.0])
|
||||
|
||||
|
||||
class SVGMobject(VMobject):
|
||||
@@ -40,25 +33,234 @@ class SVGMobject(VMobject):
|
||||
"should_center": True,
|
||||
"height": 2,
|
||||
"width": None,
|
||||
# Must be filled in in a subclass, or when called
|
||||
"file_name": None,
|
||||
"unpack_groups": True, # if False, creates a hierarchy of VGroups
|
||||
# TODO, style components should be read in, not defaulted
|
||||
"stroke_width": DEFAULT_STROKE_WIDTH,
|
||||
"fill_opacity": 1.0,
|
||||
"path_string_config": {}
|
||||
# Style that overrides the original svg
|
||||
"color": None,
|
||||
"opacity": None,
|
||||
"fill_color": None,
|
||||
"fill_opacity": None,
|
||||
"stroke_width": None,
|
||||
"stroke_color": None,
|
||||
"stroke_opacity": None,
|
||||
# Style that fills only when not specified
|
||||
# If None, regarded as default values from svg standard
|
||||
"svg_default": {
|
||||
"color": None,
|
||||
"opacity": None,
|
||||
"fill_color": None,
|
||||
"fill_opacity": None,
|
||||
"stroke_width": None,
|
||||
"stroke_color": None,
|
||||
"stroke_opacity": None,
|
||||
},
|
||||
"path_string_config": {},
|
||||
}
|
||||
|
||||
def __init__(self, file_name=None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.file_name = file_name or self.file_name
|
||||
if file_name is None:
|
||||
raise Exception("Must specify file for SVGMobject")
|
||||
self.file_path = get_full_vector_image_path(file_name)
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self.file_name = file_name or self.file_name
|
||||
self.init_svg_mobject()
|
||||
self.init_colors()
|
||||
self.move_into_position()
|
||||
|
||||
def init_svg_mobject(self):
|
||||
hash_val = hash_obj(self.hash_seed)
|
||||
if hash_val in SVG_HASH_TO_MOB_MAP:
|
||||
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
|
||||
self.add(*mob)
|
||||
return
|
||||
|
||||
self.generate_mobject()
|
||||
SVG_HASH_TO_MOB_MAP[hash_val] = self.copy()
|
||||
|
||||
@property
|
||||
def hash_seed(self):
|
||||
# Returns data which can uniquely represent the result of `init_points`.
|
||||
# The hashed value of it is stored as a key in `SVG_HASH_TO_MOB_MAP`.
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.file_name
|
||||
)
|
||||
|
||||
def generate_mobject(self):
|
||||
file_path = self.get_file_path()
|
||||
element_tree = ET.parse(file_path)
|
||||
new_tree = self.modify_xml_tree(element_tree)
|
||||
# Create a temporary svg file to dump modified svg to be parsed
|
||||
modified_file_path = file_path.replace(".svg", "_.svg")
|
||||
new_tree.write(modified_file_path)
|
||||
|
||||
svg = se.SVG.parse(modified_file_path)
|
||||
os.remove(modified_file_path)
|
||||
|
||||
mobjects = self.get_mobjects_from(svg)
|
||||
self.add(*mobjects)
|
||||
self.flip(RIGHT) # Flip y
|
||||
|
||||
def get_file_path(self):
|
||||
if self.file_name is None:
|
||||
raise Exception("Must specify file for SVGMobject")
|
||||
return get_full_vector_image_path(self.file_name)
|
||||
|
||||
def modify_xml_tree(self, element_tree):
|
||||
config_style_dict = self.generate_config_style_dict()
|
||||
style_keys = (
|
||||
"fill",
|
||||
"fill-opacity",
|
||||
"stroke",
|
||||
"stroke-opacity",
|
||||
"stroke-width",
|
||||
"style"
|
||||
)
|
||||
root = element_tree.getroot()
|
||||
root_style_dict = {
|
||||
k: v for k, v in root.attrib.items()
|
||||
if k in style_keys
|
||||
}
|
||||
|
||||
new_root = ET.Element("svg", {})
|
||||
config_style_node = ET.SubElement(new_root, "g", config_style_dict)
|
||||
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
|
||||
root_style_node.extend(root)
|
||||
return ET.ElementTree(new_root)
|
||||
|
||||
def generate_config_style_dict(self):
|
||||
keys_converting_dict = {
|
||||
"fill": ("color", "fill_color"),
|
||||
"fill-opacity": ("opacity", "fill_opacity"),
|
||||
"stroke": ("color", "stroke_color"),
|
||||
"stroke-opacity": ("opacity", "stroke_opacity"),
|
||||
"stroke-width": ("stroke_width",)
|
||||
}
|
||||
svg_default_dict = self.svg_default
|
||||
result = {}
|
||||
for svg_key, style_keys in keys_converting_dict.items():
|
||||
for style_key in style_keys:
|
||||
if svg_default_dict[style_key] is None:
|
||||
continue
|
||||
result[svg_key] = str(svg_default_dict[style_key])
|
||||
return result
|
||||
|
||||
def get_mobjects_from(self, svg):
|
||||
result = []
|
||||
for shape in svg.elements():
|
||||
if isinstance(shape, se.Group):
|
||||
continue
|
||||
mob = self.get_mobject_from(shape)
|
||||
if mob is None:
|
||||
continue
|
||||
if isinstance(shape, se.Transformable) and shape.apply:
|
||||
self.handle_transform(mob, shape.transform)
|
||||
result.append(mob)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def handle_transform(mob, matrix):
|
||||
mat = np.array([
|
||||
[matrix.a, matrix.c],
|
||||
[matrix.b, matrix.d]
|
||||
])
|
||||
vec = np.array([matrix.e, matrix.f, 0.0])
|
||||
mob.apply_matrix(mat)
|
||||
mob.shift(vec)
|
||||
return mob
|
||||
|
||||
def get_mobject_from(self, shape):
|
||||
shape_class_to_func_map = {
|
||||
se.Path: self.path_to_mobject,
|
||||
se.SimpleLine: self.line_to_mobject,
|
||||
se.Rect: self.rect_to_mobject,
|
||||
se.Circle: self.circle_to_mobject,
|
||||
se.Ellipse: self.ellipse_to_mobject,
|
||||
se.Polygon: self.polygon_to_mobject,
|
||||
se.Polyline: self.polyline_to_mobject,
|
||||
# se.Text: self.text_to_mobject, # TODO
|
||||
}
|
||||
for shape_class, func in shape_class_to_func_map.items():
|
||||
if isinstance(shape, shape_class):
|
||||
mob = func(shape)
|
||||
self.apply_style_to_mobject(mob, shape)
|
||||
return mob
|
||||
|
||||
shape_class_name = shape.__class__.__name__
|
||||
if shape_class_name != "SVGElement":
|
||||
log.warning(f"Unsupported element type: {shape_class_name}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def apply_style_to_mobject(mob, shape):
|
||||
mob.set_style(
|
||||
stroke_width=shape.stroke_width,
|
||||
stroke_color=shape.stroke.hex,
|
||||
stroke_opacity=shape.stroke.opacity,
|
||||
fill_color=shape.fill.hex,
|
||||
fill_opacity=shape.fill.opacity
|
||||
)
|
||||
return mob
|
||||
|
||||
def path_to_mobject(self, path):
|
||||
return VMobjectFromSVGPath(path, **self.path_string_config)
|
||||
|
||||
def line_to_mobject(self, line):
|
||||
return Line(
|
||||
start=_convert_point_to_3d(line.x1, line.y1),
|
||||
end=_convert_point_to_3d(line.x2, line.y2)
|
||||
)
|
||||
|
||||
def rect_to_mobject(self, rect):
|
||||
if rect.rx == 0 or rect.ry == 0:
|
||||
mob = Rectangle(
|
||||
width=rect.width,
|
||||
height=rect.height,
|
||||
)
|
||||
else:
|
||||
mob = RoundedRectangle(
|
||||
width=rect.width,
|
||||
height=rect.height * rect.rx / rect.ry,
|
||||
corner_radius=rect.rx
|
||||
)
|
||||
mob.stretch_to_fit_height(rect.height)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
rect.x + rect.width / 2,
|
||||
rect.y + rect.height / 2
|
||||
))
|
||||
return mob
|
||||
|
||||
def circle_to_mobject(self, circle):
|
||||
# svgelements supports `rx` & `ry` but `r`
|
||||
mob = Circle(radius=circle.rx)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
circle.cx, circle.cy
|
||||
))
|
||||
return mob
|
||||
|
||||
def ellipse_to_mobject(self, ellipse):
|
||||
mob = Circle(radius=ellipse.rx)
|
||||
mob.stretch_to_fit_height(2 * ellipse.ry)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
ellipse.cx, ellipse.cy
|
||||
))
|
||||
return mob
|
||||
|
||||
def polygon_to_mobject(self, polygon):
|
||||
points = [
|
||||
_convert_point_to_3d(*point)
|
||||
for point in polygon
|
||||
]
|
||||
return Polygon(*points)
|
||||
|
||||
def polyline_to_mobject(self, polyline):
|
||||
points = [
|
||||
_convert_point_to_3d(*point)
|
||||
for point in polyline
|
||||
]
|
||||
return Polyline(*points)
|
||||
|
||||
def text_to_mobject(self, text):
|
||||
pass
|
||||
|
||||
def move_into_position(self):
|
||||
if self.should_center:
|
||||
self.center()
|
||||
@@ -67,275 +269,26 @@ class SVGMobject(VMobject):
|
||||
if self.width is not None:
|
||||
self.set_width(self.width)
|
||||
|
||||
def init_points(self):
|
||||
doc = minidom.parse(self.file_path)
|
||||
self.ref_to_element = {}
|
||||
|
||||
for svg in doc.getElementsByTagName("svg"):
|
||||
mobjects = self.get_mobjects_from(svg)
|
||||
if self.unpack_groups:
|
||||
self.add(*mobjects)
|
||||
else:
|
||||
self.add(*mobjects[0].submobjects)
|
||||
doc.unlink()
|
||||
|
||||
def get_mobjects_from(self, element):
|
||||
result = []
|
||||
if not isinstance(element, minidom.Element):
|
||||
return result
|
||||
if element.tagName == 'defs':
|
||||
self.update_ref_to_element(element)
|
||||
elif element.tagName == 'style':
|
||||
pass # TODO, handle style
|
||||
elif element.tagName in ['g', 'svg', 'symbol']:
|
||||
result += it.chain(*(
|
||||
self.get_mobjects_from(child)
|
||||
for child in element.childNodes
|
||||
))
|
||||
elif element.tagName == 'path':
|
||||
result.append(self.path_string_to_mobject(
|
||||
element.getAttribute('d')
|
||||
))
|
||||
elif element.tagName == 'use':
|
||||
result += self.use_to_mobjects(element)
|
||||
elif element.tagName == 'rect':
|
||||
result.append(self.rect_to_mobject(element))
|
||||
elif element.tagName == 'circle':
|
||||
result.append(self.circle_to_mobject(element))
|
||||
elif element.tagName == 'ellipse':
|
||||
result.append(self.ellipse_to_mobject(element))
|
||||
elif element.tagName in ['polygon', 'polyline']:
|
||||
result.append(self.polygon_to_mobject(element))
|
||||
else:
|
||||
pass # TODO
|
||||
# warnings.warn("Unknown element type: " + element.tagName)
|
||||
result = [m for m in result if m is not None]
|
||||
self.handle_transforms(element, VGroup(*result))
|
||||
if len(result) > 1 and not self.unpack_groups:
|
||||
result = [VGroup(*result)]
|
||||
|
||||
return result
|
||||
|
||||
def g_to_mobjects(self, g_element):
|
||||
mob = VGroup(*self.get_mobjects_from(g_element))
|
||||
self.handle_transforms(g_element, mob)
|
||||
return mob.submobjects
|
||||
|
||||
def path_string_to_mobject(self, path_string):
|
||||
return VMobjectFromSVGPathstring(
|
||||
path_string,
|
||||
**self.path_string_config,
|
||||
)
|
||||
|
||||
def use_to_mobjects(self, use_element):
|
||||
# Remove initial "#" character
|
||||
ref = use_element.getAttribute("xlink:href")[1:]
|
||||
if ref not in self.ref_to_element:
|
||||
warnings.warn(f"{ref} not recognized")
|
||||
return VGroup()
|
||||
return self.get_mobjects_from(
|
||||
self.ref_to_element[ref]
|
||||
)
|
||||
|
||||
def attribute_to_float(self, attr):
|
||||
stripped_attr = "".join([
|
||||
char for char in attr
|
||||
if char in string.digits + "." + "-"
|
||||
])
|
||||
return float(stripped_attr)
|
||||
|
||||
def polygon_to_mobject(self, polygon_element):
|
||||
path_string = polygon_element.getAttribute("points")
|
||||
for digit in string.digits:
|
||||
path_string = path_string.replace(f" {digit}", f"L {digit}")
|
||||
path_string = path_string.replace("L", "M", 1)
|
||||
return self.path_string_to_mobject(path_string)
|
||||
|
||||
def circle_to_mobject(self, circle_element):
|
||||
x, y, r = [
|
||||
self.attribute_to_float(
|
||||
circle_element.getAttribute(key)
|
||||
)
|
||||
if circle_element.hasAttribute(key)
|
||||
else 0.0
|
||||
for key in ("cx", "cy", "r")
|
||||
]
|
||||
return Circle(radius=r).shift(x * RIGHT + y * DOWN)
|
||||
|
||||
def ellipse_to_mobject(self, circle_element):
|
||||
x, y, rx, ry = [
|
||||
self.attribute_to_float(
|
||||
circle_element.getAttribute(key)
|
||||
)
|
||||
if circle_element.hasAttribute(key)
|
||||
else 0.0
|
||||
for key in ("cx", "cy", "rx", "ry")
|
||||
]
|
||||
result = Circle()
|
||||
result.stretch(rx, 0)
|
||||
result.stretch(ry, 1)
|
||||
result.shift(x * RIGHT + y * DOWN)
|
||||
return result
|
||||
|
||||
def rect_to_mobject(self, rect_element):
|
||||
fill_color = rect_element.getAttribute("fill")
|
||||
stroke_color = rect_element.getAttribute("stroke")
|
||||
stroke_width = rect_element.getAttribute("stroke-width")
|
||||
corner_radius = rect_element.getAttribute("rx")
|
||||
|
||||
# input preprocessing
|
||||
fill_opacity = 1
|
||||
if fill_color in ["", "none", "#FFF", "#FFFFFF"] or Color(fill_color) == Color(WHITE):
|
||||
fill_opacity = 0
|
||||
fill_color = BLACK # shdn't be necessary but avoids error msgs
|
||||
if fill_color in ["#000", "#000000"]:
|
||||
fill_color = WHITE
|
||||
if stroke_color in ["", "none", "#FFF", "#FFFFFF"] or Color(stroke_color) == Color(WHITE):
|
||||
stroke_width = 0
|
||||
stroke_color = BLACK
|
||||
if stroke_color in ["#000", "#000000"]:
|
||||
stroke_color = WHITE
|
||||
if stroke_width in ["", "none", "0"]:
|
||||
stroke_width = 0
|
||||
|
||||
if corner_radius in ["", "0", "none"]:
|
||||
corner_radius = 0
|
||||
|
||||
corner_radius = float(corner_radius)
|
||||
|
||||
if corner_radius == 0:
|
||||
mob = Rectangle(
|
||||
width=self.attribute_to_float(
|
||||
rect_element.getAttribute("width")
|
||||
),
|
||||
height=self.attribute_to_float(
|
||||
rect_element.getAttribute("height")
|
||||
),
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity
|
||||
)
|
||||
else:
|
||||
mob = RoundedRectangle(
|
||||
width=self.attribute_to_float(
|
||||
rect_element.getAttribute("width")
|
||||
),
|
||||
height=self.attribute_to_float(
|
||||
rect_element.getAttribute("height")
|
||||
),
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=opacity,
|
||||
corner_radius=corner_radius
|
||||
)
|
||||
|
||||
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
|
||||
return mob
|
||||
|
||||
def handle_transforms(self, element, mobject):
|
||||
# TODO, this could use some cleaning...
|
||||
x, y = 0, 0
|
||||
try:
|
||||
x = self.attribute_to_float(element.getAttribute('x'))
|
||||
# Flip y
|
||||
y = -self.attribute_to_float(element.getAttribute('y'))
|
||||
mobject.shift([x, y, 0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
transform = element.getAttribute('transform')
|
||||
|
||||
try: # transform matrix
|
||||
prefix = "matrix("
|
||||
suffix = ")"
|
||||
if not transform.startswith(prefix) or not transform.endswith(suffix):
|
||||
raise Exception()
|
||||
transform = transform[len(prefix):-len(suffix)]
|
||||
transform = string_to_numbers(transform)
|
||||
transform = np.array(transform).reshape([3, 2])
|
||||
x = transform[2][0]
|
||||
y = -transform[2][1]
|
||||
matrix = np.identity(self.dim)
|
||||
matrix[:2, :2] = transform[:2, :]
|
||||
matrix[1] *= -1
|
||||
matrix[:, 1] *= -1
|
||||
|
||||
for mob in mobject.family_members_with_points():
|
||||
mob.apply_matrix(matrix.T)
|
||||
mobject.shift(x * RIGHT + y * UP)
|
||||
except:
|
||||
pass
|
||||
|
||||
try: # transform scale
|
||||
prefix = "scale("
|
||||
suffix = ")"
|
||||
if not transform.startswith(prefix) or not transform.endswith(suffix):
|
||||
raise Exception()
|
||||
transform = transform[len(prefix):-len(suffix)]
|
||||
scale_values = string_to_numbers(transform)
|
||||
if len(scale_values) == 2:
|
||||
scale_x, scale_y = scale_values
|
||||
mobject.scale(np.array([scale_x, scale_y, 1]), about_point=ORIGIN)
|
||||
elif len(scale_values) == 1:
|
||||
scale = scale_values[0]
|
||||
mobject.scale(np.array([scale, scale, 1]), about_point=ORIGIN)
|
||||
except:
|
||||
pass
|
||||
|
||||
try: # transform translate
|
||||
prefix = "translate("
|
||||
suffix = ")"
|
||||
if not transform.startswith(prefix) or not transform.endswith(suffix):
|
||||
raise Exception()
|
||||
transform = transform[len(prefix):-len(suffix)]
|
||||
x, y = string_to_numbers(transform)
|
||||
mobject.shift(x * RIGHT + y * DOWN)
|
||||
except:
|
||||
pass
|
||||
# TODO, ...
|
||||
|
||||
def flatten(self, input_list):
|
||||
output_list = []
|
||||
for i in input_list:
|
||||
if isinstance(i, list):
|
||||
output_list.extend(self.flatten(i))
|
||||
else:
|
||||
output_list.append(i)
|
||||
return output_list
|
||||
|
||||
def get_all_childNodes_have_id(self, element):
|
||||
all_childNodes_have_id = []
|
||||
if not isinstance(element, minidom.Element):
|
||||
return
|
||||
if element.hasAttribute('id'):
|
||||
return [element]
|
||||
for e in element.childNodes:
|
||||
all_childNodes_have_id.append(self.get_all_childNodes_have_id(e))
|
||||
return self.flatten([e for e in all_childNodes_have_id if e])
|
||||
|
||||
def update_ref_to_element(self, defs):
|
||||
new_refs = dict([(e.getAttribute('id'), e) for e in self.get_all_childNodes_have_id(defs)])
|
||||
self.ref_to_element.update(new_refs)
|
||||
|
||||
|
||||
class VMobjectFromSVGPathstring(VMobject):
|
||||
class VMobjectFromSVGPath(VMobject):
|
||||
CONFIG = {
|
||||
"long_lines": False,
|
||||
"should_subdivide_sharp_curves": False,
|
||||
"should_remove_null_curves": False,
|
||||
}
|
||||
|
||||
def __init__(self, path_string, **kwargs):
|
||||
self.path_string = path_string
|
||||
def __init__(self, path_obj, **kwargs):
|
||||
# Get rid of arcs
|
||||
path_obj.approximate_arcs_with_quads()
|
||||
self.path_obj = path_obj
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
# After a given svg_path has been converted into points, the result
|
||||
# will be saved to a file so that future calls for the same path
|
||||
# don't need to retrace the same computation.
|
||||
hasher = hashlib.sha256(self.path_string.encode())
|
||||
path_string = self.path_obj.d()
|
||||
hasher = hashlib.sha256(path_string.encode())
|
||||
path_hash = hasher.hexdigest()[:16]
|
||||
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
|
||||
tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy")
|
||||
@@ -345,211 +298,30 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
self.triangulation = np.load(tris_filepath)
|
||||
self.needs_new_triangulation = False
|
||||
else:
|
||||
self.relative_point = np.array(ORIGIN)
|
||||
for command, coord_string in self.get_commands_and_coord_strings():
|
||||
new_points = self.string_to_points(command, coord_string)
|
||||
self.handle_command(command, new_points)
|
||||
self.handle_commands()
|
||||
if self.should_subdivide_sharp_curves:
|
||||
# For a healthy triangulation later
|
||||
self.subdivide_sharp_curves()
|
||||
if self.should_remove_null_curves:
|
||||
# Get rid of any null curves
|
||||
self.set_points(self.get_points_without_null_curves())
|
||||
# SVG treats y-coordinate differently
|
||||
self.stretch(-1, 1, about_point=ORIGIN)
|
||||
# Save to a file for future use
|
||||
np.save(points_filepath, self.get_points())
|
||||
np.save(tris_filepath, self.get_triangulation())
|
||||
|
||||
def get_commands_and_coord_strings(self):
|
||||
all_commands = list(self.get_command_to_function_map().keys())
|
||||
all_commands += [c.lower() for c in all_commands]
|
||||
pattern = "[{}]".format("".join(all_commands))
|
||||
return zip(
|
||||
re.findall(pattern, self.path_string),
|
||||
re.split(pattern, self.path_string)[1:]
|
||||
)
|
||||
|
||||
def handle_command(self, command, new_points):
|
||||
if command.islower():
|
||||
# Treat it as a relative command
|
||||
if command == "a":
|
||||
# Only the last `self.dim` columns refer to points
|
||||
new_points[:, -self.dim:] += self.relative_point
|
||||
else:
|
||||
new_points += self.relative_point
|
||||
|
||||
func, n_points = self.command_to_function(command)
|
||||
command_points = new_points[:n_points]
|
||||
if command.upper() == "A":
|
||||
func(*command_points[0][:-self.dim], np.array(command_points[0][-self.dim:]))
|
||||
else:
|
||||
func(*command_points)
|
||||
leftover_points = new_points[n_points:]
|
||||
|
||||
# Recursively handle the rest of the points
|
||||
if len(leftover_points) > 0:
|
||||
if command.upper() == "M":
|
||||
# Treat following points as relative line coordinates
|
||||
command = "l"
|
||||
if command.islower():
|
||||
if command == "a":
|
||||
leftover_points[:, -self.dim:] -= self.relative_point
|
||||
else:
|
||||
leftover_points -= self.relative_point
|
||||
self.relative_point = self.get_last_point()
|
||||
self.handle_command(command, leftover_points)
|
||||
else:
|
||||
# Command is over, reset for future relative commands
|
||||
self.relative_point = self.get_last_point()
|
||||
|
||||
def string_to_points(self, command, coord_string):
|
||||
numbers = string_to_numbers(coord_string)
|
||||
if command.upper() == "A":
|
||||
# Only the last `self.dim` columns refer to points
|
||||
# Each "point" returned here has a size of `(5 + self.dim)`
|
||||
params = np.array(numbers).reshape((-1, 7))
|
||||
result = np.zeros((params.shape[0], 5 + self.dim))
|
||||
result[:, :7] = params
|
||||
return result
|
||||
if command.upper() in ["H", "V"]:
|
||||
i = {"H": 0, "V": 1}[command.upper()]
|
||||
xy = np.zeros((len(numbers), 2))
|
||||
xy[:, i] = numbers
|
||||
if command.isupper():
|
||||
xy[:, 1 - i] = self.relative_point[1 - i]
|
||||
else:
|
||||
xy = np.array(numbers).reshape((-1, 2))
|
||||
result = np.zeros((xy.shape[0], self.dim))
|
||||
result[:, :2] = xy
|
||||
return result
|
||||
|
||||
def add_elliptical_arc_to(self, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, point):
|
||||
"""
|
||||
In fact, this method only suits 2d VMobjects.
|
||||
"""
|
||||
def close_to_zero(a, threshold=1e-5):
|
||||
return abs(a) < threshold
|
||||
|
||||
def solve_2d_linear_equation(a, b, c):
|
||||
"""
|
||||
Using Crammer's rule to solve the linear equation `[a b]x = c`
|
||||
where `a`, `b` and `c` are all 2d vectors.
|
||||
"""
|
||||
def det(a, b):
|
||||
return a[0] * b[1] - a[1] * b[0]
|
||||
d = det(a, b)
|
||||
if close_to_zero(d):
|
||||
raise Exception("Cannot handle 0 determinant.")
|
||||
return [det(c, b) / d, det(a, c) / d]
|
||||
|
||||
def get_arc_center_and_angles(x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, x1, y1):
|
||||
"""
|
||||
The parameter functions of an ellipse rotated `phi` radians counterclockwise is (on `alpha`):
|
||||
x = cx + rx * cos(alpha) * cos(phi) + ry * sin(alpha) * sin(phi),
|
||||
y = cy + rx * cos(alpha) * sin(phi) - ry * sin(alpha) * cos(phi).
|
||||
Now we have two points sitting on the ellipse: `(x0, y0)`, `(x1, y1)`, corresponding to 4 equations,
|
||||
and we want to hunt for 4 variables: `cx`, `cy`, `alpha0` and `alpha_1`.
|
||||
Let `d_alpha = alpha1 - alpha0`, then:
|
||||
if `sweep_flag = 0` and `large_arc_flag = 1`, then `PI <= d_alpha < 2 * PI`;
|
||||
if `sweep_flag = 0` and `large_arc_flag = 0`, then `0 < d_alpha <= PI`;
|
||||
if `sweep_flag = 1` and `large_arc_flag = 0`, then `-PI <= d_alpha < 0`;
|
||||
if `sweep_flag = 1` and `large_arc_flag = 1`, then `-2 * PI < d_alpha <= -PI`.
|
||||
"""
|
||||
xd = x1 - x0
|
||||
yd = y1 - y0
|
||||
if close_to_zero(xd) and close_to_zero(yd):
|
||||
raise Exception("Cannot find arc center since the start point and the end point meet.")
|
||||
# Find `p = cos(alpha1) - cos(alpha0)`, `q = sin(alpha1) - sin(alpha0)`
|
||||
eq0 = [rx * np.cos(phi), ry * np.sin(phi), xd]
|
||||
eq1 = [rx * np.sin(phi), -ry * np.cos(phi), yd]
|
||||
p, q = solve_2d_linear_equation(*zip(eq0, eq1))
|
||||
# Find `s = (alpha1 - alpha0) / 2`, `t = (alpha1 + alpha0) / 2`
|
||||
# If `sin(s) = 0`, this requires `p = q = 0`,
|
||||
# implying `xd = yd = 0`, which is impossible.
|
||||
sin_s = (p ** 2 + q ** 2) ** 0.5 / 2
|
||||
if sweep_flag:
|
||||
sin_s = -sin_s
|
||||
sin_s = clip(sin_s, -1, 1)
|
||||
s = np.arcsin(sin_s)
|
||||
if large_arc_flag:
|
||||
if not sweep_flag:
|
||||
s = PI - s
|
||||
else:
|
||||
s = -PI - s
|
||||
sin_t = -p / (2 * sin_s)
|
||||
cos_t = q / (2 * sin_s)
|
||||
cos_t = clip(cos_t, -1, 1)
|
||||
t = np.arccos(cos_t)
|
||||
if sin_t <= 0:
|
||||
t = -t
|
||||
# We can make sure `0 < abs(s) < PI`, `-PI <= t < PI`.
|
||||
alpha0 = t - s
|
||||
alpha_1 = t + s
|
||||
cx = x0 - rx * np.cos(alpha0) * np.cos(phi) - ry * np.sin(alpha0) * np.sin(phi)
|
||||
cy = y0 - rx * np.cos(alpha0) * np.sin(phi) + ry * np.sin(alpha0) * np.cos(phi)
|
||||
return cx, cy, alpha0, alpha_1
|
||||
|
||||
def get_point_on_ellipse(cx, cy, rx, ry, phi, angle):
|
||||
return np.array([
|
||||
cx + rx * np.cos(angle) * np.cos(phi) + ry * np.sin(angle) * np.sin(phi),
|
||||
cy + rx * np.cos(angle) * np.sin(phi) - ry * np.sin(angle) * np.cos(phi),
|
||||
0
|
||||
])
|
||||
|
||||
def convert_elliptical_arc_to_quadratic_bezier_curve(
|
||||
cx, cy, rx, ry, phi, start_angle, end_angle, n_components=8
|
||||
):
|
||||
theta = (end_angle - start_angle) / n_components / 2
|
||||
handles = np.array([
|
||||
get_point_on_ellipse(cx, cy, rx / np.cos(theta), ry / np.cos(theta), phi, a)
|
||||
for a in np.linspace(
|
||||
start_angle + theta,
|
||||
end_angle - theta,
|
||||
n_components,
|
||||
)
|
||||
])
|
||||
anchors = np.array([
|
||||
get_point_on_ellipse(cx, cy, rx, ry, phi, a)
|
||||
for a in np.linspace(
|
||||
start_angle + theta * 2,
|
||||
end_angle,
|
||||
n_components,
|
||||
)
|
||||
])
|
||||
return handles, anchors
|
||||
|
||||
phi = x_axis_rotation * DEGREES
|
||||
x0, y0 = self.get_last_point()[:2]
|
||||
cx, cy, start_angle, end_angle = get_arc_center_and_angles(
|
||||
x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, point[0], point[1]
|
||||
)
|
||||
handles, anchors = convert_elliptical_arc_to_quadratic_bezier_curve(
|
||||
cx, cy, rx, ry, phi, start_angle, end_angle
|
||||
)
|
||||
for handle, anchor in zip(handles, anchors):
|
||||
self.add_quadratic_bezier_curve_to(handle, anchor)
|
||||
|
||||
def command_to_function(self, command):
|
||||
return self.get_command_to_function_map()[command.upper()]
|
||||
|
||||
def get_command_to_function_map(self):
|
||||
"""
|
||||
Associates svg command to VMobject function, and
|
||||
the number of arguments it takes in
|
||||
"""
|
||||
return {
|
||||
"M": (self.start_new_path, 1),
|
||||
"L": (self.add_line_to, 1),
|
||||
"H": (self.add_line_to, 1),
|
||||
"V": (self.add_line_to, 1),
|
||||
"C": (self.add_cubic_bezier_curve_to, 3),
|
||||
"S": (self.add_smooth_cubic_curve_to, 2),
|
||||
"Q": (self.add_quadratic_bezier_curve_to, 2),
|
||||
"T": (self.add_smooth_curve_to, 1),
|
||||
"A": (self.add_elliptical_arc_to, 1),
|
||||
"Z": (self.close_path, 0),
|
||||
def handle_commands(self):
|
||||
segment_class_to_func_map = {
|
||||
se.Move: (self.start_new_path, ("end",)),
|
||||
se.Close: (self.close_path, ()),
|
||||
se.Line: (self.add_line_to, ("end",)),
|
||||
se.QuadraticBezier: (self.add_quadratic_bezier_curve_to, ("control", "end")),
|
||||
se.CubicBezier: (self.add_cubic_bezier_curve_to, ("control1", "control2", "end"))
|
||||
}
|
||||
|
||||
def get_original_path_string(self):
|
||||
return self.path_string
|
||||
for segment in self.path_obj:
|
||||
segment_class = segment.__class__
|
||||
func, attr_names = segment_class_to_func_map[segment_class]
|
||||
points = [
|
||||
_convert_point_to_3d(*segment.__getattribute__(attr_name))
|
||||
for attr_name in attr_names
|
||||
]
|
||||
func(*points)
|
||||
|
||||
@@ -5,7 +5,6 @@ import re
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.tex_file_writing import tex_to_svg_file
|
||||
@@ -16,49 +15,51 @@ from manimlib.utils.tex_file_writing import display_during_execution
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
|
||||
|
||||
tex_string_to_mob_map = {}
|
||||
|
||||
|
||||
class SingleStringTex(VMobject):
|
||||
class SingleStringTex(SVGMobject):
|
||||
CONFIG = {
|
||||
"height": None,
|
||||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0,
|
||||
"should_center": True,
|
||||
"svg_default": {
|
||||
"color": WHITE,
|
||||
},
|
||||
"path_string_config": {
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
},
|
||||
"font_size": 48,
|
||||
"height": None,
|
||||
"organize_left_to_right": False,
|
||||
"alignment": "\\centering",
|
||||
"math_mode": True,
|
||||
"organize_left_to_right": False,
|
||||
}
|
||||
|
||||
def __init__(self, tex_string, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
assert(isinstance(tex_string, str))
|
||||
assert isinstance(tex_string, str)
|
||||
self.tex_string = tex_string
|
||||
if tex_string not in tex_string_to_mob_map:
|
||||
with display_during_execution(f" Writing \"{tex_string}\""):
|
||||
full_tex = self.get_tex_file_body(tex_string)
|
||||
filename = tex_to_svg_file(full_tex)
|
||||
svg_mob = SVGMobject(
|
||||
filename,
|
||||
height=None,
|
||||
path_string_config={
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
}
|
||||
)
|
||||
tex_string_to_mob_map[tex_string] = svg_mob
|
||||
self.add(*(
|
||||
sm.copy()
|
||||
for sm in tex_string_to_mob_map[tex_string]
|
||||
))
|
||||
self.init_colors()
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if self.height is None:
|
||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
||||
if self.organize_left_to_right:
|
||||
self.organize_submobjects_left_to_right()
|
||||
|
||||
@property
|
||||
def hash_seed(self):
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.tex_string,
|
||||
self.alignment,
|
||||
self.math_mode
|
||||
)
|
||||
|
||||
def get_file_path(self):
|
||||
full_tex = self.get_tex_file_body(self.tex_string)
|
||||
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||
file_path = tex_to_svg_file(full_tex)
|
||||
return file_path
|
||||
|
||||
def get_tex_file_body(self, tex_string):
|
||||
new_tex = self.get_modified_expression(tex_string)
|
||||
if self.math_mode:
|
||||
@@ -129,15 +130,22 @@ 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 i in range(len(tex)):
|
||||
if i > 0 and tex[i - 1] == "\\":
|
||||
# So as to not count '\{' type expressions
|
||||
continue
|
||||
char = tex[i]
|
||||
if char == "{":
|
||||
num_unclosed_brackets += 1
|
||||
elif char == "}":
|
||||
if num_unclosed_brackets == 0:
|
||||
tex = "{" + tex
|
||||
else:
|
||||
num_unclosed_brackets -= 1
|
||||
tex += num_unclosed_brackets * "}"
|
||||
return tex
|
||||
|
||||
def get_tex(self):
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import io
|
||||
import typing
|
||||
import warnings
|
||||
import xml.etree.ElementTree as ET
|
||||
import functools
|
||||
import pygments
|
||||
@@ -15,6 +13,7 @@ from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import manimpango
|
||||
from manimlib.logger import log
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
@@ -55,10 +54,9 @@ class Text(SVGMobject):
|
||||
self.full2short(kwargs)
|
||||
digest_config(self, kwargs)
|
||||
if self.size:
|
||||
warnings.warn(
|
||||
"self.size has been deprecated and will "
|
||||
log.warning(
|
||||
"`self.size` has been deprecated and will "
|
||||
"be removed in future.",
|
||||
DeprecationWarning
|
||||
)
|
||||
self.font_size = self.size
|
||||
if self.lsh == -1:
|
||||
|
||||
@@ -6,6 +6,8 @@ from manimlib.mobject.types.surface import SGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.geometry import Square
|
||||
from manimlib.mobject.geometry import Polygon
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.space_ops import z_to_vector
|
||||
@@ -14,9 +16,9 @@ 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,
|
||||
}
|
||||
@@ -32,8 +34,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()
|
||||
@@ -42,12 +47,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)
|
||||
|
||||
|
||||
@@ -208,6 +222,53 @@ class VCube(VGroup):
|
||||
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):
|
||||
CONFIG = {
|
||||
"dimensions": [3, 2, 1]
|
||||
|
||||
@@ -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,
|
||||
@@ -85,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()
|
||||
@@ -98,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
|
||||
@@ -112,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,
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ class PMobject(Mobject):
|
||||
return self
|
||||
|
||||
def set_points(self, points):
|
||||
if len(points) == 0:
|
||||
points = np.zeros((0, 3))
|
||||
super().set_points(points)
|
||||
self.resize_points(len(points))
|
||||
return self
|
||||
@@ -34,14 +36,18 @@ class PMobject(Mobject):
|
||||
if color is not None:
|
||||
if opacity is None:
|
||||
opacity = self.data["rgbas"][-1, 3]
|
||||
new_rgbas = np.repeat(
|
||||
rgbas = np.repeat(
|
||||
[color_to_rgba(color, opacity)],
|
||||
len(points),
|
||||
axis=0
|
||||
)
|
||||
elif rgbas is not None:
|
||||
new_rgbas = rgbas
|
||||
self.data["rgbas"][-len(new_rgbas):] = new_rgbas
|
||||
if rgbas is not None:
|
||||
self.data["rgbas"][-len(rgbas):] = rgbas
|
||||
return self
|
||||
|
||||
def add_point(self, point, rgba=None, color=None, opacity=None):
|
||||
rgbas = None if rgba is None else [rgba]
|
||||
self.add_points([point], rgbas, color, opacity)
|
||||
return self
|
||||
|
||||
def set_color_by_gradient(self, *colors):
|
||||
|
||||
@@ -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
|
||||
@@ -74,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
|
||||
@@ -135,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 +154,7 @@ class VMobject(Mobject):
|
||||
stroke_rgba=None,
|
||||
stroke_width=None,
|
||||
stroke_background=True,
|
||||
reflectiveness=None,
|
||||
gloss=None,
|
||||
shadow=None,
|
||||
recurse=True):
|
||||
@@ -177,6 +182,8 @@ class VMobject(Mobject):
|
||||
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:
|
||||
@@ -185,10 +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(),
|
||||
}
|
||||
@@ -218,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):
|
||||
@@ -277,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()
|
||||
@@ -373,7 +382,10 @@ class VMobject(Mobject):
|
||||
|
||||
def add_smooth_cubic_curve_to(self, handle, point):
|
||||
self.throw_error_if_no_points()
|
||||
new_handle = self.get_reflection_of_last_handle()
|
||||
if self.get_num_points() == 1:
|
||||
new_handle = self.get_points()[-1]
|
||||
else:
|
||||
new_handle = self.get_reflection_of_last_handle()
|
||||
self.add_cubic_bezier_curve_to(new_handle, handle, point)
|
||||
|
||||
def has_new_path_started(self):
|
||||
@@ -504,10 +516,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())
|
||||
@@ -543,12 +555,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,
|
||||
@@ -629,17 +664,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
|
||||
@@ -701,7 +738,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
|
||||
@@ -797,7 +834,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
|
||||
@@ -853,7 +890,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()
|
||||
@@ -999,6 +1036,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 = {
|
||||
@@ -1010,7 +1051,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]))
|
||||
|
||||
|
||||
|
||||
@@ -277,7 +277,6 @@ class DiscreteGraphScene(Scene):
|
||||
def trace_cycle(self, cycle=None, color="yellow", run_time=2.0):
|
||||
if cycle is None:
|
||||
cycle = self.graph.region_cycles[0]
|
||||
time_per_edge = run_time / len(cycle)
|
||||
next_in_cycle = it.cycle(cycle)
|
||||
next(next_in_cycle) # jump one ahead
|
||||
self.traced_cycle = Mobject(*[
|
||||
|
||||
@@ -36,6 +36,7 @@ class Scene(object):
|
||||
"end_at_animation_number": None,
|
||||
"leave_progress_bars": False,
|
||||
"preview": True,
|
||||
"presenter_mode": False,
|
||||
"linger_after_completion": True,
|
||||
}
|
||||
|
||||
@@ -56,10 +57,13 @@ 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()
|
||||
self.mouse_drag_point = Point()
|
||||
self.hold_on_wait = self.presenter_mode
|
||||
|
||||
# Much nicer to work with deterministic scenes
|
||||
if self.random_seed is not None:
|
||||
@@ -112,7 +116,7 @@ class Scene(object):
|
||||
if self.quit_interaction:
|
||||
self.unlock_mobject_data()
|
||||
|
||||
def embed(self):
|
||||
def embed(self, close_scene_on_exit=True):
|
||||
if not self.preview:
|
||||
# If the scene is just being
|
||||
# written, ignore embed calls
|
||||
@@ -135,10 +139,11 @@ class Scene(object):
|
||||
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()`")
|
||||
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
||||
shell(local_ns=local_ns, stack_depth=2)
|
||||
# End scene when exiting an embed.
|
||||
raise EndSceneEarlyException()
|
||||
# End scene when exiting an embed
|
||||
if close_scene_on_exit:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
@@ -184,6 +189,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
|
||||
@@ -271,59 +283,54 @@ class Scene(object):
|
||||
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):
|
||||
"""
|
||||
@@ -428,6 +435,11 @@ class Scene(object):
|
||||
def unlock_mobject_data(self):
|
||||
self.camera.release_static_mobjects()
|
||||
|
||||
def refresh_locked_data(self):
|
||||
self.unlock_mobject_data()
|
||||
self.lock_static_mobject_data()
|
||||
return self
|
||||
|
||||
def begin_animations(self, animations):
|
||||
for animation in animations:
|
||||
animation.begin()
|
||||
@@ -473,10 +485,20 @@ class Scene(object):
|
||||
self.unlock_mobject_data()
|
||||
|
||||
@handle_play_like_call
|
||||
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
|
||||
def wait(self,
|
||||
duration=DEFAULT_WAIT_TIME,
|
||||
stop_condition=None,
|
||||
note=None,
|
||||
ignore_presenter_mode=False):
|
||||
if note:
|
||||
log.info(note)
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
if self.should_update_mobjects():
|
||||
self.lock_static_mobject_data()
|
||||
self.lock_static_mobject_data()
|
||||
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
||||
while self.hold_on_wait:
|
||||
self.update_frame(dt=1 / self.camera.frame_rate)
|
||||
self.hold_on_wait = True
|
||||
else:
|
||||
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||
last_t = 0
|
||||
for t in time_progression:
|
||||
@@ -487,15 +509,7 @@ class Scene(object):
|
||||
if stop_condition is not None and stop_condition():
|
||||
time_progression.close()
|
||||
break
|
||||
self.unlock_mobject_data()
|
||||
elif self.skip_animations:
|
||||
# Do nothing
|
||||
return self
|
||||
else:
|
||||
self.update_frame(duration)
|
||||
n_frames = int(duration * self.camera.frame_rate)
|
||||
for n in range(n_frames):
|
||||
self.emit_frame()
|
||||
self.unlock_mobject_data()
|
||||
return self
|
||||
|
||||
def wait_until(self, stop_condition, max_time=60):
|
||||
@@ -615,6 +629,10 @@ class Scene(object):
|
||||
self.camera.frame.to_default_state()
|
||||
elif char == "q":
|
||||
self.quit_interaction = True
|
||||
elif char == " " or symbol == 65363: # Space or right arrow
|
||||
self.hold_on_wait = False
|
||||
elif char == "e" and modifiers == 3: # ctrl + shift + e
|
||||
self.embed(close_scene_on_exit=False)
|
||||
|
||||
def on_resize(self, width: int, height: int):
|
||||
self.camera.reset_pixel_shape(width, height)
|
||||
|
||||
@@ -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
|
||||
@@ -35,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()
|
||||
|
||||
@@ -72,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
|
||||
@@ -205,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):
|
||||
@@ -276,7 +308,7 @@ class SceneFileWriter(object):
|
||||
)
|
||||
temp_file_path = stem + "_temp" + ext
|
||||
commands = [
|
||||
"ffmpeg",
|
||||
FFMPEG_BIN,
|
||||
"-i", movie_file_path,
|
||||
"-i", sound_file_path,
|
||||
'-y', # overwrite output file if it exists
|
||||
@@ -301,7 +333,8 @@ class SceneFileWriter(object):
|
||||
self.print_file_ready_message(file_path)
|
||||
|
||||
def print_file_ready_message(self, file_path):
|
||||
log.info(f"File ready at {file_path}")
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -287,9 +287,6 @@ class LinearTransformationScene(VectorScene):
|
||||
},
|
||||
"background_plane_kwargs": {
|
||||
"color": GREY,
|
||||
"axis_config": {
|
||||
"stroke_color": GREY_B,
|
||||
},
|
||||
"axis_config": {
|
||||
"color": GREY,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -71,6 +73,8 @@ void main() {
|
||||
xyz_coords,
|
||||
vec3(0.0, 0.0, 1.0),
|
||||
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;
|
||||
@@ -75,7 +77,7 @@ vec2 seek_root(vec2 z, vec2[MAX_DEGREE + 1] coefs, int max_steps, out float n_it
|
||||
}
|
||||
z = z - step;
|
||||
}
|
||||
n_iters -= clamp((threshold - curr_len) / (last_len - curr_len), 0.0, 1.0);
|
||||
n_iters -= log(curr_len) / log(threshold);
|
||||
|
||||
return z;
|
||||
}
|
||||
@@ -118,7 +120,7 @@ void main() {
|
||||
color = colors[i];
|
||||
}
|
||||
}
|
||||
color *= 1.0 + (0.01 * saturation_factor) * (n_iters - 5 * saturation_factor);
|
||||
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);
|
||||
@@ -151,6 +153,8 @@ void main() {
|
||||
xyz_coords,
|
||||
vec3(0.0, 0.0, 1.0),
|
||||
light_source_position,
|
||||
camera_position,
|
||||
reflectiveness,
|
||||
gloss,
|
||||
shadow
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -14,10 +14,10 @@ def bezier(points):
|
||||
n = len(points) - 1
|
||||
|
||||
def result(t):
|
||||
return sum([
|
||||
return sum(
|
||||
((1 - t)**(n - k)) * (t**k) * choose(n, k) * point
|
||||
for k, point in enumerate(points)
|
||||
])
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ from manimlib.constants import WHITE
|
||||
from manimlib.constants import COLORMAP_3B1B
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.simple_functions import clip_in_place
|
||||
from manimlib.utils.space_ops import normalize
|
||||
|
||||
|
||||
def color_to_rgb(color):
|
||||
@@ -105,16 +103,6 @@ def random_color():
|
||||
return Color(rgb=(random.random() for i in range(3)))
|
||||
|
||||
|
||||
def get_shaded_rgb(rgb, point, unit_normal_vect, light_source):
|
||||
to_sun = normalize(light_source - point)
|
||||
factor = 0.5 * np.dot(unit_normal_vect, to_sun)**3
|
||||
if factor < 0:
|
||||
factor *= 0.5
|
||||
result = rgb + factor
|
||||
clip_in_place(rgb + factor, 0, 1)
|
||||
return result
|
||||
|
||||
|
||||
def get_colormap_list(map_name="viridis", n_colors=9):
|
||||
"""
|
||||
Options for map_name:
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
import yaml
|
||||
import os
|
||||
import yaml
|
||||
import inspect
|
||||
import importlib
|
||||
|
||||
from rich import box
|
||||
from rich.rule import Rule
|
||||
from rich.table import Table
|
||||
from rich.console import Console
|
||||
from rich.prompt import Prompt, Confirm
|
||||
|
||||
|
||||
def get_manim_dir():
|
||||
manimlib_module = importlib.import_module("manimlib")
|
||||
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
|
||||
return os.path.abspath(os.path.join(manimlib_dir, ".."))
|
||||
|
||||
|
||||
def remove_empty_value(dictionary):
|
||||
for key in list(dictionary.keys()):
|
||||
if dictionary[key] == "":
|
||||
dictionary.pop(key)
|
||||
elif isinstance(dictionary[key], dict):
|
||||
remove_empty_value(dictionary[key])
|
||||
|
||||
|
||||
def init_customization():
|
||||
configuration = {
|
||||
@@ -24,6 +47,7 @@ def init_customization():
|
||||
},
|
||||
"window_position": "UR",
|
||||
"window_monitor": 0,
|
||||
"full_screen": False,
|
||||
"break_into_partial_movies": False,
|
||||
"camera_qualities": {
|
||||
"low": {
|
||||
@@ -46,41 +70,103 @@ 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
|
||||
file_name = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
else:
|
||||
file_name = os.path.join(os.getcwd(), "custom_config.yml")
|
||||
console = Console()
|
||||
console.print(Rule("[bold]Configuration Guide[/bold]"))
|
||||
# print("Initialize configuration")
|
||||
try:
|
||||
scope = Prompt.ask(
|
||||
" Select the scope of the configuration",
|
||||
choices=["global", "local"],
|
||||
default="local"
|
||||
)
|
||||
|
||||
print("\n directories:")
|
||||
configuration["directories"]["output"] = input(" [1/8] Where should manim output video and image files place: ")
|
||||
configuration["directories"]["raster_images"] = input(" [2/8] Which folder should manim find raster images (.jpg .png .gif) in (optional): ")
|
||||
configuration["directories"]["vector_images"] = input(" [3/8] Which folder should manim find vector images (.svg .xdv) in (optional): ")
|
||||
configuration["directories"]["sounds"] = input(" [4/8] Which folder should manim find sound files (.mp3 .wav) in (optional): ")
|
||||
configuration["directories"]["temporary_storage"] = input(" [5/8] Which folder should manim storage temporary files: ")
|
||||
console.print("[bold]Directories:[/bold]")
|
||||
dir_config = configuration["directories"]
|
||||
dir_config["output"] = Prompt.ask(
|
||||
" Where should manim [bold]output[/bold] video and image files place [prompt.default](optional, default is none)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["raster_images"] = Prompt.ask(
|
||||
" Which folder should manim find [bold]raster images[/bold] (.jpg .png .gif) in "
|
||||
"[prompt.default](optional, default is none)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["vector_images"] = Prompt.ask(
|
||||
" Which folder should manim find [bold]vector images[/bold] (.svg .xdv) in "
|
||||
"[prompt.default](optional, default is none)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["sounds"] = Prompt.ask(
|
||||
" Which folder should manim find [bold]sound files[/bold] (.mp3 .wav) in "
|
||||
"[prompt.default](optional, default is none)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
dir_config["temporary_storage"] = Prompt.ask(
|
||||
" Which folder should manim storage [bold]temporary files[/bold] "
|
||||
"[prompt.default](recommended, use system temporary folder by default)",
|
||||
default="",
|
||||
show_default=False
|
||||
)
|
||||
|
||||
print("\n tex:")
|
||||
tex = input(" [6/8] Which executable file to use to compile [latex/xelatex]: ")
|
||||
if tex == "latex":
|
||||
configuration["tex"]["executable"] = "latex"
|
||||
configuration["tex"]["template_file"] = "tex_template.tex"
|
||||
configuration["tex"]["intermediate_filetype"] = "dvi"
|
||||
else:
|
||||
configuration["tex"]["executable"] = "xelatex -no-pdf"
|
||||
configuration["tex"]["template_file"] = "ctex_template.tex"
|
||||
configuration["tex"]["intermediate_filetype"] = "xdv"
|
||||
console.print("[bold]LaTeX:[/bold]")
|
||||
tex_config = configuration["tex"]
|
||||
tex = Prompt.ask(
|
||||
" Select an executable program to use to compile a LaTeX source file",
|
||||
choices=["latex", "xelatex"],
|
||||
default="latex"
|
||||
)
|
||||
if tex == "latex":
|
||||
tex_config["executable"] = "latex"
|
||||
tex_config["template_file"] = "tex_template.tex"
|
||||
tex_config["intermediate_filetype"] = "dvi"
|
||||
else:
|
||||
tex_config["executable"] = "xelatex -no-pdf"
|
||||
tex_config["template_file"] = "ctex_template.tex"
|
||||
tex_config["intermediate_filetype"] = "xdv"
|
||||
|
||||
console.print("[bold]Styles:[/bold]")
|
||||
configuration["style"]["background_color"] = Prompt.ask(
|
||||
" Which [bold]background color[/bold] do you want [italic](hex code)",
|
||||
default="#333333"
|
||||
)
|
||||
|
||||
print("\n style:")
|
||||
configuration["style"]["background_color"] = input(" [7/8] Which background color do you want (hex code): ")
|
||||
console.print("[bold]Camera qualities:[/bold]")
|
||||
table = Table(
|
||||
"low", "medium", "high", "ultra_high",
|
||||
title="Four defined qualities",
|
||||
box=box.ROUNDED
|
||||
)
|
||||
table.add_row("480p15", "720p30", "1080p60", "2160p60")
|
||||
console.print(table)
|
||||
configuration["camera_qualities"]["default_quality"] = Prompt.ask(
|
||||
" Which one to choose as the default rendering quality",
|
||||
choices=["low", "medium", "high", "ultra_high"],
|
||||
default="high"
|
||||
)
|
||||
|
||||
print("\n camera_qualities:")
|
||||
print(" Four defined qualities: low: 480p15 medium: 720p30 high: 1080p60 ultra_high: 2160p60")
|
||||
configuration["camera_qualities"]["default_quality"] = input(" [8/8] Which one to choose as the default rendering quality [low/medium/high/ultra_high]: ")
|
||||
write_to_file = Confirm.ask(
|
||||
"\n[bold]Are you sure to write these configs to file?[/bold]",
|
||||
default=True
|
||||
)
|
||||
if not write_to_file:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
with open(file_name, 'w', encoding="utf_8") as file:
|
||||
yaml.dump(configuration, file)
|
||||
global_file_name = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
if scope == "global":
|
||||
file_name = global_file_name
|
||||
else:
|
||||
if os.path.exists(global_file_name):
|
||||
remove_empty_value(configuration)
|
||||
file_name = os.path.join(os.getcwd(), "custom_config.yml")
|
||||
with open(file_name, "w", encoding="utf-8") as f:
|
||||
yaml.dump(configuration, f)
|
||||
|
||||
console.print(f"\n:rocket: You have successfully set up a {scope} configuration file!")
|
||||
console.print(f"You can manually modify it in: [cyan]`{file_name}`[/cyan]")
|
||||
|
||||
print(f"\nYou have set up a {scope} configuration file")
|
||||
print(f"You can manually modify it again in: {file_name}\n")
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[green]Exit configuration guide[/green]")
|
||||
|
||||
@@ -139,3 +139,14 @@ def remove_nones(sequence):
|
||||
|
||||
def concatenate_lists(*list_of_lists):
|
||||
return [item for l in list_of_lists for item in l]
|
||||
|
||||
|
||||
def hash_obj(obj):
|
||||
if isinstance(obj, dict):
|
||||
new_obj = {k: hash_obj(v) for k, v in obj.items()}
|
||||
return hash(tuple(frozenset(sorted(new_obj.items()))))
|
||||
|
||||
if isinstance(obj, (set, tuple, list)):
|
||||
return hash(tuple(hash_obj(e) for e in obj))
|
||||
|
||||
return hash(obj)
|
||||
|
||||
@@ -1,34 +1,20 @@
|
||||
from functools import reduce
|
||||
import inspect
|
||||
import numpy as np
|
||||
import operator as op
|
||||
import math
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
def sigmoid(x):
|
||||
return 1.0 / (1 + np.exp(-x))
|
||||
|
||||
|
||||
CHOOSE_CACHE = {}
|
||||
@lru_cache(maxsize=10)
|
||||
def choose(n, k):
|
||||
return math.comb(n, k)
|
||||
|
||||
|
||||
def choose_using_cache(n, r):
|
||||
if n not in CHOOSE_CACHE:
|
||||
CHOOSE_CACHE[n] = {}
|
||||
if r not in CHOOSE_CACHE[n]:
|
||||
CHOOSE_CACHE[n][r] = choose(n, r, use_cache=False)
|
||||
return CHOOSE_CACHE[n][r]
|
||||
|
||||
|
||||
def choose(n, r, use_cache=True):
|
||||
if use_cache:
|
||||
return choose_using_cache(n, r)
|
||||
if n < r:
|
||||
return 0
|
||||
if r == 0:
|
||||
return 1
|
||||
denom = reduce(op.mul, range(1, r + 1), 1)
|
||||
numer = reduce(op.mul, range(n, n - r, -1), 1)
|
||||
return numer // denom
|
||||
def gen_choose(n, r):
|
||||
return np.prod(np.arange(n, n - r, -1)) / math.factorial(r)
|
||||
|
||||
|
||||
def get_num_args(function):
|
||||
@@ -53,14 +39,6 @@ def clip(a, min_a, max_a):
|
||||
return a
|
||||
|
||||
|
||||
def clip_in_place(array, min_val=None, max_val=None):
|
||||
if max_val is not None:
|
||||
array[array > max_val] = max_val
|
||||
if min_val is not None:
|
||||
array[array < min_val] = min_val
|
||||
return array
|
||||
|
||||
|
||||
def fdiv(a, b, zero_over_zero_value=None):
|
||||
if zero_over_zero_value is not None:
|
||||
out = np.full_like(a, zero_over_zero_value)
|
||||
|
||||
@@ -10,6 +10,15 @@ 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):
|
||||
@@ -147,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
|
||||
@@ -159,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):
|
||||
@@ -186,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)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import re
|
||||
import string
|
||||
|
||||
|
||||
def to_camel_case(name):
|
||||
return "".join([
|
||||
[c for c in part if c not in string.punctuation + string.whitespace].capitalize()
|
||||
for part in name.split("_")
|
||||
])
|
||||
|
||||
|
||||
def initials(name, sep_values=[" ", "_"]):
|
||||
return "".join([
|
||||
(s[0] if s else "")
|
||||
for s in re.split("|".join(sep_values), name)
|
||||
])
|
||||
|
||||
|
||||
def camel_case_initials(name):
|
||||
return [c for c in name if c.isupper()]
|
||||
|
||||
|
||||
def complex_string(complex_num):
|
||||
return [c for c in str(complex_num) if c not in "()"]
|
||||
|
||||
|
||||
def split_string_to_isolate_substrings(full_string, *isolate):
|
||||
"""
|
||||
Given a string, and an arbitrary number of possible substrings,
|
||||
to isolate, this returns a list of strings which would concatenate
|
||||
to make the full string, and in which these special substrings
|
||||
appear as their own elements.
|
||||
|
||||
For example,split_string_to_isolate_substrings("to be or not to be", "to", "be")
|
||||
would return ["to", " ", "be", " or not ", "to", " ", "be"]
|
||||
"""
|
||||
pattern = "|".join(*(
|
||||
"({})".format(re.escape(ss))
|
||||
for ss in isolate
|
||||
))
|
||||
pieces = re.split(pattern, full_string)
|
||||
return list(filter(lambda s: s, pieces))
|
||||
@@ -126,6 +126,9 @@ def dvi_to_svg(dvi_file, regen_if_exists=False):
|
||||
def display_during_execution(message):
|
||||
# Only show top line
|
||||
to_print = message.split("\n")[0]
|
||||
max_characters = os.get_terminal_size().columns - 1
|
||||
if len(to_print) > max_characters:
|
||||
to_print = to_print[:max_characters - 3] + "..."
|
||||
try:
|
||||
print(to_print, end="\r")
|
||||
yield
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
argparse
|
||||
colour
|
||||
numpy
|
||||
Pillow
|
||||
@@ -9,13 +8,15 @@ mapbox-earcut
|
||||
matplotlib
|
||||
moderngl
|
||||
moderngl_window
|
||||
skia-pathops
|
||||
pydub
|
||||
pygments
|
||||
pyyaml
|
||||
rich
|
||||
screeninfo
|
||||
pyreadline; sys_platform == 'win32'
|
||||
validators
|
||||
ipython
|
||||
PyOpenGL
|
||||
manimpango>=0.2.0,<0.4.0
|
||||
isosurfaces
|
||||
svgelements
|
||||
|
||||
24
setup.cfg
24
setup.cfg
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = manimgl
|
||||
version = 1.2.0
|
||||
version = 1.5.0
|
||||
author = Grant Sanderson
|
||||
author_email= grant@3blue1brown.com
|
||||
description = Animation engine for explanatory math videos
|
||||
@@ -12,12 +12,24 @@ project_urls =
|
||||
Documentation = https://3b1b.github.io/manim/
|
||||
Source Code = https://github.com/3b1b/manim
|
||||
license = MIT
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
License :: OSI Approved :: MIT License
|
||||
Topic :: Scientific/Engineering
|
||||
Topic :: Multimedia :: Video
|
||||
Topic :: Multimedia :: Graphics
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Natural Language :: English
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
include_package_data=True
|
||||
install_requires =
|
||||
argparse
|
||||
include_package_data = True
|
||||
install_requires =
|
||||
colour
|
||||
numpy
|
||||
Pillow
|
||||
@@ -28,16 +40,18 @@ install_requires =
|
||||
matplotlib
|
||||
moderngl
|
||||
moderngl_window
|
||||
skia-pathops
|
||||
pydub
|
||||
pygments
|
||||
pyyaml
|
||||
rich
|
||||
screeninfo
|
||||
pyreadline; sys_platform == 'win32'
|
||||
validators
|
||||
ipython
|
||||
PyOpenGL
|
||||
manimpango>=0.2.0,<0.4.0
|
||||
isosurfaces
|
||||
svgelements
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
|
||||
Reference in New Issue
Block a user