mirror of
https://github.com/3b1b/manim.git
synced 2026-01-13 16:37:55 -05:00
Compare commits
187 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bda7f98d2e | ||
|
|
9d74e8bce3 | ||
|
|
845ee83f71 | ||
|
|
55684af27d | ||
|
|
859680d5ab | ||
|
|
dc4b9bc93c | ||
|
|
705f1a528b | ||
|
|
773520bcd9 | ||
|
|
d26b8a826c | ||
|
|
12bfe88f40 | ||
|
|
36d62ae1a3 | ||
|
|
e23f667c3d | ||
|
|
2277679111 | ||
|
|
9d7db7aacd | ||
|
|
e8430b38b2 | ||
|
|
1f32a9e674 | ||
|
|
d31f3df5af | ||
|
|
e9bf13882e | ||
|
|
3550108ff7 | ||
|
|
557707ea75 | ||
|
|
13c731e166 | ||
|
|
d349c9283d | ||
|
|
18963fb9fe | ||
|
|
a69c9887f9 | ||
|
|
93f8d3f1ca | ||
|
|
e4ccbdfba9 | ||
|
|
fc97bfb647 | ||
|
|
f9d8a76767 | ||
|
|
55a91a2354 | ||
|
|
50ffcbc5c7 | ||
|
|
b764791258 | ||
|
|
7f616987a3 | ||
|
|
648855dae0 | ||
|
|
974d9d5ab0 | ||
|
|
3c3264d7d6 | ||
|
|
39673a80d7 | ||
|
|
84c56b3624 | ||
|
|
dc816c9f8d | ||
|
|
d5ab9a91c4 | ||
|
|
106f2a3837 | ||
|
|
724a500cc6 | ||
|
|
461500637e | ||
|
|
fc4f649570 | ||
|
|
852da9ac2a | ||
|
|
637d779190 | ||
|
|
9bbbed3a83 | ||
|
|
1cde28838f | ||
|
|
a8039d803e | ||
|
|
0add9b6e3a | ||
|
|
c5ec47b0e9 | ||
|
|
769a4bbaf9 | ||
|
|
0f8d7ed597 | ||
|
|
2a7a7ac518 | ||
|
|
0610f331a4 | ||
|
|
a0ba9c8b30 | ||
|
|
7e8b3a4c6b | ||
|
|
393f77cb03 | ||
|
|
82c972b946 | ||
|
|
89e139009b | ||
|
|
45faa9063b | ||
|
|
0e31ff12e2 | ||
|
|
473aaea399 | ||
|
|
c4ea794107 | ||
|
|
cfba6c431f | ||
|
|
e11c5def63 | ||
|
|
a3e4246938 | ||
|
|
305c6e6ee9 | ||
|
|
3b01ec48e6 | ||
|
|
969aa82f04 | ||
|
|
e44a2fc8c6 | ||
|
|
9ac1805e7e | ||
|
|
6ad8636fab | ||
|
|
4a03d196a6 | ||
|
|
519e2f4f1e | ||
|
|
aefde2969f | ||
|
|
b7a3201fb3 | ||
|
|
a9349057ad | ||
|
|
e812b99594 | ||
|
|
0c8b333a42 | ||
|
|
f690164087 | ||
|
|
9d0cc810c5 | ||
|
|
8b1f0a8749 | ||
|
|
c0b7b55e49 | ||
|
|
41b52c6117 | ||
|
|
e5ce0ca286 | ||
|
|
a8c2a9fa3f | ||
|
|
cabc1322d6 | ||
|
|
c51811d2f1 | ||
|
|
7bf3615bb1 | ||
|
|
1872b0516b | ||
|
|
625460467f | ||
|
|
e19f35585d | ||
|
|
66819f5dbc | ||
|
|
f249da95fb | ||
|
|
bb3bd41605 | ||
|
|
67f8007764 | ||
|
|
2a0709664d | ||
|
|
de46df78dc | ||
|
|
bd6c731e67 | ||
|
|
c3e13fff05 | ||
|
|
bf2d9edfe6 | ||
|
|
fa38b56fd8 | ||
|
|
dfbbb34035 | ||
|
|
0cef9a1e61 | ||
|
|
2d764e12f4 | ||
|
|
d744311f15 | ||
|
|
11af9508f2 | ||
|
|
a227ffde05 | ||
|
|
e0b0ae280e | ||
|
|
fce38fd8a5 | ||
|
|
52a99a0c49 | ||
|
|
956e3a69c7 | ||
|
|
95a3ac6876 | ||
|
|
b06a5d3f23 | ||
|
|
fa8962e024 | ||
|
|
0a4c4d5849 | ||
|
|
e879da32d5 | ||
|
|
6b12bc2f5e | ||
|
|
4aeccd7769 | ||
|
|
4fbe948b63 | ||
|
|
05bee011d2 | ||
|
|
37b548395c | ||
|
|
4356c42e00 | ||
|
|
aea79be6cc | ||
|
|
a08e9b01de | ||
|
|
9f3b404df6 | ||
|
|
8ef42fae24 | ||
|
|
6be6bd3075 | ||
|
|
a33eac7aa8 | ||
|
|
9d6a28bc29 | ||
|
|
06405d5758 | ||
|
|
46e356e791 | ||
|
|
97ca42d454 | ||
|
|
a4eee6f44c | ||
|
|
8cac16b452 | ||
|
|
719cd8cde3 | ||
|
|
0bb9216c14 | ||
|
|
6f9df8db26 | ||
|
|
3756605a45 | ||
|
|
0e4d4155a3 | ||
|
|
0cab23b2ba | ||
|
|
854f7cd2bf | ||
|
|
41c4023986 | ||
|
|
d19e0cb9ab | ||
|
|
f085e6c2dd | ||
|
|
91ffdeb2d4 | ||
|
|
db71ed1ae9 | ||
|
|
4c16bfc2c0 | ||
|
|
aef02bfcf9 | ||
|
|
3744844efa | ||
|
|
9d04e287d7 | ||
|
|
97c0f4857b | ||
|
|
7f9b0a7eac | ||
|
|
133724d29a | ||
|
|
559b96e7ce | ||
|
|
773e013af9 | ||
|
|
61c70b426c | ||
|
|
9bdcc8b635 | ||
|
|
66caf0c1ad | ||
|
|
62cab9feaf | ||
|
|
be5de32d70 | ||
|
|
09ce4717aa | ||
|
|
7fb6f352c4 | ||
|
|
f29ef87bba | ||
|
|
e39f81ccff | ||
|
|
a0ed9edb42 | ||
|
|
fc1e916f42 | ||
|
|
b3b7d214ad | ||
|
|
602809758e | ||
|
|
960463d143 | ||
|
|
9a8aee481d | ||
|
|
1064e2bb30 | ||
|
|
992e61ddf2 | ||
|
|
19187ead06 | ||
|
|
7f8216bb09 | ||
|
|
e78113373a | ||
|
|
35025631eb | ||
|
|
f9351536e4 | ||
|
|
6e292daf58 | ||
|
|
67f5b10626 | ||
|
|
baba6929df | ||
|
|
d6b20a7306 | ||
|
|
4c3ba7f674 | ||
|
|
3883f57bf8 | ||
|
|
d2e0811285 | ||
|
|
1e2a6ffb8a | ||
|
|
56e5696163 |
13
.github/workflows/publish.yml
vendored
13
.github/workflows/publish.yml
vendored
@@ -8,6 +8,11 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python: ["py37", "py38", "py39", "py310"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -20,11 +25,13 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine build
|
||||
|
||||
- name: Build and publish
|
||||
|
||||
- name: Build wheels
|
||||
run: python setup.py bdist_wheel --python-tag ${{ matrix.python }}
|
||||
|
||||
- name: Upload wheels
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python -m build
|
||||
twine upload dist/*
|
||||
@@ -19,7 +19,7 @@ Note, there are two versions of manim. This repository began as a personal proj
|
||||
>
|
||||
> **Note**: To install manim directly through pip, please pay attention to the name of the installed package. This repository is ManimGL of 3b1b. The package name is `manimgl` instead of `manim` or `manimlib`. Please use `pip install manimgl` to install the version in this repository.
|
||||
|
||||
Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
|
||||
Manim runs on Python 3.7 or higher.
|
||||
|
||||
System requirements are [FFmpeg](https://ffmpeg.org/), [OpenGL](https://www.opengl.org/) and [LaTeX](https://www.latex-project.org) (optional, if you want to use LaTeX).
|
||||
For Linux, [Pango](https://pango.gnome.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
|
||||
|
||||
@@ -1,38 +1,148 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
v1.6.1
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Fixed the bug of ``MTex`` with multi-line tex string (`#1785 <https://github.com/3b1b/manim/pull/1785>`__)
|
||||
- Fixed ``interpolate`` (`#1788 <https://github.com/3b1b/manim/pull/1788>`__)
|
||||
- Fixed ``ImageMobject`` (`#1791 <https://github.com/3b1b/manim/pull/1791>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Added ``\overset`` as a special string in ``Tex`` (`#1783 <https://github.com/3b1b/manim/pull/1783>`__)
|
||||
- Added ``outer_interpolate`` to perform interpolation using ``np.outer`` on arrays (`#1788 <https://github.com/3b1b/manim/pull/1788>`__)
|
||||
|
||||
v1.6.0
|
||||
------
|
||||
|
||||
Breaking changes
|
||||
^^^^^^^^^^^^^^^^
|
||||
- **Python 3.6 is no longer supported** (`#1736 <https://github.com/3b1b/manim/pull/1736>`__)
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Fixed the width of riemann rectangles (`#1762 <https://github.com/3b1b/manim/pull/1762>`__)
|
||||
- Bug fixed in cases where empty array is passed to shader (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/fa38b56fd87f713657c7f778f39dca7faf15baa8>`__)
|
||||
- Fixed ``AddTextWordByWord`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
|
||||
- Fixed ``ControlsExample`` (`#1781 <https://github.com/3b1b/manim/pull/1781>`__)
|
||||
|
||||
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
- Added more functions to ``Text`` (details: `#1751 <https://github.com/3b1b/manim/pull/1751>`__)
|
||||
- Allowed ``interpolate`` to work on an array of alpha values (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/bf2d9edfe67c7e63ac0107d1d713df7ae7c3fb8f>`__)
|
||||
- Allowed ``Numberline.number_to_point`` and ``CoordinateSystem.coords_to_point`` to work on an array of inputs (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/c3e13fff0587d3bb007e71923af7eaf9e4926560>`__)
|
||||
- Added a basic ``Prismify`` to turn a flat ``VMobject`` into something with depth (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/f249da95fb65ed5495cd1db1f12ece7e90061af6>`__)
|
||||
- Added ``GlowDots``, analogous to ``GlowDot`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/e19f35585d817e74b40bc30b1ab7cee84b24da05>`__)
|
||||
- Added ``TransformMatchingStrings`` which is compatible with ``Text`` and ``MTex`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
|
||||
- Added support for ``substring`` and ``case_sensitive`` parameters for ``LabelledString.get_parts_by_string`` (`#1780 <https://github.com/3b1b/manim/pull/1780>`__)
|
||||
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Added type hints (`#1736 <https://github.com/3b1b/manim/pull/1736>`__)
|
||||
- Specifid UTF-8 encoding for tex files (`#1748 <https://github.com/3b1b/manim/pull/1748>`__)
|
||||
- Refactored ``Text`` with the latest manimpango (`#1751 <https://github.com/3b1b/manim/pull/1751>`__)
|
||||
- Reorganized getters for ``ParametricCurve`` (`#1757 <https://github.com/3b1b/manim/pull/1757>`__)
|
||||
- Refactored ``CameraFrame`` to use ``scipy.spatial.transform.Rotation`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/625460467fdc01fc1b6621cbb3d2612195daedb9>`__)
|
||||
- Refactored rotation methods to use ``scipy.spatial.transform.Rotation`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/7bf3615bb15cc6d15506d48ac800a23313054c8e>`__)
|
||||
- Used ``stroke_color`` to init ``Arrow`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/c0b7b55e49f06b75ae133b5a810bebc28c212cd6>`__)
|
||||
- Refactored ``Mobject.set_rgba_array_by_color`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/8b1f0a8749d91eeda4b674ed156cbc7f8e1e48a8>`__)
|
||||
- Made panning more sensitive to mouse movements (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/9d0cc810c5fcb4252990e706c6bf880d571cb1a2>`__)
|
||||
- Added loading progress for large SVGs (`#1766 <https://github.com/3b1b/manim/pull/1766>`__)
|
||||
- Added getter/setter of ``field_of_view`` for ``CameraFrame`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0610f331a4f7a126a3aae34f8a2a86eabcb692f4>`__)
|
||||
- Renamed ``focal_distance`` to ``focal_dist_to_height`` and added getter/setter (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0610f331a4f7a126a3aae34f8a2a86eabcb692f4>`__)
|
||||
- Added getter and setter for ``VMobject.joint_type`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/2a7a7ac5189a14170f883533137e8a2ae09aac41>`__)
|
||||
- Refactored ``VCube`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0f8d7ed59751d42d5011813ba5694ecb506082f7>`__)
|
||||
- Refactored ``Prism`` to receive ``width height depth`` instead of ``dimensions`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0f8d7ed59751d42d5011813ba5694ecb506082f7>`__)
|
||||
- Refactored ``Text``, ``MarkupText`` and ``MTex`` based on ``LabelledString`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
|
||||
- Refactored ``LabelledString`` and relevant classes (`#1779 <https://github.com/3b1b/manim/pull/1779>`__)
|
||||
|
||||
|
||||
v1.5.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Bug fix for the case of calling ``Write`` on a null object (`#1740 <https://github.com/3b1b/manim/pull/1740>`__)
|
||||
|
||||
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
- Added ``TransformMatchingMTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
|
||||
- Added ``ImplicitFunction`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
|
||||
- Added ``Polyline`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
- Allowed ``Mobject.set_points`` to take in an empty list, and added ``Mobject.add_point`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/a64259158538eae6043566aaf3d3329ff4ac394b>`__)
|
||||
- Added ``Scene.refresh_locked_data`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/33d2894c167c577a15fdadbaf26488ff1f5bff87>`__)
|
||||
- Added presenter mode to scenes with ``-p`` option (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9a9cc8bdacb7541b7cd4a52ad705abc21f3e27fe>`__ and `#1742 <https://github.com/3b1b/manim/pull/1742>`__)
|
||||
- Allowed for an embed by hitting ``ctrl+shift+e`` during interaction (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9df12fcb7d8360e51cd7021d6877ca1a5c31835e>`__ and `#1746 <https://github.com/3b1b/manim/pull/1746>`__)
|
||||
- Added ``Mobject.set_min_width/height/depth`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2798d15591a0375ae6bb9135473e6f5328267323>`__)
|
||||
- Allowed ``Mobject.match_coord/x/y/z`` to take in a point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/29a4d3e82ba94c007c996b2d1d0f923941452698>`__)
|
||||
- Added ``text_config`` to ``DecimalNumber`` (`#1744 <https://github.com/3b1b/manim/pull/1744>`__)
|
||||
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
- Refactored ``MTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
|
||||
- Refactored ``SVGMobject`` with svgelements (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
- Made sure ``ParametricCurve`` has at least one point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2488b9e866fb1ecb842a27dd9f4956ec167e3dee>`__)
|
||||
- Set default to no tips on ``Axes`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/6c6d387a210756c38feca7d34838aa9ac99bb58a>`__)
|
||||
- Stopped displaying when writing tex string is happening (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/58e06e8f6b7c5059ff315d51fd0018fec5cfbb05>`__)
|
||||
- Reorganize inheriting order and refactor SVGMobject (`#1745 <https://github.com/3b1b/manim/pull/1745>`__)
|
||||
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
- Added dependency on ``isosurfaces`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
|
||||
- Removed dependency on ``argparse`` since it's a built-in module (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
|
||||
- Removed dependency on ``pyreadline`` (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
|
||||
- Removed dependency on ``cssselect2`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
- Added dependency on ``svgelements`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
|
||||
|
||||
|
||||
v1.4.1
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- Temporarily fixed boolean operations' bug (`#1724 <https://github.com/3b1b/manim/pull/1724>`__)
|
||||
- Import ``Iterable`` from ``collections.abc`` instead of ``collections`` which is deprecated since python 3.9 (`d2e0811 <https://github.com/3b1b/manim/commit/d2e0811285f7908e71a65e664fec88b1af1c6144>`__)
|
||||
|
||||
v1.4.0
|
||||
------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
- `f1996f8 <https://github.com/3b1b/manim/pull/1697/commits/f1996f8479f9e33d626b3b66e9eb6995ce231d86>`__: Temporarily fixed ``Lightbulb``
|
||||
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Fixed some bugs of ``SVGMobject``
|
||||
- `#1717 <https://github.com/3b1b/manim/pull/1717>`__: Fixed some bugs of SVG path string parser
|
||||
- `#1720 <https://github.com/3b1b/manim/pull/1720>`__: Fixed some bugs of ``MTex``
|
||||
- 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
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
- `#1694 <https://github.com/3b1b/manim/pull/1694>`__: Added option to add ticks on x-axis in ``BarChart``
|
||||
- `#1704 <https://github.com/3b1b/manim/pull/1704>`__: Added ``lable_buff`` config parameter for ``Brace``
|
||||
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Added support for ``rotate skewX skewY`` transform in SVG
|
||||
- `#1717 <https://github.com/3b1b/manim/pull/1717>`__: Added style support to ``SVGMobject``
|
||||
- `#1719 <https://github.com/3b1b/manim/pull/1719>`__: Added parser to <style> element of SVG
|
||||
- `#1719 <https://github.com/3b1b/manim/pull/1719>`__: Added support for <line> element in ``SVGMobject``
|
||||
- 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
|
||||
^^^^^^^^
|
||||
- `5aa8d15 <https://github.com/3b1b/manim/pull/1697/commits/5aa8d15d85797f68a8f169ca69fd90d441a3abbe>`__: Used ``FFMPEG_BIN`` instead of ``"ffmpeg"`` for sound incorporation
|
||||
- `#1709 <https://github.com/3b1b/manim/pull/1709>`__: Decorated ``CoordinateSystem.get_axes`` and ``.get_all_ranges`` as abstract method
|
||||
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Refactored SVG path string parser
|
||||
- `#1712 <https://github.com/3b1b/manim/pull/1712>`__: Allowed ``Mobject.scale`` to receive iterable ``scale_factor``
|
||||
- `#1716 <https://github.com/3b1b/manim/pull/1716>`__: Refactored ``MTex``
|
||||
- `#1721 <https://github.com/3b1b/manim/pull/1721>`__: Improved config helper (``manimgl --config``)
|
||||
- `#1723 <https://github.com/3b1b/manim/pull/1723>`__: Refactored ``MTex``
|
||||
- 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
|
||||
^^^^^^^^^^^^
|
||||
- `#1719 <https://github.com/3b1b/manim/pull/1719>`__: Added dependency on python package `cssselect2 <https://github.com/Kozea/cssselect2>`__
|
||||
- Added dependency on python package `cssselect2 <https://github.com/Kozea/cssselect2>`__ (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
|
||||
|
||||
|
||||
v1.3.0
|
||||
@@ -41,63 +151,63 @@ v1.3.0
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- `#1653 <https://github.com/3b1b/manim/pull/1653>`__: Fixed ``Mobject.stretch_to_fit_depth``
|
||||
- `#1655 <https://github.com/3b1b/manim/pull/1655>`__: Fixed the bug of rotating camera
|
||||
- `c73d507 <https://github.com/3b1b/manim/pull/1688/commits/c73d507c76af5c8602d4118bc7538ba04c03ebae>`__: Fixed ``SurfaceMesh`` to be evenly spaced
|
||||
- `82bd02d <https://github.com/3b1b/manim/pull/1688/commits/82bd02d21fbd89b71baa21e077e143f440df9014>`__: Fixed ``angle_between_vectors`` add ``rotation_between_vectors``
|
||||
- `a717314 <https://github.com/3b1b/manim/pull/1688/commits/a7173142bf93fd309def0cc10f3c56f5e6972332>`__: Fixed ``VMobject.fade``
|
||||
- `fbc329d <https://github.com/3b1b/manim/pull/1688/commits/fbc329d7ce3b11821d47adf6052d932f7eff724a>`__: Fixed ``angle_between_vectors``
|
||||
- `bcd0990 <https://github.com/3b1b/manim/pull/1688/commits/bcd09906bea5eaaa5352e7bee8f3153f434cf606>`__: Fixed bug in ``ShowSubmobjectsOneByOne``
|
||||
- `7023548 <https://github.com/3b1b/manim/pull/1691/commits/7023548ec62c4adb2f371aab6a8c7f62deb7c33c>`__: Fixed bug in ``TransformMatchingParts``
|
||||
- Fixed ``Mobject.stretch_to_fit_depth`` (`#1653 <https://github.com/3b1b/manim/pull/1653>`__)
|
||||
- Fixed the bug of rotating camera (`#1655 <https://github.com/3b1b/manim/pull/1655>`__)
|
||||
- Fixed ``SurfaceMesh`` to be evenly spaced (`c73d507 <https://github.com/3b1b/manim/pull/1688/commits/c73d507c76af5c8602d4118bc7538ba04c03ebae>`__)
|
||||
- Fixed ``angle_between_vectors`` add ``rotation_between_vectors`` (`82bd02d <https://github.com/3b1b/manim/pull/1688/commits/82bd02d21fbd89b71baa21e077e143f440df9014>`__)
|
||||
- Fixed ``VMobject.fade`` (`a717314 <https://github.com/3b1b/manim/pull/1688/commits/a7173142bf93fd309def0cc10f3c56f5e6972332>`__)
|
||||
- Fixed ``angle_between_vectors`` (`fbc329d <https://github.com/3b1b/manim/pull/1688/commits/fbc329d7ce3b11821d47adf6052d932f7eff724a>`__)
|
||||
- Fixed bug in ``ShowSubmobjectsOneByOne`` (`bcd0990 <https://github.com/3b1b/manim/pull/1688/commits/bcd09906bea5eaaa5352e7bee8f3153f434cf606>`__)
|
||||
- Fixed bug in ``TransformMatchingParts`` (`7023548 <https://github.com/3b1b/manim/pull/1691/commits/7023548ec62c4adb2f371aab6a8c7f62deb7c33c>`__)
|
||||
|
||||
New Features
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `e10f850 <https://github.com/3b1b/manim/commit/e10f850d0d9f971931cc85d44befe67dc842af6d>`__: Added CLI flag ``--log-level`` to specify log level
|
||||
- `#1667 <https://github.com/3b1b/manim/pull/1667>`__: Added operations (``+`` and ``*``) for ``Mobject``
|
||||
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added 4 boolean operations for ``VMobject`` in ``manimlib/mobject/boolean_ops.py``
|
||||
- Added CLI flag ``--log-level`` to specify log level (`e10f850 <https://github.com/3b1b/manim/commit/e10f850d0d9f971931cc85d44befe67dc842af6d>`__)
|
||||
- Added operations (``+`` and ``*``) for ``Mobject`` (`#1667 <https://github.com/3b1b/manim/pull/1667>`__)
|
||||
- Added 4 boolean operations for ``VMobject`` in ``manimlib/mobject/boolean_ops.py`` (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
|
||||
|
||||
- ``Union(*vmobjects, **kwargs)``
|
||||
- ``Difference(subject, clip, **kwargs)``
|
||||
- ``Intersection(*vmobjects, **kwargs)``
|
||||
- ``Exclusion(*vmobjects, **kwargs)``
|
||||
- `81c3ae3 <https://github.com/3b1b/manim/pull/1688/commits/81c3ae30372e288dc772633dbd17def6e603753e>`__: Added reflectiveness
|
||||
- `2c7689e <https://github.com/3b1b/manim/pull/1688/commits/2c7689ed9e81229ce87c648f97f26267956c0bc9>`__: Enabled ``glow_factor`` on ``DotCloud``
|
||||
- `d065e19 <https://github.com/3b1b/manim/pull/1688/commits/d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772>`__: Added option ``-e`` to insert embed line from the command line
|
||||
- `0e78027 <https://github.com/3b1b/manim/pull/1688/commits/0e78027186a976f7e5fa8d586f586bf6e6baab8d>`__: Improved ``point_from_proportion`` to account for arc length
|
||||
- `781a993 <https://github.com/3b1b/manim/pull/1688/commits/781a9934fda6ba11f22ba32e8ccddcb3ba78592e>`__: Added shortcut ``set_backstroke`` for setting black background stroke
|
||||
- `0b898a5 <https://github.com/3b1b/manim/pull/1688/commits/0b898a5594203668ed9cad38b490ab49ba233bd4>`__: Added ``Suface.always_sort_to_camera``
|
||||
- `e899604 <https://github.com/3b1b/manim/pull/1688/commits/e899604a2d05f78202fcb3b9824ec34647237eae>`__: Added getter methods for specific euler angles
|
||||
- `407c53f <https://github.com/3b1b/manim/pull/1688/commits/407c53f97c061bfd8a53beacd88af4c786f9e9ee>`__: Hade ``rotation_between_vectors`` handle identical/similar vectors
|
||||
- `49743da <https://github.com/3b1b/manim/pull/1688/commits/49743daf3244bfa11a427040bdde8e2bb79589e8>`__: Added ``Mobject.insert_submobject`` method
|
||||
- `9dd1f47 <https://github.com/3b1b/manim/pull/1688/commits/9dd1f47dabca1580d6102e34e44574b0cba556e7>`__: Created single progress display for full scene render
|
||||
- `264f7b1 <https://github.com/3b1b/manim/pull/1691/commits/264f7b11726e9e736f0fe472f66e38539f74e848>`__: Added ``Circle.get_radius``
|
||||
- `83841ae <https://github.com/3b1b/manim/pull/1691/commits/83841ae41568a9c9dff44cd163106c19a74ac281>`__: Added ``Dodecahedron``
|
||||
- `a1d5147 <https://github.com/3b1b/manim/pull/1691/commits/a1d51474ea1ce3b7aa3efbe4c5e221be70ee2f5b>`__: Added ``GlowDot``
|
||||
- `#1678 <https://github.com/3b1b/manim/pull/1678>`__: Added ``MTex`` , see `#1678 <https://github.com/3b1b/manim/pull/1678>`__ for details
|
||||
- Added reflectiveness (`81c3ae3 <https://github.com/3b1b/manim/pull/1688/commits/81c3ae30372e288dc772633dbd17def6e603753e>`__)
|
||||
- Enabled ``glow_factor`` on ``DotCloud`` (`2c7689e <https://github.com/3b1b/manim/pull/1688/commits/2c7689ed9e81229ce87c648f97f26267956c0bc9>`__)
|
||||
- Added option ``-e`` to insert embed line from the command line (`d065e19 <https://github.com/3b1b/manim/pull/1688/commits/d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772>`__)
|
||||
- Improved ``point_from_proportion`` to account for arc length (`0e78027 <https://github.com/3b1b/manim/pull/1688/commits/0e78027186a976f7e5fa8d586f586bf6e6baab8d>`__)
|
||||
- Added shortcut ``set_backstroke`` for setting black background stroke (`781a993 <https://github.com/3b1b/manim/pull/1688/commits/781a9934fda6ba11f22ba32e8ccddcb3ba78592e>`__)
|
||||
- Added ``Suface.always_sort_to_camera`` (`0b898a5 <https://github.com/3b1b/manim/pull/1688/commits/0b898a5594203668ed9cad38b490ab49ba233bd4>`__)
|
||||
- Added getter methods for specific euler angles (`e899604 <https://github.com/3b1b/manim/pull/1688/commits/e899604a2d05f78202fcb3b9824ec34647237eae>`__)
|
||||
- Hade ``rotation_between_vectors`` handle identical/similar vectors (`407c53f <https://github.com/3b1b/manim/pull/1688/commits/407c53f97c061bfd8a53beacd88af4c786f9e9ee>`__)
|
||||
- Added ``Mobject.insert_submobject`` method (`49743da <https://github.com/3b1b/manim/pull/1688/commits/49743daf3244bfa11a427040bdde8e2bb79589e8>`__)
|
||||
- Created single progress display for full scene render (`9dd1f47 <https://github.com/3b1b/manim/pull/1688/commits/9dd1f47dabca1580d6102e34e44574b0cba556e7>`__)
|
||||
- Added ``Circle.get_radius`` (`264f7b1 <https://github.com/3b1b/manim/pull/1691/commits/264f7b11726e9e736f0fe472f66e38539f74e848>`__)
|
||||
- Added ``Dodecahedron`` (`83841ae <https://github.com/3b1b/manim/pull/1691/commits/83841ae41568a9c9dff44cd163106c19a74ac281>`__)
|
||||
- Added ``GlowDot`` (`a1d5147 <https://github.com/3b1b/manim/pull/1691/commits/a1d51474ea1ce3b7aa3efbe4c5e221be70ee2f5b>`__)
|
||||
- Added ``MTex`` , see `#1678 <https://github.com/3b1b/manim/pull/1678>`__ for details (`#1678 <https://github.com/3b1b/manim/pull/1678>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
|
||||
- `#1662 <https://github.com/3b1b/manim/pull/1662>`__: Refactored support for command ``A`` in path of SVG
|
||||
- `#1662 <https://github.com/3b1b/manim/pull/1662>`__: Refactored ``SingleStringTex.balance_braces``
|
||||
- `8b454fb <https://github.com/3b1b/manim/pull/1688/commits/8b454fbe9335a7011e947093230b07a74ba9c653>`__: Slight tweaks to how saturation_factor works on newton-fractal
|
||||
- `317a5d6 <https://github.com/3b1b/manim/pull/1688/commits/317a5d6226475b6b54a78db7116c373ef84ea923>`__: Made it possible to set full screen preview as a default
|
||||
- `e764da3 <https://github.com/3b1b/manim/pull/1688/commits/e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc>`__: Used ``quick_point_from_proportion`` for graph points
|
||||
- `d2182b9 <https://github.com/3b1b/manim/pull/1688/commits/d2182b9112300558b6c074cefd685f97c10b3898>`__: Made sure ``Line.set_length`` returns self
|
||||
- `eea3c6b <https://github.com/3b1b/manim/pull/1688/commits/eea3c6b29438f9e9325329c4355e76b9f635e97a>`__: Better align ``SurfaceMesh`` to the corresponding surface polygons
|
||||
- `ee1594a <https://github.com/3b1b/manim/pull/1688/commits/ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3>`__: Match ``fix_in_frame`` status for ``FlashAround`` mobject
|
||||
- `ba23fbe <https://github.com/3b1b/manim/pull/1688/commits/ba23fbe71e4a038201cd7df1d200514ed1c13bc2>`__: Made sure ``Mobject.is_fixed_in_frame`` stays updated with uniforms
|
||||
- `98b0d26 <https://github.com/3b1b/manim/pull/1691/commits/98b0d266d2475926a606331923cca3dc1dea97ad>`__: Made sure ``skip_animations`` and ``start_at_animation_number`` play well together
|
||||
- `f8e6e7d <https://github.com/3b1b/manim/pull/1691/commits/f8e6e7df3ceb6f3d845ced4b690a85b35e0b8d00>`__: Updated progress display for full scene render
|
||||
- `8f1dfab <https://github.com/3b1b/manim/pull/1691/commits/8f1dfabff04a8456f5c4df75b0f97d50b2755003>`__: ``VectorizedPoint`` should call ``__init__`` for both super classes
|
||||
- `758f329 <https://github.com/3b1b/manim/pull/1691/commits/758f329a06a0c198b27a48c577575d94554305bf>`__: Used array copy when checking need for refreshing triangulation
|
||||
- Refactored support for command ``A`` in path of SVG (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
|
||||
- Refactored ``SingleStringTex.balance_braces`` (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
|
||||
- Slight tweaks to how saturation_factor works on newton-fractal (`8b454fb <https://github.com/3b1b/manim/pull/1688/commits/8b454fbe9335a7011e947093230b07a74ba9c653>`__)
|
||||
- Made it possible to set full screen preview as a default (`317a5d6 <https://github.com/3b1b/manim/pull/1688/commits/317a5d6226475b6b54a78db7116c373ef84ea923>`__)
|
||||
- Used ``quick_point_from_proportion`` for graph points (`e764da3 <https://github.com/3b1b/manim/pull/1688/commits/e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc>`__)
|
||||
- Made sure ``Line.set_length`` returns self (`d2182b9 <https://github.com/3b1b/manim/pull/1688/commits/d2182b9112300558b6c074cefd685f97c10b3898>`__)
|
||||
- Better align ``SurfaceMesh`` to the corresponding surface polygons (`eea3c6b <https://github.com/3b1b/manim/pull/1688/commits/eea3c6b29438f9e9325329c4355e76b9f635e97a>`__)
|
||||
- Match ``fix_in_frame`` status for ``FlashAround`` mobject (`ee1594a <https://github.com/3b1b/manim/pull/1688/commits/ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3>`__)
|
||||
- Made sure ``Mobject.is_fixed_in_frame`` stays updated with uniforms (`ba23fbe <https://github.com/3b1b/manim/pull/1688/commits/ba23fbe71e4a038201cd7df1d200514ed1c13bc2>`__)
|
||||
- Made sure ``skip_animations`` and ``start_at_animation_number`` play well together (`98b0d26 <https://github.com/3b1b/manim/pull/1691/commits/98b0d266d2475926a606331923cca3dc1dea97ad>`__)
|
||||
- Updated progress display for full scene render (`f8e6e7d <https://github.com/3b1b/manim/pull/1691/commits/f8e6e7df3ceb6f3d845ced4b690a85b35e0b8d00>`__)
|
||||
- ``VectorizedPoint`` should call ``__init__`` for both super classes (`8f1dfab <https://github.com/3b1b/manim/pull/1691/commits/8f1dfabff04a8456f5c4df75b0f97d50b2755003>`__)
|
||||
- Used array copy when checking need for refreshing triangulation (`758f329 <https://github.com/3b1b/manim/pull/1691/commits/758f329a06a0c198b27a48c577575d94554305bf>`__)
|
||||
|
||||
|
||||
Dependencies
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__
|
||||
- Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__ (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
|
||||
|
||||
v1.2.0
|
||||
------
|
||||
@@ -105,57 +215,57 @@ v1.2.0
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- `#1592 <https://github.com/3b1b/manim/pull/1592>`__: Fixed ``put_start_and_end_on`` in 3D
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Fixed ``DecimalNumber``'s scaling issue
|
||||
- `56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__: Fixed bug with common range array used for all coordinate systems
|
||||
- `8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__: Fixed ``CoordinateSystem`` init bug
|
||||
- `0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__: Fixed bug for single-valued ``ValueTracker``
|
||||
- `54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__: Fixed bug with SVG rectangles
|
||||
- `d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__: Fixed ``DotCloud.set_radii``
|
||||
- `b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__: Temporarily fixed bug for ``PMobject`` array resizing
|
||||
- `5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__: Fixed ``match_style``
|
||||
- `719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__: Fixed negative ``path_arc`` case
|
||||
- `c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__: Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis``
|
||||
- `7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__: Fixed ``ComplexPlane`` -i display bug
|
||||
- Fixed ``put_start_and_end_on`` in 3D (`#1592 <https://github.com/3b1b/manim/pull/1592>`__)
|
||||
- Fixed ``DecimalNumber``'s scaling issue (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
|
||||
- Fixed bug with common range array used for all coordinate systems (`56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__)
|
||||
- Fixed ``CoordinateSystem`` init bug (`8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__)
|
||||
- Fixed bug for single-valued ``ValueTracker`` (`0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__)
|
||||
- Fixed bug with SVG rectangles (`54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__)
|
||||
- Fixed ``DotCloud.set_radii`` (`d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__)
|
||||
- Temporarily fixed bug for ``PMobject`` array resizing (`b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__)
|
||||
- Fixed ``match_style`` (`5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__)
|
||||
- Fixed negative ``path_arc`` case (`719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__)
|
||||
- Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis`` (`c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__)
|
||||
- Fixed ``ComplexPlane`` -i display bug (`7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__)
|
||||
|
||||
New Features
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- `#1598 <https://github.com/3b1b/manim/pull/1598>`__: Supported the elliptical arc command ``A`` for ``SVGMobject``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Added ``FlashyFadeIn``
|
||||
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Save triangulation
|
||||
- `#1625 <https://github.com/3b1b/manim/pull/1625>`__: Added new ``Code`` mobject
|
||||
- `#1637 <https://github.com/3b1b/manim/pull/1637>`__: Add warnings and use rich to display log
|
||||
- `bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__: Added ``VCube``
|
||||
- `6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__: Supported ``ValueTracker`` to track vectors
|
||||
- `3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__: Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject``
|
||||
- `a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__: Added ``TracgTail``
|
||||
- `acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__: Added ``Scene.point_to_mobject``
|
||||
- `f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__: Added poly_fractal shader
|
||||
- `b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__: Added kwargs to ``TipableVMobject.set_length``
|
||||
- `17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__: Added ``Mobject.replicate``
|
||||
- `33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__: Added mandelbrot_fractal shader
|
||||
- `f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__: Saved state before each embed
|
||||
- `e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__: Allowed releasing of Textures
|
||||
- `14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__: Consolidated and renamed newton_fractal shader
|
||||
- `6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__: Hade ``ImageMoject`` remember the filepath to the Image
|
||||
- Supported the elliptical arc command ``A`` for ``SVGMobject`` (`#1598 <https://github.com/3b1b/manim/pull/1598>`__)
|
||||
- Added ``FlashyFadeIn`` (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
|
||||
- Save triangulation (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
|
||||
- Added new ``Code`` mobject (`#1625 <https://github.com/3b1b/manim/pull/1625>`__)
|
||||
- Add warnings and use rich to display log (`#1637 <https://github.com/3b1b/manim/pull/1637>`__)
|
||||
- Added ``VCube`` (`bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__)
|
||||
- Supported ``ValueTracker`` to track vectors (`6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__)
|
||||
- Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject`` (`3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__)
|
||||
- Added ``TracgTail`` (`a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__)
|
||||
- Added ``Scene.point_to_mobject`` (`acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__)
|
||||
- Added poly_fractal shader (`f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__)
|
||||
- Added kwargs to ``TipableVMobject.set_length`` (`b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__)
|
||||
- Added ``Mobject.replicate`` (`17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__)
|
||||
- Added mandelbrot_fractal shader (`33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__)
|
||||
- Saved state before each embed (`f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__)
|
||||
- Allowed releasing of Textures (`e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__)
|
||||
- Consolidated and renamed newton_fractal shader (`14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__)
|
||||
- Hade ``ImageMoject`` remember the filepath to the Image (`6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__)
|
||||
|
||||
Refactor
|
||||
^^^^^^^^
|
||||
|
||||
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Changed back to simpler ``Mobject.scale`` implementation
|
||||
- `b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__: Simplified ``Square``
|
||||
- `40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__: Removed unused parameter ``triangulation_locked``
|
||||
- `8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__: Reimplemented ``Arrow``
|
||||
- `d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__: Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default
|
||||
- `7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__: Refactored to call ``_handle_scale_side_effects`` after scaling takes place
|
||||
- `7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__: Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end``
|
||||
- `0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__: Made sure framerate is 30 for previewed scenes
|
||||
- `c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__: Pushed ``pixel_coords_to_space_coords`` to ``Window``
|
||||
- `d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__: Refactored to pass tuples and not arrays to uniforms
|
||||
- `9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__: Refactored to copy uniform arrays in ``Mobject.copy``
|
||||
- `ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__: Added ``bounding_box`` as exceptional key to point_cloud mobject
|
||||
- `329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__: Made sure stroke width is always a float
|
||||
- Changed back to simpler ``Mobject.scale`` implementation (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
|
||||
- Simplified ``Square`` (`b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__)
|
||||
- Removed unused parameter ``triangulation_locked`` (`40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__)
|
||||
- Reimplemented ``Arrow`` (`8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__)
|
||||
- Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default (`d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__)
|
||||
- Refactored to call ``_handle_scale_side_effects`` after scaling takes place (`7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__)
|
||||
- Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end`` (`7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__)
|
||||
- Made sure framerate is 30 for previewed scenes (`0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__)
|
||||
- Pushed ``pixel_coords_to_space_coords`` to ``Window`` (`c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__)
|
||||
- Refactored to pass tuples and not arrays to uniforms (`d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__)
|
||||
- Refactored to copy uniform arrays in ``Mobject.copy`` (`9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__)
|
||||
- Added ``bounding_box`` as exceptional key to point_cloud mobject (`ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__)
|
||||
- Made sure stroke width is always a float (`329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__)
|
||||
|
||||
|
||||
v1.1.0
|
||||
@@ -181,7 +291,7 @@ Fixed bugs
|
||||
- Rewrote ``earclip_triangulation`` to fix triangulation
|
||||
- Allowed sound_file_name to be taken in without extensions
|
||||
|
||||
New Features
|
||||
New features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Added :class:`~manimlib.animation.indication.VShowPassingFlash`
|
||||
|
||||
@@ -84,8 +84,6 @@ Text
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
START_X = 30
|
||||
START_Y = 20
|
||||
NORMAL = "NORMAL"
|
||||
ITALIC = "ITALIC"
|
||||
OBLIQUE = "OBLIQUE"
|
||||
|
||||
@@ -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
|
||||
@@ -58,7 +59,7 @@ flag abbr function
|
||||
``--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
|
||||
========================================================== ====== =================================================================================================================================================================================================
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
|
||||
Manim runs on Python 3.7 or higher.
|
||||
|
||||
System requirements are:
|
||||
|
||||
|
||||
@@ -650,7 +650,7 @@ class ControlsExample(Scene):
|
||||
|
||||
def text_updater(old_text):
|
||||
assert(isinstance(old_text, Text))
|
||||
new_text = Text(self.textbox.get_value(), size=old_text.size)
|
||||
new_text = Text(self.textbox.get_value(), font_size=old_text.font_size)
|
||||
# new_text.align_data_and_family(old_text)
|
||||
new_text.move_to(old_text)
|
||||
if self.checkbox.get_value():
|
||||
|
||||
@@ -37,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.labelled_string import *
|
||||
from manimlib.mobject.svg.mtex_mobject import *
|
||||
from manimlib.mobject.svg.svg_mobject import *
|
||||
from manimlib.mobject.svg.tex_mobject import *
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -6,6 +9,11 @@ from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
DEFAULT_ANIMATION_RUN_TIME = 1.0
|
||||
DEFAULT_ANIMATION_LAG_RATIO = 0
|
||||
@@ -29,17 +37,17 @@ class Animation(object):
|
||||
"suspend_mobject_updating": True,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
assert(isinstance(mobject, Mobject))
|
||||
digest_config(self, kwargs)
|
||||
self.mobject = mobject
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
if self.name:
|
||||
return self.name
|
||||
return self.__class__.__name__ + str(self.mobject)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
# This is called right as an animation is being
|
||||
# played. As much initialization as possible,
|
||||
# especially any mobject copying, should live in
|
||||
@@ -56,32 +64,32 @@ class Animation(object):
|
||||
self.families = list(self.get_all_families_zipped())
|
||||
self.interpolate(0)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
self.interpolate(self.final_alpha_value)
|
||||
if self.suspend_mobject_updating:
|
||||
self.mobject.resume_updating()
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
if self.is_remover():
|
||||
scene.remove(self.mobject)
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
# Keep track of where the mobject starts
|
||||
return self.mobject.copy()
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> tuple[Mobject, Mobject]:
|
||||
"""
|
||||
Ordering must match the ording of arguments to interpolate_submobject
|
||||
"""
|
||||
return self.mobject, self.starting_mobject
|
||||
|
||||
def get_all_families_zipped(self):
|
||||
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
|
||||
return zip(*[
|
||||
mob.get_family()
|
||||
for mob in self.get_all_mobjects()
|
||||
])
|
||||
|
||||
def update_mobjects(self, dt):
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
"""
|
||||
Updates things like starting_mobject, and (for
|
||||
Transforms) target_mobject. Note, since typically
|
||||
@@ -92,7 +100,7 @@ class Animation(object):
|
||||
for mob in self.get_all_mobjects_to_update():
|
||||
mob.update(dt)
|
||||
|
||||
def get_all_mobjects_to_update(self):
|
||||
def get_all_mobjects_to_update(self) -> list[Mobject]:
|
||||
# The surrounding scene typically handles
|
||||
# updating of self.mobject. Besides, in
|
||||
# most cases its updating is suspended anyway
|
||||
@@ -109,27 +117,37 @@ class Animation(object):
|
||||
return self
|
||||
|
||||
# Methods for interpolation, the mean of an Animation
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
alpha = clip(alpha, 0, 1)
|
||||
self.interpolate_mobject(self.rate_func(alpha))
|
||||
|
||||
def update(self, alpha):
|
||||
def update(self, alpha: float) -> None:
|
||||
"""
|
||||
This method shouldn't exist, but it's here to
|
||||
keep many old scenes from breaking
|
||||
"""
|
||||
self.interpolate(alpha)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
for i, mobs in enumerate(self.families):
|
||||
sub_alpha = self.get_sub_alpha(alpha, i, len(self.families))
|
||||
self.interpolate_submobject(*mobs, sub_alpha)
|
||||
|
||||
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
alpha: float
|
||||
):
|
||||
# Typically ipmlemented by subclass
|
||||
pass
|
||||
|
||||
def get_sub_alpha(self, alpha, index, num_submobjects):
|
||||
def get_sub_alpha(
|
||||
self,
|
||||
alpha: float,
|
||||
index: int,
|
||||
num_submobjects: int
|
||||
) -> float:
|
||||
# TODO, make this more understanable, and/or combine
|
||||
# its functionality with AnimationGroup's method
|
||||
# build_animations_with_timings
|
||||
@@ -140,29 +158,29 @@ class Animation(object):
|
||||
return clip((value - lower), 0, 1)
|
||||
|
||||
# Getters and setters
|
||||
def set_run_time(self, run_time):
|
||||
def set_run_time(self, run_time: float):
|
||||
self.run_time = run_time
|
||||
return self
|
||||
|
||||
def get_run_time(self):
|
||||
def get_run_time(self) -> float:
|
||||
return self.run_time
|
||||
|
||||
def set_rate_func(self, rate_func):
|
||||
def set_rate_func(self, rate_func: Callable[[float], float]):
|
||||
self.rate_func = rate_func
|
||||
return self
|
||||
|
||||
def get_rate_func(self):
|
||||
def get_rate_func(self) -> Callable[[float], float]:
|
||||
return self.rate_func
|
||||
|
||||
def set_name(self, name):
|
||||
def set_name(self, name: str):
|
||||
self.name = name
|
||||
return self
|
||||
|
||||
def is_remover(self):
|
||||
def is_remover(self) -> bool:
|
||||
return self.remover
|
||||
|
||||
|
||||
def prepare_animation(anim):
|
||||
def prepare_animation(anim: Animation | _AnimationBuilder):
|
||||
if isinstance(anim, _AnimationBuilder):
|
||||
return anim.build()
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.animation.animation import Animation, prepare_animation
|
||||
from manimlib.mobject.mobject import Group
|
||||
@@ -9,6 +12,12 @@ from manimlib.utils.iterables import remove_list_redundancies
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
DEFAULT_LAGGED_START_LAG_RATIO = 0.05
|
||||
|
||||
@@ -27,7 +36,7 @@ class AnimationGroup(Animation):
|
||||
"group": None,
|
||||
}
|
||||
|
||||
def __init__(self, *animations, **kwargs):
|
||||
def __init__(self, *animations: Animation, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.animations = [prepare_animation(anim) for anim in animations]
|
||||
if self.group is None:
|
||||
@@ -37,27 +46,27 @@ class AnimationGroup(Animation):
|
||||
self.init_run_time()
|
||||
Animation.__init__(self, self.group, **kwargs)
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> Group:
|
||||
return self.group
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
for anim in self.animations:
|
||||
anim.begin()
|
||||
# self.init_run_time()
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
for anim in self.animations:
|
||||
anim.finish()
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
for anim in self.animations:
|
||||
anim.clean_up_from_scene(scene)
|
||||
|
||||
def update_mobjects(self, dt):
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
for anim in self.animations:
|
||||
anim.update_mobjects(dt)
|
||||
|
||||
def init_run_time(self):
|
||||
def init_run_time(self) -> None:
|
||||
self.build_animations_with_timings()
|
||||
if self.anims_with_timings:
|
||||
self.max_end_time = np.max([
|
||||
@@ -68,7 +77,7 @@ class AnimationGroup(Animation):
|
||||
if self.run_time is None:
|
||||
self.run_time = self.max_end_time
|
||||
|
||||
def build_animations_with_timings(self):
|
||||
def build_animations_with_timings(self) -> None:
|
||||
"""
|
||||
Creates a list of triplets of the form
|
||||
(anim, start_time, end_time)
|
||||
@@ -87,7 +96,7 @@ class AnimationGroup(Animation):
|
||||
start_time, end_time, self.lag_ratio
|
||||
)
|
||||
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
# Note, if the run_time of AnimationGroup has been
|
||||
# set to something other than its default, these
|
||||
# times might not correspond to actual times,
|
||||
@@ -111,19 +120,19 @@ class Succession(AnimationGroup):
|
||||
"lag_ratio": 1,
|
||||
}
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
assert(len(self.animations) > 0)
|
||||
self.init_run_time()
|
||||
self.active_animation = self.animations[0]
|
||||
self.active_animation.begin()
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
self.active_animation.finish()
|
||||
|
||||
def update_mobjects(self, dt):
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
self.active_animation.update_mobjects(dt)
|
||||
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
index, subalpha = integer_interpolate(
|
||||
0, len(self.animations), alpha
|
||||
)
|
||||
@@ -146,7 +155,13 @@ class LaggedStartMap(LaggedStart):
|
||||
"run_time": 2,
|
||||
}
|
||||
|
||||
def __init__(self, AnimationClass, mobject, arg_creator=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
AnimationClass: type,
|
||||
mobject: Mobject,
|
||||
arg_creator: Callable[[Mobject], tuple] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
args_list = []
|
||||
for submob in mobject:
|
||||
if arg_creator:
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
from abc import abstractmethod
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.composition import Succession
|
||||
from manimlib.mobject.svg.labelled_string import LabelledString
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.rate_functions import double_smooth
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Group
|
||||
|
||||
|
||||
class ShowPartial(Animation):
|
||||
@@ -20,21 +29,27 @@ class ShowPartial(Animation):
|
||||
"should_match_start": False,
|
||||
}
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
super().begin()
|
||||
if not self.should_match_start:
|
||||
self.mobject.lock_matching_data(self.mobject, self.starting_mobject)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
|
||||
def interpolate_submobject(self, submob, start_submob, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: VMobject,
|
||||
start_submob: VMobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
submob.pointwise_become_partial(
|
||||
start_submob, *self.get_bounds(alpha)
|
||||
)
|
||||
|
||||
def get_bounds(self, alpha):
|
||||
@abstractmethod
|
||||
def get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
raise Exception("Not Implemented")
|
||||
|
||||
|
||||
@@ -43,7 +58,7 @@ class ShowCreation(ShowPartial):
|
||||
"lag_ratio": 1,
|
||||
}
|
||||
|
||||
def get_bounds(self, alpha):
|
||||
def get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
return (0, alpha)
|
||||
|
||||
|
||||
@@ -65,7 +80,7 @@ class DrawBorderThenFill(Animation):
|
||||
"fill_animation_config": {},
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
assert(isinstance(vmobject, VMobject))
|
||||
self.sm_to_index = dict([
|
||||
(hash(sm), 0)
|
||||
@@ -73,7 +88,7 @@ class DrawBorderThenFill(Animation):
|
||||
])
|
||||
super().__init__(vmobject, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
# Trigger triangulation calculation
|
||||
for submob in self.mobject.get_family():
|
||||
submob.get_triangulation()
|
||||
@@ -83,11 +98,11 @@ class DrawBorderThenFill(Animation):
|
||||
self.mobject.match_style(self.outline)
|
||||
self.mobject.lock_matching_data(self.mobject, self.outline)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
|
||||
def get_outline(self):
|
||||
def get_outline(self) -> VMobject:
|
||||
outline = self.mobject.copy()
|
||||
outline.set_fill(opacity=0)
|
||||
for sm in outline.get_family():
|
||||
@@ -97,17 +112,23 @@ class DrawBorderThenFill(Animation):
|
||||
)
|
||||
return outline
|
||||
|
||||
def get_stroke_color(self, vmobject):
|
||||
def get_stroke_color(self, vmobject: VMobject) -> str:
|
||||
if self.stroke_color:
|
||||
return self.stroke_color
|
||||
elif vmobject.get_stroke_width() > 0:
|
||||
return vmobject.get_stroke_color()
|
||||
return vmobject.get_color()
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> list[VMobject]:
|
||||
return [*super().get_all_mobjects(), self.outline]
|
||||
|
||||
def interpolate_submobject(self, submob, start, outline, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: VMobject,
|
||||
start: VMobject,
|
||||
outline: VMobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
index, subalpha = integer_interpolate(0, 2, alpha)
|
||||
|
||||
if index == 1 and self.sm_to_index[hash(submob)] == 0:
|
||||
@@ -134,20 +155,20 @@ class Write(DrawBorderThenFill):
|
||||
"rate_func": linear,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.set_default_config_from_length(mobject)
|
||||
super().__init__(mobject, **kwargs)
|
||||
self.set_default_config_from_length(vmobject)
|
||||
super().__init__(vmobject, **kwargs)
|
||||
|
||||
def set_default_config_from_length(self, mobject):
|
||||
length = len(mobject.family_members_with_points())
|
||||
def set_default_config_from_length(self, vmobject: VMobject) -> None:
|
||||
length = len(vmobject.family_members_with_points())
|
||||
if self.run_time is None:
|
||||
if length < 15:
|
||||
self.run_time = 1
|
||||
else:
|
||||
self.run_time = 2
|
||||
if self.lag_ratio is None:
|
||||
self.lag_ratio = min(4.0 / length, 0.2)
|
||||
self.lag_ratio = min(4.0 / (length + 1.0), 0.2)
|
||||
|
||||
|
||||
class ShowIncreasingSubsets(Animation):
|
||||
@@ -156,16 +177,16 @@ class ShowIncreasingSubsets(Animation):
|
||||
"int_func": np.round,
|
||||
}
|
||||
|
||||
def __init__(self, group, **kwargs):
|
||||
def __init__(self, group: Group, **kwargs):
|
||||
self.all_submobs = list(group.submobjects)
|
||||
super().__init__(group, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
n_submobs = len(self.all_submobs)
|
||||
index = int(self.int_func(alpha * n_submobs))
|
||||
self.update_submobject_list(index)
|
||||
|
||||
def update_submobject_list(self, index):
|
||||
def update_submobject_list(self, index: int) -> None:
|
||||
self.mobject.set_submobjects(self.all_submobs[:index])
|
||||
|
||||
|
||||
@@ -174,7 +195,7 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
|
||||
"int_func": np.ceil,
|
||||
}
|
||||
|
||||
def update_submobject_list(self, index):
|
||||
def update_submobject_list(self, index: int) -> None:
|
||||
# N = len(self.all_submobs)
|
||||
if index == 0:
|
||||
self.mobject.set_submobjects([])
|
||||
@@ -182,23 +203,19 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
|
||||
self.mobject.set_submobjects([self.all_submobs[index - 1]])
|
||||
|
||||
|
||||
# TODO, this is broken...
|
||||
class AddTextWordByWord(Succession):
|
||||
class AddTextWordByWord(ShowIncreasingSubsets):
|
||||
CONFIG = {
|
||||
# If given a value for run_time, it will
|
||||
# override the time_per_char
|
||||
# override the time_per_word
|
||||
"run_time": None,
|
||||
"time_per_char": 0.06,
|
||||
"time_per_word": 0.2,
|
||||
"rate_func": linear,
|
||||
}
|
||||
|
||||
def __init__(self, text_mobject, **kwargs):
|
||||
def __init__(self, string_mobject, **kwargs):
|
||||
assert isinstance(string_mobject, LabelledString)
|
||||
grouped_mobject = string_mobject.submob_groups
|
||||
digest_config(self, kwargs)
|
||||
tpc = self.time_per_char
|
||||
anims = it.chain(*[
|
||||
[
|
||||
ShowIncreasingSubsets(word, run_time=tpc * len(word)),
|
||||
Animation(word, run_time=0.005 * len(word)**1.5),
|
||||
]
|
||||
for word in text_mobject
|
||||
])
|
||||
super().__init__(*anims, **kwargs)
|
||||
if self.run_time is None:
|
||||
self.run_time = self.time_per_word * len(grouped_mobject)
|
||||
super().__init__(grouped_mobject, **kwargs)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
@@ -7,6 +9,13 @@ from manimlib.constants import ORIGIN
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.rate_functions import there_and_back
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
DEFAULT_FADE_LAG_RATIO = 0
|
||||
|
||||
@@ -16,7 +25,13 @@ class Fade(Transform):
|
||||
"lag_ratio": DEFAULT_FADE_LAG_RATIO,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, shift=ORIGIN, scale=1, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
shift: np.ndarray = ORIGIN,
|
||||
scale: float = 1,
|
||||
**kwargs
|
||||
):
|
||||
self.shift_vect = shift
|
||||
self.scale_factor = scale
|
||||
super().__init__(mobject, **kwargs)
|
||||
@@ -27,10 +42,10 @@ class FadeIn(Fade):
|
||||
"lag_ratio": DEFAULT_FADE_LAG_RATIO,
|
||||
}
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
start = super().create_starting_mobject()
|
||||
start.set_opacity(0)
|
||||
start.scale(1.0 / self.scale_factor)
|
||||
@@ -45,7 +60,7 @@ class FadeOut(Fade):
|
||||
"final_alpha_value": 0,
|
||||
}
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
result = self.mobject.copy()
|
||||
result.set_opacity(0)
|
||||
result.shift(self.shift_vect)
|
||||
@@ -54,7 +69,7 @@ class FadeOut(Fade):
|
||||
|
||||
|
||||
class FadeInFromPoint(FadeIn):
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
def __init__(self, mobject: Mobject, point: np.ndarray, **kwargs):
|
||||
super().__init__(
|
||||
mobject,
|
||||
shift=mobject.get_center() - point,
|
||||
@@ -64,7 +79,7 @@ class FadeInFromPoint(FadeIn):
|
||||
|
||||
|
||||
class FadeOutToPoint(FadeOut):
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
def __init__(self, mobject: Mobject, point: np.ndarray, **kwargs):
|
||||
super().__init__(
|
||||
mobject,
|
||||
shift=point - mobject.get_center(),
|
||||
@@ -79,7 +94,7 @@ class FadeTransform(Transform):
|
||||
"dim_to_match": 1,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, target_mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs):
|
||||
self.to_add_on_completion = target_mobject
|
||||
mobject.save_state()
|
||||
super().__init__(
|
||||
@@ -87,7 +102,7 @@ class FadeTransform(Transform):
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.ending_mobject = self.mobject.copy()
|
||||
Animation.begin(self)
|
||||
# Both 'start' and 'end' consists of the source and target mobjects.
|
||||
@@ -97,21 +112,21 @@ class FadeTransform(Transform):
|
||||
for m0, m1 in ((start[1], start[0]), (end[0], end[1])):
|
||||
self.ghost_to(m0, m1)
|
||||
|
||||
def ghost_to(self, source, target):
|
||||
def ghost_to(self, source: Mobject, target: Mobject) -> None:
|
||||
source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match)
|
||||
source.set_opacity(0)
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> list[Mobject]:
|
||||
return [
|
||||
self.mobject,
|
||||
self.starting_mobject,
|
||||
self.ending_mobject,
|
||||
]
|
||||
|
||||
def get_all_families_zipped(self):
|
||||
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
|
||||
return Animation.get_all_families_zipped(self)
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
Animation.clean_up_from_scene(self, scene)
|
||||
scene.remove(self.mobject)
|
||||
self.mobject[0].restore()
|
||||
@@ -119,11 +134,11 @@ class FadeTransform(Transform):
|
||||
|
||||
|
||||
class FadeTransformPieces(FadeTransform):
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.mobject[0].align_family(self.mobject[1])
|
||||
super().begin()
|
||||
|
||||
def ghost_to(self, source, target):
|
||||
def ghost_to(self, source: Mobject, target: Mobject) -> None:
|
||||
for sm0, sm1 in zip(source.get_family(), target.get_family()):
|
||||
super().ghost_to(sm0, sm1)
|
||||
|
||||
@@ -136,7 +151,12 @@ class VFadeIn(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def interpolate_submobject(self, submob, start, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: VMobject,
|
||||
start: VMobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
submob.set_stroke(
|
||||
opacity=interpolate(0, start.get_stroke_opacity(), alpha)
|
||||
)
|
||||
@@ -152,7 +172,12 @@ class VFadeOut(VFadeIn):
|
||||
"final_alpha_value": 0,
|
||||
}
|
||||
|
||||
def interpolate_submobject(self, submob, start, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: VMobject,
|
||||
start: VMobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
super().interpolate_submobject(submob, start, 1 - alpha)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
from manimlib.animation.transform import Transform
|
||||
# from manimlib.utils.paths import counterclockwise_path
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import PI
|
||||
from manimlib.animation.transform import Transform
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
|
||||
|
||||
class GrowFromPoint(Transform):
|
||||
@@ -8,14 +16,14 @@ class GrowFromPoint(Transform):
|
||||
"point_color": None,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
def __init__(self, mobject: Mobject, point: np.ndarray, **kwargs):
|
||||
self.point = point
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
start = super().create_starting_mobject()
|
||||
start.scale(0)
|
||||
start.move_to(self.point)
|
||||
@@ -25,19 +33,19 @@ class GrowFromPoint(Transform):
|
||||
|
||||
|
||||
class GrowFromCenter(GrowFromPoint):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
point = mobject.get_center()
|
||||
super().__init__(mobject, point, **kwargs)
|
||||
|
||||
|
||||
class GrowFromEdge(GrowFromPoint):
|
||||
def __init__(self, mobject, edge, **kwargs):
|
||||
def __init__(self, mobject: Mobject, edge: np.ndarray, **kwargs):
|
||||
point = mobject.get_bounding_box_point(edge)
|
||||
super().__init__(mobject, point, **kwargs)
|
||||
|
||||
|
||||
class GrowArrow(GrowFromPoint):
|
||||
def __init__(self, arrow, **kwargs):
|
||||
def __init__(self, arrow: Arrow, **kwargs):
|
||||
point = arrow.get_start()
|
||||
super().__init__(arrow, point, **kwargs)
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Union, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.animation.animation import Animation
|
||||
@@ -25,6 +29,13 @@ from manimlib.utils.rate_functions import wiggle
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.rate_functions import squish_rate_func
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import colour
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
class FocusOn(Transform):
|
||||
CONFIG = {
|
||||
@@ -34,13 +45,13 @@ class FocusOn(Transform):
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def __init__(self, focus_point, **kwargs):
|
||||
def __init__(self, focus_point: np.ndarray, **kwargs):
|
||||
self.focus_point = focus_point
|
||||
# Initialize with blank mobject, while create_target
|
||||
# and create_starting_mobject handle the meat
|
||||
super().__init__(VMobject(), **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Dot:
|
||||
little_dot = Dot(radius=0)
|
||||
little_dot.set_fill(self.color, opacity=self.opacity)
|
||||
little_dot.add_updater(
|
||||
@@ -48,7 +59,7 @@ class FocusOn(Transform):
|
||||
)
|
||||
return little_dot
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Dot:
|
||||
return Dot(
|
||||
radius=FRAME_X_RADIUS + FRAME_Y_RADIUS,
|
||||
stroke_width=0,
|
||||
@@ -64,7 +75,7 @@ class Indicate(Transform):
|
||||
"color": YELLOW,
|
||||
}
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
target = self.mobject.copy()
|
||||
target.scale(self.scale_factor)
|
||||
target.set_color(self.color)
|
||||
@@ -80,7 +91,12 @@ class Flash(AnimationGroup):
|
||||
"run_time": 1,
|
||||
}
|
||||
|
||||
def __init__(self, point, color=YELLOW, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
color: ManimColor = YELLOW,
|
||||
**kwargs
|
||||
):
|
||||
self.point = point
|
||||
self.color = color
|
||||
digest_config(self, kwargs)
|
||||
@@ -92,7 +108,7 @@ class Flash(AnimationGroup):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def create_lines(self):
|
||||
def create_lines(self) -> VGroup:
|
||||
lines = VGroup()
|
||||
for angle in np.arange(0, TAU, TAU / self.num_lines):
|
||||
line = Line(ORIGIN, self.line_length * RIGHT)
|
||||
@@ -106,7 +122,7 @@ class Flash(AnimationGroup):
|
||||
lines.add_updater(lambda l: l.move_to(self.point))
|
||||
return lines
|
||||
|
||||
def create_line_anims(self):
|
||||
def create_line_anims(self) -> list[Animation]:
|
||||
return [
|
||||
ShowCreationThenDestruction(line)
|
||||
for line in self.lines
|
||||
@@ -122,17 +138,17 @@ class CircleIndicate(Indicate):
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
circle = self.get_circle(mobject)
|
||||
super().__init__(circle, **kwargs)
|
||||
|
||||
def get_circle(self, mobject):
|
||||
def get_circle(self, mobject: Mobject) -> Circle:
|
||||
circle = Circle(**self.circle_config)
|
||||
circle.add_updater(lambda c: c.surround(mobject))
|
||||
return circle
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
super().interpolate_mobject(alpha)
|
||||
self.mobject.set_stroke(opacity=alpha)
|
||||
|
||||
@@ -143,7 +159,7 @@ class ShowPassingFlash(ShowPartial):
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def get_bounds(self, alpha):
|
||||
def get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
tw = self.time_width
|
||||
upper = interpolate(0, 1 + tw, alpha)
|
||||
lower = upper - tw
|
||||
@@ -151,7 +167,7 @@ class ShowPassingFlash(ShowPartial):
|
||||
lower = max(lower, 0)
|
||||
return (lower, upper)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
for submob, start in self.get_all_families_zipped():
|
||||
submob.pointwise_become_partial(start, 0, 1)
|
||||
@@ -164,7 +180,7 @@ class VShowPassingFlash(Animation):
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.mobject.align_stroke_width_data_to_points()
|
||||
# Compute an array of stroke widths for each submobject
|
||||
# which tapers out at either end
|
||||
@@ -184,7 +200,12 @@ class VShowPassingFlash(Animation):
|
||||
self.submob_to_anchor_widths[hash(sm)] = anchor_widths * taper_array
|
||||
super().begin()
|
||||
|
||||
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submobject: VMobject,
|
||||
starting_sumobject: None,
|
||||
alpha: float
|
||||
) -> None:
|
||||
anchor_widths = self.submob_to_anchor_widths[hash(submobject)]
|
||||
# Create a gaussian such that 3 sigmas out on either side
|
||||
# will equals time_width
|
||||
@@ -206,7 +227,7 @@ class VShowPassingFlash(Animation):
|
||||
new_widths[1::3] = (new_widths[0::3] + new_widths[2::3]) / 2
|
||||
submobject.set_stroke(width=new_widths)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
for submob, start in self.get_all_families_zipped():
|
||||
submob.match_style(start)
|
||||
@@ -221,7 +242,7 @@ class FlashAround(VShowPassingFlash):
|
||||
"n_inserted_curves": 20,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
path = self.get_path(mobject)
|
||||
if mobject.is_fixed_in_frame:
|
||||
@@ -231,12 +252,12 @@ class FlashAround(VShowPassingFlash):
|
||||
path.set_stroke(self.color, self.stroke_width)
|
||||
super().__init__(path, **kwargs)
|
||||
|
||||
def get_path(self, mobject):
|
||||
def get_path(self, mobject: Mobject) -> SurroundingRectangle:
|
||||
return SurroundingRectangle(mobject, buff=self.buff)
|
||||
|
||||
|
||||
class FlashUnder(FlashAround):
|
||||
def get_path(self, mobject):
|
||||
def get_path(self, mobject: Mobject) -> Underline:
|
||||
return Underline(mobject, buff=self.buff)
|
||||
|
||||
|
||||
@@ -252,7 +273,7 @@ class ShowCreationThenFadeOut(Succession):
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(
|
||||
ShowCreation(mobject),
|
||||
FadeOut(mobject),
|
||||
@@ -269,7 +290,7 @@ class AnimationOnSurroundingRectangle(AnimationGroup):
|
||||
"rect_animation": Animation
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
if "surrounding_rectangle_config" in kwargs:
|
||||
kwargs.pop("surrounding_rectangle_config")
|
||||
@@ -282,7 +303,7 @@ class AnimationOnSurroundingRectangle(AnimationGroup):
|
||||
self.rect_animation(rect, **kwargs),
|
||||
)
|
||||
|
||||
def get_rect(self):
|
||||
def get_rect(self) -> SurroundingRectangle:
|
||||
return SurroundingRectangle(
|
||||
self.mobject_to_surround,
|
||||
**self.surrounding_rectangle_config
|
||||
@@ -314,7 +335,7 @@ class ApplyWave(Homotopy):
|
||||
"run_time": 1,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
left_x = mobject.get_left()[0]
|
||||
right_x = mobject.get_right()[0]
|
||||
@@ -339,15 +360,20 @@ class WiggleOutThenIn(Animation):
|
||||
"rotate_about_point": None,
|
||||
}
|
||||
|
||||
def get_scale_about_point(self):
|
||||
def get_scale_about_point(self) -> np.ndarray:
|
||||
if self.scale_about_point is None:
|
||||
return self.mobject.get_center()
|
||||
|
||||
def get_rotate_about_point(self):
|
||||
def get_rotate_about_point(self) -> np.ndarray:
|
||||
if self.rotate_about_point is None:
|
||||
return self.mobject.get_center()
|
||||
|
||||
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submobject: Mobject,
|
||||
starting_sumobject: Mobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
submobject.match_points(starting_sumobject)
|
||||
submobject.scale(
|
||||
interpolate(1, self.scale_value, there_and_back(alpha)),
|
||||
@@ -364,7 +390,7 @@ class TurnInsideOut(Transform):
|
||||
"path_arc": TAU / 4,
|
||||
}
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject.copy().reverse_points()
|
||||
|
||||
|
||||
@@ -373,7 +399,7 @@ class FlashyFadeIn(AnimationGroup):
|
||||
"fade_lag": 0,
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, stroke_width=2, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, stroke_width: float = 2, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
outline = vmobject.copy()
|
||||
outline.set_fill(opacity=0)
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Sequence
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.utils.rate_functions import linear
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class Homotopy(Animation):
|
||||
CONFIG = {
|
||||
@@ -8,7 +18,12 @@ class Homotopy(Animation):
|
||||
"apply_function_kwargs": {},
|
||||
}
|
||||
|
||||
def __init__(self, homotopy, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
homotopy: Callable[[float, float, float, float], Sequence[float]],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Homotopy is a function from
|
||||
(x, y, z, t) to (x', y', z')
|
||||
@@ -16,10 +31,18 @@ class Homotopy(Animation):
|
||||
self.homotopy = homotopy
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def function_at_time_t(self, t):
|
||||
def function_at_time_t(
|
||||
self,
|
||||
t: float
|
||||
) -> Callable[[np.ndarray], Sequence[float]]:
|
||||
return lambda p: self.homotopy(*p, t)
|
||||
|
||||
def interpolate_submobject(self, submob, start, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: Mobject,
|
||||
start: Mobject,
|
||||
alpha: float
|
||||
) -> None:
|
||||
submob.match_points(start)
|
||||
submob.apply_function(
|
||||
self.function_at_time_t(alpha),
|
||||
@@ -34,7 +57,12 @@ class SmoothedVectorizedHomotopy(Homotopy):
|
||||
|
||||
|
||||
class ComplexHomotopy(Homotopy):
|
||||
def __init__(self, complex_homotopy, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
complex_homotopy: Callable[[complex, float], Sequence[float]],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Given a function form (z, t) -> w, where z and w
|
||||
are complex numbers and t is time, this animates
|
||||
@@ -53,11 +81,16 @@ class PhaseFlow(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
if hasattr(self, "last_alpha"):
|
||||
dt = self.virtual_time * (alpha - self.last_alpha)
|
||||
self.mobject.apply_function(
|
||||
@@ -71,10 +104,10 @@ class MoveAlongPath(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, path, **kwargs):
|
||||
def __init__(self, mobject: Mobject, path: Mobject, **kwargs):
|
||||
self.path = path
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
point = self.path.point_from_proportion(alpha)
|
||||
self.mobject.move_to(point)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.utils.bezier import interpolate
|
||||
@@ -8,19 +12,29 @@ class ChangingDecimal(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, decimal_mob, number_update_func, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
number_update_func: Callable[[float], float],
|
||||
**kwargs
|
||||
):
|
||||
assert(isinstance(decimal_mob, DecimalNumber))
|
||||
self.number_update_func = number_update_func
|
||||
super().__init__(decimal_mob, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.set_value(
|
||||
self.number_update_func(alpha)
|
||||
)
|
||||
|
||||
|
||||
class ChangeDecimalToValue(ChangingDecimal):
|
||||
def __init__(self, decimal_mob, target_number, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
target_number: float | complex,
|
||||
**kwargs
|
||||
):
|
||||
start_number = decimal_mob.number
|
||||
super().__init__(
|
||||
decimal_mob,
|
||||
@@ -30,7 +44,12 @@ class ChangeDecimalToValue(ChangingDecimal):
|
||||
|
||||
|
||||
class CountInFrom(ChangingDecimal):
|
||||
def __init__(self, decimal_mob, source_number=0, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
source_number: float | complex = 0,
|
||||
**kwargs
|
||||
):
|
||||
start_number = decimal_mob.number
|
||||
super().__init__(
|
||||
decimal_mob,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.constants import OUT
|
||||
from manimlib.constants import PI
|
||||
@@ -6,6 +8,12 @@ from manimlib.constants import ORIGIN
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class Rotating(Animation):
|
||||
CONFIG = {
|
||||
@@ -18,12 +26,18 @@ class Rotating(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, angle=TAU, axis=OUT, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
angle: float = TAU,
|
||||
axis: np.ndarray = OUT,
|
||||
**kwargs
|
||||
):
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
for sm1, sm2 in self.get_all_families_zipped():
|
||||
sm1.set_points(sm2.get_points())
|
||||
self.mobject.rotate(
|
||||
@@ -41,5 +55,11 @@ class Rotate(Rotating):
|
||||
"about_edge": ORIGIN,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, angle=PI, axis=OUT, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
angle: float = PI,
|
||||
axis: np.ndarray = OUT,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject, angle, axis, **kwargs)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.composition import LaggedStart
|
||||
from manimlib.animation.transform import Restore
|
||||
from manimlib.constants import WHITE
|
||||
@@ -17,10 +21,9 @@ class Broadcast(LaggedStart):
|
||||
"remover": True,
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": 3,
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def __init__(self, focal_point, **kwargs):
|
||||
def __init__(self, focal_point: np.ndarray, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
circles = VGroup()
|
||||
for x in range(self.n_circles):
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from typing import Callable, Union, Sequence
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.constants import DEFAULT_POINTWISE_FUNCTION_RUN_TIME
|
||||
@@ -14,6 +18,13 @@ from manimlib.utils.paths import straight_path
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.rate_functions import squish_rate_func
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import colour
|
||||
from manimlib.scene.scene import Scene
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
class Transform(Animation):
|
||||
CONFIG = {
|
||||
@@ -23,12 +34,17 @@ class Transform(Animation):
|
||||
"replace_mobject_with_target_in_scene": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, target_mobject=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
target_mobject: Mobject | None = None,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject, **kwargs)
|
||||
self.target_mobject = target_mobject
|
||||
self.init_path_func()
|
||||
|
||||
def init_path_func(self):
|
||||
def init_path_func(self) -> None:
|
||||
if self.path_func is not None:
|
||||
return
|
||||
elif self.path_arc == 0:
|
||||
@@ -39,7 +55,7 @@ class Transform(Animation):
|
||||
self.path_arc_axis,
|
||||
)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.target_mobject = self.create_target()
|
||||
self.check_target_mobject_validity()
|
||||
# Use a copy of target_mobject for the align_data_and_family
|
||||
@@ -54,28 +70,28 @@ class Transform(Animation):
|
||||
self.target_copy,
|
||||
)
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
# Has no meaningful effect here, but may be useful
|
||||
# in subclasses
|
||||
return self.target_mobject
|
||||
|
||||
def check_target_mobject_validity(self):
|
||||
def check_target_mobject_validity(self) -> None:
|
||||
if self.target_mobject is None:
|
||||
raise Exception(
|
||||
f"{self.__class__.__name__}.create_target not properly implemented"
|
||||
)
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
if self.replace_mobject_with_target_in_scene:
|
||||
scene.remove(self.mobject)
|
||||
scene.add(self.target_mobject)
|
||||
|
||||
def update_config(self, **kwargs):
|
||||
def update_config(self, **kwargs) -> None:
|
||||
Animation.update_config(self, **kwargs)
|
||||
if "path_arc" in kwargs:
|
||||
self.path_func = path_along_arc(
|
||||
@@ -83,7 +99,7 @@ class Transform(Animation):
|
||||
kwargs.get("path_arc_axis", OUT)
|
||||
)
|
||||
|
||||
def get_all_mobjects(self):
|
||||
def get_all_mobjects(self) -> list[Mobject]:
|
||||
return [
|
||||
self.mobject,
|
||||
self.starting_mobject,
|
||||
@@ -91,7 +107,7 @@ class Transform(Animation):
|
||||
self.target_copy,
|
||||
]
|
||||
|
||||
def get_all_families_zipped(self):
|
||||
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
|
||||
return zip(*[
|
||||
mob.get_family()
|
||||
for mob in [
|
||||
@@ -101,7 +117,13 @@ class Transform(Animation):
|
||||
]
|
||||
])
|
||||
|
||||
def interpolate_submobject(self, submob, start, target_copy, alpha):
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submob: Mobject,
|
||||
start: Mobject,
|
||||
target_copy: Mobject,
|
||||
alpha: float
|
||||
):
|
||||
submob.interpolate(start, target_copy, alpha, self.path_func)
|
||||
return self
|
||||
|
||||
@@ -117,10 +139,10 @@ class TransformFromCopy(Transform):
|
||||
Performs a reversed Transform
|
||||
"""
|
||||
|
||||
def __init__(self, mobject, target_mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs):
|
||||
super().__init__(target_mobject, mobject, **kwargs)
|
||||
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
super().interpolate(1 - alpha)
|
||||
|
||||
|
||||
@@ -137,11 +159,11 @@ class CounterclockwiseTransform(Transform):
|
||||
|
||||
|
||||
class MoveToTarget(Transform):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
self.check_validity_of_input(mobject)
|
||||
super().__init__(mobject, mobject.target, **kwargs)
|
||||
|
||||
def check_validity_of_input(self, mobject):
|
||||
def check_validity_of_input(self, mobject: Mobject) -> None:
|
||||
if not hasattr(mobject, "target"):
|
||||
raise Exception(
|
||||
"MoveToTarget called on mobject"
|
||||
@@ -150,13 +172,13 @@ class MoveToTarget(Transform):
|
||||
|
||||
|
||||
class _MethodAnimation(MoveToTarget):
|
||||
def __init__(self, mobject, methods):
|
||||
def __init__(self, mobject: Mobject, methods: Callable):
|
||||
self.methods = methods
|
||||
super().__init__(mobject)
|
||||
|
||||
|
||||
class ApplyMethod(Transform):
|
||||
def __init__(self, method, *args, **kwargs):
|
||||
def __init__(self, method: Callable, *args, **kwargs):
|
||||
"""
|
||||
method is a method of Mobject, *args are arguments for
|
||||
that method. Key word arguments should be passed in
|
||||
@@ -170,7 +192,7 @@ class ApplyMethod(Transform):
|
||||
self.method_args = args
|
||||
super().__init__(method.__self__, **kwargs)
|
||||
|
||||
def check_validity_of_input(self, method):
|
||||
def check_validity_of_input(self, method: Callable) -> None:
|
||||
if not inspect.ismethod(method):
|
||||
raise Exception(
|
||||
"Whoops, looks like you accidentally invoked "
|
||||
@@ -178,7 +200,7 @@ class ApplyMethod(Transform):
|
||||
)
|
||||
assert(isinstance(method.__self__, Mobject))
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
method = self.method
|
||||
# Make sure it's a list so that args.pop() works
|
||||
args = list(self.method_args)
|
||||
@@ -197,16 +219,26 @@ class ApplyPointwiseFunction(ApplyMethod):
|
||||
"run_time": DEFAULT_POINTWISE_FUNCTION_RUN_TIME
|
||||
}
|
||||
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject.apply_function, function, **kwargs)
|
||||
|
||||
|
||||
class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction):
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
super().__init__(mobject.move_to, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
self.method_args = [
|
||||
self.function(self.mobject.get_center())
|
||||
]
|
||||
@@ -214,31 +246,46 @@ class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction):
|
||||
|
||||
|
||||
class FadeToColor(ApplyMethod):
|
||||
def __init__(self, mobject, color, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
color: ManimColor,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject.set_color, color, **kwargs)
|
||||
|
||||
|
||||
class ScaleInPlace(ApplyMethod):
|
||||
def __init__(self, mobject, scale_factor, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
scale_factor: npt.ArrayLike,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(mobject.scale, scale_factor, **kwargs)
|
||||
|
||||
|
||||
class ShrinkToCenter(ScaleInPlace):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(mobject, 0, **kwargs)
|
||||
|
||||
|
||||
class Restore(ApplyMethod):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(mobject.restore, **kwargs)
|
||||
|
||||
|
||||
class ApplyFunction(Transform):
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[Mobject], Mobject],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
target = self.function(self.mobject.copy())
|
||||
if not isinstance(target, Mobject):
|
||||
raise Exception("Functions passed to ApplyFunction must return object of type Mobject")
|
||||
@@ -246,7 +293,12 @@ class ApplyFunction(Transform):
|
||||
|
||||
|
||||
class ApplyMatrix(ApplyPointwiseFunction):
|
||||
def __init__(self, matrix, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
matrix: npt.ArrayLike,
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
matrix = self.initialize_matrix(matrix)
|
||||
|
||||
def func(p):
|
||||
@@ -254,7 +306,7 @@ class ApplyMatrix(ApplyPointwiseFunction):
|
||||
|
||||
super().__init__(func, mobject, **kwargs)
|
||||
|
||||
def initialize_matrix(self, matrix):
|
||||
def initialize_matrix(self, matrix: npt.ArrayLike) -> np.ndarray:
|
||||
matrix = np.array(matrix)
|
||||
if matrix.shape == (2, 2):
|
||||
new_matrix = np.identity(3)
|
||||
@@ -266,12 +318,17 @@ class ApplyMatrix(ApplyPointwiseFunction):
|
||||
|
||||
|
||||
class ApplyComplexFunction(ApplyMethod):
|
||||
def __init__(self, function, mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[complex], complex],
|
||||
mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.function = function
|
||||
method = mobject.apply_complex_function
|
||||
super().__init__(method, function, **kwargs)
|
||||
|
||||
def init_path_func(self):
|
||||
def init_path_func(self) -> None:
|
||||
func1 = self.function(complex(1))
|
||||
self.path_arc = np.log(func1).imag
|
||||
super().init_path_func()
|
||||
@@ -284,11 +341,11 @@ class CyclicReplace(Transform):
|
||||
"path_arc": 90 * DEGREES,
|
||||
}
|
||||
|
||||
def __init__(self, *mobjects, **kwargs):
|
||||
def __init__(self, *mobjects: Mobject, **kwargs):
|
||||
self.group = Group(*mobjects)
|
||||
super().__init__(self.group, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
target = self.group.copy()
|
||||
cycled_targets = [target[-1], *target[:-1]]
|
||||
for m1, m2 in zip(cycled_targets, self.group):
|
||||
@@ -306,7 +363,7 @@ class TransformAnimations(Transform):
|
||||
"rate_func": squish_rate_func(smooth)
|
||||
}
|
||||
|
||||
def __init__(self, start_anim, end_anim, **kwargs):
|
||||
def __init__(self, start_anim: Animation, end_anim: Animation, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
if "run_time" in kwargs:
|
||||
self.run_time = kwargs.pop("run_time")
|
||||
@@ -327,7 +384,7 @@ class TransformAnimations(Transform):
|
||||
start_anim.mobject = self.starting_mobject
|
||||
end_anim.mobject = self.target_mobject
|
||||
|
||||
def interpolate(self, alpha):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
self.start_anim.interpolate(alpha)
|
||||
self.end_anim.interpolate(alpha)
|
||||
Transform.interpolate(self, alpha)
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.animation.fading import FadeTransformPieces
|
||||
from manimlib.animation.fading import FadeInFromPoint
|
||||
from manimlib.animation.fading import FadeOutToPoint
|
||||
from manimlib.animation.transform import ReplacementTransform
|
||||
from manimlib.animation.transform import Transform
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.svg.labelled_string import LabelledString
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.mobject.svg.tex_mobject import Tex, SingleStringTex
|
||||
|
||||
|
||||
class TransformMatchingParts(AnimationGroup):
|
||||
@@ -22,7 +34,7 @@ class TransformMatchingParts(AnimationGroup):
|
||||
"key_map": dict(),
|
||||
}
|
||||
|
||||
def __init__(self, mobject, target_mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
assert(isinstance(mobject, self.mobject_type))
|
||||
assert(isinstance(target_mobject, self.mobject_type))
|
||||
@@ -79,8 +91,8 @@ class TransformMatchingParts(AnimationGroup):
|
||||
self.to_remove = mobject
|
||||
self.to_add = target_mobject
|
||||
|
||||
def get_shape_map(self, mobject):
|
||||
shape_map = {}
|
||||
def get_shape_map(self, mobject: Mobject) -> dict[int, VGroup]:
|
||||
shape_map: dict[int, VGroup] = {}
|
||||
for sm in self.get_mobject_parts(mobject):
|
||||
key = self.get_mobject_key(sm)
|
||||
if key not in shape_map:
|
||||
@@ -88,7 +100,7 @@ class TransformMatchingParts(AnimationGroup):
|
||||
shape_map[key].add(sm)
|
||||
return shape_map
|
||||
|
||||
def clean_up_from_scene(self, scene):
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
for anim in self.animations:
|
||||
anim.update(0)
|
||||
scene.remove(self.mobject)
|
||||
@@ -96,12 +108,12 @@ class TransformMatchingParts(AnimationGroup):
|
||||
scene.add(self.to_add)
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject):
|
||||
def get_mobject_parts(mobject: Mobject) -> Mobject:
|
||||
# To be implemented in subclass
|
||||
return mobject
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
def get_mobject_key(mobject: Mobject) -> int:
|
||||
# To be implemented in subclass
|
||||
return hash(mobject)
|
||||
|
||||
@@ -113,11 +125,11 @@ class TransformMatchingShapes(TransformMatchingParts):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject):
|
||||
def get_mobject_parts(mobject: VMobject) -> list[VMobject]:
|
||||
return mobject.family_members_with_points()
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
def get_mobject_key(mobject: VMobject) -> int:
|
||||
mobject.save_state()
|
||||
mobject.center()
|
||||
mobject.set_height(1)
|
||||
@@ -133,9 +145,116 @@ class TransformMatchingTex(TransformMatchingParts):
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject):
|
||||
def get_mobject_parts(mobject: Tex) -> list[SingleStringTex]:
|
||||
return mobject.submobjects
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject):
|
||||
def get_mobject_key(mobject: Tex) -> str:
|
||||
return mobject.get_tex()
|
||||
|
||||
|
||||
class TransformMatchingStrings(AnimationGroup):
|
||||
CONFIG = {
|
||||
"key_map": dict(),
|
||||
"transform_mismatches": False,
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
source: LabelledString,
|
||||
target: LabelledString,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
assert isinstance(source, LabelledString)
|
||||
assert isinstance(target, LabelledString)
|
||||
anims = []
|
||||
source_indices = list(range(len(source.labelled_submobjects)))
|
||||
target_indices = list(range(len(target.labelled_submobjects)))
|
||||
|
||||
def get_indices_lists(mobject, parts):
|
||||
return [
|
||||
[
|
||||
mobject.labelled_submobjects.index(submob)
|
||||
for submob in part
|
||||
]
|
||||
for part in parts
|
||||
]
|
||||
|
||||
def add_anims_from(anim_class, func, source_args, target_args=None):
|
||||
if target_args is None:
|
||||
target_args = source_args.copy()
|
||||
for source_arg, target_arg in zip(source_args, target_args):
|
||||
source_parts = func(source, source_arg)
|
||||
target_parts = func(target, target_arg)
|
||||
source_indices_lists = list(filter(
|
||||
lambda indices_list: all([
|
||||
index in source_indices
|
||||
for index in indices_list
|
||||
]), get_indices_lists(source, source_parts)
|
||||
))
|
||||
target_indices_lists = list(filter(
|
||||
lambda indices_list: all([
|
||||
index in target_indices
|
||||
for index in indices_list
|
||||
]), get_indices_lists(target, target_parts)
|
||||
))
|
||||
if not source_indices_lists or not target_indices_lists:
|
||||
continue
|
||||
anims.append(anim_class(source_parts, target_parts, **kwargs))
|
||||
for index in it.chain(*source_indices_lists):
|
||||
source_indices.remove(index)
|
||||
for index in it.chain(*target_indices_lists):
|
||||
target_indices.remove(index)
|
||||
|
||||
def get_common_substrs(substrs_from_source, substrs_from_target):
|
||||
return sorted([
|
||||
substr for substr in substrs_from_source
|
||||
if substr and substr in substrs_from_target
|
||||
], key=len, reverse=True)
|
||||
|
||||
def get_parts_from_keys(mobject, keys):
|
||||
if isinstance(keys, str):
|
||||
keys = [keys]
|
||||
result = VGroup()
|
||||
for key in keys:
|
||||
if not isinstance(key, str):
|
||||
raise TypeError(key)
|
||||
result.add(*mobject.get_parts_by_string(key))
|
||||
return result
|
||||
|
||||
add_anims_from(
|
||||
ReplacementTransform, get_parts_from_keys,
|
||||
self.key_map.keys(), self.key_map.values()
|
||||
)
|
||||
add_anims_from(
|
||||
FadeTransformPieces,
|
||||
LabelledString.get_parts_by_string,
|
||||
get_common_substrs(
|
||||
source.specified_substrs,
|
||||
target.specified_substrs
|
||||
)
|
||||
)
|
||||
add_anims_from(
|
||||
FadeTransformPieces,
|
||||
LabelledString.get_parts_by_group_substr,
|
||||
get_common_substrs(
|
||||
source.group_substrs,
|
||||
target.group_substrs
|
||||
)
|
||||
)
|
||||
|
||||
rest_source = VGroup(*[source[index] for index in source_indices])
|
||||
rest_target = VGroup(*[target[index] for index in target_indices])
|
||||
if self.transform_mismatches:
|
||||
anims.append(
|
||||
ReplacementTransform(rest_source, rest_target, **kwargs)
|
||||
)
|
||||
else:
|
||||
anims.append(
|
||||
FadeOutToPoint(rest_source, target.get_center(), **kwargs)
|
||||
)
|
||||
anims.append(
|
||||
FadeInFromPoint(rest_target, source.get_center(), **kwargs)
|
||||
)
|
||||
|
||||
super().__init__(*anims)
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import operator as op
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class UpdateFromFunc(Animation):
|
||||
"""
|
||||
@@ -13,21 +21,31 @@ class UpdateFromFunc(Animation):
|
||||
"suspend_mobject_updating": False,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, update_function, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
update_function: Callable[[Mobject]],
|
||||
**kwargs
|
||||
):
|
||||
self.update_function = update_function
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject)
|
||||
|
||||
|
||||
class UpdateFromAlphaFunc(UpdateFromFunc):
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject, alpha)
|
||||
|
||||
|
||||
class MaintainPositionRelativeTo(Animation):
|
||||
def __init__(self, mobject, tracked_mobject, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
tracked_mobject: Mobject,
|
||||
**kwargs
|
||||
):
|
||||
self.tracked_mobject = tracked_mobject
|
||||
self.diff = op.sub(
|
||||
mobject.get_center(),
|
||||
@@ -35,7 +53,7 @@ class MaintainPositionRelativeTo(Animation):
|
||||
)
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_mobject(self, alpha):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
target = self.tracked_mobject.get_center()
|
||||
location = self.mobject.get_center()
|
||||
self.mobject.shift(target - location + self.diff)
|
||||
|
||||
@@ -1,94 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import moderngl
|
||||
import math
|
||||
from colour import Color
|
||||
import OpenGL.GL as gl
|
||||
import math
|
||||
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
|
||||
import numpy as np
|
||||
from scipy.spatial.transform import Rotation
|
||||
from PIL import Image
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.simple_functions import fdiv
|
||||
from manimlib.utils.simple_functions import clip
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
from manimlib.utils.space_ops import rotation_matrix_transpose_from_quaternion
|
||||
from manimlib.utils.space_ops import rotation_matrix_transpose
|
||||
from manimlib.utils.space_ops import quaternion_from_angle_axis
|
||||
from manimlib.utils.space_ops import quaternion_mult
|
||||
from manimlib.utils.space_ops import normalize
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.shader_wrapper import ShaderWrapper
|
||||
|
||||
|
||||
class CameraFrame(Mobject):
|
||||
CONFIG = {
|
||||
"frame_shape": (FRAME_WIDTH, FRAME_HEIGHT),
|
||||
"center_point": ORIGIN,
|
||||
# Theta, phi, gamma
|
||||
"euler_angles": [0, 0, 0],
|
||||
"focal_distance": 2,
|
||||
"focal_dist_to_height": 2,
|
||||
}
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
self.data["euler_angles"] = np.array(self.euler_angles, dtype=float)
|
||||
self.refresh_rotation_matrix()
|
||||
def init_uniforms(self) -> None:
|
||||
super().init_uniforms()
|
||||
# As a quaternion
|
||||
self.uniforms["orientation"] = Rotation.identity().as_quat()
|
||||
self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP])
|
||||
self.set_width(self.frame_shape[0], stretch=True)
|
||||
self.set_height(self.frame_shape[1], stretch=True)
|
||||
self.move_to(self.center_point)
|
||||
|
||||
def set_orientation(self, rotation: Rotation):
|
||||
self.uniforms["orientation"][:] = rotation.as_quat()
|
||||
return self
|
||||
|
||||
def get_orientation(self):
|
||||
return Rotation.from_quat(self.uniforms["orientation"])
|
||||
|
||||
def to_default_state(self):
|
||||
self.center()
|
||||
self.set_height(FRAME_HEIGHT)
|
||||
self.set_width(FRAME_WIDTH)
|
||||
self.set_euler_angles(0, 0, 0)
|
||||
self.set_orientation(Rotation.identity())
|
||||
return self
|
||||
|
||||
def get_euler_angles(self):
|
||||
return self.data["euler_angles"]
|
||||
return self.get_orientation().as_euler("zxz")[::-1]
|
||||
|
||||
def get_inverse_camera_rotation_matrix(self):
|
||||
return self.inverse_camera_rotation_matrix
|
||||
return self.get_orientation().as_matrix().T
|
||||
|
||||
def refresh_rotation_matrix(self):
|
||||
# Rotate based on camera orientation
|
||||
theta, phi, gamma = self.get_euler_angles()
|
||||
quat = quaternion_mult(
|
||||
quaternion_from_angle_axis(theta, OUT, axis_normalized=True),
|
||||
quaternion_from_angle_axis(phi, RIGHT, axis_normalized=True),
|
||||
quaternion_from_angle_axis(gamma, OUT, axis_normalized=True),
|
||||
)
|
||||
self.inverse_camera_rotation_matrix = rotation_matrix_transpose_from_quaternion(quat)
|
||||
|
||||
def rotate(self, angle, axis=OUT, **kwargs):
|
||||
curr_rot_T = self.get_inverse_camera_rotation_matrix()
|
||||
added_rot_T = rotation_matrix_transpose(angle, axis)
|
||||
new_rot_T = np.dot(curr_rot_T, added_rot_T)
|
||||
Fz = new_rot_T[2]
|
||||
phi = np.arccos(clip(Fz[2], -1, 1))
|
||||
theta = angle_of_vector(Fz[:2]) + PI / 2
|
||||
partial_rot_T = np.dot(
|
||||
rotation_matrix_transpose(phi, RIGHT),
|
||||
rotation_matrix_transpose(theta, OUT),
|
||||
)
|
||||
gamma = angle_of_vector(np.dot(partial_rot_T, new_rot_T.T)[:, 0])
|
||||
self.set_euler_angles(theta, phi, gamma)
|
||||
def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
|
||||
rot = Rotation.from_rotvec(angle * normalize(axis))
|
||||
self.set_orientation(rot * self.get_orientation())
|
||||
return self
|
||||
|
||||
def set_euler_angles(self, theta=None, phi=None, gamma=None, units=RADIANS):
|
||||
if theta is not None:
|
||||
self.data["euler_angles"][0] = theta * units
|
||||
if phi is not None:
|
||||
self.data["euler_angles"][1] = phi * units
|
||||
if gamma is not None:
|
||||
self.data["euler_angles"][2] = gamma * units
|
||||
self.refresh_rotation_matrix()
|
||||
def set_euler_angles(
|
||||
self,
|
||||
theta: float | None = None,
|
||||
phi: float | None = None,
|
||||
gamma: float | None = None,
|
||||
units: float = RADIANS
|
||||
):
|
||||
eulers = self.get_euler_angles() # theta, phi, gamma
|
||||
for i, var in enumerate([theta, phi, gamma]):
|
||||
if var is not None:
|
||||
eulers[i] = var * units
|
||||
self.set_orientation(Rotation.from_euler("zxz", eulers[::-1]))
|
||||
return self
|
||||
|
||||
def reorient(self, theta_degrees=None, phi_degrees=None, gamma_degrees=None):
|
||||
def reorient(
|
||||
self,
|
||||
theta_degrees: float | None = None,
|
||||
phi_degrees: float | None = None,
|
||||
gamma_degrees: float | None = None,
|
||||
):
|
||||
"""
|
||||
Shortcut for set_euler_angles, defaulting to taking
|
||||
in angles in degrees
|
||||
@@ -96,72 +95,60 @@ class CameraFrame(Mobject):
|
||||
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES)
|
||||
return self
|
||||
|
||||
def set_theta(self, theta):
|
||||
def set_theta(self, theta: float):
|
||||
return self.set_euler_angles(theta=theta)
|
||||
|
||||
def set_phi(self, phi):
|
||||
def set_phi(self, phi: float):
|
||||
return self.set_euler_angles(phi=phi)
|
||||
|
||||
def set_gamma(self, gamma):
|
||||
def set_gamma(self, gamma: float):
|
||||
return self.set_euler_angles(gamma=gamma)
|
||||
|
||||
def increment_theta(self, dtheta):
|
||||
self.data["euler_angles"][0] += dtheta
|
||||
self.refresh_rotation_matrix()
|
||||
def increment_theta(self, dtheta: float):
|
||||
self.rotate(dtheta, OUT)
|
||||
return self
|
||||
|
||||
def increment_phi(self, dphi):
|
||||
phi = self.data["euler_angles"][1]
|
||||
new_phi = clip(phi + dphi, 0, PI)
|
||||
self.data["euler_angles"][1] = new_phi
|
||||
self.refresh_rotation_matrix()
|
||||
def increment_phi(self, dphi: float):
|
||||
self.rotate(dphi, self.get_inverse_camera_rotation_matrix()[0])
|
||||
return self
|
||||
|
||||
def increment_gamma(self, dgamma):
|
||||
self.data["euler_angles"][2] += dgamma
|
||||
self.refresh_rotation_matrix()
|
||||
def increment_gamma(self, dgamma: float):
|
||||
self.rotate(dgamma, self.get_inverse_camera_rotation_matrix()[2])
|
||||
return self
|
||||
|
||||
def get_theta(self):
|
||||
return self.data["euler_angles"][0]
|
||||
def set_focal_distance(self, focal_distance: float):
|
||||
self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height()
|
||||
return self
|
||||
|
||||
def get_phi(self):
|
||||
return self.data["euler_angles"][1]
|
||||
|
||||
def get_gamma(self):
|
||||
return self.data["euler_angles"][2]
|
||||
def set_field_of_view(self, field_of_view: float):
|
||||
self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2)
|
||||
return self
|
||||
|
||||
def get_shape(self):
|
||||
return (self.get_width(), self.get_height())
|
||||
|
||||
def get_center(self):
|
||||
def get_center(self) -> np.ndarray:
|
||||
# Assumes first point is at the center
|
||||
return self.get_points()[0]
|
||||
|
||||
def get_width(self):
|
||||
def get_width(self) -> float:
|
||||
points = self.get_points()
|
||||
return points[2, 0] - points[1, 0]
|
||||
|
||||
def get_height(self):
|
||||
def get_height(self) -> float:
|
||||
points = self.get_points()
|
||||
return points[4, 1] - points[3, 1]
|
||||
|
||||
def get_focal_distance(self):
|
||||
return self.focal_distance * self.get_height()
|
||||
def get_focal_distance(self) -> float:
|
||||
return self.uniforms["focal_dist_to_height"] * self.get_height()
|
||||
|
||||
def get_implied_camera_location(self):
|
||||
theta, phi, gamma = self.get_euler_angles()
|
||||
def get_field_of_view(self) -> float:
|
||||
return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2)
|
||||
|
||||
def get_implied_camera_location(self) -> np.ndarray:
|
||||
to_camera = self.get_inverse_camera_rotation_matrix()[2]
|
||||
dist = self.get_focal_distance()
|
||||
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()
|
||||
return self.get_center() + dist * to_camera
|
||||
|
||||
|
||||
class Camera(object):
|
||||
@@ -190,10 +177,10 @@ class Camera(object):
|
||||
"samples": 0,
|
||||
}
|
||||
|
||||
def __init__(self, ctx=None, **kwargs):
|
||||
def __init__(self, ctx: moderngl.Context | None = None, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
|
||||
self.background_rgba = [
|
||||
self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max
|
||||
self.background_rgba: list[float] = [
|
||||
*Color(self.background_color).get_rgb(),
|
||||
self.background_opacity
|
||||
]
|
||||
@@ -205,10 +192,10 @@ class Camera(object):
|
||||
self.refresh_perspective_uniforms()
|
||||
self.static_mobject_to_render_group_list = {}
|
||||
|
||||
def init_frame(self):
|
||||
def init_frame(self) -> None:
|
||||
self.frame = CameraFrame(**self.frame_config)
|
||||
|
||||
def init_context(self, ctx=None):
|
||||
def init_context(self, ctx: moderngl.Context | None = None) -> None:
|
||||
if ctx is None:
|
||||
ctx = moderngl.create_standalone_context()
|
||||
fbo = self.get_fbo(ctx, 0)
|
||||
@@ -223,7 +210,7 @@ class Camera(object):
|
||||
fbo_msaa.use()
|
||||
self.fbo_msaa = fbo_msaa
|
||||
|
||||
def set_ctx_blending(self, enable=True):
|
||||
def set_ctx_blending(self, enable: bool = True) -> None:
|
||||
if enable:
|
||||
self.ctx.enable(moderngl.BLEND)
|
||||
else:
|
||||
@@ -233,17 +220,21 @@ class Camera(object):
|
||||
# moderngl.ONE, moderngl.ONE
|
||||
)
|
||||
|
||||
def set_ctx_depth_test(self, enable=True):
|
||||
def set_ctx_depth_test(self, enable: bool = True) -> None:
|
||||
if enable:
|
||||
self.ctx.enable(moderngl.DEPTH_TEST)
|
||||
else:
|
||||
self.ctx.disable(moderngl.DEPTH_TEST)
|
||||
|
||||
def init_light_source(self):
|
||||
def init_light_source(self) -> None:
|
||||
self.light_source = Point(self.light_source_position)
|
||||
|
||||
# Methods associated with the frame buffer
|
||||
def get_fbo(self, ctx, samples=0):
|
||||
def get_fbo(
|
||||
self,
|
||||
ctx: moderngl.Context,
|
||||
samples: int = 0
|
||||
) -> moderngl.Framebuffer:
|
||||
pw = self.pixel_width
|
||||
ph = self.pixel_height
|
||||
return ctx.framebuffer(
|
||||
@@ -258,16 +249,16 @@ class Camera(object):
|
||||
)
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
def clear(self) -> None:
|
||||
self.fbo.clear(*self.background_rgba)
|
||||
self.fbo_msaa.clear(*self.background_rgba)
|
||||
|
||||
def reset_pixel_shape(self, new_width, new_height):
|
||||
def reset_pixel_shape(self, new_width: int, new_height: int) -> None:
|
||||
self.pixel_width = new_width
|
||||
self.pixel_height = new_height
|
||||
self.refresh_perspective_uniforms()
|
||||
|
||||
def get_raw_fbo_data(self, dtype='f1'):
|
||||
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
|
||||
# Copy blocks from the fbo_msaa to the drawn fbo using Blit
|
||||
pw, ph = (self.pixel_width, self.pixel_height)
|
||||
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo_msaa.glo)
|
||||
@@ -279,7 +270,7 @@ class Camera(object):
|
||||
dtype=dtype,
|
||||
)
|
||||
|
||||
def get_image(self, pixel_array=None):
|
||||
def get_image(self) -> Image.Image:
|
||||
return Image.frombytes(
|
||||
'RGBA',
|
||||
self.get_pixel_shape(),
|
||||
@@ -287,7 +278,7 @@ class Camera(object):
|
||||
'raw', 'RGBA', 0, -1
|
||||
)
|
||||
|
||||
def get_pixel_array(self):
|
||||
def get_pixel_array(self) -> np.ndarray:
|
||||
raw = self.get_raw_fbo_data(dtype='f4')
|
||||
flat_arr = np.frombuffer(raw, dtype='f4')
|
||||
arr = flat_arr.reshape([*self.fbo.size, self.n_channels])
|
||||
@@ -295,7 +286,7 @@ class Camera(object):
|
||||
return (self.rgb_max_val * arr).astype(self.pixel_array_dtype)
|
||||
|
||||
# Needed?
|
||||
def get_texture(self):
|
||||
def get_texture(self) -> moderngl.Texture:
|
||||
texture = self.ctx.texture(
|
||||
size=self.fbo.size,
|
||||
components=4,
|
||||
@@ -305,32 +296,32 @@ class Camera(object):
|
||||
return texture
|
||||
|
||||
# Getting camera attributes
|
||||
def get_pixel_shape(self):
|
||||
def get_pixel_shape(self) -> tuple[int, int]:
|
||||
return self.fbo.viewport[2:4]
|
||||
# return (self.pixel_width, self.pixel_height)
|
||||
|
||||
def get_pixel_width(self):
|
||||
def get_pixel_width(self) -> int:
|
||||
return self.get_pixel_shape()[0]
|
||||
|
||||
def get_pixel_height(self):
|
||||
def get_pixel_height(self) -> int:
|
||||
return self.get_pixel_shape()[1]
|
||||
|
||||
def get_frame_height(self):
|
||||
def get_frame_height(self) -> float:
|
||||
return self.frame.get_height()
|
||||
|
||||
def get_frame_width(self):
|
||||
def get_frame_width(self) -> float:
|
||||
return self.frame.get_width()
|
||||
|
||||
def get_frame_shape(self):
|
||||
def get_frame_shape(self) -> tuple[float, float]:
|
||||
return (self.get_frame_width(), self.get_frame_height())
|
||||
|
||||
def get_frame_center(self):
|
||||
def get_frame_center(self) -> np.ndarray:
|
||||
return self.frame.get_center()
|
||||
|
||||
def get_location(self):
|
||||
def get_location(self) -> tuple[float, float, float]:
|
||||
return self.frame.get_implied_camera_location()
|
||||
|
||||
def resize_frame_shape(self, fixed_dimension=0):
|
||||
def resize_frame_shape(self, fixed_dimension: bool = False) -> None:
|
||||
"""
|
||||
Changes frame_shape to match the aspect ratio
|
||||
of the pixels, where fixed_dimension determines
|
||||
@@ -342,7 +333,7 @@ class Camera(object):
|
||||
frame_height = self.get_frame_height()
|
||||
frame_width = self.get_frame_width()
|
||||
aspect_ratio = fdiv(pixel_width, pixel_height)
|
||||
if fixed_dimension == 0:
|
||||
if not fixed_dimension:
|
||||
frame_height = frame_width / aspect_ratio
|
||||
else:
|
||||
frame_width = aspect_ratio * frame_height
|
||||
@@ -350,13 +341,13 @@ class Camera(object):
|
||||
self.frame.set_width(frame_width)
|
||||
|
||||
# Rendering
|
||||
def capture(self, *mobjects, **kwargs):
|
||||
def capture(self, *mobjects: Mobject, **kwargs) -> None:
|
||||
self.refresh_perspective_uniforms()
|
||||
for mobject in mobjects:
|
||||
for render_group in self.get_render_group_list(mobject):
|
||||
self.render(render_group)
|
||||
|
||||
def render(self, render_group):
|
||||
def render(self, render_group: dict[str]) -> None:
|
||||
shader_wrapper = render_group["shader_wrapper"]
|
||||
shader_program = render_group["prog"]
|
||||
self.set_shader_uniforms(shader_program, shader_wrapper)
|
||||
@@ -365,13 +356,17 @@ class Camera(object):
|
||||
if render_group["single_use"]:
|
||||
self.release_render_group(render_group)
|
||||
|
||||
def get_render_group_list(self, mobject):
|
||||
def get_render_group_list(self, mobject: Mobject) -> list[dict[str]] | map[dict[str]]:
|
||||
try:
|
||||
return self.static_mobject_to_render_group_list[id(mobject)]
|
||||
except KeyError:
|
||||
return map(self.get_render_group, mobject.get_shader_wrapper_list())
|
||||
|
||||
def get_render_group(self, shader_wrapper, single_use=True):
|
||||
def get_render_group(
|
||||
self,
|
||||
shader_wrapper: ShaderWrapper,
|
||||
single_use: bool = True
|
||||
) -> dict[str]:
|
||||
# Data buffers
|
||||
vbo = self.ctx.buffer(shader_wrapper.vert_data.tobytes())
|
||||
if shader_wrapper.vert_indices is None:
|
||||
@@ -399,12 +394,12 @@ class Camera(object):
|
||||
"single_use": single_use,
|
||||
}
|
||||
|
||||
def release_render_group(self, render_group):
|
||||
def release_render_group(self, render_group: dict[str]) -> None:
|
||||
for key in ["vbo", "ibo", "vao"]:
|
||||
if render_group[key] is not None:
|
||||
render_group[key].release()
|
||||
|
||||
def set_mobjects_as_static(self, *mobjects):
|
||||
def set_mobjects_as_static(self, *mobjects: Mobject) -> None:
|
||||
# Creates buffer and array objects holding each mobjects shader data
|
||||
for mob in mobjects:
|
||||
self.static_mobject_to_render_group_list[id(mob)] = [
|
||||
@@ -412,18 +407,23 @@ class Camera(object):
|
||||
for sw in mob.get_shader_wrapper_list()
|
||||
]
|
||||
|
||||
def release_static_mobjects(self):
|
||||
def release_static_mobjects(self) -> None:
|
||||
for rg_list in self.static_mobject_to_render_group_list.values():
|
||||
for render_group in rg_list:
|
||||
self.release_render_group(render_group)
|
||||
self.static_mobject_to_render_group_list = {}
|
||||
|
||||
# Shaders
|
||||
def init_shaders(self):
|
||||
def init_shaders(self) -> None:
|
||||
# Initialize with the null id going to None
|
||||
self.id_to_shader_program = {"": None}
|
||||
self.id_to_shader_program: dict[
|
||||
int | str, tuple[moderngl.Program, str] | None
|
||||
] = {"": None}
|
||||
|
||||
def get_shader_program(self, shader_wrapper):
|
||||
def get_shader_program(
|
||||
self,
|
||||
shader_wrapper: ShaderWrapper
|
||||
) -> tuple[moderngl.Program, str]:
|
||||
sid = shader_wrapper.get_program_id()
|
||||
if sid not in self.id_to_shader_program:
|
||||
# Create shader program for the first time, then cache
|
||||
@@ -433,19 +433,23 @@ class Camera(object):
|
||||
self.id_to_shader_program[sid] = (program, vert_format)
|
||||
return self.id_to_shader_program[sid]
|
||||
|
||||
def set_shader_uniforms(self, shader, shader_wrapper):
|
||||
def set_shader_uniforms(
|
||||
self,
|
||||
shader: moderngl.Program,
|
||||
shader_wrapper: ShaderWrapper
|
||||
) -> None:
|
||||
for name, path in shader_wrapper.texture_paths.items():
|
||||
tid = self.get_texture_id(path)
|
||||
shader[name].value = tid
|
||||
for name, value in it.chain(self.perspective_uniforms.items(), shader_wrapper.uniforms.items()):
|
||||
try:
|
||||
if isinstance(value, np.ndarray):
|
||||
if isinstance(value, np.ndarray) and value.ndim > 0:
|
||||
value = tuple(value)
|
||||
shader[name].value = value
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def refresh_perspective_uniforms(self):
|
||||
def refresh_perspective_uniforms(self) -> None:
|
||||
frame = self.frame
|
||||
pw, ph = self.get_pixel_shape()
|
||||
fw, fh = frame.get_shape()
|
||||
@@ -470,11 +474,13 @@ class Camera(object):
|
||||
"focal_distance": frame.get_focal_distance(),
|
||||
}
|
||||
|
||||
def init_textures(self):
|
||||
self.n_textures = 0
|
||||
self.path_to_texture = {}
|
||||
def init_textures(self) -> None:
|
||||
self.n_textures: int = 0
|
||||
self.path_to_texture: dict[
|
||||
str, tuple[int, moderngl.Texture]
|
||||
] = {}
|
||||
|
||||
def get_texture_id(self, path):
|
||||
def get_texture_id(self, path: str) -> int:
|
||||
if path not in self.path_to_texture:
|
||||
if self.n_textures == 15: # I have no clue why this is needed
|
||||
self.n_textures += 1
|
||||
@@ -490,7 +496,7 @@ class Camera(object):
|
||||
self.path_to_texture[path] = (tid, texture)
|
||||
return self.path_to_texture[path][0]
|
||||
|
||||
def release_texture(self, path):
|
||||
def release_texture(self, path: str):
|
||||
tid_and_texture = self.path_to_texture.pop(path, None)
|
||||
if tid_and_texture:
|
||||
tid_and_texture[1].release()
|
||||
|
||||
@@ -23,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",
|
||||
@@ -65,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",
|
||||
@@ -306,6 +312,7 @@ def get_configuration(args):
|
||||
"start_at_animation_number": args.start_at_animation_number,
|
||||
"end_at_animation_number": None,
|
||||
"preview": not write_file,
|
||||
"presenter_mode": args.presenter_mode,
|
||||
"leave_progress_bars": args.leave_progress_bars,
|
||||
}
|
||||
|
||||
|
||||
@@ -64,8 +64,6 @@ JOINT_TYPE_MAP = {
|
||||
}
|
||||
|
||||
# Related to Text
|
||||
START_X = 30
|
||||
START_Y = 20
|
||||
NORMAL = "NORMAL"
|
||||
ITALIC = "ITALIC"
|
||||
OBLIQUE = "OBLIQUE"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
@@ -6,21 +8,23 @@ from manimlib.event_handler.event_listner import EventListner
|
||||
|
||||
class EventDispatcher(object):
|
||||
def __init__(self):
|
||||
self.event_listners = {
|
||||
self.event_listners: dict[
|
||||
EventType, list[EventListner]
|
||||
] = {
|
||||
event_type: []
|
||||
for event_type in EventType
|
||||
}
|
||||
self.mouse_point = np.array((0., 0., 0.))
|
||||
self.mouse_drag_point = np.array((0., 0., 0.))
|
||||
self.pressed_keys = set()
|
||||
self.draggable_object_listners = []
|
||||
self.pressed_keys: set[int] = set()
|
||||
self.draggable_object_listners: list[EventListner] = []
|
||||
|
||||
def add_listner(self, event_listner):
|
||||
def add_listner(self, event_listner: EventListner):
|
||||
assert(isinstance(event_listner, EventListner))
|
||||
self.event_listners[event_listner.event_type].append(event_listner)
|
||||
return self
|
||||
|
||||
def remove_listner(self, event_listner):
|
||||
def remove_listner(self, event_listner: EventListner):
|
||||
assert(isinstance(event_listner, EventListner))
|
||||
try:
|
||||
while event_listner in self.event_listners[event_listner.event_type]:
|
||||
@@ -30,8 +34,7 @@ class EventDispatcher(object):
|
||||
pass
|
||||
return self
|
||||
|
||||
def dispatch(self, event_type, **event_data):
|
||||
|
||||
def dispatch(self, event_type: EventType, **event_data):
|
||||
if event_type == EventType.MouseMotionEvent:
|
||||
self.mouse_point = event_data["point"]
|
||||
elif event_type == EventType.MouseDragEvent:
|
||||
@@ -74,16 +77,16 @@ class EventDispatcher(object):
|
||||
|
||||
return propagate_event
|
||||
|
||||
def get_listners_count(self):
|
||||
def get_listners_count(self) -> int:
|
||||
return sum([len(value) for key, value in self.event_listners.items()])
|
||||
|
||||
def get_mouse_point(self):
|
||||
def get_mouse_point(self) -> np.ndarray:
|
||||
return self.mouse_point
|
||||
|
||||
def get_mouse_drag_point(self):
|
||||
def get_mouse_drag_point(self) -> np.ndarray:
|
||||
return self.mouse_drag_point
|
||||
|
||||
def is_key_pressed(self, symbol):
|
||||
def is_key_pressed(self, symbol: int) -> bool:
|
||||
return (symbol in self.pressed_keys)
|
||||
|
||||
__iadd__ = add_listner
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
|
||||
class EventListner(object):
|
||||
def __init__(self, mobject, event_type, event_callback):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
event_type: EventType,
|
||||
event_callback: Callable[[Mobject, dict[str]]]
|
||||
):
|
||||
self.mobject = mobject
|
||||
self.event_type = event_type
|
||||
self.callback = event_callback
|
||||
|
||||
@@ -64,6 +64,7 @@ def get_scene_config(config):
|
||||
"end_at_animation_number",
|
||||
"leave_progress_bars",
|
||||
"preview",
|
||||
"presenter_mode",
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pathops
|
||||
|
||||
@@ -7,7 +9,7 @@ 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):
|
||||
def _convert_vmobject_to_skia_path(vmobject: VMobject) -> pathops.Path:
|
||||
path = pathops.Path()
|
||||
subpaths = vmobject.get_subpaths_from_points(vmobject.get_all_points())
|
||||
for subpath in subpaths:
|
||||
@@ -21,7 +23,10 @@ def _convert_vmobject_to_skia_path(vmobject):
|
||||
return path
|
||||
|
||||
|
||||
def _convert_skia_path_to_vmobject(path, vmobject):
|
||||
def _convert_skia_path_to_vmobject(
|
||||
path: pathops.Path,
|
||||
vmobject: VMobject
|
||||
) -> VMobject:
|
||||
PathVerb = pathops.PathVerb
|
||||
current_path_start = np.array([0.0, 0.0, 0.0])
|
||||
for path_verb, points in path:
|
||||
@@ -41,11 +46,11 @@ def _convert_skia_path_to_vmobject(path, vmobject):
|
||||
vmobject.add_quadratic_bezier_curve_to(*points)
|
||||
else:
|
||||
raise Exception(f"Unsupported: {path_verb}")
|
||||
return vmobject
|
||||
return vmobject.reverse_points()
|
||||
|
||||
|
||||
class Union(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Union.")
|
||||
super().__init__(**kwargs)
|
||||
@@ -59,7 +64,7 @@ class Union(VMobject):
|
||||
|
||||
|
||||
class Difference(VMobject):
|
||||
def __init__(self, subject, clip, **kwargs):
|
||||
def __init__(self, subject: VMobject, clip: VMobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
outpen = pathops.Path()
|
||||
pathops.difference(
|
||||
@@ -71,7 +76,7 @@ class Difference(VMobject):
|
||||
|
||||
|
||||
class Intersection(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Intersection.")
|
||||
super().__init__(**kwargs)
|
||||
@@ -94,7 +99,7 @@ class Intersection(VMobject):
|
||||
|
||||
|
||||
class Exclusion(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Exclusion.")
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import BLUE_D
|
||||
from manimlib.constants import BLUE_B
|
||||
from manimlib.constants import BLUE_E
|
||||
@@ -20,10 +25,10 @@ class AnimatedBoundary(VGroup):
|
||||
"fade_rate_func": smooth,
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.vmobject = vmobject
|
||||
self.boundary_copies = [
|
||||
self.vmobject: VMobject = vmobject
|
||||
self.boundary_copies: list[VMobject] = [
|
||||
vmobject.copy().set_style(
|
||||
stroke_width=0,
|
||||
fill_opacity=0
|
||||
@@ -31,12 +36,12 @@ class AnimatedBoundary(VGroup):
|
||||
for x in range(2)
|
||||
]
|
||||
self.add(*self.boundary_copies)
|
||||
self.total_time = 0
|
||||
self.total_time: float = 0
|
||||
self.add_updater(
|
||||
lambda m, dt: self.update_boundary_copies(dt)
|
||||
)
|
||||
|
||||
def update_boundary_copies(self, dt):
|
||||
def update_boundary_copies(self, dt: float) -> None:
|
||||
# Not actual time, but something which passes at
|
||||
# an altered rate to make the implementation below
|
||||
# cleaner
|
||||
@@ -67,7 +72,13 @@ class AnimatedBoundary(VGroup):
|
||||
|
||||
self.total_time += dt
|
||||
|
||||
def full_family_become_partial(self, mob1, mob2, a, b):
|
||||
def full_family_become_partial(
|
||||
self,
|
||||
mob1: VMobject,
|
||||
mob2: VMobject,
|
||||
a: float,
|
||||
b: float
|
||||
):
|
||||
family1 = mob1.family_members_with_points()
|
||||
family2 = mob2.family_members_with_points()
|
||||
for sm1, sm2 in zip(family1, family2):
|
||||
@@ -84,14 +95,14 @@ class TracedPath(VMobject):
|
||||
"time_per_anchor": 1 / 15,
|
||||
}
|
||||
|
||||
def __init__(self, traced_point_func, **kwargs):
|
||||
def __init__(self, traced_point_func: Callable[[], np.ndarray], **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.time = 0
|
||||
self.traced_points = []
|
||||
self.time: float = 0
|
||||
self.traced_points: list[np.ndarray] = []
|
||||
self.add_updater(lambda m, dt: m.update_path(dt))
|
||||
|
||||
def update_path(self, dt):
|
||||
def update_path(self, dt: float):
|
||||
if dt == 0:
|
||||
return self
|
||||
point = self.traced_point_func().copy()
|
||||
@@ -133,7 +144,11 @@ class TracingTail(TracedPath):
|
||||
"time_traced": 1.0,
|
||||
}
|
||||
|
||||
def __init__(self, mobject_or_func, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject_or_func: Mobject | Callable[[], np.ndarray],
|
||||
**kwargs
|
||||
):
|
||||
if isinstance(mobject_or_func, Mobject):
|
||||
func = mobject_or_func.get_center
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from abc import abstractmethod
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import numbers
|
||||
from abc import abstractmethod
|
||||
from typing import Type, TypeVar, Union, Callable, Iterable, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.functions import ParametricCurve
|
||||
@@ -18,6 +22,15 @@ from manimlib.utils.space_ops import angle_of_vector
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import colour
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
T = TypeVar("T", bound=Mobject)
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
EPSILON = 1e-8
|
||||
|
||||
|
||||
@@ -39,56 +52,77 @@ class CoordinateSystem():
|
||||
self.x_range = np.array(self.default_x_range)
|
||||
self.y_range = np.array(self.default_y_range)
|
||||
|
||||
def coords_to_point(self, *coords):
|
||||
@abstractmethod
|
||||
def coords_to_point(self, *coords: float) -> np.ndarray:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def point_to_coords(self, point):
|
||||
@abstractmethod
|
||||
def point_to_coords(self, point: np.ndarray) -> tuple[float, ...]:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def c2p(self, *coords):
|
||||
def c2p(self, *coords: float):
|
||||
"""Abbreviation for coords_to_point"""
|
||||
return self.coords_to_point(*coords)
|
||||
|
||||
def p2c(self, point):
|
||||
def p2c(self, point: np.ndarray):
|
||||
"""Abbreviation for point_to_coords"""
|
||||
return self.point_to_coords(point)
|
||||
|
||||
def get_origin(self):
|
||||
def get_origin(self) -> np.ndarray:
|
||||
return self.c2p(*[0] * self.dimension)
|
||||
|
||||
@abstractmethod
|
||||
def get_axes(self):
|
||||
def get_axes(self) -> VGroup:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
@abstractmethod
|
||||
def get_all_ranges(self):
|
||||
def get_all_ranges(self) -> list[np.ndarray]:
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def get_axis(self, index):
|
||||
def get_axis(self, index: int) -> NumberLine:
|
||||
return self.get_axes()[index]
|
||||
|
||||
def get_x_axis(self):
|
||||
def get_x_axis(self) -> NumberLine:
|
||||
return self.get_axis(0)
|
||||
|
||||
def get_y_axis(self):
|
||||
def get_y_axis(self) -> NumberLine:
|
||||
return self.get_axis(1)
|
||||
|
||||
def get_z_axis(self):
|
||||
def get_z_axis(self) -> NumberLine:
|
||||
return self.get_axis(2)
|
||||
|
||||
def get_x_axis_label(self, label_tex, edge=RIGHT, direction=DL, **kwargs):
|
||||
def get_x_axis_label(
|
||||
self,
|
||||
label_tex: str,
|
||||
edge: np.ndarray = RIGHT,
|
||||
direction: np.ndarray = DL,
|
||||
**kwargs
|
||||
) -> Tex:
|
||||
return self.get_axis_label(
|
||||
label_tex, self.get_x_axis(),
|
||||
edge, direction, **kwargs
|
||||
)
|
||||
|
||||
def get_y_axis_label(self, label_tex, edge=UP, direction=DR, **kwargs):
|
||||
def get_y_axis_label(
|
||||
self,
|
||||
label_tex: str,
|
||||
edge: np.ndarray = UP,
|
||||
direction: np.ndarray = DR,
|
||||
**kwargs
|
||||
) -> Tex:
|
||||
return self.get_axis_label(
|
||||
label_tex, self.get_y_axis(),
|
||||
edge, direction, **kwargs
|
||||
)
|
||||
|
||||
def get_axis_label(self, label_tex, axis, edge, direction, buff=MED_SMALL_BUFF):
|
||||
def get_axis_label(
|
||||
self,
|
||||
label_tex: str,
|
||||
axis: np.ndarray,
|
||||
edge: np.ndarray,
|
||||
direction: np.ndarray,
|
||||
buff: float = MED_SMALL_BUFF
|
||||
) -> Tex:
|
||||
label = Tex(label_tex)
|
||||
label.next_to(
|
||||
axis.get_edge_center(edge), direction,
|
||||
@@ -97,30 +131,43 @@ class CoordinateSystem():
|
||||
label.shift_onto_screen(buff=MED_SMALL_BUFF)
|
||||
return label
|
||||
|
||||
def get_axis_labels(self, x_label_tex="x", y_label_tex="y"):
|
||||
def get_axis_labels(
|
||||
self,
|
||||
x_label_tex: str = "x",
|
||||
y_label_tex: str = "y"
|
||||
) -> VGroup:
|
||||
self.axis_labels = VGroup(
|
||||
self.get_x_axis_label(x_label_tex),
|
||||
self.get_y_axis_label(y_label_tex),
|
||||
)
|
||||
return self.axis_labels
|
||||
|
||||
def get_line_from_axis_to_point(self, index, point,
|
||||
line_func=DashedLine,
|
||||
color=GREY_A,
|
||||
stroke_width=2):
|
||||
def get_line_from_axis_to_point(
|
||||
self,
|
||||
index: int,
|
||||
point: np.ndarray,
|
||||
line_func: Type[T] = DashedLine,
|
||||
color: ManimColor = GREY_A,
|
||||
stroke_width: float = 2
|
||||
) -> T:
|
||||
axis = self.get_axis(index)
|
||||
line = line_func(axis.get_projection(point), point)
|
||||
line.set_stroke(color, stroke_width)
|
||||
return line
|
||||
|
||||
def get_v_line(self, point, **kwargs):
|
||||
def get_v_line(self, point: np.ndarray, **kwargs):
|
||||
return self.get_line_from_axis_to_point(0, point, **kwargs)
|
||||
|
||||
def get_h_line(self, point, **kwargs):
|
||||
def get_h_line(self, point: np.ndarray, **kwargs):
|
||||
return self.get_line_from_axis_to_point(1, point, **kwargs)
|
||||
|
||||
# Useful for graphing
|
||||
def get_graph(self, function, x_range=None, **kwargs):
|
||||
def get_graph(
|
||||
self,
|
||||
function: Callable[[float], float],
|
||||
x_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
) -> ParametricCurve:
|
||||
t_range = np.array(self.x_range, dtype=float)
|
||||
if x_range is not None:
|
||||
t_range[:len(x_range)] = x_range
|
||||
@@ -139,7 +186,11 @@ class CoordinateSystem():
|
||||
graph.x_range = x_range
|
||||
return graph
|
||||
|
||||
def get_parametric_curve(self, function, **kwargs):
|
||||
def get_parametric_curve(
|
||||
self,
|
||||
function: Callable[[float], np.ndarray],
|
||||
**kwargs
|
||||
) -> ParametricCurve:
|
||||
dim = self.dimension
|
||||
graph = ParametricCurve(
|
||||
lambda t: self.coords_to_point(*function(t)[:dim]),
|
||||
@@ -148,7 +199,11 @@ class CoordinateSystem():
|
||||
graph.underlying_function = function
|
||||
return graph
|
||||
|
||||
def input_to_graph_point(self, x, graph):
|
||||
def input_to_graph_point(
|
||||
self,
|
||||
x: float,
|
||||
graph: ParametricCurve
|
||||
) -> np.ndarray | None:
|
||||
if hasattr(graph, "underlying_function"):
|
||||
return self.coords_to_point(x, graph.underlying_function(x))
|
||||
else:
|
||||
@@ -165,19 +220,21 @@ class CoordinateSystem():
|
||||
else:
|
||||
return None
|
||||
|
||||
def i2gp(self, x, graph):
|
||||
def i2gp(self, x: float, graph: ParametricCurve) -> np.ndarray | None:
|
||||
"""
|
||||
Alias for input_to_graph_point
|
||||
"""
|
||||
return self.input_to_graph_point(x, graph)
|
||||
|
||||
def get_graph_label(self,
|
||||
graph,
|
||||
label="f(x)",
|
||||
x=None,
|
||||
direction=RIGHT,
|
||||
buff=MED_SMALL_BUFF,
|
||||
color=None):
|
||||
def get_graph_label(
|
||||
self,
|
||||
graph: ParametricCurve,
|
||||
label: str | Mobject = "f(x)",
|
||||
x: float | None = None,
|
||||
direction: np.ndarray = RIGHT,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
color: ManimColor | None = None
|
||||
) -> Tex | Mobject:
|
||||
if isinstance(label, str):
|
||||
label = Tex(label)
|
||||
if color is None:
|
||||
@@ -204,38 +261,57 @@ class CoordinateSystem():
|
||||
label.shift_onto_screen()
|
||||
return label
|
||||
|
||||
def get_v_line_to_graph(self, x, graph, **kwargs):
|
||||
def get_v_line_to_graph(self, x: float, graph: ParametricCurve, **kwargs):
|
||||
return self.get_v_line(self.i2gp(x, graph), **kwargs)
|
||||
|
||||
def get_h_line_to_graph(self, x, graph, **kwargs):
|
||||
def get_h_line_to_graph(self, x: float, graph: ParametricCurve, **kwargs):
|
||||
return self.get_h_line(self.i2gp(x, graph), **kwargs)
|
||||
|
||||
# For calculus
|
||||
def angle_of_tangent(self, x, graph, dx=EPSILON):
|
||||
def angle_of_tangent(
|
||||
self,
|
||||
x: float,
|
||||
graph: ParametricCurve,
|
||||
dx: float = EPSILON
|
||||
) -> float:
|
||||
p0 = self.input_to_graph_point(x, graph)
|
||||
p1 = self.input_to_graph_point(x + dx, graph)
|
||||
return angle_of_vector(p1 - p0)
|
||||
|
||||
def slope_of_tangent(self, x, graph, **kwargs):
|
||||
def slope_of_tangent(
|
||||
self,
|
||||
x: float,
|
||||
graph: ParametricCurve,
|
||||
**kwargs
|
||||
) -> float:
|
||||
return np.tan(self.angle_of_tangent(x, graph, **kwargs))
|
||||
|
||||
def get_tangent_line(self, x, graph, length=5, line_func=Line):
|
||||
def get_tangent_line(
|
||||
self,
|
||||
x: float,
|
||||
graph: ParametricCurve,
|
||||
length: float = 5,
|
||||
line_func: Type[T] = Line
|
||||
) -> T:
|
||||
line = line_func(LEFT, RIGHT)
|
||||
line.set_width(length)
|
||||
line.rotate(self.angle_of_tangent(x, graph))
|
||||
line.move_to(self.input_to_graph_point(x, graph))
|
||||
return line
|
||||
|
||||
def get_riemann_rectangles(self,
|
||||
graph,
|
||||
x_range=None,
|
||||
dx=None,
|
||||
input_sample_type="left",
|
||||
stroke_width=1,
|
||||
stroke_color=BLACK,
|
||||
fill_opacity=1,
|
||||
colors=(BLUE, GREEN),
|
||||
show_signed_area=True):
|
||||
def get_riemann_rectangles(
|
||||
self,
|
||||
graph: ParametricCurve,
|
||||
x_range: Sequence[float] = None,
|
||||
dx: float | None = None,
|
||||
input_sample_type: str = "left",
|
||||
stroke_width: float = 1,
|
||||
stroke_color: ManimColor = BLACK,
|
||||
fill_opacity: float = 1,
|
||||
colors: Iterable[ManimColor] = (BLUE, GREEN),
|
||||
stroke_background: bool = True,
|
||||
show_signed_area: bool = True
|
||||
) -> VGroup:
|
||||
if x_range is None:
|
||||
x_range = self.x_range[:2]
|
||||
if dx is None:
|
||||
@@ -257,7 +333,8 @@ class CoordinateSystem():
|
||||
height = get_norm(
|
||||
self.i2gp(sample, graph) - self.c2p(sample, 0)
|
||||
)
|
||||
rect = Rectangle(width=x1 - x0, height=height)
|
||||
rect = Rectangle(width=self.x_axis.n2p(x1)[0] - self.x_axis.n2p(x0)[0],
|
||||
height=height)
|
||||
rect.move_to(self.c2p(x0, 0), DL)
|
||||
rects.append(rect)
|
||||
result = VGroup(*rects)
|
||||
@@ -266,6 +343,7 @@ class CoordinateSystem():
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_background=stroke_background
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -277,7 +355,7 @@ class CoordinateSystem():
|
||||
class Axes(VGroup, CoordinateSystem):
|
||||
CONFIG = {
|
||||
"axis_config": {
|
||||
"include_tip": True,
|
||||
"include_tip": False,
|
||||
"numbers_to_exclude": [0],
|
||||
},
|
||||
"x_axis_config": {},
|
||||
@@ -288,10 +366,12 @@ class Axes(VGroup, CoordinateSystem):
|
||||
"width": FRAME_WIDTH - 2,
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
x_range=None,
|
||||
y_range=None,
|
||||
**kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
x_range: Sequence[float] | None = None,
|
||||
y_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
CoordinateSystem.__init__(self, **kwargs)
|
||||
VGroup.__init__(self, **kwargs)
|
||||
|
||||
@@ -314,36 +394,43 @@ class Axes(VGroup, CoordinateSystem):
|
||||
self.add(*self.axes)
|
||||
self.center()
|
||||
|
||||
def create_axis(self, range_terms, axis_config, length):
|
||||
def create_axis(
|
||||
self,
|
||||
range_terms: Sequence[float],
|
||||
axis_config: dict[str],
|
||||
length: float
|
||||
) -> NumberLine:
|
||||
new_config = merge_dicts_recursively(self.axis_config, axis_config)
|
||||
new_config["width"] = length
|
||||
axis = NumberLine(range_terms, **new_config)
|
||||
axis.shift(-axis.n2p(0))
|
||||
return axis
|
||||
|
||||
def coords_to_point(self, *coords):
|
||||
def coords_to_point(self, *coords: float) -> np.ndarray:
|
||||
origin = self.x_axis.number_to_point(0)
|
||||
result = origin.copy()
|
||||
for axis, coord in zip(self.get_axes(), coords):
|
||||
result += (axis.number_to_point(coord) - origin)
|
||||
return result
|
||||
return origin + sum(
|
||||
axis.number_to_point(coord) - origin
|
||||
for axis, coord in zip(self.get_axes(), coords)
|
||||
)
|
||||
|
||||
def point_to_coords(self, point):
|
||||
def point_to_coords(self, point: np.ndarray) -> tuple[float, ...]:
|
||||
return tuple([
|
||||
axis.point_to_number(point)
|
||||
for axis in self.get_axes()
|
||||
])
|
||||
|
||||
def get_axes(self):
|
||||
def get_axes(self) -> VGroup:
|
||||
return self.axes
|
||||
|
||||
def get_all_ranges(self):
|
||||
def get_all_ranges(self) -> list[Sequence[float]]:
|
||||
return [self.x_range, self.y_range]
|
||||
|
||||
def add_coordinate_labels(self,
|
||||
x_values=None,
|
||||
y_values=None,
|
||||
**kwargs):
|
||||
def add_coordinate_labels(
|
||||
self,
|
||||
x_values: Iterable[float] | None = None,
|
||||
y_values: Iterable[float] | None = None,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
axes = self.get_axes()
|
||||
self.coordinate_labels = VGroup()
|
||||
for axis, values in zip(axes, [x_values, y_values]):
|
||||
@@ -367,7 +454,13 @@ class ThreeDAxes(Axes):
|
||||
"gloss": 0.5,
|
||||
}
|
||||
|
||||
def __init__(self, x_range=None, y_range=None, z_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
x_range: Sequence[float] | None = None,
|
||||
y_range: Sequence[float] | None = None,
|
||||
z_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
Axes.__init__(self, x_range, y_range, **kwargs)
|
||||
if z_range is not None:
|
||||
self.z_range[:len(z_range)] = z_range
|
||||
@@ -390,7 +483,7 @@ class ThreeDAxes(Axes):
|
||||
for axis in self.axes:
|
||||
axis.insert_n_curves(self.num_axis_pieces - 1)
|
||||
|
||||
def get_all_ranges(self):
|
||||
def get_all_ranges(self) -> list[Sequence[float]]:
|
||||
return [self.x_range, self.y_range, self.z_range]
|
||||
|
||||
|
||||
@@ -420,11 +513,16 @@ class NumberPlane(Axes):
|
||||
"make_smooth_after_applying_functions": True,
|
||||
}
|
||||
|
||||
def __init__(self, x_range=None, y_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
x_range: Sequence[float] | None = None,
|
||||
y_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(x_range, y_range, **kwargs)
|
||||
self.init_background_lines()
|
||||
|
||||
def init_background_lines(self):
|
||||
def init_background_lines(self) -> None:
|
||||
if self.faded_line_style is None:
|
||||
style = dict(self.background_line_style)
|
||||
# For anything numerical, like stroke_width
|
||||
@@ -442,7 +540,7 @@ class NumberPlane(Axes):
|
||||
self.background_lines,
|
||||
)
|
||||
|
||||
def get_lines(self):
|
||||
def get_lines(self) -> tuple[VGroup, VGroup]:
|
||||
x_axis = self.get_x_axis()
|
||||
y_axis = self.get_y_axis()
|
||||
|
||||
@@ -452,7 +550,11 @@ class NumberPlane(Axes):
|
||||
lines2 = VGroup(*x_lines2, *y_lines2)
|
||||
return lines1, lines2
|
||||
|
||||
def get_lines_parallel_to_axis(self, axis1, axis2):
|
||||
def get_lines_parallel_to_axis(
|
||||
self,
|
||||
axis1: NumberLine,
|
||||
axis2: NumberLine
|
||||
) -> tuple[VGroup, VGroup]:
|
||||
freq = axis2.x_step
|
||||
ratio = self.faded_line_ratio
|
||||
line = Line(axis1.get_start(), axis1.get_end())
|
||||
@@ -471,20 +573,20 @@ class NumberPlane(Axes):
|
||||
lines2.add(new_line)
|
||||
return lines1, lines2
|
||||
|
||||
def get_x_unit_size(self):
|
||||
def get_x_unit_size(self) -> float:
|
||||
return self.get_x_axis().get_unit_size()
|
||||
|
||||
def get_y_unit_size(self):
|
||||
def get_y_unit_size(self) -> list:
|
||||
return self.get_x_axis().get_unit_size()
|
||||
|
||||
def get_axes(self):
|
||||
def get_axes(self) -> VGroup:
|
||||
return self.axes
|
||||
|
||||
def get_vector(self, coords, **kwargs):
|
||||
def get_vector(self, coords: Iterable[float], **kwargs) -> Arrow:
|
||||
kwargs["buff"] = 0
|
||||
return Arrow(self.c2p(0, 0), self.c2p(*coords), **kwargs)
|
||||
|
||||
def prepare_for_nonlinear_transform(self, num_inserted_curves=50):
|
||||
def prepare_for_nonlinear_transform(self, num_inserted_curves: int = 50):
|
||||
for mob in self.family_members_with_points():
|
||||
num_curves = mob.get_num_curves()
|
||||
if num_inserted_curves > num_curves:
|
||||
@@ -499,27 +601,35 @@ class ComplexPlane(NumberPlane):
|
||||
"line_frequency": 1,
|
||||
}
|
||||
|
||||
def number_to_point(self, number):
|
||||
def number_to_point(self, number: complex | float) -> np.ndarray:
|
||||
number = complex(number)
|
||||
return self.coords_to_point(number.real, number.imag)
|
||||
|
||||
def n2p(self, number):
|
||||
def n2p(self, number: complex | float) -> np.ndarray:
|
||||
return self.number_to_point(number)
|
||||
|
||||
def point_to_number(self, point):
|
||||
def point_to_number(self, point: np.ndarray) -> complex:
|
||||
x, y = self.point_to_coords(point)
|
||||
return complex(x, y)
|
||||
|
||||
def p2n(self, point):
|
||||
def p2n(self, point: np.ndarray) -> complex:
|
||||
return self.point_to_number(point)
|
||||
|
||||
def get_default_coordinate_values(self, skip_first=True):
|
||||
def get_default_coordinate_values(
|
||||
self,
|
||||
skip_first: bool = True
|
||||
) -> list[complex]:
|
||||
x_numbers = self.get_x_axis().get_tick_range()[1:]
|
||||
y_numbers = self.get_y_axis().get_tick_range()[1:]
|
||||
y_numbers = [complex(0, y) for y in y_numbers if y != 0]
|
||||
return [*x_numbers, *y_numbers]
|
||||
|
||||
def add_coordinate_labels(self, numbers=None, skip_first=True, **kwargs):
|
||||
def add_coordinate_labels(
|
||||
self,
|
||||
numbers: list[complex] | None = None,
|
||||
skip_first: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
if numbers is None:
|
||||
numbers = self.get_default_coordinate_values(skip_first)
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Sequence
|
||||
|
||||
from isosurfaces import plot_isoline
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
@@ -12,7 +18,12 @@ class ParametricCurve(VMobject):
|
||||
"use_smoothing": True,
|
||||
}
|
||||
|
||||
def __init__(self, t_func, t_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
t_func: Callable[[float], np.ndarray],
|
||||
t_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
if t_range is not None:
|
||||
self.t_range[:len(t_range)] = t_range
|
||||
@@ -25,7 +36,7 @@ class ParametricCurve(VMobject):
|
||||
self.t_func = t_func
|
||||
VMobject.__init__(self, **kwargs)
|
||||
|
||||
def get_point_from_function(self, t):
|
||||
def get_point_from_function(self, t: float) -> np.ndarray:
|
||||
return self.t_func(t)
|
||||
|
||||
def init_points(self):
|
||||
@@ -42,8 +53,22 @@ class ParametricCurve(VMobject):
|
||||
self.add_points_as_corners(points[1:])
|
||||
if self.use_smoothing:
|
||||
self.make_approximately_smooth()
|
||||
if not self.has_points():
|
||||
self.set_points([self.t_func(t_min)])
|
||||
return self
|
||||
|
||||
def get_t_func(self):
|
||||
return self.t_func
|
||||
|
||||
def get_function(self):
|
||||
if hasattr(self, "underlying_function"):
|
||||
return self.underlying_function
|
||||
if hasattr(self, "function"):
|
||||
return self.function
|
||||
|
||||
def get_x_range(self):
|
||||
if hasattr(self, "x_range"):
|
||||
return self.x_range
|
||||
|
||||
class FunctionGraph(ParametricCurve):
|
||||
CONFIG = {
|
||||
@@ -51,7 +76,12 @@ class FunctionGraph(ParametricCurve):
|
||||
"x_range": [-8, 8, 0.25],
|
||||
}
|
||||
|
||||
def __init__(self, function, x_range=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[float], float],
|
||||
x_range: Sequence[float] | None = None,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
self.function = function
|
||||
|
||||
@@ -63,8 +93,43 @@ class FunctionGraph(ParametricCurve):
|
||||
|
||||
super().__init__(parametric_function, self.x_range, **kwargs)
|
||||
|
||||
def get_function(self):
|
||||
return self.function
|
||||
|
||||
def get_point_from_function(self, x):
|
||||
return self.t_func(x)
|
||||
class ImplicitFunction(VMobject):
|
||||
CONFIG = {
|
||||
"x_range": [-FRAME_X_RADIUS, FRAME_X_RADIUS],
|
||||
"y_range": [-FRAME_Y_RADIUS, FRAME_Y_RADIUS],
|
||||
"min_depth": 5,
|
||||
"max_quads": 1500,
|
||||
"use_smoothing": True
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], float],
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
self.function = func
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
p_min, p_max = (
|
||||
np.array([self.x_range[0], self.y_range[0]]),
|
||||
np.array([self.x_range[1], self.y_range[1]]),
|
||||
)
|
||||
curves = plot_isoline(
|
||||
fn=lambda u: self.function(u[0], u[1]),
|
||||
pmin=p_min,
|
||||
pmax=p_max,
|
||||
min_depth=self.min_depth,
|
||||
max_quads=self.max_quads,
|
||||
) # returns a list of lists of 2D points
|
||||
curves = [
|
||||
np.pad(curve, [(0, 0), (0, 1)]) for curve in curves if curve != []
|
||||
] # add z coord as 0
|
||||
for curve in curves:
|
||||
self.start_new_path(curve[0])
|
||||
self.add_points_as_corners(curve[1:])
|
||||
if self.use_smoothing:
|
||||
self.make_smooth()
|
||||
return self
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import numbers
|
||||
from typing import Sequence, Union
|
||||
|
||||
import colour
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -21,6 +26,8 @@ from manimlib.utils.space_ops import normalize
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
from manimlib.utils.space_ops import rotation_matrix_transpose
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
DEFAULT_DOT_RADIUS = 0.08
|
||||
DEFAULT_SMALL_DOT_RADIUS = 0.04
|
||||
@@ -58,7 +65,7 @@ class TipableVMobject(VMobject):
|
||||
}
|
||||
|
||||
# Adding, Creating, Modifying tips
|
||||
def add_tip(self, at_start=False, **kwargs):
|
||||
def add_tip(self, at_start: bool = False, **kwargs):
|
||||
"""
|
||||
Adds a tip to the TipableVMobject instance, recognising
|
||||
that the endpoints might need to be switched if it's
|
||||
@@ -71,7 +78,7 @@ class TipableVMobject(VMobject):
|
||||
self.add(tip)
|
||||
return self
|
||||
|
||||
def create_tip(self, at_start=False, **kwargs):
|
||||
def create_tip(self, at_start: bool = False, **kwargs) -> ArrowTip:
|
||||
"""
|
||||
Stylises the tip, positions it spacially, and returns
|
||||
the newly instantiated tip to the caller.
|
||||
@@ -80,7 +87,7 @@ class TipableVMobject(VMobject):
|
||||
self.position_tip(tip, at_start)
|
||||
return tip
|
||||
|
||||
def get_unpositioned_tip(self, **kwargs):
|
||||
def get_unpositioned_tip(self, **kwargs) -> ArrowTip:
|
||||
"""
|
||||
Returns a tip that has been stylistically configured,
|
||||
but has not yet been given a position in space.
|
||||
@@ -90,7 +97,7 @@ class TipableVMobject(VMobject):
|
||||
config.update(kwargs)
|
||||
return ArrowTip(**config)
|
||||
|
||||
def position_tip(self, tip, at_start=False):
|
||||
def position_tip(self, tip: ArrowTip, at_start: bool = False) -> ArrowTip:
|
||||
# Last two control points, defining both
|
||||
# the end, and the tangency direction
|
||||
if at_start:
|
||||
@@ -103,7 +110,7 @@ class TipableVMobject(VMobject):
|
||||
tip.shift(anchor - tip.get_tip_point())
|
||||
return tip
|
||||
|
||||
def reset_endpoints_based_on_tip(self, tip, at_start):
|
||||
def reset_endpoints_based_on_tip(self, tip: ArrowTip, at_start: bool):
|
||||
if self.get_length() == 0:
|
||||
# Zero length, put_start_and_end_on wouldn't
|
||||
# work
|
||||
@@ -118,7 +125,7 @@ class TipableVMobject(VMobject):
|
||||
self.put_start_and_end_on(start, end)
|
||||
return self
|
||||
|
||||
def asign_tip_attr(self, tip, at_start):
|
||||
def asign_tip_attr(self, tip: ArrowTip, at_start: bool):
|
||||
if at_start:
|
||||
self.start_tip = tip
|
||||
else:
|
||||
@@ -126,14 +133,14 @@ class TipableVMobject(VMobject):
|
||||
return self
|
||||
|
||||
# Checking for tips
|
||||
def has_tip(self):
|
||||
def has_tip(self) -> bool:
|
||||
return hasattr(self, "tip") and self.tip in self
|
||||
|
||||
def has_start_tip(self):
|
||||
def has_start_tip(self) -> bool:
|
||||
return hasattr(self, "start_tip") and self.start_tip in self
|
||||
|
||||
# Getters
|
||||
def pop_tips(self):
|
||||
def pop_tips(self) -> VGroup:
|
||||
start, end = self.get_start_and_end()
|
||||
result = VGroup()
|
||||
if self.has_tip():
|
||||
@@ -145,7 +152,7 @@ class TipableVMobject(VMobject):
|
||||
self.put_start_and_end_on(start, end)
|
||||
return result
|
||||
|
||||
def get_tips(self):
|
||||
def get_tips(self) -> VGroup:
|
||||
"""
|
||||
Returns a VGroup (collection of VMobjects) containing
|
||||
the TipableVMObject instance's tips.
|
||||
@@ -157,7 +164,7 @@ class TipableVMobject(VMobject):
|
||||
result.add(self.start_tip)
|
||||
return result
|
||||
|
||||
def get_tip(self):
|
||||
def get_tip(self) -> ArrowTip:
|
||||
"""Returns the TipableVMobject instance's (first) tip,
|
||||
otherwise throws an exception."""
|
||||
tips = self.get_tips()
|
||||
@@ -166,28 +173,28 @@ class TipableVMobject(VMobject):
|
||||
else:
|
||||
return tips[0]
|
||||
|
||||
def get_default_tip_length(self):
|
||||
def get_default_tip_length(self) -> float:
|
||||
return self.tip_length
|
||||
|
||||
def get_first_handle(self):
|
||||
def get_first_handle(self) -> np.ndarray:
|
||||
return self.get_points()[1]
|
||||
|
||||
def get_last_handle(self):
|
||||
def get_last_handle(self) -> np.ndarray:
|
||||
return self.get_points()[-2]
|
||||
|
||||
def get_end(self):
|
||||
def get_end(self) -> np.ndarray:
|
||||
if self.has_tip():
|
||||
return self.tip.get_start()
|
||||
else:
|
||||
return VMobject.get_end(self)
|
||||
|
||||
def get_start(self):
|
||||
def get_start(self) -> np.ndarray:
|
||||
if self.has_start_tip():
|
||||
return self.start_tip.get_start()
|
||||
else:
|
||||
return VMobject.get_start(self)
|
||||
|
||||
def get_length(self):
|
||||
def get_length(self) -> float:
|
||||
start, end = self.get_start_and_end()
|
||||
return get_norm(start - end)
|
||||
|
||||
@@ -200,12 +207,17 @@ class Arc(TipableVMobject):
|
||||
"arc_center": ORIGIN,
|
||||
}
|
||||
|
||||
def __init__(self, start_angle=0, angle=TAU / 4, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start_angle: float = 0,
|
||||
angle: float = TAU / 4,
|
||||
**kwargs
|
||||
):
|
||||
self.start_angle = start_angle
|
||||
self.angle = angle
|
||||
VMobject.__init__(self, **kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.set_points(Arc.create_quadratic_bezier_points(
|
||||
angle=self.angle,
|
||||
start_angle=self.start_angle,
|
||||
@@ -215,7 +227,11 @@ class Arc(TipableVMobject):
|
||||
self.shift(self.arc_center)
|
||||
|
||||
@staticmethod
|
||||
def create_quadratic_bezier_points(angle, start_angle=0, n_components=8):
|
||||
def create_quadratic_bezier_points(
|
||||
angle: float,
|
||||
start_angle: float = 0,
|
||||
n_components: int = 8
|
||||
) -> np.ndarray:
|
||||
samples = np.array([
|
||||
[np.cos(a), np.sin(a), 0]
|
||||
for a in np.linspace(
|
||||
@@ -233,7 +249,7 @@ class Arc(TipableVMobject):
|
||||
points[2::3] = samples[2::2]
|
||||
return points
|
||||
|
||||
def get_arc_center(self):
|
||||
def get_arc_center(self) -> np.ndarray:
|
||||
"""
|
||||
Looks at the normals to the first two
|
||||
anchors, and finds their intersection points
|
||||
@@ -248,21 +264,27 @@ class Arc(TipableVMobject):
|
||||
n2 = rotate_vector(t2, TAU / 4)
|
||||
return find_intersection(a1, n1, a2, n2)
|
||||
|
||||
def get_start_angle(self):
|
||||
def get_start_angle(self) -> float:
|
||||
angle = angle_of_vector(self.get_start() - self.get_arc_center())
|
||||
return angle % TAU
|
||||
|
||||
def get_stop_angle(self):
|
||||
def get_stop_angle(self) -> float:
|
||||
angle = angle_of_vector(self.get_end() - self.get_arc_center())
|
||||
return angle % TAU
|
||||
|
||||
def move_arc_center_to(self, point):
|
||||
def move_arc_center_to(self, point: np.ndarray):
|
||||
self.shift(point - self.get_arc_center())
|
||||
return self
|
||||
|
||||
|
||||
class ArcBetweenPoints(Arc):
|
||||
def __init__(self, start, end, angle=TAU / 4, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start: np.ndarray,
|
||||
end: np.ndarray,
|
||||
angle: float = TAU / 4,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(angle=angle, **kwargs)
|
||||
if angle == 0:
|
||||
self.set_points_as_corners([LEFT, RIGHT])
|
||||
@@ -270,13 +292,23 @@ class ArcBetweenPoints(Arc):
|
||||
|
||||
|
||||
class CurvedArrow(ArcBetweenPoints):
|
||||
def __init__(self, start_point, end_point, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start_point: np.ndarray,
|
||||
end_point: np.ndarray,
|
||||
**kwargs
|
||||
):
|
||||
ArcBetweenPoints.__init__(self, start_point, end_point, **kwargs)
|
||||
self.add_tip()
|
||||
|
||||
|
||||
class CurvedDoubleArrow(CurvedArrow):
|
||||
def __init__(self, start_point, end_point, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start_point: np.ndarray,
|
||||
end_point: np.ndarray,
|
||||
**kwargs
|
||||
):
|
||||
CurvedArrow.__init__(self, start_point, end_point, **kwargs)
|
||||
self.add_tip(at_start=True)
|
||||
|
||||
@@ -291,7 +323,13 @@ class Circle(Arc):
|
||||
def __init__(self, **kwargs):
|
||||
Arc.__init__(self, 0, TAU, **kwargs)
|
||||
|
||||
def surround(self, mobject, dim_to_match=0, stretch=False, buff=MED_SMALL_BUFF):
|
||||
def surround(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
dim_to_match: int = 0,
|
||||
stretch: bool = False,
|
||||
buff: float = MED_SMALL_BUFF
|
||||
):
|
||||
# Ignores dim_to_match and stretch; result will always be a circle
|
||||
# TODO: Perhaps create an ellipse class to handle singele-dimension stretching
|
||||
|
||||
@@ -299,13 +337,13 @@ class Circle(Arc):
|
||||
self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0)
|
||||
self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1)
|
||||
|
||||
def point_at_angle(self, angle):
|
||||
def point_at_angle(self, angle: float) -> np.ndarray:
|
||||
start_angle = self.get_start_angle()
|
||||
return self.point_from_proportion(
|
||||
(angle - start_angle) / TAU
|
||||
)
|
||||
|
||||
def get_radius(self):
|
||||
def get_radius(self) -> float:
|
||||
return get_norm(self.get_start() - self.get_center())
|
||||
|
||||
|
||||
@@ -317,7 +355,7 @@ class Dot(Circle):
|
||||
"color": WHITE
|
||||
}
|
||||
|
||||
def __init__(self, point=ORIGIN, **kwargs):
|
||||
def __init__(self, point: np.ndarray = ORIGIN, **kwargs):
|
||||
super().__init__(arc_center=point, **kwargs)
|
||||
|
||||
|
||||
@@ -401,15 +439,26 @@ class Line(TipableVMobject):
|
||||
"path_arc": 0,
|
||||
}
|
||||
|
||||
def __init__(self, start=LEFT, end=RIGHT, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start: np.ndarray = LEFT,
|
||||
end: np.ndarray = RIGHT,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs)
|
||||
self.set_start_and_end_attrs(start, end)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.set_points_by_ends(self.start, self.end, self.buff, self.path_arc)
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
def set_points_by_ends(
|
||||
self,
|
||||
start: np.ndarray,
|
||||
end: np.ndarray,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0
|
||||
):
|
||||
vect = end - start
|
||||
dist = get_norm(vect)
|
||||
if np.isclose(dist, 0):
|
||||
@@ -438,11 +487,11 @@ class Line(TipableVMobject):
|
||||
self.set_points_as_corners([start, end])
|
||||
return self
|
||||
|
||||
def set_path_arc(self, new_value):
|
||||
def set_path_arc(self, new_value: float) -> None:
|
||||
self.path_arc = new_value
|
||||
self.init_points()
|
||||
|
||||
def set_start_and_end_attrs(self, start, end):
|
||||
def set_start_and_end_attrs(self, start: np.ndarray, end: np.ndarray):
|
||||
# If either start or end are Mobjects, this
|
||||
# gives their centers
|
||||
rough_start = self.pointify(start)
|
||||
@@ -454,7 +503,11 @@ class Line(TipableVMobject):
|
||||
self.start = self.pointify(start, vect)
|
||||
self.end = self.pointify(end, -vect)
|
||||
|
||||
def pointify(self, mob_or_point, direction=None):
|
||||
def pointify(
|
||||
self,
|
||||
mob_or_point: Mobject | np.ndarray,
|
||||
direction: np.ndarray | None = None
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Take an argument passed into Line (or subclass) and turn
|
||||
it into a 3d point.
|
||||
@@ -471,7 +524,7 @@ class Line(TipableVMobject):
|
||||
result[:len(point)] = point
|
||||
return result
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
def put_start_and_end_on(self, start: np.ndarray, end: np.ndarray):
|
||||
curr_start, curr_end = self.get_start_and_end()
|
||||
if np.isclose(curr_start, curr_end).all():
|
||||
# Handle null lines more gracefully
|
||||
@@ -479,16 +532,16 @@ class Line(TipableVMobject):
|
||||
return self
|
||||
return super().put_start_and_end_on(start, end)
|
||||
|
||||
def get_vector(self):
|
||||
def get_vector(self) -> np.ndarray:
|
||||
return self.get_end() - self.get_start()
|
||||
|
||||
def get_unit_vector(self):
|
||||
def get_unit_vector(self) -> np.ndarray:
|
||||
return normalize(self.get_vector())
|
||||
|
||||
def get_angle(self):
|
||||
def get_angle(self) -> float:
|
||||
return angle_of_vector(self.get_vector())
|
||||
|
||||
def get_projection(self, point):
|
||||
def get_projection(self, point: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Return projection of a point onto the line
|
||||
"""
|
||||
@@ -496,10 +549,10 @@ class Line(TipableVMobject):
|
||||
start = self.get_start()
|
||||
return start + np.dot(point - start, unit_vect) * unit_vect
|
||||
|
||||
def get_slope(self):
|
||||
def get_slope(self) -> float:
|
||||
return np.tan(self.get_angle())
|
||||
|
||||
def set_angle(self, angle, about_point=None):
|
||||
def set_angle(self, angle: float, about_point: np.ndarray | None = None):
|
||||
if about_point is None:
|
||||
about_point = self.get_start()
|
||||
self.rotate(
|
||||
@@ -508,7 +561,7 @@ class Line(TipableVMobject):
|
||||
)
|
||||
return self
|
||||
|
||||
def set_length(self, length, **kwargs):
|
||||
def set_length(self, length: float, **kwargs):
|
||||
self.scale(length / self.get_length(), **kwargs)
|
||||
return self
|
||||
|
||||
@@ -532,35 +585,35 @@ class DashedLine(Line):
|
||||
self.clear_points()
|
||||
self.add(*dashes)
|
||||
|
||||
def calculate_num_dashes(self, positive_space_ratio):
|
||||
def calculate_num_dashes(self, positive_space_ratio: float) -> int:
|
||||
try:
|
||||
full_length = self.dash_length / positive_space_ratio
|
||||
return int(np.ceil(self.get_length() / full_length))
|
||||
except ZeroDivisionError:
|
||||
return 1
|
||||
|
||||
def calculate_positive_space_ratio(self):
|
||||
def calculate_positive_space_ratio(self) -> float:
|
||||
return fdiv(
|
||||
self.dash_length,
|
||||
self.dash_length + self.dash_spacing,
|
||||
)
|
||||
|
||||
def get_start(self):
|
||||
def get_start(self) -> np.ndarray:
|
||||
if len(self.submobjects) > 0:
|
||||
return self.submobjects[0].get_start()
|
||||
else:
|
||||
return Line.get_start(self)
|
||||
|
||||
def get_end(self):
|
||||
def get_end(self) -> np.ndarray:
|
||||
if len(self.submobjects) > 0:
|
||||
return self.submobjects[-1].get_end()
|
||||
else:
|
||||
return Line.get_end(self)
|
||||
|
||||
def get_first_handle(self):
|
||||
def get_first_handle(self) -> np.ndarray:
|
||||
return self.submobjects[0].get_points()[1]
|
||||
|
||||
def get_last_handle(self):
|
||||
def get_last_handle(self) -> np.ndarray:
|
||||
return self.submobjects[-1].get_points()[-2]
|
||||
|
||||
|
||||
@@ -570,7 +623,7 @@ class TangentLine(Line):
|
||||
"d_alpha": 1e-6
|
||||
}
|
||||
|
||||
def __init__(self, vmob, alpha, **kwargs):
|
||||
def __init__(self, vmob: VMobject, alpha: float, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
da = self.d_alpha
|
||||
a1 = clip(alpha - da, 0, 1)
|
||||
@@ -594,7 +647,7 @@ class Elbow(VMobject):
|
||||
|
||||
class Arrow(Line):
|
||||
CONFIG = {
|
||||
"stroke_color": GREY_A,
|
||||
"color": GREY_A,
|
||||
"stroke_width": 5,
|
||||
"tip_width_ratio": 4,
|
||||
"width_to_tip_len": 0.0075,
|
||||
@@ -603,16 +656,22 @@ class Arrow(Line):
|
||||
"buff": 0.25,
|
||||
}
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
def set_points_by_ends(
|
||||
self,
|
||||
start: np.ndarray,
|
||||
end: np.ndarray,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0
|
||||
):
|
||||
super().set_points_by_ends(start, end, buff, path_arc)
|
||||
self.insert_tip_anchor()
|
||||
return self
|
||||
|
||||
def init_colors(self, override=True):
|
||||
super().init_colors(override)
|
||||
def init_colors(self) -> None:
|
||||
super().init_colors()
|
||||
self.create_tip_with_stroke_width()
|
||||
|
||||
def get_arc_length(self):
|
||||
def get_arc_length(self) -> float:
|
||||
# Push up into Line?
|
||||
arc_len = get_norm(self.get_vector())
|
||||
if self.path_arc > 0:
|
||||
@@ -655,14 +714,19 @@ class Arrow(Line):
|
||||
self.create_tip_with_stroke_width()
|
||||
return self
|
||||
|
||||
def set_stroke(self, color=None, width=None, *args, **kwargs):
|
||||
def set_stroke(
|
||||
self,
|
||||
color: ManimColor | None = None,
|
||||
width: float | None = None,
|
||||
*args, **kwargs
|
||||
):
|
||||
super().set_stroke(color=color, width=width, *args, **kwargs)
|
||||
if isinstance(width, numbers.Number):
|
||||
self.max_stroke_width = width
|
||||
self.reset_tip()
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor):
|
||||
def _handle_scale_side_effects(self, scale_factor: float):
|
||||
return self.reset_tip()
|
||||
|
||||
|
||||
@@ -679,7 +743,13 @@ class FillArrow(Line):
|
||||
"max_width_to_length_ratio": 0.1,
|
||||
}
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
def set_points_by_ends(
|
||||
self,
|
||||
start: np.ndarray,
|
||||
end: np.ndarray,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0
|
||||
) -> None:
|
||||
# Find the right tip length and thickness
|
||||
vect = end - start
|
||||
length = max(get_norm(vect), 1e-8)
|
||||
@@ -748,15 +818,15 @@ class FillArrow(Line):
|
||||
)
|
||||
return self
|
||||
|
||||
def get_start(self):
|
||||
def get_start(self) -> np.ndarray:
|
||||
nppc = self.n_points_per_curve
|
||||
points = self.get_points()
|
||||
return (points[0] + points[-nppc]) / 2
|
||||
|
||||
def get_end(self):
|
||||
def get_end(self) -> np.ndarray:
|
||||
return self.get_points()[self.tip_index]
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
def put_start_and_end_on(self, start: np.ndarray, end: np.ndarray):
|
||||
self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
|
||||
return self
|
||||
|
||||
@@ -765,12 +835,12 @@ class FillArrow(Line):
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
|
||||
def set_thickness(self, thickness):
|
||||
def set_thickness(self, thickness: float):
|
||||
self.thickness = thickness
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
|
||||
def set_path_arc(self, path_arc):
|
||||
def set_path_arc(self, path_arc: float):
|
||||
self.path_arc = path_arc
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
@@ -781,7 +851,7 @@ class Vector(Arrow):
|
||||
"buff": 0,
|
||||
}
|
||||
|
||||
def __init__(self, direction=RIGHT, **kwargs):
|
||||
def __init__(self, direction: np.ndarray = RIGHT, **kwargs):
|
||||
if len(direction) == 2:
|
||||
direction = np.hstack([direction, 0])
|
||||
super().__init__(ORIGIN, direction, **kwargs)
|
||||
@@ -794,24 +864,31 @@ class DoubleArrow(Arrow):
|
||||
|
||||
|
||||
class CubicBezier(VMobject):
|
||||
def __init__(self, a0, h0, h1, a1, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
a0: np.ndarray,
|
||||
h0: np.ndarray,
|
||||
h1: np.ndarray,
|
||||
a1: np.ndarray,
|
||||
**kwargs
|
||||
):
|
||||
VMobject.__init__(self, **kwargs)
|
||||
self.add_cubic_bezier_curve(a0, h0, h1, a1)
|
||||
|
||||
|
||||
class Polygon(VMobject):
|
||||
def __init__(self, *vertices, **kwargs):
|
||||
def __init__(self, *vertices: np.ndarray, **kwargs):
|
||||
self.vertices = vertices
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
verts = self.vertices
|
||||
self.set_points_as_corners([*verts, verts[0]])
|
||||
|
||||
def get_vertices(self):
|
||||
def get_vertices(self) -> list[np.ndarray]:
|
||||
return self.get_start_anchors()
|
||||
|
||||
def round_corners(self, radius=0.5):
|
||||
def round_corners(self, radius: float = 0.5):
|
||||
vertices = self.get_vertices()
|
||||
arcs = []
|
||||
for v1, v2, v3 in adjacent_n_tuples(vertices, 3):
|
||||
@@ -849,12 +926,17 @@ class Polygon(VMobject):
|
||||
return self
|
||||
|
||||
|
||||
class Polyline(Polygon):
|
||||
def init_points(self) -> None:
|
||||
self.set_points_as_corners(self.vertices)
|
||||
|
||||
|
||||
class RegularPolygon(Polygon):
|
||||
CONFIG = {
|
||||
"start_angle": None,
|
||||
}
|
||||
|
||||
def __init__(self, n=6, **kwargs):
|
||||
def __init__(self, n: int = 6, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
if self.start_angle is None:
|
||||
# 0 for odd, 90 for even
|
||||
@@ -893,19 +975,19 @@ class ArrowTip(Triangle):
|
||||
self.data["points"] = Dot().set_width(h).get_points()
|
||||
self.rotate(self.angle)
|
||||
|
||||
def get_base(self):
|
||||
def get_base(self) -> np.ndarray:
|
||||
return self.point_from_proportion(0.5)
|
||||
|
||||
def get_tip_point(self):
|
||||
def get_tip_point(self) -> np.ndarray:
|
||||
return self.get_points()[0]
|
||||
|
||||
def get_vector(self):
|
||||
def get_vector(self) -> np.ndarray:
|
||||
return self.get_tip_point() - self.get_base()
|
||||
|
||||
def get_angle(self):
|
||||
def get_angle(self) -> float:
|
||||
return angle_of_vector(self.get_vector())
|
||||
|
||||
def get_length(self):
|
||||
def get_length(self) -> float:
|
||||
return get_norm(self.get_vector())
|
||||
|
||||
|
||||
@@ -918,7 +1000,12 @@ class Rectangle(Polygon):
|
||||
"close_new_points": True,
|
||||
}
|
||||
|
||||
def __init__(self, width=None, height=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
width: float | None = None,
|
||||
height: float | None = None,
|
||||
**kwargs
|
||||
):
|
||||
Polygon.__init__(self, UR, UL, DL, DR, **kwargs)
|
||||
|
||||
if width is None:
|
||||
@@ -931,7 +1018,7 @@ class Rectangle(Polygon):
|
||||
|
||||
|
||||
class Square(Rectangle):
|
||||
def __init__(self, side_length=2.0, **kwargs):
|
||||
def __init__(self, side_length: float = 2.0, **kwargs):
|
||||
self.side_length = side_length
|
||||
super().__init__(side_length, side_length, **kwargs)
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
from pyglet.window import key as PygletWindowKeys
|
||||
|
||||
@@ -21,8 +25,7 @@ class MotionMobject(Mobject):
|
||||
"""
|
||||
You could hold and drag this object to any position
|
||||
"""
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
assert(isinstance(mobject, Mobject))
|
||||
self.mobject = mobject
|
||||
@@ -31,7 +34,7 @@ class MotionMobject(Mobject):
|
||||
self.mobject.add_updater(lambda mob: None)
|
||||
self.add(mobject)
|
||||
|
||||
def mob_on_mouse_drag(self, mob, event_data):
|
||||
def mob_on_mouse_drag(self, mob: Mobject, event_data: dict[str, np.ndarray]) -> bool:
|
||||
mob.move_to(event_data["point"])
|
||||
return False
|
||||
|
||||
@@ -43,7 +46,7 @@ class Button(Mobject):
|
||||
The on_click method takes mobject as argument like updater
|
||||
"""
|
||||
|
||||
def __init__(self, mobject, on_click, **kwargs):
|
||||
def __init__(self, mobject: Mobject, on_click: Callable[[Mobject]], **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
assert(isinstance(mobject, Mobject))
|
||||
self.on_click = on_click
|
||||
@@ -51,7 +54,7 @@ class Button(Mobject):
|
||||
self.mobject.add_mouse_press_listner(self.mob_on_mouse_press)
|
||||
self.add(self.mobject)
|
||||
|
||||
def mob_on_mouse_press(self, mob, event_data):
|
||||
def mob_on_mouse_press(self, mob: Mobject, event_data) -> bool:
|
||||
self.on_click(mob)
|
||||
return False
|
||||
|
||||
@@ -59,7 +62,7 @@ class Button(Mobject):
|
||||
# Controls
|
||||
|
||||
class ControlMobject(ValueTracker):
|
||||
def __init__(self, value, *mobjects, **kwargs):
|
||||
def __init__(self, value: float, *mobjects: Mobject, **kwargs):
|
||||
super().__init__(value=value, **kwargs)
|
||||
self.add(*mobjects)
|
||||
|
||||
@@ -67,7 +70,7 @@ class ControlMobject(ValueTracker):
|
||||
self.add_updater(lambda mob: None)
|
||||
self.fix_in_frame()
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: float):
|
||||
self.assert_value(value)
|
||||
self.set_value_anim(value)
|
||||
return ValueTracker.set_value(self, value)
|
||||
@@ -93,25 +96,25 @@ class EnableDisableButton(ControlMobject):
|
||||
"disable_color": RED
|
||||
}
|
||||
|
||||
def __init__(self, value=True, **kwargs):
|
||||
def __init__(self, value: bool = True, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.box = Rectangle(**self.rect_kwargs)
|
||||
super().__init__(value, self.box, **kwargs)
|
||||
self.add_mouse_press_listner(self.on_mouse_press)
|
||||
|
||||
def assert_value(self, value):
|
||||
def assert_value(self, value: bool) -> None:
|
||||
assert(isinstance(value, bool))
|
||||
|
||||
def set_value_anim(self, value):
|
||||
def set_value_anim(self, value: bool) -> None:
|
||||
if value:
|
||||
self.box.set_fill(self.enable_color)
|
||||
else:
|
||||
self.box.set_fill(self.disable_color)
|
||||
|
||||
def toggle_value(self):
|
||||
def toggle_value(self) -> None:
|
||||
super().set_value(not self.get_value())
|
||||
|
||||
def on_mouse_press(self, mob, event_data):
|
||||
def on_mouse_press(self, mob: Mobject, event_data) -> bool:
|
||||
mob.toggle_value()
|
||||
return False
|
||||
|
||||
@@ -136,32 +139,32 @@ class Checkbox(ControlMobject):
|
||||
"box_content_buff": SMALL_BUFF
|
||||
}
|
||||
|
||||
def __init__(self, value=True, **kwargs):
|
||||
def __init__(self, value: bool = True, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.box = Rectangle(**self.rect_kwargs)
|
||||
self.box_content = self.get_checkmark() if value else self.get_cross()
|
||||
super().__init__(value, self.box, self.box_content, **kwargs)
|
||||
self.add_mouse_press_listner(self.on_mouse_press)
|
||||
|
||||
def assert_value(self, value):
|
||||
def assert_value(self, value: bool) -> None:
|
||||
assert(isinstance(value, bool))
|
||||
|
||||
def toggle_value(self):
|
||||
def toggle_value(self) -> None:
|
||||
super().set_value(not self.get_value())
|
||||
|
||||
def set_value_anim(self, value):
|
||||
def set_value_anim(self, value: bool) -> None:
|
||||
if value:
|
||||
self.box_content.become(self.get_checkmark())
|
||||
else:
|
||||
self.box_content.become(self.get_cross())
|
||||
|
||||
def on_mouse_press(self, mob, event_data):
|
||||
def on_mouse_press(self, mob: Mobject, event_data) -> None:
|
||||
mob.toggle_value()
|
||||
return False
|
||||
|
||||
# Helper methods
|
||||
|
||||
def get_checkmark(self):
|
||||
def get_checkmark(self) -> VGroup:
|
||||
checkmark = VGroup(
|
||||
Line(UP / 2 + 2 * LEFT, DOWN + LEFT, **self.checkmark_kwargs),
|
||||
Line(DOWN + LEFT, UP + RIGHT, **self.checkmark_kwargs)
|
||||
@@ -173,7 +176,7 @@ class Checkbox(ControlMobject):
|
||||
checkmark.move_to(self.box)
|
||||
return checkmark
|
||||
|
||||
def get_cross(self):
|
||||
def get_cross(self) -> VGroup:
|
||||
cross = VGroup(
|
||||
Line(UP + LEFT, DOWN + RIGHT, **self.cross_kwargs),
|
||||
Line(UP + RIGHT, DOWN + LEFT, **self.cross_kwargs)
|
||||
@@ -206,7 +209,7 @@ class LinearNumberSlider(ControlMobject):
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
def __init__(self, value: float = 0, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.bar = RoundedRectangle(**self.rounded_rect_kwargs)
|
||||
self.slider = Circle(**self.circle_kwargs)
|
||||
@@ -219,22 +222,22 @@ class LinearNumberSlider(ControlMobject):
|
||||
|
||||
self.slider.add_mouse_drag_listner(self.slider_on_mouse_drag)
|
||||
|
||||
super().__init__(value, self.bar, self.slider, self.slider_axis, ** kwargs)
|
||||
super().__init__(value, self.bar, self.slider, self.slider_axis, **kwargs)
|
||||
|
||||
def assert_value(self, value):
|
||||
def assert_value(self, value: float) -> None:
|
||||
assert(self.min_value <= value <= self.max_value)
|
||||
|
||||
def set_value_anim(self, value):
|
||||
def set_value_anim(self, value: float) -> None:
|
||||
prop = (value - self.min_value) / (self.max_value - self.min_value)
|
||||
self.slider.move_to(self.slider_axis.point_from_proportion(prop))
|
||||
|
||||
def slider_on_mouse_drag(self, mob, event_data):
|
||||
def slider_on_mouse_drag(self, mob, event_data: dict[str, np.ndarray]) -> bool:
|
||||
self.set_value(self.get_value_from_point(event_data["point"]))
|
||||
return False
|
||||
|
||||
# Helper Methods
|
||||
|
||||
def get_value_from_point(self, point):
|
||||
def get_value_from_point(self, point: np.ndarray) -> float:
|
||||
start, end = self.slider_axis.get_start_and_end()
|
||||
point_on_line = get_closest_point_on_line(start, end, point)
|
||||
prop = get_norm(point_on_line - start) / get_norm(end - start)
|
||||
@@ -300,7 +303,7 @@ class ColorSliders(Group):
|
||||
|
||||
self.arrange(DOWN)
|
||||
|
||||
def get_background(self):
|
||||
def get_background(self) -> VGroup:
|
||||
single_square_len = self.background_grid_kwargs["single_square_len"]
|
||||
colors = self.background_grid_kwargs["colors"]
|
||||
width = self.rect_kwargs["width"]
|
||||
@@ -322,24 +325,24 @@ class ColorSliders(Group):
|
||||
|
||||
return grid
|
||||
|
||||
def set_value(self, r, g, b, a):
|
||||
def set_value(self, r: float, g: float, b: float, a: float):
|
||||
self.r_slider.set_value(r)
|
||||
self.g_slider.set_value(g)
|
||||
self.b_slider.set_value(b)
|
||||
self.a_slider.set_value(a)
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> np.ndarary:
|
||||
r = self.r_slider.get_value() / 255
|
||||
g = self.g_slider.get_value() / 255
|
||||
b = self.b_slider.get_value() / 255
|
||||
alpha = self.a_slider.get_value()
|
||||
return color_to_rgba(rgb_to_color((r, g, b)), alpha=alpha)
|
||||
|
||||
def get_picked_color(self):
|
||||
def get_picked_color(self) -> str:
|
||||
rgba = self.get_value()
|
||||
return rgb_to_hex(rgba[:3])
|
||||
|
||||
def get_picked_opacity(self):
|
||||
def get_picked_opacity(self) -> float:
|
||||
rgba = self.get_value()
|
||||
return rgba[3]
|
||||
|
||||
@@ -363,7 +366,7 @@ class Textbox(ControlMobject):
|
||||
"deactive_color": RED,
|
||||
}
|
||||
|
||||
def __init__(self, value="", **kwargs):
|
||||
def __init__(self, value: str = "", **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.isActive = self.isInitiallyActive
|
||||
self.box = Rectangle(**self.box_kwargs)
|
||||
@@ -374,10 +377,10 @@ class Textbox(ControlMobject):
|
||||
self.active_anim(self.isActive)
|
||||
self.add_key_press_listner(self.on_key_press)
|
||||
|
||||
def set_value_anim(self, value):
|
||||
def set_value_anim(self, value: str) -> None:
|
||||
self.update_text(value)
|
||||
|
||||
def update_text(self, value):
|
||||
def update_text(self, value: str) -> None:
|
||||
text = self.text
|
||||
self.remove(text)
|
||||
text.__init__(value, **self.text_kwargs)
|
||||
@@ -389,18 +392,18 @@ class Textbox(ControlMobject):
|
||||
text.fix_in_frame()
|
||||
self.add(text)
|
||||
|
||||
def active_anim(self, isActive):
|
||||
def active_anim(self, isActive: bool) -> None:
|
||||
if isActive:
|
||||
self.box.set_stroke(self.active_color)
|
||||
else:
|
||||
self.box.set_stroke(self.deactive_color)
|
||||
|
||||
def box_on_mouse_press(self, mob, event_data):
|
||||
def box_on_mouse_press(self, mob, event_data) -> bool:
|
||||
self.isActive = not self.isActive
|
||||
self.active_anim(self.isActive)
|
||||
return False
|
||||
|
||||
def on_key_press(self, mob, event_data):
|
||||
def on_key_press(self, mob: Mobject, event_data: dict[str, int]) -> bool | None:
|
||||
symbol = event_data["symbol"]
|
||||
modifiers = event_data["modifiers"]
|
||||
char = chr(symbol)
|
||||
@@ -443,7 +446,7 @@ class ControlPanel(Group):
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, *controls, **kwargs):
|
||||
def __init__(self, *controls: ControlMobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
|
||||
self.panel = Rectangle(**self.panel_kwargs)
|
||||
@@ -472,7 +475,7 @@ class ControlPanel(Group):
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
self.fix_in_frame()
|
||||
|
||||
def move_panel_and_controls_to_panel_opener(self):
|
||||
def move_panel_and_controls_to_panel_opener(self) -> None:
|
||||
self.panel.next_to(
|
||||
self.panel_opener_rect,
|
||||
direction=UP,
|
||||
@@ -488,11 +491,11 @@ class ControlPanel(Group):
|
||||
|
||||
self.controls.set_x(controls_old_x)
|
||||
|
||||
def add_controls(self, *new_controls):
|
||||
def add_controls(self, *new_controls: ControlMobject) -> None:
|
||||
self.controls.add(*new_controls)
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
|
||||
def remove_controls(self, *controls_to_remove):
|
||||
def remove_controls(self, *controls_to_remove: ControlMobject) -> None:
|
||||
self.controls.remove(*controls_to_remove)
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
|
||||
@@ -510,13 +513,13 @@ class ControlPanel(Group):
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
return self
|
||||
|
||||
def panel_opener_on_mouse_drag(self, mob, event_data):
|
||||
def panel_opener_on_mouse_drag(self, mob, event_data: dict[str, np.ndarray]) -> bool:
|
||||
point = event_data["point"]
|
||||
self.panel_opener.match_y(Dot(point))
|
||||
self.move_panel_and_controls_to_panel_opener()
|
||||
return False
|
||||
|
||||
def panel_on_mouse_scroll(self, mob, event_data):
|
||||
def panel_on_mouse_scroll(self, mob, event_data: dict[str, np.ndarray]) -> bool:
|
||||
offset = event_data["offset"]
|
||||
factor = 10 * offset[1]
|
||||
self.controls.set_y(self.controls.get_y() + factor)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
from typing import Union, Sequence
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
@@ -10,10 +15,18 @@ from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import colour
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
VECTOR_LABEL_SCALE_FACTOR = 0.8
|
||||
|
||||
|
||||
def matrix_to_tex_string(matrix):
|
||||
def matrix_to_tex_string(matrix: npt.ArrayLike) -> str:
|
||||
matrix = np.array(matrix).astype("str")
|
||||
if matrix.ndim == 1:
|
||||
matrix = matrix.reshape((matrix.size, 1))
|
||||
@@ -27,12 +40,16 @@ def matrix_to_tex_string(matrix):
|
||||
return prefix + " \\\\ ".join(rows) + suffix
|
||||
|
||||
|
||||
def matrix_to_mobject(matrix):
|
||||
def matrix_to_mobject(matrix: npt.ArrayLike) -> Tex:
|
||||
return Tex(matrix_to_tex_string(matrix))
|
||||
|
||||
|
||||
def vector_coordinate_label(vector_mob, integer_labels=True,
|
||||
n_dim=2, color=WHITE):
|
||||
def vector_coordinate_label(
|
||||
vector_mob: VMobject,
|
||||
integer_labels: bool = True,
|
||||
n_dim: int = 2,
|
||||
color: ManimColor = WHITE
|
||||
) -> Matrix:
|
||||
vect = np.array(vector_mob.get_end())
|
||||
if integer_labels:
|
||||
vect = np.round(vect).astype(int)
|
||||
@@ -66,7 +83,7 @@ class Matrix(VMobject):
|
||||
"element_alignment_corner": DOWN,
|
||||
}
|
||||
|
||||
def __init__(self, matrix, **kwargs):
|
||||
def __init__(self, matrix: npt.ArrayLike, **kwargs):
|
||||
"""
|
||||
Matrix can either include numbers, tex_strings,
|
||||
or mobjects
|
||||
@@ -87,7 +104,7 @@ class Matrix(VMobject):
|
||||
if self.include_background_rectangle:
|
||||
self.add_background_rectangle()
|
||||
|
||||
def matrix_to_mob_matrix(self, matrix):
|
||||
def matrix_to_mob_matrix(self, matrix: npt.ArrayLike) -> list[list[Mobject]]:
|
||||
return [
|
||||
[
|
||||
self.element_to_mobject(item, **self.element_to_mobject_config)
|
||||
@@ -96,7 +113,7 @@ class Matrix(VMobject):
|
||||
for row in matrix
|
||||
]
|
||||
|
||||
def organize_mob_matrix(self, matrix):
|
||||
def organize_mob_matrix(self, matrix: npt.ArrayLike):
|
||||
for i, row in enumerate(matrix):
|
||||
for j, elem in enumerate(row):
|
||||
mob = matrix[i][j]
|
||||
@@ -112,7 +129,7 @@ class Matrix(VMobject):
|
||||
"\\left[",
|
||||
"\\begin{array}{c}",
|
||||
*height * ["\\quad \\\\"],
|
||||
"\\end{array}"
|
||||
"\\end{array}",
|
||||
"\\right]",
|
||||
]))[0]
|
||||
bracket_pair.set_height(
|
||||
@@ -126,19 +143,19 @@ class Matrix(VMobject):
|
||||
self.brackets = VGroup(l_bracket, r_bracket)
|
||||
return self
|
||||
|
||||
def get_columns(self):
|
||||
def get_columns(self) -> VGroup:
|
||||
return VGroup(*[
|
||||
VGroup(*[row[i] for row in self.mob_matrix])
|
||||
for i in range(len(self.mob_matrix[0]))
|
||||
])
|
||||
|
||||
def get_rows(self):
|
||||
def get_rows(self) -> VGroup:
|
||||
return VGroup(*[
|
||||
VGroup(*row)
|
||||
for row in self.mob_matrix
|
||||
])
|
||||
|
||||
def set_column_colors(self, *colors):
|
||||
def set_column_colors(self, *colors: ManimColor):
|
||||
columns = self.get_columns()
|
||||
for color, column in zip(colors, columns):
|
||||
column.set_color(color)
|
||||
@@ -149,13 +166,13 @@ class Matrix(VMobject):
|
||||
mob.add_background_rectangle()
|
||||
return self
|
||||
|
||||
def get_mob_matrix(self):
|
||||
def get_mob_matrix(self) -> list[list[Mobject]]:
|
||||
return self.mob_matrix
|
||||
|
||||
def get_entries(self):
|
||||
def get_entries(self) -> VGroup:
|
||||
return self.elements
|
||||
|
||||
def get_brackets(self):
|
||||
def get_brackets(self) -> VGroup:
|
||||
return self.brackets
|
||||
|
||||
|
||||
@@ -179,7 +196,12 @@ class MobjectMatrix(Matrix):
|
||||
}
|
||||
|
||||
|
||||
def get_det_text(matrix, determinant=None, background_rect=False, initial_scale_factor=2):
|
||||
def get_det_text(
|
||||
matrix: Matrix,
|
||||
determinant: int | str | None = None,
|
||||
background_rect: bool = False,
|
||||
initial_scale_factor: int = 2
|
||||
) -> VGroup:
|
||||
parens = Tex("(", ")")
|
||||
parens.scale(initial_scale_factor)
|
||||
parens.stretch_to_fit_height(matrix.get_height())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
|
||||
def assert_is_mobject_method(method):
|
||||
assert(inspect.ismethod(method))
|
||||
@@ -41,27 +50,39 @@ def f_always(method, *arg_generators, **kwargs):
|
||||
return mobject
|
||||
|
||||
|
||||
def always_redraw(func, *args, **kwargs):
|
||||
def always_redraw(func: Callable[..., Mobject], *args, **kwargs) -> Mobject:
|
||||
mob = func(*args, **kwargs)
|
||||
mob.add_updater(lambda m: mob.become(func(*args, **kwargs)))
|
||||
return mob
|
||||
|
||||
|
||||
def always_shift(mobject, direction=RIGHT, rate=0.1):
|
||||
def always_shift(
|
||||
mobject: Mobject,
|
||||
direction: np.ndarray = RIGHT,
|
||||
rate: float = 0.1
|
||||
) -> Mobject:
|
||||
mobject.add_updater(
|
||||
lambda m, dt: m.shift(dt * rate * direction)
|
||||
)
|
||||
return mobject
|
||||
|
||||
|
||||
def always_rotate(mobject, rate=20 * DEGREES, **kwargs):
|
||||
def always_rotate(
|
||||
mobject: Mobject,
|
||||
rate: float = 20 * DEGREES,
|
||||
**kwargs
|
||||
) -> Mobject:
|
||||
mobject.add_updater(
|
||||
lambda m, dt: m.rotate(dt * rate, **kwargs)
|
||||
)
|
||||
return mobject
|
||||
|
||||
|
||||
def turn_animation_into_updater(animation, cycle=False, **kwargs):
|
||||
def turn_animation_into_updater(
|
||||
animation: Animation,
|
||||
cycle: bool = False,
|
||||
**kwargs
|
||||
) -> Mobject:
|
||||
"""
|
||||
Add an updater to the animation's mobject which applies
|
||||
the interpolation and update functions of the animation
|
||||
@@ -94,7 +115,7 @@ def turn_animation_into_updater(animation, cycle=False, **kwargs):
|
||||
return mobject
|
||||
|
||||
|
||||
def cycle_animation(animation, **kwargs):
|
||||
def cycle_animation(animation: Animation, **kwargs) -> Mobject:
|
||||
return turn_animation_into_updater(
|
||||
animation, cycle=True, **kwargs
|
||||
)
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Sequence
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.numbers import DecimalNumber
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.bezier import outer_interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.simple_functions import fdiv
|
||||
@@ -38,7 +43,7 @@ class NumberLine(Line):
|
||||
"numbers_to_exclude": None
|
||||
}
|
||||
|
||||
def __init__(self, x_range=None, **kwargs):
|
||||
def __init__(self, x_range: Sequence[float] | None = None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
if x_range is None:
|
||||
x_range = self.x_range
|
||||
@@ -48,9 +53,9 @@ class NumberLine(Line):
|
||||
x_min, x_max, x_step = x_range
|
||||
# A lot of old scenes pass in x_min or x_max explicitly,
|
||||
# so this is just here to keep those workin
|
||||
self.x_min = kwargs.get("x_min", x_min)
|
||||
self.x_max = kwargs.get("x_max", x_max)
|
||||
self.x_step = kwargs.get("x_step", x_step)
|
||||
self.x_min: float = kwargs.get("x_min", x_min)
|
||||
self.x_max: float = kwargs.get("x_max", x_max)
|
||||
self.x_step: float = kwargs.get("x_step", x_step)
|
||||
|
||||
super().__init__(self.x_min * RIGHT, self.x_max * RIGHT, **kwargs)
|
||||
if self.width:
|
||||
@@ -71,14 +76,14 @@ class NumberLine(Line):
|
||||
if self.include_numbers:
|
||||
self.add_numbers(excluding=self.numbers_to_exclude)
|
||||
|
||||
def get_tick_range(self):
|
||||
def get_tick_range(self) -> np.ndarray:
|
||||
if self.include_tip:
|
||||
x_max = self.x_max
|
||||
else:
|
||||
x_max = self.x_max + self.x_step
|
||||
return np.arange(self.x_min, x_max, self.x_step)
|
||||
|
||||
def add_ticks(self):
|
||||
def add_ticks(self) -> None:
|
||||
ticks = VGroup()
|
||||
for x in self.get_tick_range():
|
||||
size = self.tick_size
|
||||
@@ -88,7 +93,7 @@ class NumberLine(Line):
|
||||
self.add(ticks)
|
||||
self.ticks = ticks
|
||||
|
||||
def get_tick(self, x, size=None):
|
||||
def get_tick(self, x: float, size: float | None = None) -> Line:
|
||||
if size is None:
|
||||
size = self.tick_size
|
||||
result = Line(size * DOWN, size * UP)
|
||||
@@ -97,14 +102,14 @@ class NumberLine(Line):
|
||||
result.match_style(self)
|
||||
return result
|
||||
|
||||
def get_tick_marks(self):
|
||||
def get_tick_marks(self) -> VGroup:
|
||||
return self.ticks
|
||||
|
||||
def number_to_point(self, number):
|
||||
alpha = float(number - self.x_min) / (self.x_max - self.x_min)
|
||||
return interpolate(self.get_start(), self.get_end(), alpha)
|
||||
def number_to_point(self, number: float | np.ndarray) -> np.ndarray:
|
||||
alpha = (number - self.x_min) / (self.x_max - self.x_min)
|
||||
return outer_interpolate(self.get_start(), self.get_end(), alpha)
|
||||
|
||||
def point_to_number(self, point):
|
||||
def point_to_number(self, point: np.ndarray) -> float:
|
||||
points = self.get_points()
|
||||
start = points[0]
|
||||
end = points[-1]
|
||||
@@ -115,21 +120,24 @@ class NumberLine(Line):
|
||||
)
|
||||
return interpolate(self.x_min, self.x_max, proportion)
|
||||
|
||||
def n2p(self, number):
|
||||
def n2p(self, number: float) -> np.ndarray:
|
||||
"""Abbreviation for number_to_point"""
|
||||
return self.number_to_point(number)
|
||||
|
||||
def p2n(self, point):
|
||||
def p2n(self, point: np.ndarray) -> float:
|
||||
"""Abbreviation for point_to_number"""
|
||||
return self.point_to_number(point)
|
||||
|
||||
def get_unit_size(self):
|
||||
def get_unit_size(self) -> float:
|
||||
return self.get_length() / (self.x_max - self.x_min)
|
||||
|
||||
def get_number_mobject(self, x,
|
||||
direction=None,
|
||||
buff=None,
|
||||
**number_config):
|
||||
def get_number_mobject(
|
||||
self,
|
||||
x: float,
|
||||
direction: np.ndarray | None = None,
|
||||
buff: float | None = None,
|
||||
**number_config
|
||||
) -> DecimalNumber:
|
||||
number_config = merge_dicts_recursively(
|
||||
self.decimal_number_config, number_config
|
||||
)
|
||||
@@ -149,7 +157,13 @@ class NumberLine(Line):
|
||||
num_mob.shift(num_mob[0].get_width() * LEFT / 2)
|
||||
return num_mob
|
||||
|
||||
def add_numbers(self, x_values=None, excluding=None, font_size=24, **kwargs):
|
||||
def add_numbers(
|
||||
self,
|
||||
x_values: Iterable[float] | None = None,
|
||||
excluding: Iterable[float] | None = None,
|
||||
font_size: int = 24,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
if x_values is None:
|
||||
x_values = self.get_tick_range()
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar, Type
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.svg.tex_mobject import SingleStringTex
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
string_to_mob_map = {}
|
||||
T = TypeVar("T", bound=VMobject)
|
||||
|
||||
|
||||
class DecimalNumber(VMobject):
|
||||
@@ -20,23 +23,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):
|
||||
def __init__(self, number: float | complex = 0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_submobjects_from_number(number)
|
||||
self.init_colors()
|
||||
|
||||
def set_submobjects_from_number(self, number):
|
||||
def set_submobjects_from_number(self, number: float | complex) -> None:
|
||||
self.number = number
|
||||
self.set_submobjects([])
|
||||
|
||||
string_to_mob_ = lambda s: self.string_to_mob(s, **self.text_config)
|
||||
num_string = self.get_num_string(number)
|
||||
self.add(*map(self.string_to_mob, num_string))
|
||||
self.add(*map(string_to_mob_, num_string))
|
||||
|
||||
# Add non-numerical bits
|
||||
if self.show_ellipsis:
|
||||
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:
|
||||
@@ -62,7 +65,7 @@ class DecimalNumber(VMobject):
|
||||
if self.include_background_rectangle:
|
||||
self.add_background_rectangle()
|
||||
|
||||
def get_num_string(self, number):
|
||||
def get_num_string(self, number: float | complex) -> str:
|
||||
if isinstance(number, complex):
|
||||
formatter = self.get_complex_formatter()
|
||||
else:
|
||||
@@ -78,21 +81,19 @@ class DecimalNumber(VMobject):
|
||||
num_string = num_string.replace("-", "–")
|
||||
return num_string
|
||||
|
||||
def init_data(self):
|
||||
def init_data(self) -> None:
|
||||
super().init_data()
|
||||
self.data["font_size"] = np.array([self.font_size], dtype=float)
|
||||
|
||||
def get_font_size(self):
|
||||
def get_font_size(self) -> float:
|
||||
return self.data["font_size"][0]
|
||||
|
||||
def string_to_mob(self, string, mob_class=Text):
|
||||
if string not in string_to_mob_map:
|
||||
string_to_mob_map[string] = mob_class(string, font_size=1)
|
||||
mob = string_to_mob_map[string].copy()
|
||||
def string_to_mob(self, string: str, mob_class: Type[T] = Text, **kwargs) -> T:
|
||||
mob = mob_class(string, font_size=1, **kwargs)
|
||||
mob.scale(self.get_font_size())
|
||||
return mob
|
||||
|
||||
def get_formatter(self, **kwargs):
|
||||
def get_formatter(self, **kwargs) -> str:
|
||||
"""
|
||||
Configuration is based first off instance attributes,
|
||||
but overwritten by any kew word argument. Relevant
|
||||
@@ -121,14 +122,14 @@ class DecimalNumber(VMobject):
|
||||
"}",
|
||||
])
|
||||
|
||||
def get_complex_formatter(self, **kwargs):
|
||||
def get_complex_formatter(self, **kwargs) -> str:
|
||||
return "".join([
|
||||
self.get_formatter(field_name="0.real"),
|
||||
self.get_formatter(field_name="0.imag", include_sign=True),
|
||||
"i"
|
||||
])
|
||||
|
||||
def set_value(self, number):
|
||||
def set_value(self, number: float | complex):
|
||||
move_to_point = self.get_edge_center(self.edge_to_fix)
|
||||
old_submobjects = list(self.submobjects)
|
||||
self.set_submobjects_from_number(number)
|
||||
@@ -137,13 +138,13 @@ class DecimalNumber(VMobject):
|
||||
sm1.match_style(sm2)
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor):
|
||||
def _handle_scale_side_effects(self, scale_factor: float) -> None:
|
||||
self.data["font_size"] *= scale_factor
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> float | complex:
|
||||
return self.number
|
||||
|
||||
def increment_value(self, delta_t=1):
|
||||
def increment_value(self, delta_t: float | complex = 1) -> None:
|
||||
self.set_value(self.get_value() + delta_t)
|
||||
|
||||
|
||||
@@ -152,5 +153,5 @@ class Integer(DecimalNumber):
|
||||
"num_decimal_places": 0,
|
||||
}
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> int:
|
||||
return int(np.round(super().get_value()))
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Union, Sequence
|
||||
import colour
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
@@ -9,6 +14,8 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.color import color_gradient
|
||||
from manimlib.utils.iterables import listify
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
EPSILON = 0.0001
|
||||
|
||||
|
||||
@@ -24,7 +31,11 @@ class SampleSpace(Rectangle):
|
||||
"default_label_scale_val": 1,
|
||||
}
|
||||
|
||||
def add_title(self, title="Sample space", buff=MED_SMALL_BUFF):
|
||||
def add_title(
|
||||
self,
|
||||
title: str = "Sample space",
|
||||
buff: float = MED_SMALL_BUFF
|
||||
) -> None:
|
||||
# TODO, should this really exist in SampleSpaceScene
|
||||
title_mob = TexText(title)
|
||||
if title_mob.get_width() > self.get_width():
|
||||
@@ -33,17 +44,23 @@ class SampleSpace(Rectangle):
|
||||
self.title = title_mob
|
||||
self.add(title_mob)
|
||||
|
||||
def add_label(self, label):
|
||||
def add_label(self, label: str) -> None:
|
||||
self.label = label
|
||||
|
||||
def complete_p_list(self, p_list):
|
||||
def complete_p_list(self, p_list: list[float]) -> list[float]:
|
||||
new_p_list = listify(p_list)
|
||||
remainder = 1.0 - sum(new_p_list)
|
||||
if abs(remainder) > EPSILON:
|
||||
new_p_list.append(remainder)
|
||||
return new_p_list
|
||||
|
||||
def get_division_along_dimension(self, p_list, dim, colors, vect):
|
||||
def get_division_along_dimension(
|
||||
self,
|
||||
p_list: list[float],
|
||||
dim: int,
|
||||
colors: Iterable[ManimColor],
|
||||
vect: np.ndarray
|
||||
) -> VGroup:
|
||||
p_list = self.complete_p_list(p_list)
|
||||
colors = color_gradient(colors, len(p_list))
|
||||
|
||||
@@ -60,38 +77,41 @@ class SampleSpace(Rectangle):
|
||||
return parts
|
||||
|
||||
def get_horizontal_division(
|
||||
self, p_list,
|
||||
colors=[GREEN_E, BLUE_E],
|
||||
vect=DOWN
|
||||
):
|
||||
self,
|
||||
p_list: list[float],
|
||||
colors: Iterable[ManimColor] = [GREEN_E, BLUE_E],
|
||||
vect: np.ndarray = DOWN
|
||||
) -> VGroup:
|
||||
return self.get_division_along_dimension(p_list, 1, colors, vect)
|
||||
|
||||
def get_vertical_division(
|
||||
self, p_list,
|
||||
colors=[MAROON_B, YELLOW],
|
||||
vect=RIGHT
|
||||
):
|
||||
self,
|
||||
p_list: list[float],
|
||||
colors: Iterable[ManimColor] = [MAROON_B, YELLOW],
|
||||
vect: np.ndarray = RIGHT
|
||||
) -> VGroup:
|
||||
return self.get_division_along_dimension(p_list, 0, colors, vect)
|
||||
|
||||
def divide_horizontally(self, *args, **kwargs):
|
||||
def divide_horizontally(self, *args, **kwargs) -> None:
|
||||
self.horizontal_parts = self.get_horizontal_division(*args, **kwargs)
|
||||
self.add(self.horizontal_parts)
|
||||
|
||||
def divide_vertically(self, *args, **kwargs):
|
||||
def divide_vertically(self, *args, **kwargs) -> None:
|
||||
self.vertical_parts = self.get_vertical_division(*args, **kwargs)
|
||||
self.add(self.vertical_parts)
|
||||
|
||||
def get_subdivision_braces_and_labels(
|
||||
self, parts, labels, direction,
|
||||
buff=SMALL_BUFF,
|
||||
min_num_quads=1
|
||||
):
|
||||
self,
|
||||
parts: VGroup,
|
||||
labels: str,
|
||||
direction: np.ndarray,
|
||||
buff: float = SMALL_BUFF,
|
||||
) -> VGroup:
|
||||
label_mobs = VGroup()
|
||||
braces = VGroup()
|
||||
for label, part in zip(labels, parts):
|
||||
brace = Brace(
|
||||
part, direction,
|
||||
min_num_quads=min_num_quads,
|
||||
buff=buff
|
||||
)
|
||||
if isinstance(label, Mobject):
|
||||
@@ -112,22 +132,35 @@ class SampleSpace(Rectangle):
|
||||
}
|
||||
return VGroup(parts.braces, parts.labels)
|
||||
|
||||
def get_side_braces_and_labels(self, labels, direction=LEFT, **kwargs):
|
||||
def get_side_braces_and_labels(
|
||||
self,
|
||||
labels: str,
|
||||
direction: np.ndarray = LEFT,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
assert(hasattr(self, "horizontal_parts"))
|
||||
parts = self.horizontal_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, direction, **kwargs)
|
||||
|
||||
def get_top_braces_and_labels(self, labels, **kwargs):
|
||||
def get_top_braces_and_labels(
|
||||
self,
|
||||
labels: str,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
assert(hasattr(self, "vertical_parts"))
|
||||
parts = self.vertical_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, UP, **kwargs)
|
||||
|
||||
def get_bottom_braces_and_labels(self, labels, **kwargs):
|
||||
def get_bottom_braces_and_labels(
|
||||
self,
|
||||
labels: str,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
assert(hasattr(self, "vertical_parts"))
|
||||
parts = self.vertical_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, DOWN, **kwargs)
|
||||
|
||||
def add_braces_and_labels(self):
|
||||
def add_braces_and_labels(self) -> None:
|
||||
for attr in "horizontal_parts", "vertical_parts":
|
||||
if not hasattr(self, attr):
|
||||
continue
|
||||
@@ -136,7 +169,7 @@ class SampleSpace(Rectangle):
|
||||
if hasattr(parts, subattr):
|
||||
self.add(getattr(parts, subattr))
|
||||
|
||||
def __getitem__(self, index):
|
||||
def __getitem__(self, index: int | slice) -> VGroup:
|
||||
if hasattr(self, "horizontal_parts"):
|
||||
return self.horizontal_parts[index]
|
||||
elif hasattr(self, "vertical_parts"):
|
||||
@@ -162,7 +195,7 @@ class BarChart(VGroup):
|
||||
"bar_label_scale_val": 0.75,
|
||||
}
|
||||
|
||||
def __init__(self, values, **kwargs):
|
||||
def __init__(self, values: Iterable[float], **kwargs):
|
||||
VGroup.__init__(self, **kwargs)
|
||||
if self.max_value is None:
|
||||
self.max_value = max(values)
|
||||
@@ -172,7 +205,7 @@ class BarChart(VGroup):
|
||||
self.add_bars(values)
|
||||
self.center()
|
||||
|
||||
def add_axes(self):
|
||||
def add_axes(self) -> None:
|
||||
x_axis = Line(self.tick_width * LEFT / 2, self.width * RIGHT)
|
||||
y_axis = Line(MED_LARGE_BUFF * DOWN, self.height * UP)
|
||||
y_ticks = VGroup()
|
||||
@@ -209,7 +242,7 @@ class BarChart(VGroup):
|
||||
self.y_axis_labels = labels
|
||||
self.add(labels)
|
||||
|
||||
def add_bars(self, values):
|
||||
def add_bars(self, values: Iterable[float]) -> None:
|
||||
buff = float(self.width) / (2 * len(values))
|
||||
bars = VGroup()
|
||||
for i, value in enumerate(values):
|
||||
@@ -234,7 +267,7 @@ class BarChart(VGroup):
|
||||
self.bars = bars
|
||||
self.bar_labels = bar_labels
|
||||
|
||||
def change_bar_values(self, values):
|
||||
def change_bar_values(self, values: Iterable[float]) -> None:
|
||||
for bar, value in zip(self.bars, values):
|
||||
bar_bottom = bar.get_bottom()
|
||||
bar.stretch_to_fit_height(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
@@ -7,6 +9,13 @@ from manimlib.utils.color import Color
|
||||
from manimlib.utils.customization import get_customization
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Union, Sequence
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
ManimColor = Union[str, Color, Sequence[float]]
|
||||
|
||||
|
||||
class SurroundingRectangle(Rectangle):
|
||||
CONFIG = {
|
||||
@@ -14,7 +23,7 @@ class SurroundingRectangle(Rectangle):
|
||||
"buff": SMALL_BUFF,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
kwargs["width"] = mobject.get_width() + 2 * self.buff
|
||||
kwargs["height"] = mobject.get_height() + 2 * self.buff
|
||||
@@ -30,23 +39,24 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||
"buff": 0
|
||||
}
|
||||
|
||||
def __init__(self, mobject, color=None, **kwargs):
|
||||
def __init__(self, mobject: Mobject, color: ManimColor = None, **kwargs):
|
||||
if color is None:
|
||||
color = get_customization()['style']['background_color']
|
||||
SurroundingRectangle.__init__(self, mobject, color=color, **kwargs)
|
||||
self.original_fill_opacity = self.fill_opacity
|
||||
|
||||
def pointwise_become_partial(self, mobject, a, b):
|
||||
def pointwise_become_partial(self, mobject: Mobject, a: float, b: float):
|
||||
self.set_fill(opacity=b * self.original_fill_opacity)
|
||||
return self
|
||||
|
||||
def set_style_data(self,
|
||||
stroke_color=None,
|
||||
stroke_width=None,
|
||||
fill_color=None,
|
||||
fill_opacity=None,
|
||||
family=True
|
||||
):
|
||||
def set_style_data(
|
||||
self,
|
||||
stroke_color: ManimColor | None = None,
|
||||
stroke_width: float | None = None,
|
||||
fill_color: ManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
family: bool = True
|
||||
):
|
||||
# Unchangeable style, except for fill_opacity
|
||||
VMobject.set_style_data(
|
||||
self,
|
||||
@@ -57,7 +67,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
||||
)
|
||||
return self
|
||||
|
||||
def get_fill_color(self):
|
||||
def get_fill_color(self) -> Color:
|
||||
return Color(self.color)
|
||||
|
||||
|
||||
@@ -67,7 +77,7 @@ class Cross(VGroup):
|
||||
"stroke_width": [0, 6, 0],
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(
|
||||
Line(UL, DR),
|
||||
Line(UR, DL),
|
||||
@@ -82,7 +92,7 @@ class Underline(Line):
|
||||
"buff": SMALL_BUFF,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
def __init__(self, mobject: Mobject, **kwargs):
|
||||
super().__init__(LEFT, RIGHT, **kwargs)
|
||||
self.match_width(mobject)
|
||||
self.next_to(mobject, DOWN, buff=self.buff)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import copy
|
||||
from typing import Iterable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.constants import *
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.animation.growing import GrowFromCenter
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import SingleStringTex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
@@ -14,6 +18,11 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
class Brace(SingleStringTex):
|
||||
CONFIG = {
|
||||
@@ -21,7 +30,12 @@ class Brace(SingleStringTex):
|
||||
"tex_string": r"\underbrace{\qquad}"
|
||||
}
|
||||
|
||||
def __init__(self, mobject, direction=DOWN, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
direction: np.ndarray = DOWN,
|
||||
**kwargs
|
||||
):
|
||||
digest_config(self, kwargs, locals())
|
||||
angle = -math.atan2(*direction[:2]) + PI
|
||||
mobject.rotate(-angle, about_point=ORIGIN)
|
||||
@@ -36,7 +50,7 @@ class Brace(SingleStringTex):
|
||||
for mob in mobject, self:
|
||||
mob.rotate(angle, about_point=ORIGIN)
|
||||
|
||||
def set_initial_width(self, width):
|
||||
def set_initial_width(self, width: float):
|
||||
width_diff = width - self.get_width()
|
||||
if width_diff > 0:
|
||||
for tip, rect, vect in [(self[0], self[1], RIGHT), (self[5], self[4], LEFT)]:
|
||||
@@ -49,7 +63,12 @@ class Brace(SingleStringTex):
|
||||
self.set_width(width, stretch=True)
|
||||
return self
|
||||
|
||||
def put_at_tip(self, mob, use_next_to=True, **kwargs):
|
||||
def put_at_tip(
|
||||
self,
|
||||
mob: Mobject,
|
||||
use_next_to: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
if use_next_to:
|
||||
mob.next_to(
|
||||
self.get_tip(),
|
||||
@@ -63,24 +82,24 @@ class Brace(SingleStringTex):
|
||||
mob.shift(self.get_direction() * shift_distance)
|
||||
return self
|
||||
|
||||
def get_text(self, text, **kwargs):
|
||||
def get_text(self, text: str, **kwargs) -> Text:
|
||||
buff = kwargs.pop("buff", SMALL_BUFF)
|
||||
text_mob = Text(text, **kwargs)
|
||||
self.put_at_tip(text_mob, buff=buff)
|
||||
return text_mob
|
||||
|
||||
def get_tex(self, *tex, **kwargs):
|
||||
def get_tex(self, *tex: str, **kwargs) -> Tex:
|
||||
tex_mob = Tex(*tex)
|
||||
self.put_at_tip(tex_mob, **kwargs)
|
||||
return tex_mob
|
||||
|
||||
def get_tip(self):
|
||||
def get_tip(self) -> np.ndarray:
|
||||
# Very specific to the LaTeX representation
|
||||
# of a brace, but it's the only way I can think
|
||||
# of to get the tip regardless of orientation.
|
||||
return self.get_all_points()[self.tip_point_index]
|
||||
|
||||
def get_direction(self):
|
||||
def get_direction(self) -> np.ndarray:
|
||||
vect = self.get_tip() - self.get_center()
|
||||
return vect / get_norm(vect)
|
||||
|
||||
@@ -92,14 +111,20 @@ class BraceLabel(VMobject):
|
||||
"label_buff": DEFAULT_MOBJECT_TO_MOBJECT_BUFFER
|
||||
}
|
||||
|
||||
def __init__(self, obj, text, brace_direction=DOWN, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
obj: list[VMobject] | Mobject,
|
||||
text: Iterable[str] | str,
|
||||
brace_direction: np.ndarray = DOWN,
|
||||
**kwargs
|
||||
) -> None:
|
||||
VMobject.__init__(self, **kwargs)
|
||||
self.brace_direction = brace_direction
|
||||
if isinstance(obj, list):
|
||||
obj = VMobject(*obj)
|
||||
self.brace = Brace(obj, brace_direction, **kwargs)
|
||||
|
||||
if isinstance(text, tuple) or isinstance(text, list):
|
||||
if isinstance(text, Iterable):
|
||||
self.label = self.label_constructor(*text, **kwargs)
|
||||
else:
|
||||
self.label = self.label_constructor(str(text))
|
||||
@@ -109,10 +134,14 @@ class BraceLabel(VMobject):
|
||||
self.brace.put_at_tip(self.label, buff=self.label_buff)
|
||||
self.set_submobjects([self.brace, self.label])
|
||||
|
||||
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):
|
||||
def creation_anim(
|
||||
self,
|
||||
label_anim: Animation = FadeIn,
|
||||
brace_anim: Animation=GrowFromCenter
|
||||
) -> AnimationGroup:
|
||||
return AnimationGroup(brace_anim(self.brace), label_anim(self.label))
|
||||
|
||||
def shift_brace(self, obj, **kwargs):
|
||||
def shift_brace(self, obj: list[VMobject] | Mobject, **kwargs):
|
||||
if isinstance(obj, list):
|
||||
obj = VMobject(*obj)
|
||||
self.brace = Brace(obj, self.brace_direction, **kwargs)
|
||||
@@ -120,7 +149,7 @@ class BraceLabel(VMobject):
|
||||
self.submobjects[0] = self.brace
|
||||
return self
|
||||
|
||||
def change_label(self, *text, **kwargs):
|
||||
def change_label(self, *text: str, **kwargs):
|
||||
self.label = self.label_constructor(*text, **kwargs)
|
||||
if self.label_scale != 1:
|
||||
self.label.scale(self.label_scale)
|
||||
@@ -129,7 +158,7 @@ class BraceLabel(VMobject):
|
||||
self.submobjects[1] = self.label
|
||||
return self
|
||||
|
||||
def change_brace_label(self, obj, *text):
|
||||
def change_brace_label(self, obj: list[VMobject] | Mobject, *text: str):
|
||||
self.shift_brace(obj)
|
||||
self.change_label(*text)
|
||||
return self
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from manimlib.animation.animation import Animation
|
||||
from manimlib.animation.rotation import Rotating
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.boolean_ops import Difference
|
||||
from manimlib.mobject.geometry import Arc
|
||||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Line
|
||||
@@ -12,6 +13,7 @@ from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.three_dimensions import Cube
|
||||
from manimlib.mobject.three_dimensions import Prismify
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
@@ -19,6 +21,7 @@ from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
from manimlib.utils.space_ops import complex_to_R3
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
from manimlib.utils.space_ops import midpoint
|
||||
|
||||
|
||||
class Checkmark(TexText):
|
||||
@@ -433,3 +436,84 @@ class VectorizedEarth(SVGMobject):
|
||||
)
|
||||
circle.replace(self)
|
||||
self.add_to_back(circle)
|
||||
|
||||
|
||||
class Piano(VGroup):
|
||||
n_white_keys = 52
|
||||
black_pattern = [0, 2, 3, 5, 6]
|
||||
white_keys_per_octave = 7
|
||||
white_key_dims = (0.15, 1.0)
|
||||
black_key_dims = (0.1, 0.66)
|
||||
key_buff = 0.02
|
||||
white_key_color = WHITE
|
||||
black_key_color = GREY_E
|
||||
total_width = 13
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.add_white_keys()
|
||||
self.add_black_keys()
|
||||
self.sort_keys()
|
||||
self[:-1].reverse_points()
|
||||
self.set_width(self.total_width)
|
||||
|
||||
def add_white_keys(self):
|
||||
key = Rectangle(*self.white_key_dims)
|
||||
key.set_fill(self.white_key_color, 1)
|
||||
key.set_stroke(width=0)
|
||||
self.white_keys = key.get_grid(1, self.n_white_keys, buff=self.key_buff)
|
||||
self.add(*self.white_keys)
|
||||
|
||||
def add_black_keys(self):
|
||||
key = Rectangle(*self.black_key_dims)
|
||||
key.set_fill(self.black_key_color, 1)
|
||||
key.set_stroke(width=0)
|
||||
|
||||
self.black_keys = VGroup()
|
||||
for i in range(len(self.white_keys) - 1):
|
||||
if i % self.white_keys_per_octave not in self.black_pattern:
|
||||
continue
|
||||
wk1 = self.white_keys[i]
|
||||
wk2 = self.white_keys[i + 1]
|
||||
bk = key.copy()
|
||||
bk.move_to(midpoint(wk1.get_top(), wk2.get_top()), UP)
|
||||
big_bk = bk.copy()
|
||||
big_bk.stretch((bk.get_width() + self.key_buff) / bk.get_width(), 0)
|
||||
big_bk.stretch((bk.get_height() + self.key_buff) / bk.get_height(), 1)
|
||||
big_bk.move_to(bk, UP)
|
||||
for wk in wk1, wk2:
|
||||
wk.become(Difference(wk, big_bk).match_style(wk))
|
||||
self.black_keys.add(bk)
|
||||
self.add(*self.black_keys)
|
||||
|
||||
def sort_keys(self):
|
||||
self.sort(lambda p: p[0])
|
||||
|
||||
|
||||
class Piano3D(VGroup):
|
||||
CONFIG = {
|
||||
"depth_test": True,
|
||||
"reflectiveness": 1.0,
|
||||
"stroke_width": 0.25,
|
||||
"stroke_color": BLACK,
|
||||
"key_depth": 0.1,
|
||||
"black_key_shift": 0.05,
|
||||
}
|
||||
piano_2d_config = {
|
||||
"white_key_color": GREY_A,
|
||||
"key_buff": 0.001
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
piano_2d = Piano(**self.piano_2d_config)
|
||||
super().__init__(*(
|
||||
Prismify(key, self.key_depth)
|
||||
for key in piano_2d
|
||||
))
|
||||
self.set_stroke(self.stroke_color, self.stroke_width)
|
||||
self.apply_depth_test()
|
||||
# Elevate black keys
|
||||
for i, key in enumerate(self):
|
||||
if piano_2d[i] in piano_2d.black_keys:
|
||||
key.shift(self.black_key_shift * OUT)
|
||||
|
||||
546
manimlib/mobject/svg/labelled_string.py
Normal file
546
manimlib/mobject/svg/labelled_string.py
Normal file
@@ -0,0 +1,546 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import colour
|
||||
import itertools as it
|
||||
from typing import Iterable, Union, Sequence
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from manimlib.constants import BLACK, WHITE
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.color import color_to_int_rgb
|
||||
from manimlib.utils.color import color_to_rgb
|
||||
from manimlib.utils.color import rgb_to_hex
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
Span = tuple[int, int]
|
||||
|
||||
|
||||
class _StringSVG(SVGMobject):
|
||||
CONFIG = {
|
||||
"height": None,
|
||||
"stroke_width": 0,
|
||||
"stroke_color": WHITE,
|
||||
"path_string_config": {
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class LabelledString(_StringSVG, ABC):
|
||||
"""
|
||||
An abstract base class for `MTex` and `MarkupText`
|
||||
"""
|
||||
CONFIG = {
|
||||
"base_color": WHITE,
|
||||
"use_plain_file": False,
|
||||
"isolate": [],
|
||||
}
|
||||
|
||||
def __init__(self, string: str, **kwargs):
|
||||
self.string = string
|
||||
digest_config(self, kwargs)
|
||||
|
||||
# Convert `base_color` to hex code.
|
||||
self.base_color = rgb_to_hex(color_to_rgb(
|
||||
self.base_color \
|
||||
or self.svg_default.get("color", None) \
|
||||
or self.svg_default.get("fill_color", None) \
|
||||
or WHITE
|
||||
))
|
||||
self.svg_default["fill_color"] = BLACK
|
||||
|
||||
self.pre_parse()
|
||||
self.parse()
|
||||
super().__init__()
|
||||
self.post_parse()
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
return self.get_file_path_(use_plain_file=False)
|
||||
|
||||
def get_file_path_(self, use_plain_file: bool) -> str:
|
||||
content = self.get_content(use_plain_file)
|
||||
return self.get_file_path_by_content(content)
|
||||
|
||||
@abstractmethod
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
return ""
|
||||
|
||||
def generate_mobject(self) -> None:
|
||||
super().generate_mobject()
|
||||
|
||||
submob_labels = [
|
||||
self.color_to_label(submob.get_fill_color())
|
||||
for submob in self.submobjects
|
||||
]
|
||||
if self.use_plain_file or self.has_predefined_local_colors:
|
||||
file_path = self.get_file_path_(use_plain_file=True)
|
||||
plain_svg = _StringSVG(
|
||||
file_path,
|
||||
svg_default=self.svg_default,
|
||||
path_string_config=self.path_string_config
|
||||
)
|
||||
self.set_submobjects(plain_svg.submobjects)
|
||||
else:
|
||||
self.set_fill(self.base_color)
|
||||
for submob, label in zip(self.submobjects, submob_labels):
|
||||
submob.label = label
|
||||
|
||||
def pre_parse(self) -> None:
|
||||
self.string_len = len(self.string)
|
||||
self.full_span = (0, self.string_len)
|
||||
|
||||
def parse(self) -> None:
|
||||
self.command_repl_items = self.get_command_repl_items()
|
||||
self.command_spans = self.get_command_spans()
|
||||
self.extra_entity_spans = self.get_extra_entity_spans()
|
||||
self.entity_spans = self.get_entity_spans()
|
||||
self.extra_ignored_spans = self.get_extra_ignored_spans()
|
||||
self.skipped_spans = self.get_skipped_spans()
|
||||
self.internal_specified_spans = self.get_internal_specified_spans()
|
||||
self.external_specified_spans = self.get_external_specified_spans()
|
||||
self.specified_spans = self.get_specified_spans()
|
||||
self.label_span_list = self.get_label_span_list()
|
||||
self.check_overlapping()
|
||||
|
||||
def post_parse(self) -> None:
|
||||
self.labelled_submobject_items = [
|
||||
(submob.label, submob)
|
||||
for submob in self.submobjects
|
||||
]
|
||||
self.labelled_submobjects = self.get_labelled_submobjects()
|
||||
self.specified_substrs = self.get_specified_substrs()
|
||||
self.group_items = self.get_group_items()
|
||||
self.group_substrs = self.get_group_substrs()
|
||||
self.submob_groups = self.get_submob_groups()
|
||||
|
||||
def copy(self):
|
||||
return self.deepcopy()
|
||||
|
||||
# Toolkits
|
||||
|
||||
def get_substr(self, span: Span) -> str:
|
||||
return self.string[slice(*span)]
|
||||
|
||||
def finditer(
|
||||
self, pattern: str, flags: int = 0, **kwargs
|
||||
) -> Iterable[re.Match]:
|
||||
return re.compile(pattern, flags).finditer(self.string, **kwargs)
|
||||
|
||||
def search(
|
||||
self, pattern: str, flags: int = 0, **kwargs
|
||||
) -> re.Match | None:
|
||||
return re.compile(pattern, flags).search(self.string, **kwargs)
|
||||
|
||||
def match(
|
||||
self, pattern: str, flags: int = 0, **kwargs
|
||||
) -> re.Match | None:
|
||||
return re.compile(pattern, flags).match(self.string, **kwargs)
|
||||
|
||||
def find_spans(self, pattern: str, **kwargs) -> list[Span]:
|
||||
return [
|
||||
match_obj.span()
|
||||
for match_obj in self.finditer(pattern, **kwargs)
|
||||
]
|
||||
|
||||
def find_substr(self, substr: str, **kwargs) -> list[Span]:
|
||||
if not substr:
|
||||
return []
|
||||
return self.find_spans(re.escape(substr), **kwargs)
|
||||
|
||||
def find_substrs(self, substrs: list[str], **kwargs) -> list[Span]:
|
||||
return list(it.chain(*[
|
||||
self.find_substr(substr, **kwargs)
|
||||
for substr in remove_list_redundancies(substrs)
|
||||
]))
|
||||
|
||||
@staticmethod
|
||||
def get_neighbouring_pairs(iterable: list) -> list[tuple]:
|
||||
return list(zip(iterable[:-1], iterable[1:]))
|
||||
|
||||
@staticmethod
|
||||
def span_contains(span_0: Span, span_1: Span) -> bool:
|
||||
return span_0[0] <= span_1[0] and span_0[1] >= span_1[1]
|
||||
|
||||
@staticmethod
|
||||
def get_complement_spans(
|
||||
interval_spans: list[Span], universal_span: Span
|
||||
) -> list[Span]:
|
||||
if not interval_spans:
|
||||
return [universal_span]
|
||||
|
||||
span_ends, span_begins = zip(*interval_spans)
|
||||
return list(zip(
|
||||
(universal_span[0], *span_begins),
|
||||
(*span_ends, universal_span[1])
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def compress_neighbours(vals: list[int]) -> list[tuple[int, Span]]:
|
||||
if not vals:
|
||||
return []
|
||||
|
||||
unique_vals = [vals[0]]
|
||||
indices = [0]
|
||||
for index, val in enumerate(vals):
|
||||
if val == unique_vals[-1]:
|
||||
continue
|
||||
unique_vals.append(val)
|
||||
indices.append(index)
|
||||
indices.append(len(vals))
|
||||
spans = LabelledString.get_neighbouring_pairs(indices)
|
||||
return list(zip(unique_vals, spans))
|
||||
|
||||
@staticmethod
|
||||
def find_region_index(seq: list[int], val: int) -> int:
|
||||
# Returns an integer in `range(-1, len(seq))` satisfying
|
||||
# `seq[result] <= val < seq[result + 1]`.
|
||||
# `seq` should be sorted in ascending order.
|
||||
if not seq or val < seq[0]:
|
||||
return -1
|
||||
result = len(seq) - 1
|
||||
while val < seq[result]:
|
||||
result -= 1
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def take_nearest_value(seq: list[int], val: int, index_shift: int) -> int:
|
||||
sorted_seq = sorted(seq)
|
||||
index = LabelledString.find_region_index(sorted_seq, val)
|
||||
return sorted_seq[index + index_shift]
|
||||
|
||||
@staticmethod
|
||||
def generate_span_repl_dict(
|
||||
inserted_string_pairs: list[tuple[Span, tuple[str, str]]],
|
||||
other_repl_items: list[tuple[Span, str]]
|
||||
) -> dict[Span, str]:
|
||||
result = dict(other_repl_items)
|
||||
if not inserted_string_pairs:
|
||||
return result
|
||||
|
||||
indices, _, _, inserted_strings = zip(*sorted([
|
||||
(
|
||||
span[flag],
|
||||
-flag,
|
||||
-span[1 - flag],
|
||||
str_pair[flag]
|
||||
)
|
||||
for span, str_pair in inserted_string_pairs
|
||||
for flag in range(2)
|
||||
]))
|
||||
result.update({
|
||||
(index, index): "".join(inserted_strings[slice(*item_span)])
|
||||
for index, item_span
|
||||
in LabelledString.compress_neighbours(indices)
|
||||
})
|
||||
return result
|
||||
|
||||
def get_replaced_substr(
|
||||
self, span: Span, span_repl_dict: dict[Span, str]
|
||||
) -> str:
|
||||
repl_spans = sorted(filter(
|
||||
lambda repl_span: self.span_contains(span, repl_span),
|
||||
span_repl_dict.keys()
|
||||
))
|
||||
if not all(
|
||||
span_0[1] <= span_1[0]
|
||||
for span_0, span_1 in self.get_neighbouring_pairs(repl_spans)
|
||||
):
|
||||
raise ValueError("Overlapping replacement")
|
||||
|
||||
pieces = [
|
||||
self.get_substr(piece_span)
|
||||
for piece_span in self.get_complement_spans(repl_spans, span)
|
||||
]
|
||||
repl_strs = [span_repl_dict[repl_span] for repl_span in repl_spans]
|
||||
repl_strs.append("")
|
||||
return "".join(it.chain(*zip(pieces, repl_strs)))
|
||||
|
||||
@staticmethod
|
||||
def rslide(index: int, skipped: list[Span]) -> int:
|
||||
transfer_dict = dict(sorted(skipped))
|
||||
while index in transfer_dict.keys():
|
||||
index = transfer_dict[index]
|
||||
return index
|
||||
|
||||
@staticmethod
|
||||
def lslide(index: int, skipped: list[Span]) -> int:
|
||||
transfer_dict = dict(sorted([
|
||||
skipped_span[::-1] for skipped_span in skipped
|
||||
], reverse=True))
|
||||
while index in transfer_dict.keys():
|
||||
index = transfer_dict[index]
|
||||
return index
|
||||
|
||||
@staticmethod
|
||||
def rgb_to_int(rgb_tuple: tuple[int, int, int]) -> int:
|
||||
r, g, b = rgb_tuple
|
||||
rg = r * 256 + g
|
||||
return rg * 256 + b
|
||||
|
||||
@staticmethod
|
||||
def int_to_rgb(rgb_int: int) -> tuple[int, int, int]:
|
||||
rg, b = divmod(rgb_int, 256)
|
||||
r, g = divmod(rg, 256)
|
||||
return r, g, b
|
||||
|
||||
@staticmethod
|
||||
def int_to_hex(rgb_int: int) -> str:
|
||||
return "#{:06x}".format(rgb_int).upper()
|
||||
|
||||
@staticmethod
|
||||
def hex_to_int(rgb_hex: str) -> int:
|
||||
return int(rgb_hex[1:], 16)
|
||||
|
||||
@staticmethod
|
||||
def color_to_label(color: ManimColor) -> int:
|
||||
rgb_tuple = color_to_int_rgb(color)
|
||||
rgb = LabelledString.rgb_to_int(rgb_tuple)
|
||||
return rgb - 1
|
||||
|
||||
# Parsing
|
||||
|
||||
@abstractmethod
|
||||
def get_command_repl_items(self) -> list[tuple[Span, str]]:
|
||||
return []
|
||||
|
||||
def get_command_spans(self) -> list[Span]:
|
||||
return [cmd_span for cmd_span, _ in self.command_repl_items]
|
||||
|
||||
@abstractmethod
|
||||
def get_extra_entity_spans(self) -> list[Span]:
|
||||
return []
|
||||
|
||||
def get_entity_spans(self) -> list[Span]:
|
||||
return list(it.chain(
|
||||
self.command_spans,
|
||||
self.extra_entity_spans
|
||||
))
|
||||
|
||||
@abstractmethod
|
||||
def get_extra_ignored_spans(self) -> list[int]:
|
||||
return []
|
||||
|
||||
def get_skipped_spans(self) -> list[Span]:
|
||||
return list(it.chain(
|
||||
self.find_spans(r"\s"),
|
||||
self.command_spans,
|
||||
self.extra_ignored_spans
|
||||
))
|
||||
|
||||
def shrink_span(self, span: Span) -> Span:
|
||||
return (
|
||||
self.rslide(span[0], self.skipped_spans),
|
||||
self.lslide(span[1], self.skipped_spans)
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_internal_specified_spans(self) -> list[Span]:
|
||||
return []
|
||||
|
||||
@abstractmethod
|
||||
def get_external_specified_spans(self) -> list[Span]:
|
||||
return []
|
||||
|
||||
def get_specified_spans(self) -> list[Span]:
|
||||
spans = list(it.chain(
|
||||
self.internal_specified_spans,
|
||||
self.external_specified_spans,
|
||||
self.find_substrs(self.isolate)
|
||||
))
|
||||
shrinked_spans = list(filter(
|
||||
lambda span: span[0] < span[1] and not any([
|
||||
entity_span[0] < index < entity_span[1]
|
||||
for index in span
|
||||
for entity_span in self.entity_spans
|
||||
]),
|
||||
[self.shrink_span(span) for span in spans]
|
||||
))
|
||||
return remove_list_redundancies(shrinked_spans)
|
||||
|
||||
@abstractmethod
|
||||
def get_label_span_list(self) -> list[Span]:
|
||||
return []
|
||||
|
||||
def check_overlapping(self) -> None:
|
||||
for span_0, span_1 in it.product(self.label_span_list, repeat=2):
|
||||
if not span_0[0] < span_1[0] < span_0[1] < span_1[1]:
|
||||
continue
|
||||
raise ValueError(
|
||||
"Partially overlapping substrings detected: "
|
||||
f"'{self.get_substr(span_0)}' and '{self.get_substr(span_1)}'"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_content(self, use_plain_file: bool) -> str:
|
||||
return ""
|
||||
|
||||
@abstractmethod
|
||||
def has_predefined_local_colors(self) -> bool:
|
||||
return False
|
||||
|
||||
# Post-parsing
|
||||
|
||||
def get_labelled_submobjects(self) -> list[VMobject]:
|
||||
return [submob for _, submob in self.labelled_submobject_items]
|
||||
|
||||
def get_cleaned_substr(self, span: Span) -> str:
|
||||
span_repl_dict = dict.fromkeys(self.command_spans, "")
|
||||
return self.get_replaced_substr(span, span_repl_dict)
|
||||
|
||||
def get_specified_substrs(self) -> list[str]:
|
||||
return remove_list_redundancies([
|
||||
self.get_cleaned_substr(span)
|
||||
for span in self.specified_spans
|
||||
])
|
||||
|
||||
def get_group_items(self) -> list[tuple[str, VGroup]]:
|
||||
if not self.labelled_submobject_items:
|
||||
return []
|
||||
|
||||
labels, labelled_submobjects = zip(*self.labelled_submobject_items)
|
||||
group_labels, labelled_submob_spans = zip(
|
||||
*self.compress_neighbours(labels)
|
||||
)
|
||||
ordered_spans = [
|
||||
self.label_span_list[label] if label != -1 else self.full_span
|
||||
for label in group_labels
|
||||
]
|
||||
interval_spans = [
|
||||
(
|
||||
next_span[0]
|
||||
if self.span_contains(prev_span, next_span)
|
||||
else prev_span[1],
|
||||
prev_span[1]
|
||||
if self.span_contains(next_span, prev_span)
|
||||
else next_span[0]
|
||||
)
|
||||
for prev_span, next_span in self.get_neighbouring_pairs(
|
||||
ordered_spans
|
||||
)
|
||||
]
|
||||
shrinked_spans = [
|
||||
self.shrink_span(span)
|
||||
for span in self.get_complement_spans(
|
||||
interval_spans, (ordered_spans[0][0], ordered_spans[-1][1])
|
||||
)
|
||||
]
|
||||
group_substrs = [
|
||||
self.get_cleaned_substr(span) if span[0] < span[1] else ""
|
||||
for span in shrinked_spans
|
||||
]
|
||||
submob_groups = VGroup(*[
|
||||
VGroup(*labelled_submobjects[slice(*submob_span)])
|
||||
for submob_span in labelled_submob_spans
|
||||
])
|
||||
return list(zip(group_substrs, submob_groups))
|
||||
|
||||
def get_group_substrs(self) -> list[str]:
|
||||
return [group_substr for group_substr, _ in self.group_items]
|
||||
|
||||
def get_submob_groups(self) -> list[VGroup]:
|
||||
return [submob_group for _, submob_group in self.group_items]
|
||||
|
||||
def get_parts_by_group_substr(self, substr: str) -> VGroup:
|
||||
return VGroup(*[
|
||||
group
|
||||
for group_substr, group in self.group_items
|
||||
if group_substr == substr
|
||||
])
|
||||
|
||||
# Selector
|
||||
|
||||
def find_span_components(
|
||||
self, custom_span: Span, substring: bool = True
|
||||
) -> list[Span]:
|
||||
shrinked_span = self.shrink_span(custom_span)
|
||||
if shrinked_span[0] >= shrinked_span[1]:
|
||||
return []
|
||||
|
||||
if substring:
|
||||
indices = remove_list_redundancies(list(it.chain(
|
||||
self.full_span,
|
||||
*self.label_span_list
|
||||
)))
|
||||
span_begin = self.take_nearest_value(
|
||||
indices, shrinked_span[0], 0
|
||||
)
|
||||
span_end = self.take_nearest_value(
|
||||
indices, shrinked_span[1] - 1, 1
|
||||
)
|
||||
else:
|
||||
span_begin, span_end = shrinked_span
|
||||
|
||||
span_choices = sorted(filter(
|
||||
lambda span: self.span_contains((span_begin, span_end), span),
|
||||
self.label_span_list
|
||||
))
|
||||
# Choose spans that reach the farthest.
|
||||
span_choices_dict = dict(span_choices)
|
||||
|
||||
result = []
|
||||
while span_begin < span_end:
|
||||
if span_begin not in span_choices_dict.keys():
|
||||
span_begin += 1
|
||||
continue
|
||||
next_begin = span_choices_dict[span_begin]
|
||||
result.append((span_begin, next_begin))
|
||||
span_begin = next_begin
|
||||
return result
|
||||
|
||||
def get_part_by_custom_span(self, custom_span: Span, **kwargs) -> VGroup:
|
||||
labels = [
|
||||
label for label, span in enumerate(self.label_span_list)
|
||||
if any([
|
||||
self.span_contains(span_component, span)
|
||||
for span_component in self.find_span_components(
|
||||
custom_span, **kwargs
|
||||
)
|
||||
])
|
||||
]
|
||||
return VGroup(*[
|
||||
submob for label, submob in self.labelled_submobject_items
|
||||
if label in labels
|
||||
])
|
||||
|
||||
def get_parts_by_string(
|
||||
self, substr: str,
|
||||
case_sensitive: bool = True, regex: bool = False, **kwargs
|
||||
) -> VGroup:
|
||||
flags = 0
|
||||
if not case_sensitive:
|
||||
flags |= re.I
|
||||
pattern = substr if regex else re.escape(substr)
|
||||
return VGroup(*[
|
||||
self.get_part_by_custom_span(span, **kwargs)
|
||||
for span in self.find_spans(pattern, flags=flags)
|
||||
if span[0] < span[1]
|
||||
])
|
||||
|
||||
def get_part_by_string(
|
||||
self, substr: str, index: int = 0, **kwargs
|
||||
) -> VMobject:
|
||||
return self.get_parts_by_string(substr, **kwargs)[index]
|
||||
|
||||
def set_color_by_string(self, substr: str, color: ManimColor, **kwargs):
|
||||
self.get_parts_by_string(substr, **kwargs).set_color(color)
|
||||
return self
|
||||
|
||||
def set_color_by_string_to_color_map(
|
||||
self, string_to_color_map: dict[str, ManimColor], **kwargs
|
||||
):
|
||||
for substr, color in string_to_color_map.items():
|
||||
self.set_color_by_string(substr, color, **kwargs)
|
||||
return self
|
||||
|
||||
def get_string(self) -> str:
|
||||
return self.string
|
||||
@@ -1,654 +1,333 @@
|
||||
import itertools as it
|
||||
import re
|
||||
import sys
|
||||
from types import MethodType
|
||||
from __future__ import annotations
|
||||
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.color import color_to_int_rgb
|
||||
from manimlib.utils.iterables import adjacent_pairs
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
import itertools as it
|
||||
import colour
|
||||
from typing import Union, Sequence
|
||||
|
||||
from manimlib.mobject.svg.labelled_string import LabelledString
|
||||
from manimlib.utils.tex_file_writing import tex_to_svg_file
|
||||
from manimlib.utils.tex_file_writing import get_tex_config
|
||||
from manimlib.utils.tex_file_writing import display_during_execution
|
||||
from manimlib.logger import log
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
Span = tuple[int, int]
|
||||
|
||||
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
|
||||
|
||||
TEX_HASH_TO_MOB_MAP = {}
|
||||
|
||||
|
||||
def _contains(span_0, span_1):
|
||||
return span_0[0] <= span_1[0] and span_1[1] <= span_0[1]
|
||||
|
||||
|
||||
def _get_neighbouring_pairs(iterable):
|
||||
return list(adjacent_pairs(iterable))[:-1]
|
||||
|
||||
|
||||
class _TexSVG(SVGMobject):
|
||||
class MTex(LabelledString):
|
||||
CONFIG = {
|
||||
"color": BLACK,
|
||||
"height": None,
|
||||
"path_string_config": {
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def color_to_label(fill_color):
|
||||
r, g, b = color_to_int_rgb(fill_color)
|
||||
rg = r * 256 + g
|
||||
return rg * 256 + b
|
||||
|
||||
def parse_labels(self):
|
||||
for glyph in self:
|
||||
glyph.glyph_label = _TexSVG.color_to_label(glyph.fill_color)
|
||||
return self
|
||||
|
||||
|
||||
class _TexSpan(object):
|
||||
def __init__(self, script_type, label):
|
||||
# `script_type`: 0 for normal, 1 for subscript, 2 for superscript.
|
||||
# Only those spans with `script_type == 0` will be colored.
|
||||
self.script_type = script_type
|
||||
self.label = label
|
||||
self.containing_labels = []
|
||||
|
||||
def __repr__(self):
|
||||
return "_TexSpan(" + ", ".join([
|
||||
attrib_name + "=" + str(getattr(self, attrib_name))
|
||||
for attrib_name in ["script_type", "label", "containing_labels"]
|
||||
]) + ")"
|
||||
|
||||
|
||||
class _TexParser(object):
|
||||
def __init__(self, tex_string, additional_substrings):
|
||||
self.tex_string = tex_string
|
||||
self.tex_spans_dict = {}
|
||||
self.current_label = -1
|
||||
self.brace_index_pairs = self.get_brace_index_pairs()
|
||||
self.existing_color_command_spans = self.get_existing_color_command_spans()
|
||||
self.has_existing_color_commands = any(self.existing_color_command_spans.values())
|
||||
self.specified_substring_spans = []
|
||||
self.add_tex_span((0, len(tex_string)))
|
||||
self.break_up_by_double_braces()
|
||||
self.break_up_by_scripts()
|
||||
self.break_up_by_additional_substrings(additional_substrings)
|
||||
self.specified_substrings = remove_list_redundancies([
|
||||
tex_string[slice(*span_tuple)]
|
||||
for span_tuple in self.specified_substring_spans
|
||||
])
|
||||
self.check_if_overlap()
|
||||
self.analyse_containing_labels()
|
||||
|
||||
@staticmethod
|
||||
def label_to_color_tuple(rgb):
|
||||
# Get a unique color different from black.
|
||||
rg, b = divmod(rgb, 256)
|
||||
r, g = divmod(rg, 256)
|
||||
return r, g, b
|
||||
|
||||
def add_tex_span(self, span_tuple, script_type=0, label=-1):
|
||||
if span_tuple in self.tex_spans_dict:
|
||||
return
|
||||
|
||||
if script_type == 0:
|
||||
# Should be additionally labelled.
|
||||
self.current_label += 1
|
||||
label = self.current_label
|
||||
|
||||
tex_span = _TexSpan(script_type, label)
|
||||
self.tex_spans_dict[span_tuple] = tex_span
|
||||
|
||||
def get_brace_index_pairs(self):
|
||||
result = []
|
||||
left_brace_indices = []
|
||||
for match_obj in re.finditer(r"(\\*)(\{|\})", self.tex_string):
|
||||
# Braces following even numbers of backslashes are counted.
|
||||
if len(match_obj.group(1)) % 2 == 1:
|
||||
continue
|
||||
if match_obj.group(2) == "{":
|
||||
left_brace_index = match_obj.span(2)[0]
|
||||
left_brace_indices.append(left_brace_index)
|
||||
else:
|
||||
left_brace_index = left_brace_indices.pop()
|
||||
right_brace_index = match_obj.span(2)[1]
|
||||
result.append((left_brace_index, right_brace_index))
|
||||
if left_brace_indices:
|
||||
self.raise_tex_parsing_error("unmatched braces")
|
||||
return result
|
||||
|
||||
def get_existing_color_command_spans(self):
|
||||
tex_string = self.tex_string
|
||||
color_related_commands_dict = _TexParser.get_color_related_commands_dict()
|
||||
commands = color_related_commands_dict.keys()
|
||||
result = {
|
||||
command_name: []
|
||||
for command_name in commands
|
||||
}
|
||||
brace_index_pairs = self.brace_index_pairs
|
||||
pattern = "|".join([
|
||||
re.escape(command_name)
|
||||
for command_name in commands
|
||||
])
|
||||
for match_obj in re.finditer(pattern, tex_string):
|
||||
span_tuple = match_obj.span()
|
||||
command_begin_index = span_tuple[0]
|
||||
command_name = match_obj.group()
|
||||
n_braces = color_related_commands_dict[command_name]
|
||||
for _ in range(n_braces):
|
||||
span_tuple = min(filter(
|
||||
lambda t: t[0] >= span_tuple[1],
|
||||
brace_index_pairs
|
||||
))
|
||||
result[command_name].append(
|
||||
(command_begin_index, span_tuple[1])
|
||||
)
|
||||
return result
|
||||
|
||||
def break_up_by_double_braces(self):
|
||||
# Match paired double braces (`{{...}}`).
|
||||
skip_pair = False
|
||||
for prev_span_tuple, span_tuple in _get_neighbouring_pairs(
|
||||
self.brace_index_pairs
|
||||
):
|
||||
if skip_pair:
|
||||
skip_pair = False
|
||||
continue
|
||||
if all([
|
||||
span_tuple[0] == prev_span_tuple[0] - 1,
|
||||
span_tuple[1] == prev_span_tuple[1] + 1
|
||||
]):
|
||||
self.add_tex_span(span_tuple)
|
||||
self.specified_substring_spans.append(span_tuple)
|
||||
skip_pair = True
|
||||
|
||||
def break_up_by_scripts(self):
|
||||
# Match subscripts & superscripts.
|
||||
tex_string = self.tex_string
|
||||
brace_indices_dict = dict(self.brace_index_pairs)
|
||||
for match_obj in re.finditer(r"((?<!\\)(_|\^)\s*)|(\s+(_|\^)\s*)", tex_string):
|
||||
script_type = 1 if "_" in match_obj.group() else 2
|
||||
token_begin, token_end = match_obj.span()
|
||||
if token_end in brace_indices_dict:
|
||||
content_span = (token_end, brace_indices_dict[token_end])
|
||||
else:
|
||||
content_match_obj = re.match(r"\w|\\[a-zA-Z]+", tex_string[token_end:])
|
||||
if not content_match_obj:
|
||||
self.raise_tex_parsing_error("unclear subscript/superscript")
|
||||
content_span = tuple([
|
||||
index + token_end for index in content_match_obj.span()
|
||||
])
|
||||
self.add_tex_span(content_span)
|
||||
label = self.tex_spans_dict[content_span].label
|
||||
self.add_tex_span(
|
||||
(token_begin, content_span[1]),
|
||||
script_type=script_type,
|
||||
label=label
|
||||
)
|
||||
|
||||
def break_up_by_additional_substrings(self, additional_substrings):
|
||||
tex_string = self.tex_string
|
||||
all_span_tuples = []
|
||||
for string in additional_substrings:
|
||||
# Only match non-crossing strings.
|
||||
for match_obj in re.finditer(re.escape(string), tex_string):
|
||||
all_span_tuples.append(match_obj.span())
|
||||
|
||||
script_spans_dict = dict([
|
||||
span_tuple[::-1]
|
||||
for span_tuple, tex_span in self.tex_spans_dict.items()
|
||||
if tex_span.script_type != 0
|
||||
])
|
||||
for span_begin, span_end in all_span_tuples:
|
||||
if span_end in script_spans_dict.values():
|
||||
# Deconstruct spans with subscripts & superscripts.
|
||||
while span_end in script_spans_dict:
|
||||
span_end = script_spans_dict[span_end]
|
||||
if span_begin >= span_end:
|
||||
continue
|
||||
span_tuple = (span_begin, span_end)
|
||||
self.add_tex_span(span_tuple)
|
||||
self.specified_substring_spans.append(span_tuple)
|
||||
|
||||
def check_if_overlap(self):
|
||||
span_tuples = sorted(
|
||||
self.tex_spans_dict.keys(),
|
||||
key=lambda t: (t[0], -t[1])
|
||||
)
|
||||
overlapping_span_pairs = []
|
||||
for i, span_0 in enumerate(span_tuples):
|
||||
for span_1 in span_tuples[i + 1 :]:
|
||||
if span_0[1] <= span_1[0]:
|
||||
continue
|
||||
if span_0[1] < span_1[1]:
|
||||
overlapping_span_pairs.append((span_0, span_1))
|
||||
if overlapping_span_pairs:
|
||||
tex_string = self.tex_string
|
||||
log.error("Overlapping substring pairs occur in MTex:")
|
||||
for span_tuple_pair in overlapping_span_pairs:
|
||||
log.error(", ".join(
|
||||
f"\"{tex_string[slice(*span_tuple)]}\""
|
||||
for span_tuple in span_tuple_pair
|
||||
))
|
||||
raise ValueError
|
||||
|
||||
def analyse_containing_labels(self):
|
||||
for span_0, tex_span_0 in self.tex_spans_dict.items():
|
||||
if tex_span_0.script_type != 0:
|
||||
continue
|
||||
for span_1, tex_span_1 in self.tex_spans_dict.items():
|
||||
if _contains(span_1, span_0):
|
||||
tex_span_1.containing_labels.append(tex_span_0.label)
|
||||
|
||||
def get_labelled_tex_string(self):
|
||||
tex_string = self.tex_string
|
||||
if self.current_label == 0 and not self.has_existing_color_commands:
|
||||
return tex_string
|
||||
|
||||
# Remove the span of extire tex string.
|
||||
indices_with_labels = sorted([
|
||||
(span_tuple[i], i, span_tuple[1 - i], tex_span.label)
|
||||
for span_tuple, tex_span in self.tex_spans_dict.items()
|
||||
if tex_span.script_type == 0
|
||||
for i in range(2)
|
||||
], key=lambda t: (t[0], -t[1], -t[2]))[1:]
|
||||
|
||||
# Prevent from "\\color[RGB]" being replaced.
|
||||
# Hopefully tex string doesn't contain such a substring...
|
||||
color_command_placeholder = "{{\\iffalse \\fi}}"
|
||||
result = tex_string[: indices_with_labels[0][0]]
|
||||
for index_with_label, next_index_with_label in _get_neighbouring_pairs(
|
||||
indices_with_labels
|
||||
):
|
||||
index, flag, _, label = index_with_label
|
||||
next_index, *_ = next_index_with_label
|
||||
# Adding one more pair of braces will help maintain the glyghs of tex file...
|
||||
if flag == 0:
|
||||
color_tuple = _TexParser.label_to_color_tuple(label)
|
||||
result += "".join([
|
||||
"{{",
|
||||
color_command_placeholder,
|
||||
"{",
|
||||
",".join(map(str, color_tuple)),
|
||||
"}"
|
||||
])
|
||||
else:
|
||||
result += "}}"
|
||||
result += tex_string[index : next_index]
|
||||
|
||||
color_related_commands_dict = _TexParser.get_color_related_commands_dict()
|
||||
for command_name, command_spans in self.existing_color_command_spans.items():
|
||||
if not command_spans:
|
||||
continue
|
||||
n_braces = color_related_commands_dict[command_name]
|
||||
command_to_replace = command_name + n_braces * "{black}"
|
||||
commands = {
|
||||
tex_string[slice(*span_tuple)]
|
||||
for span_tuple in command_spans
|
||||
}
|
||||
for command in commands:
|
||||
result = result.replace(command, command_to_replace)
|
||||
|
||||
return result.replace(color_command_placeholder, "\\color[RGB]")
|
||||
|
||||
def raise_tex_parsing_error(self, message):
|
||||
raise ValueError(f"Failed to parse tex ({message}): \"{self.tex_string}\"")
|
||||
|
||||
@staticmethod
|
||||
def get_color_related_commands_dict():
|
||||
# Only list a few commands that are commonly used.
|
||||
return {
|
||||
"\\color": 1,
|
||||
"\\textcolor": 1,
|
||||
"\\pagecolor": 1,
|
||||
"\\colorbox": 1,
|
||||
"\\fcolorbox": 2,
|
||||
}
|
||||
|
||||
|
||||
class MTex(VMobject):
|
||||
CONFIG = {
|
||||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0,
|
||||
"font_size": 48,
|
||||
"alignment": "\\centering",
|
||||
"tex_environment": "align*",
|
||||
"isolate": [],
|
||||
"tex_to_color_map": {},
|
||||
"use_plain_tex_file": False,
|
||||
}
|
||||
|
||||
def __init__(self, tex_string, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
tex_string = tex_string.strip()
|
||||
def __init__(self, tex_string: str, **kwargs):
|
||||
# Prevent from passing an empty string.
|
||||
if not tex_string:
|
||||
tex_string = "\\quad"
|
||||
tex_string = "\\\\"
|
||||
self.tex_string = tex_string
|
||||
|
||||
self.generate_mobject()
|
||||
super().__init__(tex_string, **kwargs)
|
||||
|
||||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
||||
|
||||
def get_additional_substrings_to_break_up(self):
|
||||
result = remove_list_redundancies([
|
||||
*self.tex_to_color_map.keys(), *self.isolate
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.base_color,
|
||||
self.use_plain_file,
|
||||
self.isolate,
|
||||
self.tex_string,
|
||||
self.alignment,
|
||||
self.tex_environment,
|
||||
self.tex_to_color_map
|
||||
)
|
||||
|
||||
def get_file_path_by_content(self, content: str) -> str:
|
||||
tex_config = get_tex_config()
|
||||
full_tex = tex_config["tex_body"].replace(
|
||||
tex_config["text_to_replace"],
|
||||
content
|
||||
)
|
||||
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||
file_path = tex_to_svg_file(full_tex)
|
||||
return file_path
|
||||
|
||||
def pre_parse(self) -> None:
|
||||
super().pre_parse()
|
||||
self.backslash_indices = self.get_backslash_indices()
|
||||
self.brace_index_pairs = self.get_brace_index_pairs()
|
||||
self.script_char_spans = self.get_script_char_spans()
|
||||
self.script_content_spans = self.get_script_content_spans()
|
||||
self.script_spans = self.get_script_spans()
|
||||
|
||||
# Toolkits
|
||||
|
||||
@staticmethod
|
||||
def get_color_command_str(rgb_int: int) -> str:
|
||||
rgb_tuple = MTex.int_to_rgb(rgb_int)
|
||||
return "".join([
|
||||
"\\color[RGB]",
|
||||
"{",
|
||||
",".join(map(str, rgb_tuple)),
|
||||
"}"
|
||||
])
|
||||
if "" in result:
|
||||
result.remove("")
|
||||
|
||||
# Pre-parsing
|
||||
|
||||
def get_backslash_indices(self) -> list[int]:
|
||||
# The latter of `\\` doesn't count.
|
||||
return list(it.chain(*[
|
||||
range(span[0], span[1], 2)
|
||||
for span in self.find_spans(r"\\+")
|
||||
]))
|
||||
|
||||
def get_unescaped_char_spans(self, chars: str):
|
||||
return sorted(filter(
|
||||
lambda span: span[0] - 1 not in self.backslash_indices,
|
||||
self.find_substrs(list(chars))
|
||||
))
|
||||
|
||||
def get_brace_index_pairs(self) -> list[Span]:
|
||||
left_brace_indices = []
|
||||
right_brace_indices = []
|
||||
left_brace_indices_stack = []
|
||||
for span in self.get_unescaped_char_spans("{}"):
|
||||
index = span[0]
|
||||
if self.get_substr(span) == "{":
|
||||
left_brace_indices_stack.append(index)
|
||||
else:
|
||||
if not left_brace_indices_stack:
|
||||
raise ValueError("Missing '{' inserted")
|
||||
left_brace_index = left_brace_indices_stack.pop()
|
||||
left_brace_indices.append(left_brace_index)
|
||||
right_brace_indices.append(index)
|
||||
if left_brace_indices_stack:
|
||||
raise ValueError("Missing '}' inserted")
|
||||
return list(zip(left_brace_indices, right_brace_indices))
|
||||
|
||||
def get_script_char_spans(self) -> list[int]:
|
||||
return self.get_unescaped_char_spans("_^")
|
||||
|
||||
def get_script_content_spans(self) -> list[Span]:
|
||||
result = []
|
||||
brace_indices_dict = dict(self.brace_index_pairs)
|
||||
script_pattern = r"[a-zA-Z0-9]|\\[a-zA-Z]+"
|
||||
for script_char_span in self.script_char_spans:
|
||||
span_begin = self.match(r"\s*", pos=script_char_span[1]).end()
|
||||
if span_begin in brace_indices_dict.keys():
|
||||
span_end = brace_indices_dict[span_begin] + 1
|
||||
else:
|
||||
match_obj = self.match(script_pattern, pos=span_begin)
|
||||
if not match_obj:
|
||||
script_name = {
|
||||
"_": "subscript",
|
||||
"^": "superscript"
|
||||
}[script_char]
|
||||
raise ValueError(
|
||||
f"Unclear {script_name} detected while parsing. "
|
||||
"Please use braces to clarify"
|
||||
)
|
||||
span_end = match_obj.end()
|
||||
result.append((span_begin, span_end))
|
||||
return result
|
||||
|
||||
def get_parser(self):
|
||||
return _TexParser(self.tex_string, self.get_additional_substrings_to_break_up())
|
||||
def get_script_spans(self) -> list[Span]:
|
||||
return [
|
||||
(
|
||||
self.search(r"\s*$", endpos=script_char_span[0]).start(),
|
||||
script_content_span[1]
|
||||
)
|
||||
for script_char_span, script_content_span in zip(
|
||||
self.script_char_spans, self.script_content_spans
|
||||
)
|
||||
]
|
||||
|
||||
def generate_mobject(self):
|
||||
tex_string = self.tex_string
|
||||
tex_parser = self.get_parser()
|
||||
self.tex_spans_dict = tex_parser.tex_spans_dict
|
||||
self.specified_substrings = tex_parser.specified_substrings
|
||||
fill_color = self.get_fill_color()
|
||||
# Parsing
|
||||
|
||||
# Cannot simultaneously be false, so at least one file is generated.
|
||||
require_labelled_tex_file = tex_parser.current_label != 0
|
||||
require_plain_tex_file = any([
|
||||
self.use_plain_tex_file,
|
||||
tex_parser.has_existing_color_commands,
|
||||
tex_parser.current_label == 0
|
||||
def get_command_repl_items(self) -> list[tuple[Span, str]]:
|
||||
color_related_command_dict = {
|
||||
"color": (1, False),
|
||||
"textcolor": (1, False),
|
||||
"pagecolor": (1, True),
|
||||
"colorbox": (1, True),
|
||||
"fcolorbox": (2, True),
|
||||
}
|
||||
result = []
|
||||
backslash_indices = self.backslash_indices
|
||||
right_brace_indices = [
|
||||
right_index
|
||||
for left_index, right_index in self.brace_index_pairs
|
||||
]
|
||||
pattern = "".join([
|
||||
r"\\",
|
||||
"(",
|
||||
"|".join(color_related_command_dict.keys()),
|
||||
")",
|
||||
r"(?![a-zA-Z])"
|
||||
])
|
||||
|
||||
if require_labelled_tex_file:
|
||||
labelled_full_tex = self.get_tex_file_body(tex_parser.get_labelled_tex_string())
|
||||
labelled_hash_val = hash(labelled_full_tex)
|
||||
if labelled_hash_val in TEX_HASH_TO_MOB_MAP:
|
||||
self.add(*TEX_HASH_TO_MOB_MAP[labelled_hash_val].copy())
|
||||
for match_obj in self.finditer(pattern):
|
||||
span_begin, cmd_end = match_obj.span()
|
||||
if span_begin not in backslash_indices:
|
||||
continue
|
||||
cmd_name = match_obj.group(1)
|
||||
n_braces, substitute_cmd = color_related_command_dict[cmd_name]
|
||||
span_end = self.take_nearest_value(
|
||||
right_brace_indices, cmd_end, n_braces
|
||||
) + 1
|
||||
if substitute_cmd:
|
||||
repl_str = "\\" + cmd_name + n_braces * "{black}"
|
||||
else:
|
||||
with display_during_execution(f"Writing \"{tex_string}\""):
|
||||
labelled_svg_glyphs = MTex.get_svg_glyphs(labelled_full_tex)
|
||||
labelled_svg_glyphs.parse_labels()
|
||||
self.add(*labelled_svg_glyphs)
|
||||
self.build_submobjects()
|
||||
TEX_HASH_TO_MOB_MAP[labelled_hash_val] = self.copy()
|
||||
if not require_plain_tex_file:
|
||||
self.set_fill(color=fill_color)
|
||||
return self
|
||||
repl_str = ""
|
||||
result.append(((span_begin, span_end), repl_str))
|
||||
return result
|
||||
|
||||
# require_plain_tex_file == True
|
||||
self.set_submobjects([])
|
||||
full_tex = self.get_tex_file_body(tex_string, fill_color=fill_color)
|
||||
hash_val = hash(full_tex)
|
||||
if hash_val in TEX_HASH_TO_MOB_MAP:
|
||||
self.add(*TEX_HASH_TO_MOB_MAP[hash_val].copy())
|
||||
def get_extra_entity_spans(self) -> list[Span]:
|
||||
return [
|
||||
self.match(r"\\([a-zA-Z]+|.)", pos=index).span()
|
||||
for index in self.backslash_indices
|
||||
]
|
||||
|
||||
def get_extra_ignored_spans(self) -> list[int]:
|
||||
return self.script_char_spans.copy()
|
||||
|
||||
def get_internal_specified_spans(self) -> list[Span]:
|
||||
# Match paired double braces (`{{...}}`).
|
||||
result = []
|
||||
reversed_brace_indices_dict = dict([
|
||||
pair[::-1] for pair in self.brace_index_pairs
|
||||
])
|
||||
skip = False
|
||||
for prev_right_index, right_index in self.get_neighbouring_pairs(
|
||||
list(reversed_brace_indices_dict.keys())
|
||||
):
|
||||
if skip:
|
||||
skip = False
|
||||
continue
|
||||
if right_index != prev_right_index + 1:
|
||||
continue
|
||||
left_index = reversed_brace_indices_dict[right_index]
|
||||
prev_left_index = reversed_brace_indices_dict[prev_right_index]
|
||||
if left_index != prev_left_index - 1:
|
||||
continue
|
||||
result.append((left_index, right_index + 1))
|
||||
skip = True
|
||||
return result
|
||||
|
||||
def get_external_specified_spans(self) -> list[Span]:
|
||||
return self.find_substrs(list(self.tex_to_color_map.keys()))
|
||||
|
||||
def get_label_span_list(self) -> list[Span]:
|
||||
result = self.script_content_spans.copy()
|
||||
for span_begin, span_end in self.specified_spans:
|
||||
shrinked_end = self.lslide(span_end, self.script_spans)
|
||||
if span_begin >= shrinked_end:
|
||||
continue
|
||||
shrinked_span = (span_begin, shrinked_end)
|
||||
if shrinked_span in result:
|
||||
continue
|
||||
result.append(shrinked_span)
|
||||
return result
|
||||
|
||||
def get_content(self, use_plain_file: bool) -> str:
|
||||
if use_plain_file:
|
||||
span_repl_dict = {}
|
||||
else:
|
||||
with display_during_execution(f"Writing \"{tex_string}\""):
|
||||
svg_glyphs = MTex.get_svg_glyphs(full_tex)
|
||||
if require_labelled_tex_file:
|
||||
labelled_svg_mob = TEX_HASH_TO_MOB_MAP[labelled_hash_val]
|
||||
for glyph, labelled_glyph in zip(svg_glyphs, it.chain(*labelled_svg_mob)):
|
||||
glyph.glyph_label = labelled_glyph.glyph_label
|
||||
else:
|
||||
for glyph in svg_glyphs:
|
||||
glyph.glyph_label = 0
|
||||
self.add(*svg_glyphs)
|
||||
self.build_submobjects()
|
||||
TEX_HASH_TO_MOB_MAP[hash_val] = self.copy()
|
||||
return self
|
||||
extended_label_span_list = [
|
||||
span
|
||||
if span in self.script_content_spans
|
||||
else (span[0], self.rslide(span[1], self.script_spans))
|
||||
for span in self.label_span_list
|
||||
]
|
||||
inserted_string_pairs = [
|
||||
(span, (
|
||||
"{{" + self.get_color_command_str(label + 1),
|
||||
"}}"
|
||||
))
|
||||
for label, span in enumerate(extended_label_span_list)
|
||||
]
|
||||
span_repl_dict = self.generate_span_repl_dict(
|
||||
inserted_string_pairs,
|
||||
self.command_repl_items
|
||||
)
|
||||
result = self.get_replaced_substr(self.full_span, span_repl_dict)
|
||||
|
||||
def get_tex_file_body(self, new_tex, fill_color=None):
|
||||
if self.tex_environment:
|
||||
new_tex = "\n".join([
|
||||
result = "\n".join([
|
||||
f"\\begin{{{self.tex_environment}}}",
|
||||
new_tex,
|
||||
result,
|
||||
f"\\end{{{self.tex_environment}}}"
|
||||
])
|
||||
if self.alignment:
|
||||
new_tex = "\n".join([self.alignment, new_tex])
|
||||
if fill_color:
|
||||
int_rgb = color_to_int_rgb(fill_color)
|
||||
color_command = "".join([
|
||||
"\\color[RGB]",
|
||||
"{",
|
||||
",".join(map(str, int_rgb)),
|
||||
"}"
|
||||
result = "\n".join([self.alignment, result])
|
||||
if use_plain_file:
|
||||
result = "\n".join([
|
||||
self.get_color_command_str(self.hex_to_int(self.base_color)),
|
||||
result
|
||||
])
|
||||
new_tex = "\n".join(
|
||||
[color_command, new_tex]
|
||||
)
|
||||
|
||||
tex_config = get_tex_config()
|
||||
return tex_config["tex_body"].replace(
|
||||
tex_config["text_to_replace"],
|
||||
new_tex
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_svg_glyphs(full_tex):
|
||||
filename = tex_to_svg_file(full_tex)
|
||||
return _TexSVG(filename)
|
||||
|
||||
def build_submobjects(self):
|
||||
if not self.submobjects:
|
||||
return
|
||||
self.init_colors()
|
||||
for glyph in self.submobjects:
|
||||
glyph.set_fill(glyph.fill_color)
|
||||
self.group_submobjects()
|
||||
self.sort_scripts_in_tex_order()
|
||||
self.assign_submob_tex_strings()
|
||||
|
||||
def group_submobjects(self):
|
||||
# Simply pack together adjacent mobjects with the same label.
|
||||
new_submobjects = []
|
||||
def append_new_submobject(glyphs):
|
||||
if glyphs:
|
||||
submobject = VGroup(*glyphs)
|
||||
submobject.submob_label = glyphs[0].glyph_label
|
||||
new_submobjects.append(submobject)
|
||||
|
||||
new_glyphs = []
|
||||
current_glyph_label = 0
|
||||
for glyph in self.submobjects:
|
||||
if glyph.glyph_label == current_glyph_label:
|
||||
new_glyphs.append(glyph)
|
||||
else:
|
||||
append_new_submobject(new_glyphs)
|
||||
new_glyphs = [glyph]
|
||||
current_glyph_label = glyph.glyph_label
|
||||
append_new_submobject(new_glyphs)
|
||||
self.set_submobjects(new_submobjects)
|
||||
|
||||
def sort_scripts_in_tex_order(self):
|
||||
# LaTeX always puts superscripts before subscripts.
|
||||
# This function sorts the submobjects of scripts in the order of tex given.
|
||||
tex_spans_dict = self.tex_spans_dict
|
||||
index_and_span_list = sorted([
|
||||
(index, span_tuple)
|
||||
for span_tuple, tex_span in tex_spans_dict.items()
|
||||
if tex_span.script_type != 0
|
||||
for index in span_tuple
|
||||
])
|
||||
|
||||
switch_range_pairs = []
|
||||
for index_and_span_0, index_and_span_1 in _get_neighbouring_pairs(
|
||||
index_and_span_list
|
||||
):
|
||||
index_0, span_tuple_0 = index_and_span_0
|
||||
index_1, span_tuple_1 = index_and_span_1
|
||||
if index_0 != index_1:
|
||||
continue
|
||||
if not all([
|
||||
tex_spans_dict[span_tuple_0].script_type == 1,
|
||||
tex_spans_dict[span_tuple_1].script_type == 2
|
||||
]):
|
||||
continue
|
||||
submob_range_0 = self.range_of_part(
|
||||
self.get_part_by_span_tuples([span_tuple_0])
|
||||
)
|
||||
submob_range_1 = self.range_of_part(
|
||||
self.get_part_by_span_tuples([span_tuple_1])
|
||||
)
|
||||
switch_range_pairs.append((submob_range_0, submob_range_1))
|
||||
|
||||
switch_range_pairs.sort(key=lambda t: (t[0].stop, -t[0].start))
|
||||
indices = list(range(len(self.submobjects)))
|
||||
for submob_range_0, submob_range_1 in switch_range_pairs:
|
||||
indices = [
|
||||
*indices[: submob_range_1.start],
|
||||
*indices[submob_range_0.start : submob_range_0.stop],
|
||||
*indices[submob_range_1.stop : submob_range_0.start],
|
||||
*indices[submob_range_1.start : submob_range_1.stop],
|
||||
*indices[submob_range_0.stop :]
|
||||
]
|
||||
|
||||
submobs = self.submobjects
|
||||
self.set_submobjects([submobs[i] for i in indices])
|
||||
|
||||
def assign_submob_tex_strings(self):
|
||||
# Not sure whether this is the best practice...
|
||||
# This temporarily supports `TransformMatchingTex`.
|
||||
tex_string = self.tex_string
|
||||
tex_spans_dict = self.tex_spans_dict
|
||||
# Use tex strings including "_", "^".
|
||||
label_dict = {}
|
||||
for span_tuple, tex_span in tex_spans_dict.items():
|
||||
if tex_span.script_type != 0:
|
||||
label_dict[tex_span.label] = span_tuple
|
||||
else:
|
||||
if tex_span.label not in label_dict:
|
||||
label_dict[tex_span.label] = span_tuple
|
||||
|
||||
curr_labels = [submob.submob_label for submob in self.submobjects]
|
||||
prev_labels = [curr_labels[-1], *curr_labels[:-1]]
|
||||
next_labels = [*curr_labels[1:], curr_labels[0]]
|
||||
tex_string_spans = []
|
||||
for curr_label, prev_label, next_label in zip(
|
||||
curr_labels, prev_labels, next_labels
|
||||
):
|
||||
curr_span_tuple = label_dict[curr_label]
|
||||
prev_span_tuple = label_dict[prev_label]
|
||||
next_span_tuple = label_dict[next_label]
|
||||
containing_labels = tex_spans_dict[curr_span_tuple].containing_labels
|
||||
tex_string_spans.append([
|
||||
prev_span_tuple[1] if prev_label in containing_labels else curr_span_tuple[0],
|
||||
next_span_tuple[0] if next_label in containing_labels else curr_span_tuple[1]
|
||||
])
|
||||
tex_string_spans[0][0] = label_dict[curr_labels[0]][0]
|
||||
tex_string_spans[-1][1] = label_dict[curr_labels[-1]][1]
|
||||
for submob, tex_string_span in zip(self.submobjects, tex_string_spans):
|
||||
submob.tex_string = tex_string[slice(*tex_string_span)]
|
||||
# Support `get_tex()` method here.
|
||||
submob.get_tex = MethodType(lambda inst: inst.tex_string, submob)
|
||||
|
||||
def get_part_by_span_tuples(self, span_tuples):
|
||||
tex_spans_dict = self.tex_spans_dict
|
||||
labels = set(it.chain(*[
|
||||
tex_spans_dict[span_tuple].containing_labels
|
||||
for span_tuple in span_tuples
|
||||
]))
|
||||
return VGroup(*filter(
|
||||
lambda submob: submob.submob_label in labels,
|
||||
self.submobjects
|
||||
))
|
||||
|
||||
def find_span_components_of_custom_span(self, custom_span_tuple):
|
||||
tex_string = self.tex_string
|
||||
span_choices = sorted(filter(
|
||||
lambda t: _contains(custom_span_tuple, t),
|
||||
self.tex_spans_dict.keys()
|
||||
))
|
||||
# Filter out spans that reach the farthest.
|
||||
span_choices_dict = dict(span_choices)
|
||||
|
||||
span_begin, span_end = custom_span_tuple
|
||||
result = []
|
||||
while span_begin != span_end:
|
||||
if span_begin not in span_choices_dict:
|
||||
if tex_string[span_begin].strip():
|
||||
return None
|
||||
# Whitespaces may occur between spans.
|
||||
span_begin += 1
|
||||
continue
|
||||
next_begin = span_choices_dict[span_begin]
|
||||
result.append((span_begin, next_begin))
|
||||
span_begin = next_begin
|
||||
return result
|
||||
|
||||
def get_part_by_custom_span_tuple(self, custom_span_tuple):
|
||||
span_tuples = self.find_span_components_of_custom_span(custom_span_tuple)
|
||||
if span_tuples is None:
|
||||
tex = self.tex_string[slice(*custom_span_tuple)]
|
||||
raise ValueError(f"Failed to get span of tex: \"{tex}\"")
|
||||
return self.get_part_by_span_tuples(span_tuples)
|
||||
@property
|
||||
def has_predefined_local_colors(self) -> bool:
|
||||
return bool(self.command_repl_items)
|
||||
|
||||
def get_parts_by_tex(self, tex):
|
||||
return VGroup(*[
|
||||
self.get_part_by_custom_span_tuple(match_obj.span())
|
||||
for match_obj in re.finditer(re.escape(tex), self.tex_string)
|
||||
# Post-parsing
|
||||
|
||||
def get_cleaned_substr(self, span: Span) -> str:
|
||||
substr = super().get_cleaned_substr(span)
|
||||
if not self.brace_index_pairs:
|
||||
return substr
|
||||
|
||||
# Balance braces.
|
||||
left_brace_indices, right_brace_indices = zip(*self.brace_index_pairs)
|
||||
unclosed_left_braces = 0
|
||||
unclosed_right_braces = 0
|
||||
for index in range(*span):
|
||||
if index in left_brace_indices:
|
||||
unclosed_left_braces += 1
|
||||
elif index in right_brace_indices:
|
||||
if unclosed_left_braces == 0:
|
||||
unclosed_right_braces += 1
|
||||
else:
|
||||
unclosed_left_braces -= 1
|
||||
return "".join([
|
||||
unclosed_right_braces * "{",
|
||||
substr,
|
||||
unclosed_left_braces * "}"
|
||||
])
|
||||
|
||||
def get_part_by_tex(self, tex, index=0):
|
||||
all_parts = self.get_parts_by_tex(tex)
|
||||
return all_parts[index]
|
||||
# Method alias
|
||||
|
||||
def set_color_by_tex(self, tex, color):
|
||||
self.get_parts_by_tex(tex).set_color(color)
|
||||
return self
|
||||
def get_parts_by_tex(self, tex: str, **kwargs) -> VGroup:
|
||||
return self.get_parts_by_string(tex, **kwargs)
|
||||
|
||||
def set_color_by_tex_to_color_map(self, tex_to_color_map):
|
||||
for tex, color in list(tex_to_color_map.items()):
|
||||
try:
|
||||
self.set_color_by_tex(tex, color)
|
||||
except:
|
||||
pass
|
||||
return self
|
||||
def get_part_by_tex(self, tex: str, **kwargs) -> VMobject:
|
||||
return self.get_part_by_string(tex, **kwargs)
|
||||
|
||||
def indices_of_part(self, part):
|
||||
indices = [
|
||||
i for i, submob in enumerate(self.submobjects)
|
||||
if submob in part
|
||||
]
|
||||
if not indices:
|
||||
raise ValueError("Failed to find part in tex")
|
||||
return indices
|
||||
def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
|
||||
return self.set_color_by_string(tex, color, **kwargs)
|
||||
|
||||
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 set_color_by_tex_to_color_map(
|
||||
self, tex_to_color_map: dict[str, ManimColor], **kwargs
|
||||
):
|
||||
return self.set_color_by_string_to_color_map(
|
||||
tex_to_color_map, **kwargs
|
||||
)
|
||||
|
||||
def range_of_part(self, part):
|
||||
indices = self.indices_of_part(part)
|
||||
return range(indices[0], indices[-1] + 1)
|
||||
|
||||
def range_of_part_by_tex(self, tex, index=0):
|
||||
part = self.get_part_by_tex(tex, index=index)
|
||||
return self.range_of_part(part)
|
||||
|
||||
def index_of_part(self, part):
|
||||
return self.indices_of_part(part)[0]
|
||||
|
||||
def index_of_part_by_tex(self, tex, index=0):
|
||||
part = self.get_part_by_tex(tex, index=index)
|
||||
return self.index_of_part(part)
|
||||
|
||||
def get_tex(self):
|
||||
return self.tex_string
|
||||
|
||||
def get_all_isolated_substrings(self):
|
||||
tex_string = self.tex_string
|
||||
return remove_list_redundancies([
|
||||
tex_string[slice(*span_tuple)]
|
||||
for span_tuple in self.tex_spans_dict.keys()
|
||||
])
|
||||
|
||||
def get_specified_substrings(self):
|
||||
return self.specified_substrings
|
||||
def get_tex(self) -> str:
|
||||
return self.get_string()
|
||||
|
||||
|
||||
class MTexText(MTex):
|
||||
|
||||
@@ -1,100 +1,34 @@
|
||||
import itertools as it
|
||||
import re
|
||||
import string
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import itertools as it
|
||||
from typing import Callable
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import cssselect2
|
||||
from colour import web2hex
|
||||
from xml.etree import ElementTree
|
||||
from tinycss2 import serialize as css_serialize
|
||||
from tinycss2 import parse_stylesheet, parse_declaration_list
|
||||
|
||||
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT, IN
|
||||
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
|
||||
|
||||
|
||||
DEFAULT_STYLE = {
|
||||
"fill": "black",
|
||||
"stroke": "none",
|
||||
"fill-opacity": "1",
|
||||
"stroke-opacity": "1",
|
||||
"stroke-width": 0,
|
||||
}
|
||||
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
|
||||
|
||||
|
||||
def cascade_element_style(element, inherited):
|
||||
style = inherited.copy()
|
||||
|
||||
for attr in DEFAULT_STYLE:
|
||||
if element.get(attr):
|
||||
style[attr] = element.get(attr)
|
||||
|
||||
if element.get("style"):
|
||||
declarations = parse_declaration_list(element.get("style"))
|
||||
for declaration in declarations:
|
||||
style[declaration.name] = css_serialize(declaration.value)
|
||||
|
||||
return style
|
||||
|
||||
|
||||
def parse_color(color):
|
||||
color = color.strip()
|
||||
|
||||
if color[0:3] == "rgb":
|
||||
splits = color[4:-1].strip().split(",")
|
||||
if splits[0].strip()[-1] == "%":
|
||||
parsed_rgbs = [float(i.strip()[:-1]) / 100.0 for i in splits]
|
||||
else:
|
||||
parsed_rgbs = [int(i) / 255.0 for i in splits]
|
||||
return rgb_to_hex(parsed_rgbs)
|
||||
|
||||
else:
|
||||
return web2hex(color)
|
||||
|
||||
|
||||
def fill_default_values(style, default_style):
|
||||
default = DEFAULT_STYLE.copy()
|
||||
default.update(default_style)
|
||||
for attr in default:
|
||||
if attr not in style:
|
||||
style[attr] = default[attr]
|
||||
|
||||
|
||||
def parse_style(style, default_style):
|
||||
manim_style = {}
|
||||
fill_default_values(style, default_style)
|
||||
|
||||
manim_style["fill_opacity"] = float(style["fill-opacity"])
|
||||
manim_style["stroke_opacity"] = float(style["stroke-opacity"])
|
||||
manim_style["stroke_width"] = float(style["stroke-width"])
|
||||
|
||||
if style["fill"] == "none":
|
||||
manim_style["fill_opacity"] = 0
|
||||
else:
|
||||
manim_style["fill_color"] = parse_color(style["fill"])
|
||||
|
||||
if style["stroke"] == "none":
|
||||
manim_style["stroke_width"] = 0
|
||||
if "fill_color" in manim_style:
|
||||
manim_style["stroke_color"] = manim_style["fill_color"]
|
||||
else:
|
||||
manim_style["stroke_color"] = parse_color(style["stroke"])
|
||||
|
||||
return manim_style
|
||||
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
|
||||
return np.array([x, y, 0.0])
|
||||
|
||||
|
||||
class SVGMobject(VMobject):
|
||||
@@ -102,25 +36,249 @@ class SVGMobject(VMobject):
|
||||
"should_center": True,
|
||||
"height": 2,
|
||||
"width": None,
|
||||
# Must be filled in in a subclass, or when called
|
||||
"file_name": None,
|
||||
"unpack_groups": True, # if False, creates a hierarchy of VGroups
|
||||
"stroke_width": 0.0,
|
||||
"fill_opacity": 1.0,
|
||||
"path_string_config": {}
|
||||
# Style that overrides the original svg
|
||||
"color": None,
|
||||
"opacity": None,
|
||||
"fill_color": None,
|
||||
"fill_opacity": None,
|
||||
"stroke_width": None,
|
||||
"stroke_color": None,
|
||||
"stroke_opacity": None,
|
||||
# Style that fills only when not specified
|
||||
# If None, regarded as default values from svg standard
|
||||
"svg_default": {
|
||||
"color": None,
|
||||
"opacity": None,
|
||||
"fill_color": None,
|
||||
"fill_opacity": None,
|
||||
"stroke_width": None,
|
||||
"stroke_color": None,
|
||||
"stroke_opacity": None,
|
||||
},
|
||||
"path_string_config": {},
|
||||
}
|
||||
|
||||
def __init__(self, file_name=None, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.file_name = file_name or self.file_name
|
||||
if file_name is None:
|
||||
raise Exception("Must specify file for SVGMobject")
|
||||
self.file_path = get_full_vector_image_path(file_name)
|
||||
|
||||
def __init__(self, file_name: str | None = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.file_name = file_name or self.file_name
|
||||
self.init_svg_mobject()
|
||||
self.init_colors()
|
||||
self.move_into_position()
|
||||
|
||||
def move_into_position(self):
|
||||
def init_svg_mobject(self) -> None:
|
||||
hash_val = hash_obj(self.hash_seed)
|
||||
if hash_val in SVG_HASH_TO_MOB_MAP:
|
||||
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
|
||||
self.add(*mob)
|
||||
return
|
||||
|
||||
self.generate_mobject()
|
||||
SVG_HASH_TO_MOB_MAP[hash_val] = self.copy()
|
||||
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
# Returns data which can uniquely represent the result of `init_points`.
|
||||
# The hashed value of it is stored as a key in `SVG_HASH_TO_MOB_MAP`.
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.file_name
|
||||
)
|
||||
|
||||
def generate_mobject(self) -> None:
|
||||
file_path = self.get_file_path()
|
||||
element_tree = ET.parse(file_path)
|
||||
new_tree = self.modify_xml_tree(element_tree)
|
||||
# Create a temporary svg file to dump modified svg to be parsed
|
||||
root, ext = os.path.splitext(file_path)
|
||||
modified_file_path = root + "_" + ext
|
||||
new_tree.write(modified_file_path)
|
||||
|
||||
svg = se.SVG.parse(modified_file_path)
|
||||
os.remove(modified_file_path)
|
||||
|
||||
mobjects = self.get_mobjects_from(svg)
|
||||
self.add(*mobjects)
|
||||
self.flip(RIGHT) # Flip y
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
if self.file_name is None:
|
||||
raise Exception("Must specify file for SVGMobject")
|
||||
return get_full_vector_image_path(self.file_name)
|
||||
|
||||
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
|
||||
config_style_dict = self.generate_config_style_dict()
|
||||
style_keys = (
|
||||
"fill",
|
||||
"fill-opacity",
|
||||
"stroke",
|
||||
"stroke-opacity",
|
||||
"stroke-width",
|
||||
"style"
|
||||
)
|
||||
root = element_tree.getroot()
|
||||
root_style_dict = {
|
||||
k: v for k, v in root.attrib.items()
|
||||
if k in style_keys
|
||||
}
|
||||
|
||||
new_root = ET.Element("svg", {})
|
||||
config_style_node = ET.SubElement(new_root, "g", config_style_dict)
|
||||
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
|
||||
root_style_node.extend(root)
|
||||
return ET.ElementTree(new_root)
|
||||
|
||||
def generate_config_style_dict(self) -> dict[str, str]:
|
||||
keys_converting_dict = {
|
||||
"fill": ("color", "fill_color"),
|
||||
"fill-opacity": ("opacity", "fill_opacity"),
|
||||
"stroke": ("color", "stroke_color"),
|
||||
"stroke-opacity": ("opacity", "stroke_opacity"),
|
||||
"stroke-width": ("stroke_width",)
|
||||
}
|
||||
svg_default_dict = self.svg_default
|
||||
result = {}
|
||||
for svg_key, style_keys in keys_converting_dict.items():
|
||||
for style_key in style_keys:
|
||||
if svg_default_dict[style_key] is None:
|
||||
continue
|
||||
result[svg_key] = str(svg_default_dict[style_key])
|
||||
return result
|
||||
|
||||
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
|
||||
result = []
|
||||
for shape in svg.elements():
|
||||
if isinstance(shape, se.Group):
|
||||
continue
|
||||
elif isinstance(shape, se.Path):
|
||||
mob = self.path_to_mobject(shape)
|
||||
elif isinstance(shape, se.SimpleLine):
|
||||
mob = self.line_to_mobject(shape)
|
||||
elif isinstance(shape, se.Rect):
|
||||
mob = self.rect_to_mobject(shape)
|
||||
elif isinstance(shape, se.Circle):
|
||||
mob = self.circle_to_mobject(shape)
|
||||
elif isinstance(shape, se.Ellipse):
|
||||
mob = self.ellipse_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polygon):
|
||||
mob = self.polygon_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polyline):
|
||||
mob = self.polyline_to_mobject(shape)
|
||||
# elif isinstance(shape, se.Text):
|
||||
# mob = self.text_to_mobject(shape)
|
||||
elif type(shape) == se.SVGElement:
|
||||
continue
|
||||
else:
|
||||
log.warning(f"Unsupported element type: {type(shape)}")
|
||||
continue
|
||||
if not mob.has_points():
|
||||
continue
|
||||
self.apply_style_to_mobject(mob, shape)
|
||||
if isinstance(shape, se.Transformable) and shape.apply:
|
||||
self.handle_transform(mob, shape.transform)
|
||||
result.append(mob)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:
|
||||
mat = np.array([
|
||||
[matrix.a, matrix.c],
|
||||
[matrix.b, matrix.d]
|
||||
])
|
||||
vec = np.array([matrix.e, matrix.f, 0.0])
|
||||
mob.apply_matrix(mat)
|
||||
mob.shift(vec)
|
||||
return mob
|
||||
|
||||
@staticmethod
|
||||
def apply_style_to_mobject(
|
||||
mob: VMobject,
|
||||
shape: se.GraphicObject
|
||||
) -> VMobject:
|
||||
mob.set_style(
|
||||
stroke_width=shape.stroke_width,
|
||||
stroke_color=shape.stroke.hex,
|
||||
stroke_opacity=shape.stroke.opacity,
|
||||
fill_color=shape.fill.hex,
|
||||
fill_opacity=shape.fill.opacity
|
||||
)
|
||||
return mob
|
||||
|
||||
@staticmethod
|
||||
def handle_transform(mob, matrix):
|
||||
mat = np.array([
|
||||
[matrix.a, matrix.c],
|
||||
[matrix.b, matrix.d]
|
||||
])
|
||||
vec = np.array([matrix.e, matrix.f, 0.0])
|
||||
mob.apply_matrix(mat)
|
||||
mob.shift(vec)
|
||||
return mob
|
||||
|
||||
def path_to_mobject(self, path: se.Path) -> VMobjectFromSVGPath:
|
||||
return VMobjectFromSVGPath(path, **self.path_string_config)
|
||||
|
||||
def line_to_mobject(self, line: se.Line) -> Line:
|
||||
return Line(
|
||||
start=_convert_point_to_3d(line.x1, line.y1),
|
||||
end=_convert_point_to_3d(line.x2, line.y2)
|
||||
)
|
||||
|
||||
def rect_to_mobject(self, rect: se.Rect) -> Rectangle:
|
||||
if rect.rx == 0 or rect.ry == 0:
|
||||
mob = Rectangle(
|
||||
width=rect.width,
|
||||
height=rect.height,
|
||||
)
|
||||
else:
|
||||
mob = RoundedRectangle(
|
||||
width=rect.width,
|
||||
height=rect.height * rect.rx / rect.ry,
|
||||
corner_radius=rect.rx
|
||||
)
|
||||
mob.stretch_to_fit_height(rect.height)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
rect.x + rect.width / 2,
|
||||
rect.y + rect.height / 2
|
||||
))
|
||||
return mob
|
||||
|
||||
def circle_to_mobject(self, circle: se.Circle) -> Circle:
|
||||
# svgelements supports `rx` & `ry` but `r`
|
||||
mob = Circle(radius=circle.rx)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
circle.cx, circle.cy
|
||||
))
|
||||
return mob
|
||||
|
||||
def ellipse_to_mobject(self, ellipse: se.Ellipse) -> Circle:
|
||||
mob = Circle(radius=ellipse.rx)
|
||||
mob.stretch_to_fit_height(2 * ellipse.ry)
|
||||
mob.shift(_convert_point_to_3d(
|
||||
ellipse.cx, ellipse.cy
|
||||
))
|
||||
return mob
|
||||
|
||||
def polygon_to_mobject(self, polygon: se.Polygon) -> Polygon:
|
||||
points = [
|
||||
_convert_point_to_3d(*point)
|
||||
for point in polygon
|
||||
]
|
||||
return Polygon(*points)
|
||||
|
||||
def polyline_to_mobject(self, polyline: se.Polyline) -> Polyline:
|
||||
points = [
|
||||
_convert_point_to_3d(*point)
|
||||
for point in polyline
|
||||
]
|
||||
return Polyline(*points)
|
||||
|
||||
def text_to_mobject(self, text: se.Text):
|
||||
pass
|
||||
|
||||
def move_into_position(self) -> None:
|
||||
if self.should_center:
|
||||
self.center()
|
||||
if self.height is not None:
|
||||
@@ -128,338 +286,26 @@ class SVGMobject(VMobject):
|
||||
if self.width is not None:
|
||||
self.set_width(self.width)
|
||||
|
||||
def init_colors(self, override=False):
|
||||
super().init_colors(override=override)
|
||||
|
||||
def init_points(self):
|
||||
etree = ElementTree.parse(self.file_path)
|
||||
wrapper = cssselect2.ElementWrapper.from_xml_root(etree)
|
||||
svg = etree.getroot()
|
||||
namespace = svg.tag.split("}")[0][1:]
|
||||
self.ref_to_element = {}
|
||||
self.css_matcher = cssselect2.Matcher()
|
||||
|
||||
for style in etree.findall(f"{{{namespace}}}style"):
|
||||
self.parse_css_style(style.text)
|
||||
|
||||
mobjects = self.get_mobjects_from(wrapper, dict())
|
||||
if self.unpack_groups:
|
||||
self.add(*mobjects)
|
||||
else:
|
||||
self.add(*mobjects[0].submobjects)
|
||||
|
||||
def get_mobjects_from(self, wrapper, style):
|
||||
result = []
|
||||
element = wrapper.etree_element
|
||||
if not isinstance(element, ElementTree.Element):
|
||||
return result
|
||||
|
||||
matches = self.css_matcher.match(wrapper)
|
||||
if matches:
|
||||
for match in matches:
|
||||
_, _, _, css_style = match
|
||||
style.update(css_style)
|
||||
style = cascade_element_style(element, style)
|
||||
|
||||
tag = element.tag.split("}")[-1]
|
||||
if tag == 'defs':
|
||||
self.update_ref_to_element(wrapper, style)
|
||||
elif tag in ['g', 'svg', 'symbol']:
|
||||
result += it.chain(*(
|
||||
self.get_mobjects_from(child, style)
|
||||
for child in wrapper.iter_children()
|
||||
))
|
||||
elif tag == 'path':
|
||||
result.append(self.path_string_to_mobject(
|
||||
element.get('d'), style
|
||||
))
|
||||
elif tag == 'use':
|
||||
result += self.use_to_mobjects(element, style)
|
||||
elif tag == 'line':
|
||||
result.append(self.line_to_mobject(element, style))
|
||||
elif tag == 'rect':
|
||||
result.append(self.rect_to_mobject(element, style))
|
||||
elif tag == 'circle':
|
||||
result.append(self.circle_to_mobject(element, style))
|
||||
elif tag == 'ellipse':
|
||||
result.append(self.ellipse_to_mobject(element, style))
|
||||
elif tag in ['polygon', 'polyline']:
|
||||
result.append(self.polygon_to_mobject(element, style))
|
||||
elif tag == 'style':
|
||||
pass
|
||||
else:
|
||||
log.warning(f"Unsupported element type: {tag}")
|
||||
pass # TODO, support <text> tag
|
||||
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 generate_default_style(self):
|
||||
style = {
|
||||
"fill-opacity": self.fill_opacity,
|
||||
"stroke-width": self.stroke_width,
|
||||
"stroke-opacity": self.stroke_opacity,
|
||||
}
|
||||
if self.color:
|
||||
style["fill"] = style["stroke"] = self.color
|
||||
if self.fill_color:
|
||||
style["fill"] = self.fill_color
|
||||
if self.stroke_color:
|
||||
style["stroke"] = self.stroke_color
|
||||
return style
|
||||
|
||||
def parse_css_style(self, css):
|
||||
rules = parse_stylesheet(css, True, True)
|
||||
for rule in rules:
|
||||
selectors = cssselect2.compile_selector_list(rule.prelude)
|
||||
declarations = parse_declaration_list(rule.content)
|
||||
style = {
|
||||
declaration.name: css_serialize(declaration.value)
|
||||
for declaration in declarations
|
||||
if declaration.name in DEFAULT_STYLE
|
||||
}
|
||||
payload = style
|
||||
for selector in selectors:
|
||||
self.css_matcher.add_selector(selector, payload)
|
||||
|
||||
def path_string_to_mobject(self, path_string, style):
|
||||
return VMobjectFromSVGPathstring(
|
||||
path_string,
|
||||
**self.path_string_config,
|
||||
**parse_style(style, self.generate_default_style()),
|
||||
)
|
||||
|
||||
def use_to_mobjects(self, use_element, local_style):
|
||||
# Remove initial "#" character
|
||||
ref = use_element.get(r"{http://www.w3.org/1999/xlink}href")[1:]
|
||||
if ref not in self.ref_to_element:
|
||||
log.warning(f"{ref} not recognized")
|
||||
return VGroup()
|
||||
def_element, def_style = self.ref_to_element[ref]
|
||||
style = local_style.copy()
|
||||
style.update(def_style)
|
||||
return self.get_mobjects_from(def_element, style)
|
||||
|
||||
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, style):
|
||||
path_string = polygon_element.get("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, style)
|
||||
|
||||
def circle_to_mobject(self, circle_element, style):
|
||||
x, y, r = (
|
||||
self.attribute_to_float(circle_element.get(key, "0.0"))
|
||||
for key in ("cx", "cy", "r")
|
||||
)
|
||||
return Circle(
|
||||
radius=r,
|
||||
**parse_style(style, self.generate_default_style())
|
||||
).shift(x * RIGHT + y * DOWN)
|
||||
|
||||
def ellipse_to_mobject(self, circle_element, style):
|
||||
x, y, rx, ry = (
|
||||
self.attribute_to_float(circle_element.get(key, "0.0"))
|
||||
for key in ("cx", "cy", "rx", "ry")
|
||||
)
|
||||
result = Circle(**parse_style(style, self.generate_default_style()))
|
||||
result.stretch(rx, 0)
|
||||
result.stretch(ry, 1)
|
||||
result.shift(x * RIGHT + y * DOWN)
|
||||
return result
|
||||
|
||||
def rect_to_mobject(self, rect_element, style):
|
||||
stroke_width = rect_element.get("stroke-width", "")
|
||||
corner_radius = rect_element.get("rx", "")
|
||||
|
||||
if stroke_width in ["", "none", "0"]:
|
||||
stroke_width = 0
|
||||
|
||||
if corner_radius in ["", "0", "none"]:
|
||||
corner_radius = 0
|
||||
|
||||
corner_radius = float(corner_radius)
|
||||
|
||||
parsed_style = parse_style(style, self.generate_default_style())
|
||||
parsed_style["stroke_width"] = stroke_width
|
||||
|
||||
if corner_radius == 0:
|
||||
mob = Rectangle(
|
||||
width=self.attribute_to_float(
|
||||
rect_element.get("width", "")
|
||||
),
|
||||
height=self.attribute_to_float(
|
||||
rect_element.get("height", "")
|
||||
),
|
||||
**parsed_style,
|
||||
)
|
||||
else:
|
||||
mob = RoundedRectangle(
|
||||
width=self.attribute_to_float(
|
||||
rect_element.get("width", "")
|
||||
),
|
||||
height=self.attribute_to_float(
|
||||
rect_element.get("height", "")
|
||||
),
|
||||
corner_radius=corner_radius,
|
||||
**parsed_style
|
||||
)
|
||||
|
||||
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
|
||||
return mob
|
||||
|
||||
def line_to_mobject(self, line_element, style):
|
||||
x1, y1, x2, y2 = (
|
||||
self.attribute_to_float(line_element.get(key, "0.0"))
|
||||
for key in ("x1", "y1", "x2", "y2")
|
||||
)
|
||||
return Line(
|
||||
[x1, -y1, 0], [x2, -y2, 0],
|
||||
**parse_style(style, self.generate_default_style())
|
||||
)
|
||||
|
||||
def handle_transforms(self, element, mobject):
|
||||
x, y = (
|
||||
self.attribute_to_float(element.get(key, "0.0"))
|
||||
for key in ("x", "y")
|
||||
)
|
||||
mobject.shift(x * RIGHT + y * DOWN)
|
||||
|
||||
transform_names = [
|
||||
"matrix",
|
||||
"translate", "translateX", "translateY",
|
||||
"scale", "scaleX", "scaleY",
|
||||
"rotate",
|
||||
"skewX", "skewY"
|
||||
]
|
||||
transform_pattern = re.compile("|".join([x + r"[^)]*\)" for x in transform_names]))
|
||||
number_pattern = re.compile(r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?")
|
||||
transforms = transform_pattern.findall(element.get("transform", ""))[::-1]
|
||||
|
||||
for transform in transforms:
|
||||
op_name, op_args = transform.split("(")
|
||||
op_name = op_name.strip()
|
||||
op_args = [float(x) for x in number_pattern.findall(op_args)]
|
||||
|
||||
if op_name == "matrix":
|
||||
self._handle_matrix_transform(mobject, op_name, op_args)
|
||||
elif op_name.startswith("translate"):
|
||||
self._handle_translate_transform(mobject, op_name, op_args)
|
||||
elif op_name.startswith("scale"):
|
||||
self._handle_scale_transform(mobject, op_name, op_args)
|
||||
elif op_name == "rotate":
|
||||
self._handle_rotate_transform(mobject, op_name, op_args)
|
||||
elif op_name.startswith("skew"):
|
||||
self._handle_skew_transform(mobject, op_name, op_args)
|
||||
|
||||
def _handle_matrix_transform(self, mobject, op_name, op_args):
|
||||
transform = np.array(op_args).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)
|
||||
|
||||
def _handle_translate_transform(self, mobject, op_name, op_args):
|
||||
if op_name.endswith("X"):
|
||||
x, y = op_args[0], 0
|
||||
elif op_name.endswith("Y"):
|
||||
x, y = 0, op_args[0]
|
||||
else:
|
||||
x, y = op_args
|
||||
mobject.shift(x * RIGHT + y * DOWN)
|
||||
|
||||
def _handle_scale_transform(self, mobject, op_name, op_args):
|
||||
if op_name.endswith("X"):
|
||||
sx, sy = op_args[0], 1
|
||||
elif op_name.endswith("Y"):
|
||||
sx, sy = 1, op_args[0]
|
||||
elif len(op_args) == 2:
|
||||
sx, sy = op_args
|
||||
else:
|
||||
sx = sy = op_args[0]
|
||||
if sx < 0:
|
||||
mobject.flip(UP)
|
||||
sx = -sx
|
||||
if sy < 0:
|
||||
mobject.flip(RIGHT)
|
||||
sy = -sy
|
||||
mobject.scale(np.array([sx, sy, 1]), about_point=ORIGIN)
|
||||
|
||||
def _handle_rotate_transform(self, mobject, op_name, op_args):
|
||||
if len(op_args) == 1:
|
||||
mobject.rotate(op_args[0] * DEGREES, axis=IN, about_point=ORIGIN)
|
||||
else:
|
||||
deg, x, y = op_args
|
||||
mobject.rotate(deg * DEGREES, axis=IN, about_point=np.array([x, y, 0]))
|
||||
|
||||
def _handle_skew_transform(self, mobject, op_name, op_args):
|
||||
rad = op_args[0] * DEGREES
|
||||
if op_name == "skewX":
|
||||
tana = np.tan(rad)
|
||||
self._handle_matrix_transform(mobject, None, [1., 0., tana, 1., 0., 0.])
|
||||
elif op_name == "skewY":
|
||||
tana = np.tan(rad)
|
||||
self._handle_matrix_transform(mobject, None, [1., tana, 0., 1., 0., 0.])
|
||||
|
||||
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_childWrappers_have_id(self, wrapper):
|
||||
all_childWrappers_have_id = []
|
||||
element = wrapper.etree_element
|
||||
if not isinstance(element, ElementTree.Element):
|
||||
return
|
||||
if element.get('id'):
|
||||
return [wrapper]
|
||||
for e in wrapper.iter_children():
|
||||
all_childWrappers_have_id.append(self.get_all_childWrappers_have_id(e))
|
||||
return self.flatten([e for e in all_childWrappers_have_id if e])
|
||||
|
||||
def update_ref_to_element(self, wrapper, style):
|
||||
new_refs = {
|
||||
e.etree_element.get('id', ''): (e, style)
|
||||
for e in self.get_all_childWrappers_have_id(wrapper)
|
||||
}
|
||||
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: se.Path, **kwargs):
|
||||
# Get rid of arcs
|
||||
path_obj.approximate_arcs_with_quads()
|
||||
self.path_obj = path_obj
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
# After a given svg_path has been converted into points, the result
|
||||
# will be saved to a file so that future calls for the same path
|
||||
# don't need to retrace the same computation.
|
||||
hasher = hashlib.sha256(self.path_string.encode())
|
||||
path_string = self.path_obj.d()
|
||||
hasher = hashlib.sha256(path_string.encode())
|
||||
path_hash = hasher.hexdigest()[:16]
|
||||
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
|
||||
tris_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_tris.npy")
|
||||
@@ -476,239 +322,27 @@ class VMobjectFromSVGPathstring(VMobject):
|
||||
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_commands(self):
|
||||
relative_point = ORIGIN
|
||||
for command, coord_string in self.get_commands_and_coord_strings():
|
||||
func, number_types_str = self.command_to_function(command)
|
||||
upper_command = command.upper()
|
||||
if upper_command == "Z":
|
||||
func() # `close_path` takes no arguments
|
||||
relative_point = self.get_last_point()
|
||||
continue
|
||||
|
||||
number_types = np.array(list(number_types_str))
|
||||
n_numbers = len(number_types_str)
|
||||
number_list = _PathStringParser(coord_string, number_types_str).args
|
||||
number_groups = np.array(number_list).reshape((-1, n_numbers))
|
||||
|
||||
for ind, numbers in enumerate(number_groups):
|
||||
if command.islower():
|
||||
# Treat it as a relative command
|
||||
numbers[number_types == "x"] += relative_point[0]
|
||||
numbers[number_types == "y"] += relative_point[1]
|
||||
|
||||
if upper_command == "A":
|
||||
args = [*numbers[:5], np.array([*numbers[5:7], 0.0])]
|
||||
elif upper_command == "H":
|
||||
args = [np.array([numbers[0], relative_point[1], 0.0])]
|
||||
elif upper_command == "V":
|
||||
args = [np.array([relative_point[0], numbers[0], 0.0])]
|
||||
else:
|
||||
args = list(np.hstack((
|
||||
numbers.reshape((-1, 2)), np.zeros((n_numbers // 2, 1))
|
||||
)))
|
||||
if upper_command == "M" and ind != 0:
|
||||
# M x1 y1 x2 y2 is equal to M x1 y1 L x2 y2
|
||||
func, _ = self.command_to_function("L")
|
||||
func(*args)
|
||||
relative_point = self.get_last_point()
|
||||
|
||||
def add_elliptical_arc_to(self, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, point):
|
||||
def close_to_zero(a, threshold=1e-5):
|
||||
return abs(a) < threshold
|
||||
|
||||
def solve_2d_linear_equation(a, b, c):
|
||||
"""
|
||||
Using Crammer's rule to solve the linear equation `[a b]x = c`
|
||||
where `a`, `b` and `c` are all 2d vectors.
|
||||
"""
|
||||
def det(a, b):
|
||||
return a[0] * b[1] - a[1] * b[0]
|
||||
d = det(a, b)
|
||||
if close_to_zero(d):
|
||||
raise Exception("Cannot handle 0 determinant.")
|
||||
return [det(c, b) / d, det(a, c) / d]
|
||||
|
||||
def get_arc_center_and_angles(x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, x1, y1):
|
||||
"""
|
||||
The parameter functions of an ellipse rotated `phi` radians counterclockwise is (on `alpha`):
|
||||
x = cx + rx * cos(alpha) * cos(phi) + ry * sin(alpha) * sin(phi),
|
||||
y = cy + rx * cos(alpha) * sin(phi) - ry * sin(alpha) * cos(phi).
|
||||
Now we have two points sitting on the ellipse: `(x0, y0)`, `(x1, y1)`, corresponding to 4 equations,
|
||||
and we want to hunt for 4 variables: `cx`, `cy`, `alpha0` and `alpha_1`.
|
||||
Let `d_alpha = alpha1 - alpha0`, then:
|
||||
if `sweep_flag = 0` and `large_arc_flag = 1`, then `PI <= d_alpha < 2 * PI`;
|
||||
if `sweep_flag = 0` and `large_arc_flag = 0`, then `0 < d_alpha <= PI`;
|
||||
if `sweep_flag = 1` and `large_arc_flag = 0`, then `-PI <= d_alpha < 0`;
|
||||
if `sweep_flag = 1` and `large_arc_flag = 1`, then `-2 * PI < d_alpha <= -PI`.
|
||||
"""
|
||||
xd = x1 - x0
|
||||
yd = y1 - y0
|
||||
if close_to_zero(xd) and close_to_zero(yd):
|
||||
raise Exception("Cannot find arc center since the start point and the end point meet.")
|
||||
# Find `p = cos(alpha1) - cos(alpha0)`, `q = sin(alpha1) - sin(alpha0)`
|
||||
eq0 = [rx * np.cos(phi), ry * np.sin(phi), xd]
|
||||
eq1 = [rx * np.sin(phi), -ry * np.cos(phi), yd]
|
||||
p, q = solve_2d_linear_equation(*zip(eq0, eq1))
|
||||
# Find `s = (alpha1 - alpha0) / 2`, `t = (alpha1 + alpha0) / 2`
|
||||
# If `sin(s) = 0`, this requires `p = q = 0`,
|
||||
# implying `xd = yd = 0`, which is impossible.
|
||||
sin_s = (p ** 2 + q ** 2) ** 0.5 / 2
|
||||
if sweep_flag:
|
||||
sin_s = -sin_s
|
||||
sin_s = clip(sin_s, -1, 1)
|
||||
s = np.arcsin(sin_s)
|
||||
if large_arc_flag:
|
||||
if not sweep_flag:
|
||||
s = PI - s
|
||||
else:
|
||||
s = -PI - s
|
||||
sin_t = -p / (2 * sin_s)
|
||||
cos_t = q / (2 * sin_s)
|
||||
cos_t = clip(cos_t, -1, 1)
|
||||
t = np.arccos(cos_t)
|
||||
if sin_t <= 0:
|
||||
t = -t
|
||||
# We can make sure `0 < abs(s) < PI`, `-PI <= t < PI`.
|
||||
alpha0 = t - s
|
||||
alpha_1 = t + s
|
||||
cx = x0 - rx * np.cos(alpha0) * np.cos(phi) - ry * np.sin(alpha0) * np.sin(phi)
|
||||
cy = y0 - rx * np.cos(alpha0) * np.sin(phi) + ry * np.sin(alpha0) * np.cos(phi)
|
||||
return cx, cy, alpha0, alpha_1
|
||||
|
||||
def get_point_on_ellipse(cx, cy, rx, ry, phi, angle):
|
||||
return np.array([
|
||||
cx + rx * np.cos(angle) * np.cos(phi) + ry * np.sin(angle) * np.sin(phi),
|
||||
cy + rx * np.cos(angle) * np.sin(phi) - ry * np.sin(angle) * np.cos(phi),
|
||||
0
|
||||
])
|
||||
|
||||
def convert_elliptical_arc_to_quadratic_bezier_curve(
|
||||
cx, cy, rx, ry, phi, start_angle, end_angle, n_components=8
|
||||
):
|
||||
theta = (end_angle - start_angle) / n_components / 2
|
||||
handles = np.array([
|
||||
get_point_on_ellipse(cx, cy, rx / np.cos(theta), ry / np.cos(theta), phi, a)
|
||||
for a in np.linspace(
|
||||
start_angle + theta,
|
||||
end_angle - theta,
|
||||
n_components,
|
||||
)
|
||||
])
|
||||
anchors = np.array([
|
||||
get_point_on_ellipse(cx, cy, rx, ry, phi, a)
|
||||
for a in np.linspace(
|
||||
start_angle + theta * 2,
|
||||
end_angle,
|
||||
n_components,
|
||||
)
|
||||
])
|
||||
return handles, anchors
|
||||
|
||||
phi = x_axis_rotation * DEGREES
|
||||
x0, y0 = self.get_last_point()[:2]
|
||||
cx, cy, start_angle, end_angle = get_arc_center_and_angles(
|
||||
x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, point[0], point[1]
|
||||
)
|
||||
handles, anchors = convert_elliptical_arc_to_quadratic_bezier_curve(
|
||||
cx, cy, rx, ry, phi, start_angle, end_angle
|
||||
)
|
||||
for handle, anchor in zip(handles, anchors):
|
||||
self.add_quadratic_bezier_curve_to(handle, anchor)
|
||||
|
||||
def command_to_function(self, command):
|
||||
return self.get_command_to_function_map()[command.upper()]
|
||||
|
||||
def get_command_to_function_map(self):
|
||||
"""
|
||||
Associates svg command to VMobject function, and
|
||||
the types of arguments it takes in
|
||||
"""
|
||||
return {
|
||||
"M": (self.start_new_path, "xy"),
|
||||
"L": (self.add_line_to, "xy"),
|
||||
"H": (self.add_line_to, "x"),
|
||||
"V": (self.add_line_to, "y"),
|
||||
"C": (self.add_cubic_bezier_curve_to, "xyxyxy"),
|
||||
"S": (self.add_smooth_cubic_curve_to, "xyxy"),
|
||||
"Q": (self.add_quadratic_bezier_curve_to, "xyxy"),
|
||||
"T": (self.add_smooth_curve_to, "xy"),
|
||||
"A": (self.add_elliptical_arc_to, "uuaffxy"),
|
||||
"Z": (self.close_path, ""),
|
||||
def handle_commands(self) -> None:
|
||||
segment_class_to_func_map = {
|
||||
se.Move: (self.start_new_path, ("end",)),
|
||||
se.Close: (self.close_path, ()),
|
||||
se.Line: (self.add_line_to, ("end",)),
|
||||
se.QuadraticBezier: (self.add_quadratic_bezier_curve_to, ("control", "end")),
|
||||
se.CubicBezier: (self.add_cubic_bezier_curve_to, ("control1", "control2", "end"))
|
||||
}
|
||||
for segment in self.path_obj:
|
||||
segment_class = segment.__class__
|
||||
func, attr_names = segment_class_to_func_map[segment_class]
|
||||
points = [
|
||||
_convert_point_to_3d(*segment.__getattribute__(attr_name))
|
||||
for attr_name in attr_names
|
||||
]
|
||||
func(*points)
|
||||
|
||||
def get_original_path_string(self):
|
||||
return self.path_string
|
||||
|
||||
|
||||
class InvalidPathError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class _PathStringParser:
|
||||
# modified from https://github.com/regebro/svg.path/
|
||||
def __init__(self, arguments, rules):
|
||||
self.args = []
|
||||
arguments = bytearray(arguments, "ascii")
|
||||
self._strip_array(arguments)
|
||||
while arguments:
|
||||
for rule in rules:
|
||||
self._rule_to_function_map[rule](arguments)
|
||||
|
||||
@property
|
||||
def _rule_to_function_map(self):
|
||||
return {
|
||||
"x": self._get_number,
|
||||
"y": self._get_number,
|
||||
"a": self._get_number,
|
||||
"u": self._get_unsigned_number,
|
||||
"f": self._get_flag,
|
||||
}
|
||||
|
||||
def _strip_array(self, arg_array):
|
||||
# wsp: (0x9, 0x20, 0xA, 0xC, 0xD) with comma 0x2C
|
||||
# https://www.w3.org/TR/SVG/paths.html#PathDataBNF
|
||||
while arg_array and arg_array[0] in [0x9, 0x20, 0xA, 0xC, 0xD, 0x2C]:
|
||||
arg_array[0:1] = b""
|
||||
|
||||
def _get_number(self, arg_array):
|
||||
pattern = re.compile(rb"^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?")
|
||||
res = pattern.search(arg_array)
|
||||
if not res:
|
||||
raise InvalidPathError(f"Expected a number, got '{arg_array}'")
|
||||
number = float(res.group())
|
||||
self.args.append(number)
|
||||
arg_array[res.start():res.end()] = b""
|
||||
self._strip_array(arg_array)
|
||||
return number
|
||||
|
||||
def _get_unsigned_number(self, arg_array):
|
||||
number = self._get_number(arg_array)
|
||||
if number < 0:
|
||||
raise InvalidPathError(f"Expected an unsigned number, got '{number}'")
|
||||
return number
|
||||
|
||||
def _get_flag(self, arg_array):
|
||||
flag = arg_array[0]
|
||||
if flag != 48 and flag != 49:
|
||||
raise InvalidPathError(f"Expected a flag (0/1), got '{chr(flag)}'")
|
||||
flag -= 48
|
||||
self.args.append(flag)
|
||||
arg_array[0:1] = b""
|
||||
self._strip_array(arg_array)
|
||||
return flag
|
||||
# Get rid of the side effect of trailing "Z M" commands.
|
||||
if self.has_new_path_started():
|
||||
self.resize_points(self.get_num_points() - 1)
|
||||
|
||||
@@ -1,67 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Sequence, Union
|
||||
from functools import reduce
|
||||
import operator as op
|
||||
import colour
|
||||
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
|
||||
from manimlib.utils.tex_file_writing import get_tex_config
|
||||
from manimlib.utils.tex_file_writing import display_during_execution
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
SCALE_FACTOR_PER_FONT_POINT = 0.001
|
||||
|
||||
|
||||
tex_string_with_color_to_mob_map = {}
|
||||
|
||||
|
||||
class SingleStringTex(VMobject):
|
||||
class SingleStringTex(SVGMobject):
|
||||
CONFIG = {
|
||||
"height": None,
|
||||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0,
|
||||
"should_center": True,
|
||||
"svg_default": {
|
||||
"color": WHITE,
|
||||
},
|
||||
"path_string_config": {
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
},
|
||||
"font_size": 48,
|
||||
"height": None,
|
||||
"organize_left_to_right": False,
|
||||
"alignment": "\\centering",
|
||||
"math_mode": True,
|
||||
"organize_left_to_right": False,
|
||||
}
|
||||
|
||||
def __init__(self, tex_string, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
assert(isinstance(tex_string, str))
|
||||
def __init__(self, tex_string: str, **kwargs):
|
||||
assert isinstance(tex_string, str)
|
||||
self.tex_string = tex_string
|
||||
if tex_string not in tex_string_with_color_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,
|
||||
color=self.color,
|
||||
stroke_width=self.stroke_width,
|
||||
path_string_config={
|
||||
"should_subdivide_sharp_curves": True,
|
||||
"should_remove_null_curves": True,
|
||||
}
|
||||
)
|
||||
tex_string_with_color_to_mob_map[(self.color, tex_string)] = svg_mob
|
||||
self.add(*(
|
||||
sm.copy()
|
||||
for sm in tex_string_with_color_to_mob_map[(self.color, tex_string)]
|
||||
))
|
||||
self.init_colors(override=False)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if self.height is None:
|
||||
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
|
||||
if self.organize_left_to_right:
|
||||
self.organize_submobjects_left_to_right()
|
||||
|
||||
def get_tex_file_body(self, tex_string):
|
||||
@property
|
||||
def hash_seed(self) -> tuple:
|
||||
return (
|
||||
self.__class__.__name__,
|
||||
self.svg_default,
|
||||
self.path_string_config,
|
||||
self.tex_string,
|
||||
self.alignment,
|
||||
self.math_mode
|
||||
)
|
||||
|
||||
def get_file_path(self) -> str:
|
||||
full_tex = self.get_tex_file_body(self.tex_string)
|
||||
with display_during_execution(f"Writing \"{self.tex_string}\""):
|
||||
file_path = tex_to_svg_file(full_tex)
|
||||
return file_path
|
||||
|
||||
def get_tex_file_body(self, tex_string: str) -> str:
|
||||
new_tex = self.get_modified_expression(tex_string)
|
||||
if self.math_mode:
|
||||
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
|
||||
@@ -74,10 +79,10 @@ class SingleStringTex(VMobject):
|
||||
new_tex
|
||||
)
|
||||
|
||||
def get_modified_expression(self, tex_string):
|
||||
def get_modified_expression(self, tex_string: str) -> str:
|
||||
return self.modify_special_strings(tex_string.strip())
|
||||
|
||||
def modify_special_strings(self, tex):
|
||||
def modify_special_strings(self, tex: str) -> str:
|
||||
tex = tex.strip()
|
||||
should_add_filler = reduce(op.or_, [
|
||||
# Fraction line needs something to be over
|
||||
@@ -95,6 +100,18 @@ class SingleStringTex(VMobject):
|
||||
filler = "{\\quad}"
|
||||
tex += filler
|
||||
|
||||
should_add_double_filler = reduce(op.or_, [
|
||||
tex == "\\overset",
|
||||
# TODO: these can't be used since they change
|
||||
# the latex draw order.
|
||||
# tex == "\\frac", # you can use \\over as a alternative
|
||||
# tex == "\\dfrac",
|
||||
# tex == "\\binom",
|
||||
])
|
||||
if should_add_double_filler:
|
||||
filler = "{\\quad}{\\quad}"
|
||||
tex += filler
|
||||
|
||||
if tex == "\\substack":
|
||||
tex = "\\quad"
|
||||
|
||||
@@ -129,12 +146,16 @@ class SingleStringTex(VMobject):
|
||||
tex = ""
|
||||
return tex
|
||||
|
||||
def balance_braces(self, tex):
|
||||
def balance_braces(self, tex: str) -> str:
|
||||
"""
|
||||
Makes Tex resiliant to unmatched braces
|
||||
"""
|
||||
num_unclosed_brackets = 0
|
||||
for char in tex:
|
||||
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 == "}":
|
||||
@@ -145,7 +166,7 @@ class SingleStringTex(VMobject):
|
||||
tex += num_unclosed_brackets * "}"
|
||||
return tex
|
||||
|
||||
def get_tex(self):
|
||||
def get_tex(self) -> str:
|
||||
return self.tex_string
|
||||
|
||||
def organize_submobjects_left_to_right(self):
|
||||
@@ -160,7 +181,7 @@ class Tex(SingleStringTex):
|
||||
"tex_to_color_map": {},
|
||||
}
|
||||
|
||||
def __init__(self, *tex_strings, **kwargs):
|
||||
def __init__(self, *tex_strings: str, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.tex_strings = self.break_up_tex_strings(tex_strings)
|
||||
full_string = self.arg_separator.join(self.tex_strings)
|
||||
@@ -171,7 +192,7 @@ class Tex(SingleStringTex):
|
||||
if self.organize_left_to_right:
|
||||
self.organize_submobjects_left_to_right()
|
||||
|
||||
def break_up_tex_strings(self, tex_strings):
|
||||
def break_up_tex_strings(self, tex_strings: Iterable[str]) -> Iterable[str]:
|
||||
# Separate out any strings specified in the isolate
|
||||
# or tex_to_color_map lists.
|
||||
substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()]
|
||||
@@ -219,7 +240,12 @@ class Tex(SingleStringTex):
|
||||
self.set_submobjects(new_submobjects)
|
||||
return self
|
||||
|
||||
def get_parts_by_tex(self, tex, substring=True, case_sensitive=True):
|
||||
def get_parts_by_tex(
|
||||
self,
|
||||
tex: str,
|
||||
substring: bool = True,
|
||||
case_sensitive: bool = True
|
||||
) -> VGroup:
|
||||
def test(tex1, tex2):
|
||||
if not case_sensitive:
|
||||
tex1 = tex1.lower()
|
||||
@@ -234,27 +260,36 @@ class Tex(SingleStringTex):
|
||||
self.submobjects
|
||||
))
|
||||
|
||||
def get_part_by_tex(self, tex, **kwargs):
|
||||
def get_part_by_tex(self, tex: str, **kwargs) -> SingleStringTex | None:
|
||||
all_parts = self.get_parts_by_tex(tex, **kwargs)
|
||||
return all_parts[0] if all_parts else None
|
||||
|
||||
def set_color_by_tex(self, tex, color, **kwargs):
|
||||
def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
|
||||
self.get_parts_by_tex(tex, **kwargs).set_color(color)
|
||||
return self
|
||||
|
||||
def set_color_by_tex_to_color_map(self, tex_to_color_map, **kwargs):
|
||||
def set_color_by_tex_to_color_map(
|
||||
self,
|
||||
tex_to_color_map: dict[str, ManimColor],
|
||||
**kwargs
|
||||
):
|
||||
for tex, color in list(tex_to_color_map.items()):
|
||||
self.set_color_by_tex(tex, color, **kwargs)
|
||||
return self
|
||||
|
||||
def index_of_part(self, part, start=0):
|
||||
def index_of_part(self, part: SingleStringTex, start: int = 0) -> int:
|
||||
return self.submobjects.index(part, start)
|
||||
|
||||
def index_of_part_by_tex(self, tex, start=0, **kwargs):
|
||||
def index_of_part_by_tex(self, tex: str, start: int = 0, **kwargs) -> int:
|
||||
part = self.get_part_by_tex(tex, **kwargs)
|
||||
return self.index_of_part(part, start)
|
||||
|
||||
def slice_by_tex(self, start_tex=None, stop_tex=None, **kwargs):
|
||||
def slice_by_tex(
|
||||
self,
|
||||
start_tex: str | None = None,
|
||||
stop_tex: str | None = None,
|
||||
**kwargs
|
||||
) -> VGroup:
|
||||
if start_tex is None:
|
||||
start_index = 0
|
||||
else:
|
||||
@@ -266,10 +301,10 @@ class Tex(SingleStringTex):
|
||||
stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
|
||||
return self[start_index:stop_index]
|
||||
|
||||
def sort_alphabetically(self):
|
||||
def sort_alphabetically(self) -> None:
|
||||
self.submobjects.sort(key=lambda m: m.get_tex())
|
||||
|
||||
def set_bstroke(self, color=BLACK, width=4):
|
||||
def set_bstroke(self, color: ManimColor = BLACK, width: float = 4):
|
||||
self.set_stroke(color, width, background=True)
|
||||
return self
|
||||
|
||||
@@ -288,7 +323,7 @@ class BulletedList(TexText):
|
||||
"alignment": "",
|
||||
}
|
||||
|
||||
def __init__(self, *items, **kwargs):
|
||||
def __init__(self, *items: str, **kwargs):
|
||||
line_separated_items = [s + "\\\\" for s in items]
|
||||
TexText.__init__(self, *line_separated_items, **kwargs)
|
||||
for part in self:
|
||||
@@ -301,7 +336,7 @@ class BulletedList(TexText):
|
||||
buff=self.buff
|
||||
)
|
||||
|
||||
def fade_all_but(self, index_or_string, opacity=0.5):
|
||||
def fade_all_but(self, index_or_string: int | str, opacity: float = 0.5) -> None:
|
||||
arg = index_or_string
|
||||
if isinstance(arg, str):
|
||||
part = self.get_part_by_tex(arg)
|
||||
@@ -339,7 +374,7 @@ class Title(TexText):
|
||||
"underline_buff": MED_SMALL_BUFF,
|
||||
}
|
||||
|
||||
def __init__(self, *text_parts, **kwargs):
|
||||
def __init__(self, *text_parts: str, **kwargs):
|
||||
TexText.__init__(self, *text_parts, **kwargs)
|
||||
self.scale(self.scale_factor)
|
||||
self.to_edge(UP)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from manimlib.constants import *
|
||||
@@ -9,6 +11,7 @@ from manimlib.mobject.geometry import Square
|
||||
from manimlib.mobject.geometry import Polygon
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.iterables import adjacent_pairs
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.space_ops import z_to_vector
|
||||
from manimlib.utils.space_ops import compass_directions
|
||||
@@ -23,13 +26,13 @@ class SurfaceMesh(VGroup):
|
||||
"flat_stroke": False,
|
||||
}
|
||||
|
||||
def __init__(self, uv_surface, **kwargs):
|
||||
def __init__(self, uv_surface: Surface, **kwargs):
|
||||
if not isinstance(uv_surface, Surface):
|
||||
raise Exception("uv_surface must be of type Surface")
|
||||
self.uv_surface = uv_surface
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
uv_surface = self.uv_surface
|
||||
|
||||
full_nu, full_nv = uv_surface.resolution
|
||||
@@ -75,7 +78,7 @@ class Sphere(Surface):
|
||||
"v_range": (0, PI),
|
||||
}
|
||||
|
||||
def uv_func(self, u, v):
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return self.radius * np.array([
|
||||
np.cos(u) * np.sin(v),
|
||||
np.sin(u) * np.sin(v),
|
||||
@@ -91,7 +94,7 @@ class Torus(Surface):
|
||||
"r2": 1,
|
||||
}
|
||||
|
||||
def uv_func(self, u, v):
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
P = np.array([math.cos(u), math.sin(u), 0])
|
||||
return (self.r1 - self.r2 * math.cos(v)) * P - math.sin(v) * OUT
|
||||
|
||||
@@ -113,8 +116,8 @@ class Cylinder(Surface):
|
||||
self.apply_matrix(z_to_vector(self.axis))
|
||||
return self
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return [np.cos(u), np.sin(u), v]
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([np.cos(u), np.sin(u), v])
|
||||
|
||||
|
||||
class Line3D(Cylinder):
|
||||
@@ -123,7 +126,7 @@ class Line3D(Cylinder):
|
||||
"resolution": (21, 25)
|
||||
}
|
||||
|
||||
def __init__(self, start, end, **kwargs):
|
||||
def __init__(self, start: np.ndarray, end: np.ndarray, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
axis = end - start
|
||||
super().__init__(
|
||||
@@ -142,16 +145,16 @@ class Disk3D(Surface):
|
||||
"resolution": (2, 25),
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
super().init_points()
|
||||
self.scale(self.radius)
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return [
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([
|
||||
u * np.cos(v),
|
||||
u * np.sin(v),
|
||||
0
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
class Square3D(Surface):
|
||||
@@ -162,12 +165,12 @@ class Square3D(Surface):
|
||||
"resolution": (2, 2),
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
super().init_points()
|
||||
self.scale(self.side_length / 2)
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return [u, v, 0]
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([u, v, 0])
|
||||
|
||||
|
||||
class Cube(SGroup):
|
||||
@@ -180,7 +183,7 @@ class Cube(SGroup):
|
||||
"square_class": Square3D,
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
face = Square3D(
|
||||
resolution=self.square_resolution,
|
||||
side_length=self.side_length,
|
||||
@@ -188,7 +191,7 @@ class Cube(SGroup):
|
||||
self.add(*self.square_to_cube_faces(face))
|
||||
|
||||
@staticmethod
|
||||
def square_to_cube_faces(square):
|
||||
def square_to_cube_faces(square: Square3D) -> list[Square3D]:
|
||||
radius = square.get_height() / 2
|
||||
square.move_to(radius * OUT)
|
||||
result = [square]
|
||||
@@ -199,10 +202,17 @@ class Cube(SGroup):
|
||||
result.append(square.copy().rotate(PI, RIGHT, about_point=ORIGIN))
|
||||
return result
|
||||
|
||||
def _get_face(self):
|
||||
def _get_face(self) -> Square3D:
|
||||
return Square3D(resolution=self.square_resolution)
|
||||
|
||||
|
||||
class Prism(Cube):
|
||||
def __init__(self, width: float = 3.0, height: float = 2.0, depth: float = 1.0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
for dim, value in enumerate([width, height, depth]):
|
||||
self.rescale_to_fit(value, dim, stretch=True)
|
||||
|
||||
|
||||
class VCube(VGroup):
|
||||
CONFIG = {
|
||||
"fill_color": BLUE_D,
|
||||
@@ -210,18 +220,25 @@ class VCube(VGroup):
|
||||
"stroke_width": 0,
|
||||
"gloss": 0.5,
|
||||
"shadow": 0.5,
|
||||
"joint_type": "round",
|
||||
}
|
||||
|
||||
def __init__(self, side_length=2, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
def __init__(self, side_length: float = 2.0, **kwargs):
|
||||
face = Square(side_length=side_length)
|
||||
face.get_triangulation()
|
||||
self.add(*Cube.square_to_cube_faces(face))
|
||||
super().__init__(*Cube.square_to_cube_faces(face), **kwargs)
|
||||
self.init_colors()
|
||||
self.set_joint_type(self.joint_type)
|
||||
self.apply_depth_test()
|
||||
self.refresh_unit_normal()
|
||||
|
||||
|
||||
class VPrism(VCube):
|
||||
def __init__(self, width: float = 3.0, height: float = 2.0, depth: float = 1.0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
for dim, value in enumerate([width, height, depth]):
|
||||
self.rescale_to_fit(value, dim, stretch=True)
|
||||
|
||||
|
||||
class Dodecahedron(VGroup):
|
||||
CONFIG = {
|
||||
"fill_color": BLUE_E,
|
||||
@@ -233,7 +250,7 @@ class Dodecahedron(VGroup):
|
||||
"depth_test": True,
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
# Star by creating two of the pentagons, meeting
|
||||
# back to back on the positive x-axis
|
||||
phi = (1 + math.sqrt(5)) / 2
|
||||
@@ -269,12 +286,20 @@ class Dodecahedron(VGroup):
|
||||
# self.add(pentagon2.copy().apply_matrix(matrix, about_point=ORIGIN))
|
||||
|
||||
|
||||
class Prism(Cube):
|
||||
class Prismify(VGroup):
|
||||
CONFIG = {
|
||||
"dimensions": [3, 2, 1]
|
||||
"apply_depth_test": True,
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
Cube.init_points(self)
|
||||
for dim, value in enumerate(self.dimensions):
|
||||
self.rescale_to_fit(value, dim, stretch=True)
|
||||
def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):
|
||||
# At the moment, this assume stright edges
|
||||
super().__init__(**kwargs)
|
||||
vect = depth * direction
|
||||
self.add(vmobject.copy())
|
||||
points = vmobject.get_points()[::vmobject.n_points_per_curve]
|
||||
for p1, p2 in adjacent_pairs(points):
|
||||
wall = VMobject()
|
||||
wall.match_style(vmobject)
|
||||
wall.set_points_as_corners([p1, p2, p2 + vect, p1 + vect])
|
||||
self.add(wall)
|
||||
self.add(vmobject.copy().shift(vect).reverse_points())
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import moderngl
|
||||
|
||||
from manimlib.constants import GREY_C
|
||||
@@ -29,27 +32,31 @@ class DotCloud(PMobject):
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, points=None, **kwargs):
|
||||
def __init__(self, points: npt.ArrayLike = None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if points is not None:
|
||||
self.set_points(points)
|
||||
|
||||
def init_data(self):
|
||||
def init_data(self) -> None:
|
||||
super().init_data()
|
||||
self.data["radii"] = np.zeros((1, 1))
|
||||
self.set_radius(self.radius)
|
||||
|
||||
def init_uniforms(self):
|
||||
def init_uniforms(self) -> None:
|
||||
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,
|
||||
v_buff_ratio=1.0,
|
||||
d_buff_ratio=1.0,
|
||||
height=DEFAULT_GRID_HEIGHT,
|
||||
):
|
||||
def to_grid(
|
||||
self,
|
||||
n_rows: int,
|
||||
n_cols: int,
|
||||
n_layers: int = 1,
|
||||
buff_ratio: float | None = None,
|
||||
h_buff_ratio: float = 1.0,
|
||||
v_buff_ratio: float = 1.0,
|
||||
d_buff_ratio: float = 1.0,
|
||||
height: float = DEFAULT_GRID_HEIGHT,
|
||||
):
|
||||
n_points = n_rows * n_cols * n_layers
|
||||
points = np.repeat(range(n_points), 3, axis=0).reshape((n_points, 3))
|
||||
points[:, 0] = points[:, 0] % n_cols
|
||||
@@ -74,50 +81,55 @@ class DotCloud(PMobject):
|
||||
self.center()
|
||||
return self
|
||||
|
||||
def set_radii(self, radii):
|
||||
def set_radii(self, radii: npt.ArrayLike):
|
||||
n_points = len(self.get_points())
|
||||
radii = np.array(radii).reshape((len(radii), 1))
|
||||
self.data["radii"] = resize_preserving_order(radii, n_points)
|
||||
self.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
def get_radii(self):
|
||||
def get_radii(self) -> np.ndarray:
|
||||
return self.data["radii"]
|
||||
|
||||
def set_radius(self, radius):
|
||||
def set_radius(self, radius: float):
|
||||
self.data["radii"][:] = radius
|
||||
self.refresh_bounding_box()
|
||||
return self
|
||||
|
||||
def get_radius(self):
|
||||
def get_radius(self) -> float:
|
||||
return self.get_radii().max()
|
||||
|
||||
def set_glow_factor(self, glow_factor):
|
||||
def set_glow_factor(self, glow_factor: float) -> None:
|
||||
self.uniforms["glow_factor"] = glow_factor
|
||||
|
||||
def get_glow_factor(self):
|
||||
def get_glow_factor(self) -> float:
|
||||
return self.uniforms["glow_factor"]
|
||||
|
||||
def compute_bounding_box(self):
|
||||
def compute_bounding_box(self) -> np.ndarray:
|
||||
bb = super().compute_bounding_box()
|
||||
radius = self.get_radius()
|
||||
bb[0] += np.full((3,), -radius)
|
||||
bb[2] += np.full((3,), radius)
|
||||
return bb
|
||||
|
||||
def scale(self, scale_factor, scale_radii=True, **kwargs):
|
||||
def scale(
|
||||
self,
|
||||
scale_factor: float | npt.ArrayLike,
|
||||
scale_radii: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
super().scale(scale_factor, **kwargs)
|
||||
if scale_radii:
|
||||
self.set_radii(scale_factor * self.get_radii())
|
||||
return self
|
||||
|
||||
def make_3d(self, reflectiveness=0.5, shadow=0.2):
|
||||
def make_3d(self, reflectiveness: float = 0.5, shadow: float = 0.2):
|
||||
self.set_reflectiveness(reflectiveness)
|
||||
self.set_shadow(shadow)
|
||||
self.apply_depth_test()
|
||||
return self
|
||||
|
||||
def get_shader_data(self):
|
||||
def get_shader_data(self) -> np.ndarray:
|
||||
shader_data = super().get_shader_data()
|
||||
self.read_data_to_shader(shader_data, "radius", "radii")
|
||||
self.read_data_to_shader(shader_data, "color", "rgbas")
|
||||
@@ -125,13 +137,17 @@ class DotCloud(PMobject):
|
||||
|
||||
|
||||
class TrueDot(DotCloud):
|
||||
def __init__(self, center=ORIGIN, **kwargs):
|
||||
def __init__(self, center: np.ndarray = ORIGIN, **kwargs):
|
||||
super().__init__(points=[center], **kwargs)
|
||||
|
||||
|
||||
class GlowDot(TrueDot):
|
||||
class GlowDots(DotCloud):
|
||||
CONFIG = {
|
||||
"glow_factor": 2,
|
||||
"radius": DEFAULT_GLOW_DOT_RADIUS,
|
||||
"color": YELLOW,
|
||||
}
|
||||
|
||||
|
||||
class GlowDot(GlowDots, TrueDot):
|
||||
pass
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from manimlib.constants import *
|
||||
@@ -21,33 +22,36 @@ class ImageMobject(Mobject):
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, filename, **kwargs):
|
||||
def __init__(self, filename: str, **kwargs):
|
||||
self.set_image_path(get_full_raster_image_path(filename))
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def set_image_path(self, path):
|
||||
def set_image_path(self, path: str) -> None:
|
||||
self.path = path
|
||||
self.image = Image.open(path)
|
||||
self.texture_paths = {"Texture": path}
|
||||
|
||||
def init_data(self):
|
||||
def init_data(self) -> None:
|
||||
self.data = {
|
||||
"points": np.array([UL, DL, UR, DR]),
|
||||
"im_coords": np.array([(0, 0), (0, 1), (1, 0), (1, 1)]),
|
||||
"opacity": np.array([[self.opacity]], dtype=np.float32),
|
||||
}
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
size = self.image.size
|
||||
self.set_width(2 * size[0] / size[1], stretch=True)
|
||||
self.set_height(self.height)
|
||||
|
||||
def set_opacity(self, opacity, recurse=True):
|
||||
def set_opacity(self, opacity: float, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
|
||||
return self
|
||||
|
||||
def point_to_rgb(self, point):
|
||||
def set_color(self, color, opacity=None, recurse=None):
|
||||
return self
|
||||
|
||||
def point_to_rgb(self, point: np.ndarray) -> np.ndarray:
|
||||
x0, y0 = self.get_corner(UL)[:2]
|
||||
x1, y1 = self.get_corner(DR)[:2]
|
||||
x_alpha = inverse_interpolate(x0, x1, point[0])
|
||||
@@ -63,7 +67,7 @@ class ImageMobject(Mobject):
|
||||
))
|
||||
return np.array(rgb) / 255
|
||||
|
||||
def get_shader_data(self):
|
||||
def get_shader_data(self) -> np.ndarray:
|
||||
shader_data = super().get_shader_data()
|
||||
self.read_data_to_shader(shader_data, "im_coords", "im_coords")
|
||||
self.read_data_to_shader(shader_data, "opacity", "opacity")
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Sequence, Union
|
||||
|
||||
import colour
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.color import color_gradient
|
||||
@@ -6,26 +13,41 @@ from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.iterables import resize_array
|
||||
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
class PMobject(Mobject):
|
||||
CONFIG = {
|
||||
"opacity": 1.0,
|
||||
}
|
||||
|
||||
def resize_points(self, size, resize_func=resize_array):
|
||||
def resize_points(
|
||||
self,
|
||||
size: int,
|
||||
resize_func: Callable[[np.ndarray, int], np.ndarray] = resize_array
|
||||
):
|
||||
# TODO
|
||||
for key in self.data:
|
||||
if key == "bounding_box":
|
||||
continue
|
||||
if len(self.data[key]) != size:
|
||||
self.data[key] = resize_array(self.data[key], size)
|
||||
self.data[key] = resize_func(self.data[key], size)
|
||||
return self
|
||||
|
||||
def set_points(self, points):
|
||||
def set_points(self, points: npt.ArrayLike):
|
||||
if len(points) == 0:
|
||||
points = np.zeros((0, 3))
|
||||
super().set_points(points)
|
||||
self.resize_points(len(points))
|
||||
return self
|
||||
|
||||
def add_points(self, points, rgbas=None, color=None, opacity=None):
|
||||
def add_points(
|
||||
self,
|
||||
points: npt.ArrayLike,
|
||||
rgbas: np.ndarray | None = None,
|
||||
color: ManimColor | None = None,
|
||||
opacity: float | None = None
|
||||
):
|
||||
"""
|
||||
points must be a Nx3 numpy array, as must rgbas if it is not None
|
||||
"""
|
||||
@@ -34,30 +56,34 @@ class PMobject(Mobject):
|
||||
if color is not None:
|
||||
if opacity is None:
|
||||
opacity = self.data["rgbas"][-1, 3]
|
||||
new_rgbas = np.repeat(
|
||||
rgbas = np.repeat(
|
||||
[color_to_rgba(color, opacity)],
|
||||
len(points),
|
||||
axis=0
|
||||
)
|
||||
elif rgbas is not None:
|
||||
new_rgbas = rgbas
|
||||
self.data["rgbas"][-len(new_rgbas):] = new_rgbas
|
||||
if rgbas is not None:
|
||||
self.data["rgbas"][-len(rgbas):] = rgbas
|
||||
return self
|
||||
|
||||
def set_color_by_gradient(self, *colors):
|
||||
def add_point(self, point, rgba=None, color=None, opacity=None):
|
||||
rgbas = None if rgba is None else [rgba]
|
||||
self.add_points([point], rgbas, color, opacity)
|
||||
return self
|
||||
|
||||
def set_color_by_gradient(self, *colors: ManimColor):
|
||||
self.data["rgbas"] = np.array(list(map(
|
||||
color_to_rgba,
|
||||
color_gradient(colors, self.get_num_points())
|
||||
)))
|
||||
return self
|
||||
|
||||
def match_colors(self, pmobject):
|
||||
def match_colors(self, pmobject: PMobject):
|
||||
self.data["rgbas"][:] = resize_with_interpolation(
|
||||
pmobject.data["rgbas"], self.get_num_points()
|
||||
)
|
||||
return self
|
||||
|
||||
def filter_out(self, condition):
|
||||
def filter_out(self, condition: Callable[[np.ndarray], bool]):
|
||||
for mob in self.family_members_with_points():
|
||||
to_keep = ~np.apply_along_axis(condition, 1, mob.get_points())
|
||||
for key in mob.data:
|
||||
@@ -66,7 +92,7 @@ class PMobject(Mobject):
|
||||
mob.data[key] = mob.data[key][to_keep]
|
||||
return self
|
||||
|
||||
def sort_points(self, function=lambda p: p[0]):
|
||||
def sort_points(self, function: Callable[[np.ndarray]] = lambda p: p[0]):
|
||||
"""
|
||||
function is any map from R^3 to R
|
||||
"""
|
||||
@@ -86,11 +112,11 @@ class PMobject(Mobject):
|
||||
])
|
||||
return self
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
def point_from_proportion(self, alpha: float) -> np.ndarray:
|
||||
index = alpha * (self.get_num_points() - 1)
|
||||
return self.get_points()[int(index)]
|
||||
|
||||
def pointwise_become_partial(self, pmobject, a, b):
|
||||
def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float):
|
||||
lower_index = int(a * pmobject.get_num_points())
|
||||
upper_index = int(b * pmobject.get_num_points())
|
||||
for key in self.data:
|
||||
@@ -101,7 +127,7 @@ class PMobject(Mobject):
|
||||
|
||||
|
||||
class PGroup(PMobject):
|
||||
def __init__(self, *pmobs, **kwargs):
|
||||
def __init__(self, *pmobs: PMobject, **kwargs):
|
||||
if not all([isinstance(m, PMobject) for m in pmobs]):
|
||||
raise Exception("All submobjects must be of type PMobject")
|
||||
super().__init__(*pmobs, **kwargs)
|
||||
@@ -112,6 +138,6 @@ class Point(PMobject):
|
||||
"color": BLACK,
|
||||
}
|
||||
|
||||
def __init__(self, location=ORIGIN, **kwargs):
|
||||
def __init__(self, location: np.ndarray = ORIGIN, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.add_points([location])
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Callable
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -9,6 +14,11 @@ from manimlib.utils.images import get_full_raster_image_path
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.space_ops import normalize_along_axis
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.camera.camera import Camera
|
||||
|
||||
|
||||
class Surface(Mobject):
|
||||
CONFIG = {
|
||||
@@ -42,7 +52,7 @@ class Surface(Mobject):
|
||||
super().__init__(**kwargs)
|
||||
self.compute_triangle_indices()
|
||||
|
||||
def uv_func(self, u, v):
|
||||
def uv_func(self, u: float, v: float) -> tuple[float, float, float]:
|
||||
# To be implemented in subclasses
|
||||
return (u, v, 0.0)
|
||||
|
||||
@@ -85,15 +95,17 @@ class Surface(Mobject):
|
||||
indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right
|
||||
self.triangle_indices = indices
|
||||
|
||||
def get_triangle_indices(self):
|
||||
def get_triangle_indices(self) -> np.ndarray:
|
||||
return self.triangle_indices
|
||||
|
||||
def get_surface_points_and_nudged_points(self):
|
||||
def get_surface_points_and_nudged_points(
|
||||
self
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
points = self.get_points()
|
||||
k = len(points) // 3
|
||||
return points[:k], points[k:2 * k], points[2 * k:]
|
||||
|
||||
def get_unit_normals(self):
|
||||
def get_unit_normals(self) -> np.ndarray:
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
normals = np.cross(
|
||||
(du_points - s_points) / self.epsilon,
|
||||
@@ -101,7 +113,13 @@ class Surface(Mobject):
|
||||
)
|
||||
return normalize_along_axis(normals, 1)
|
||||
|
||||
def pointwise_become_partial(self, smobject, a, b, axis=None):
|
||||
def pointwise_become_partial(
|
||||
self,
|
||||
smobject: "Surface",
|
||||
a: float,
|
||||
b: float,
|
||||
axis: np.ndarray | None = None
|
||||
):
|
||||
assert(isinstance(smobject, Surface))
|
||||
if axis is None:
|
||||
axis = self.prefered_creation_axis
|
||||
@@ -116,7 +134,14 @@ class Surface(Mobject):
|
||||
]))
|
||||
return self
|
||||
|
||||
def get_partial_points_array(self, points, a, b, resolution, axis):
|
||||
def get_partial_points_array(
|
||||
self,
|
||||
points: np.ndarray,
|
||||
a: float,
|
||||
b: float,
|
||||
resolution: npt.ArrayLike,
|
||||
axis: int
|
||||
) -> np.ndarray:
|
||||
if len(points) == 0:
|
||||
return points
|
||||
nu, nv = resolution[:2]
|
||||
@@ -149,7 +174,7 @@ class Surface(Mobject):
|
||||
).reshape(shape)
|
||||
return points.reshape((nu * nv, *resolution[2:]))
|
||||
|
||||
def sort_faces_back_to_front(self, vect=OUT):
|
||||
def sort_faces_back_to_front(self, vect: np.ndarray = OUT):
|
||||
tri_is = self.triangle_indices
|
||||
indices = list(range(len(tri_is) // 3))
|
||||
points = self.get_points()
|
||||
@@ -162,13 +187,13 @@ class Surface(Mobject):
|
||||
tri_is[k::3] = tri_is[k::3][indices]
|
||||
return self
|
||||
|
||||
def always_sort_to_camera(self, camera):
|
||||
def always_sort_to_camera(self, camera: Camera):
|
||||
self.add_updater(lambda m: m.sort_faces_back_to_front(
|
||||
camera.get_location() - self.get_center()
|
||||
))
|
||||
|
||||
# For shaders
|
||||
def get_shader_data(self):
|
||||
def get_shader_data(self) -> np.ndarray:
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
shader_data = self.get_resized_shader_data_array(len(s_points))
|
||||
if "points" not in self.locked_data_keys:
|
||||
@@ -178,16 +203,22 @@ class Surface(Mobject):
|
||||
self.fill_in_shader_color_info(shader_data)
|
||||
return shader_data
|
||||
|
||||
def fill_in_shader_color_info(self, shader_data):
|
||||
def fill_in_shader_color_info(self, shader_data: np.ndarray) -> np.ndarray:
|
||||
self.read_data_to_shader(shader_data, "color", "rgbas")
|
||||
return shader_data
|
||||
|
||||
def get_shader_vert_indices(self):
|
||||
def get_shader_vert_indices(self) -> np.ndarray:
|
||||
return self.get_triangle_indices()
|
||||
|
||||
|
||||
class ParametricSurface(Surface):
|
||||
def __init__(self, uv_func, u_range=(0, 1), v_range=(0, 1), **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
uv_func: Callable[[float, float], Iterable[float]],
|
||||
u_range: tuple[float, float] = (0, 1),
|
||||
v_range: tuple[float, float] = (0, 1),
|
||||
**kwargs
|
||||
):
|
||||
self.passed_uv_func = uv_func
|
||||
super().__init__(u_range=u_range, v_range=v_range, **kwargs)
|
||||
|
||||
@@ -200,7 +231,7 @@ class SGroup(Surface):
|
||||
"resolution": (0, 0),
|
||||
}
|
||||
|
||||
def __init__(self, *parametric_surfaces, **kwargs):
|
||||
def __init__(self, *parametric_surfaces: Surface, **kwargs):
|
||||
super().__init__(uv_func=None, **kwargs)
|
||||
self.add(*parametric_surfaces)
|
||||
|
||||
@@ -220,7 +251,13 @@ class TexturedSurface(Surface):
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, uv_surface, image_file, dark_image_file=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
uv_surface: Surface,
|
||||
image_file: str,
|
||||
dark_image_file: str | None = None,
|
||||
**kwargs
|
||||
):
|
||||
if not isinstance(uv_surface, Surface):
|
||||
raise Exception("uv_surface must be of type Surface")
|
||||
# Set texture information
|
||||
@@ -236,10 +273,10 @@ class TexturedSurface(Surface):
|
||||
|
||||
self.uv_surface = uv_surface
|
||||
self.uv_func = uv_surface.uv_func
|
||||
self.u_range = uv_surface.u_range
|
||||
self.v_range = uv_surface.v_range
|
||||
self.resolution = uv_surface.resolution
|
||||
self.gloss = self.uv_surface.gloss
|
||||
self.u_range: tuple[float, float] = uv_surface.u_range
|
||||
self.v_range: tuple[float, float] = uv_surface.v_range
|
||||
self.resolution: tuple[float, float] = uv_surface.resolution
|
||||
self.gloss: float = self.uv_surface.gloss
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_data(self):
|
||||
@@ -260,15 +297,21 @@ class TexturedSurface(Surface):
|
||||
super().init_uniforms()
|
||||
self.uniforms["num_textures"] = self.num_textures
|
||||
|
||||
def init_colors(self, override=True):
|
||||
def init_colors(self):
|
||||
self.data["opacity"] = np.array([self.uv_surface.data["rgbas"][:, 3]])
|
||||
|
||||
def set_opacity(self, opacity, recurse=True):
|
||||
def set_opacity(self, opacity: float, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
|
||||
return self
|
||||
|
||||
def pointwise_become_partial(self, tsmobject, a, b, axis=1):
|
||||
def pointwise_become_partial(
|
||||
self,
|
||||
tsmobject: "TexturedSurface",
|
||||
a: float,
|
||||
b: float,
|
||||
axis: int = 1
|
||||
):
|
||||
super().pointwise_become_partial(tsmobject, a, b, axis)
|
||||
im_coords = self.data["im_coords"]
|
||||
im_coords[:] = tsmobject.data["im_coords"]
|
||||
@@ -280,7 +323,7 @@ class TexturedSurface(Surface):
|
||||
)
|
||||
return self
|
||||
|
||||
def fill_in_shader_color_info(self, shader_data):
|
||||
def fill_in_shader_color_info(self, shader_data: np.ndarray) -> np.ndarray:
|
||||
self.read_data_to_shader(shader_data, "opacity", "opacity")
|
||||
self.read_data_to_shader(shader_data, "im_coords", "im_coords")
|
||||
return shader_data
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import itertools as it
|
||||
import operator as op
|
||||
import moderngl
|
||||
from __future__ import annotations
|
||||
|
||||
import operator as op
|
||||
import itertools as it
|
||||
from functools import reduce, wraps
|
||||
from typing import Iterable, Sequence, Callable, Union
|
||||
|
||||
import colour
|
||||
import moderngl
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -29,6 +34,9 @@ from manimlib.utils.space_ops import z_to_vector
|
||||
from manimlib.shader_wrapper import ShaderWrapper
|
||||
|
||||
|
||||
ManimColor = Union[str, colour.Color, Sequence[float]]
|
||||
|
||||
|
||||
class VMobject(Mobject):
|
||||
CONFIG = {
|
||||
"fill_color": None,
|
||||
@@ -90,7 +98,7 @@ class VMobject(Mobject):
|
||||
})
|
||||
|
||||
# Colors
|
||||
def init_colors(self, override=True):
|
||||
def init_colors(self):
|
||||
self.set_fill(
|
||||
color=self.fill_color or self.color,
|
||||
opacity=self.fill_opacity,
|
||||
@@ -103,12 +111,14 @@ class VMobject(Mobject):
|
||||
)
|
||||
self.set_gloss(self.gloss)
|
||||
self.set_flat_stroke(self.flat_stroke)
|
||||
if not override:
|
||||
for submobjects in self.submobjects:
|
||||
submobjects.init_colors(override=False)
|
||||
return self
|
||||
|
||||
def set_rgba_array(self, rgba_array, name=None, recurse=False):
|
||||
def set_rgba_array(
|
||||
self,
|
||||
rgba_array: npt.ArrayLike,
|
||||
name: str = None,
|
||||
recurse: bool = False
|
||||
):
|
||||
if name is None:
|
||||
names = ["fill_rgba", "stroke_rgba"]
|
||||
else:
|
||||
@@ -118,11 +128,23 @@ class VMobject(Mobject):
|
||||
super().set_rgba_array(rgba_array, name, recurse)
|
||||
return self
|
||||
|
||||
def set_fill(self, color=None, opacity=None, recurse=True):
|
||||
def set_fill(
|
||||
self,
|
||||
color: ManimColor | None = None,
|
||||
opacity: float | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
|
||||
return self
|
||||
|
||||
def set_stroke(self, color=None, width=None, opacity=None, background=None, recurse=True):
|
||||
def set_stroke(
|
||||
self,
|
||||
color: ManimColor | None = None,
|
||||
width: float | npt.ArrayLike | None = None,
|
||||
opacity: float | None = None,
|
||||
background: bool | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse)
|
||||
|
||||
if width is not None:
|
||||
@@ -138,29 +160,36 @@ class VMobject(Mobject):
|
||||
mob.draw_stroke_behind_fill = background
|
||||
return self
|
||||
|
||||
def set_backstroke(self, color=BLACK, width=3, background=True):
|
||||
def set_backstroke(
|
||||
self,
|
||||
color: ManimColor = BLACK,
|
||||
width: float | npt.ArrayLike = 3,
|
||||
background: bool = True
|
||||
):
|
||||
self.set_stroke(color, width, background=background)
|
||||
return self
|
||||
|
||||
def align_stroke_width_data_to_points(self, recurse=True):
|
||||
def align_stroke_width_data_to_points(self, recurse: bool = True) -> None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["stroke_width"] = resize_with_interpolation(
|
||||
mob.data["stroke_width"], len(mob.get_points())
|
||||
)
|
||||
|
||||
def set_style(self,
|
||||
fill_color=None,
|
||||
fill_opacity=None,
|
||||
fill_rgba=None,
|
||||
stroke_color=None,
|
||||
stroke_opacity=None,
|
||||
stroke_rgba=None,
|
||||
stroke_width=None,
|
||||
stroke_background=True,
|
||||
reflectiveness=None,
|
||||
gloss=None,
|
||||
shadow=None,
|
||||
recurse=True):
|
||||
def set_style(
|
||||
self,
|
||||
fill_color: ManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
fill_rgba: npt.ArrayLike | None = None,
|
||||
stroke_color: ManimColor | None = None,
|
||||
stroke_opacity: float | None = None,
|
||||
stroke_rgba: npt.ArrayLike | None = None,
|
||||
stroke_width: float | npt.ArrayLike | None = None,
|
||||
stroke_background: bool = True,
|
||||
reflectiveness: float | None = None,
|
||||
gloss: float | None = None,
|
||||
shadow: float | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
if fill_rgba is not None:
|
||||
self.data['fill_rgba'] = resize_with_interpolation(fill_rgba, len(fill_rgba))
|
||||
else:
|
||||
@@ -204,7 +233,7 @@ class VMobject(Mobject):
|
||||
"shadow": self.get_shadow(),
|
||||
}
|
||||
|
||||
def match_style(self, vmobject, recurse=True):
|
||||
def match_style(self, vmobject: VMobject, recurse: bool = True):
|
||||
self.set_style(**vmobject.get_style(), recurse=False)
|
||||
if recurse:
|
||||
# Does its best to match up submobject lists, and
|
||||
@@ -218,17 +247,17 @@ class VMobject(Mobject):
|
||||
sm1.match_style(sm2)
|
||||
return self
|
||||
|
||||
def set_color(self, color, recurse=True):
|
||||
def set_color(self, color: ManimColor, recurse: bool = True):
|
||||
self.set_fill(color, recurse=recurse)
|
||||
self.set_stroke(color, recurse=recurse)
|
||||
return self
|
||||
|
||||
def set_opacity(self, opacity, recurse=True):
|
||||
def set_opacity(self, opacity: float, recurse: bool = True):
|
||||
self.set_fill(opacity=opacity, recurse=recurse)
|
||||
self.set_stroke(opacity=opacity, recurse=recurse)
|
||||
return self
|
||||
|
||||
def fade(self, darkness=0.5, recurse=True):
|
||||
def fade(self, darkness: float = 0.5, recurse: bool = True):
|
||||
mobs = self.get_family() if recurse else [self]
|
||||
for mob in mobs:
|
||||
factor = 1.0 - darkness
|
||||
@@ -242,78 +271,91 @@ class VMobject(Mobject):
|
||||
)
|
||||
return self
|
||||
|
||||
def get_fill_colors(self):
|
||||
def get_fill_colors(self) -> list[str]:
|
||||
return [
|
||||
rgb_to_hex(rgba[:3])
|
||||
for rgba in self.data['fill_rgba']
|
||||
]
|
||||
|
||||
def get_fill_opacities(self):
|
||||
def get_fill_opacities(self) -> np.ndarray:
|
||||
return self.data['fill_rgba'][:, 3]
|
||||
|
||||
def get_stroke_colors(self):
|
||||
def get_stroke_colors(self) -> list[str]:
|
||||
return [
|
||||
rgb_to_hex(rgba[:3])
|
||||
for rgba in self.data['stroke_rgba']
|
||||
]
|
||||
|
||||
def get_stroke_opacities(self):
|
||||
def get_stroke_opacities(self) -> np.ndarray:
|
||||
return self.data['stroke_rgba'][:, 3]
|
||||
|
||||
def get_stroke_widths(self):
|
||||
def get_stroke_widths(self) -> np.ndarray:
|
||||
return self.data['stroke_width'][:, 0]
|
||||
|
||||
# TODO, it's weird for these to return the first of various lists
|
||||
# rather than the full information
|
||||
def get_fill_color(self):
|
||||
def get_fill_color(self) -> str:
|
||||
"""
|
||||
If there are multiple colors (for gradient)
|
||||
this returns the first one
|
||||
"""
|
||||
return self.get_fill_colors()[0]
|
||||
|
||||
def get_fill_opacity(self):
|
||||
def get_fill_opacity(self) -> float:
|
||||
"""
|
||||
If there are multiple opacities, this returns the
|
||||
first
|
||||
"""
|
||||
return self.get_fill_opacities()[0]
|
||||
|
||||
def get_stroke_color(self):
|
||||
def get_stroke_color(self) -> str:
|
||||
return self.get_stroke_colors()[0]
|
||||
|
||||
def get_stroke_width(self):
|
||||
def get_stroke_width(self) -> float | np.ndarray:
|
||||
return self.get_stroke_widths()[0]
|
||||
|
||||
def get_stroke_opacity(self):
|
||||
def get_stroke_opacity(self) -> float:
|
||||
return self.get_stroke_opacities()[0]
|
||||
|
||||
def get_color(self):
|
||||
def get_color(self) -> str:
|
||||
if self.has_fill():
|
||||
return self.get_fill_color()
|
||||
return self.get_stroke_color()
|
||||
|
||||
def has_stroke(self):
|
||||
def has_stroke(self) -> bool:
|
||||
return self.get_stroke_widths().any() and self.get_stroke_opacities().any()
|
||||
|
||||
def has_fill(self):
|
||||
def has_fill(self) -> bool:
|
||||
return any(self.get_fill_opacities())
|
||||
|
||||
def get_opacity(self):
|
||||
def get_opacity(self) -> float:
|
||||
if self.has_fill():
|
||||
return self.get_fill_opacity()
|
||||
return self.get_stroke_opacity()
|
||||
|
||||
def set_flat_stroke(self, flat_stroke=True, recurse=True):
|
||||
def set_flat_stroke(self, flat_stroke: bool = True, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.flat_stroke = flat_stroke
|
||||
return self
|
||||
|
||||
def get_flat_stroke(self):
|
||||
def get_flat_stroke(self) -> bool:
|
||||
return self.flat_stroke
|
||||
|
||||
def set_joint_type(self, joint_type: str, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.joint_type = joint_type
|
||||
return self
|
||||
|
||||
def get_joint_type(self) -> str:
|
||||
return self.joint_type
|
||||
|
||||
# Points
|
||||
def set_anchors_and_handles(self, anchors1, handles, anchors2):
|
||||
def set_anchors_and_handles(
|
||||
self,
|
||||
anchors1: np.ndarray,
|
||||
handles: np.ndarray,
|
||||
anchors2: np.ndarray
|
||||
):
|
||||
assert(len(anchors1) == len(handles) == len(anchors2))
|
||||
nppc = self.n_points_per_curve
|
||||
new_points = np.zeros((nppc * len(anchors1), self.dim))
|
||||
@@ -323,16 +365,27 @@ class VMobject(Mobject):
|
||||
self.set_points(new_points)
|
||||
return self
|
||||
|
||||
def start_new_path(self, point):
|
||||
def start_new_path(self, point: np.ndarray):
|
||||
assert(self.get_num_points() % self.n_points_per_curve == 0)
|
||||
self.append_points([point])
|
||||
return self
|
||||
|
||||
def add_cubic_bezier_curve(self, anchor1, handle1, handle2, anchor2):
|
||||
def add_cubic_bezier_curve(
|
||||
self,
|
||||
anchor1: npt.ArrayLike,
|
||||
handle1: npt.ArrayLike,
|
||||
handle2: npt.ArrayLike,
|
||||
anchor2: npt.ArrayLike
|
||||
):
|
||||
new_points = get_quadratic_approximation_of_cubic(anchor1, handle1, handle2, anchor2)
|
||||
self.append_points(new_points)
|
||||
|
||||
def add_cubic_bezier_curve_to(self, handle1, handle2, anchor):
|
||||
def add_cubic_bezier_curve_to(
|
||||
self,
|
||||
handle1: npt.ArrayLike,
|
||||
handle2: npt.ArrayLike,
|
||||
anchor: npt.ArrayLike
|
||||
):
|
||||
"""
|
||||
Add cubic bezier curve to the path.
|
||||
"""
|
||||
@@ -345,14 +398,14 @@ class VMobject(Mobject):
|
||||
else:
|
||||
self.append_points(quadratic_approx)
|
||||
|
||||
def add_quadratic_bezier_curve_to(self, handle, anchor):
|
||||
def add_quadratic_bezier_curve_to(self, handle: np.ndarray, anchor: np.ndarray):
|
||||
self.throw_error_if_no_points()
|
||||
if self.has_new_path_started():
|
||||
self.append_points([handle, anchor])
|
||||
else:
|
||||
self.append_points([self.get_last_point(), handle, anchor])
|
||||
|
||||
def add_line_to(self, point):
|
||||
def add_line_to(self, point: np.ndarray):
|
||||
end = self.get_points()[-1]
|
||||
alphas = np.linspace(0, 1, self.n_points_per_curve)
|
||||
if self.long_lines:
|
||||
@@ -374,7 +427,7 @@ class VMobject(Mobject):
|
||||
self.append_points(points)
|
||||
return self
|
||||
|
||||
def add_smooth_curve_to(self, point):
|
||||
def add_smooth_curve_to(self, point: np.ndarray):
|
||||
if self.has_new_path_started():
|
||||
self.add_line_to(point)
|
||||
else:
|
||||
@@ -383,7 +436,7 @@ class VMobject(Mobject):
|
||||
self.add_quadratic_bezier_curve_to(new_handle, point)
|
||||
return self
|
||||
|
||||
def add_smooth_cubic_curve_to(self, handle, point):
|
||||
def add_smooth_cubic_curve_to(self, handle: np.ndarray, point: np.ndarray):
|
||||
self.throw_error_if_no_points()
|
||||
if self.get_num_points() == 1:
|
||||
new_handle = self.get_points()[-1]
|
||||
@@ -391,13 +444,13 @@ class VMobject(Mobject):
|
||||
new_handle = self.get_reflection_of_last_handle()
|
||||
self.add_cubic_bezier_curve_to(new_handle, handle, point)
|
||||
|
||||
def has_new_path_started(self):
|
||||
def has_new_path_started(self) -> bool:
|
||||
return self.get_num_points() % self.n_points_per_curve == 1
|
||||
|
||||
def get_last_point(self):
|
||||
def get_last_point(self) -> np.ndarray:
|
||||
return self.get_points()[-1]
|
||||
|
||||
def get_reflection_of_last_handle(self):
|
||||
def get_reflection_of_last_handle(self) -> np.ndarray:
|
||||
points = self.get_points()
|
||||
return 2 * points[-1] - points[-2]
|
||||
|
||||
@@ -405,12 +458,16 @@ class VMobject(Mobject):
|
||||
if not self.is_closed():
|
||||
self.add_line_to(self.get_subpaths()[-1][0])
|
||||
|
||||
def is_closed(self):
|
||||
def is_closed(self) -> bool:
|
||||
return self.consider_points_equals(
|
||||
self.get_points()[0], self.get_points()[-1]
|
||||
)
|
||||
|
||||
def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, recurse=True):
|
||||
def subdivide_sharp_curves(
|
||||
self,
|
||||
angle_threshold: float = 30 * DEGREES,
|
||||
recurse: bool = True
|
||||
):
|
||||
vmobs = [vm for vm in self.get_family(recurse) if vm.has_points()]
|
||||
for vmob in vmobs:
|
||||
new_points = []
|
||||
@@ -428,12 +485,12 @@ class VMobject(Mobject):
|
||||
vmob.set_points(np.vstack(new_points))
|
||||
return self
|
||||
|
||||
def add_points_as_corners(self, points):
|
||||
def add_points_as_corners(self, points: Iterable[np.ndarray]):
|
||||
for point in points:
|
||||
self.add_line_to(point)
|
||||
return points
|
||||
|
||||
def set_points_as_corners(self, points):
|
||||
def set_points_as_corners(self, points: Iterable[np.ndarray]):
|
||||
nppc = self.n_points_per_curve
|
||||
points = np.array(points)
|
||||
self.set_anchors_and_handles(*[
|
||||
@@ -442,7 +499,11 @@ class VMobject(Mobject):
|
||||
])
|
||||
return self
|
||||
|
||||
def set_points_smoothly(self, points, true_smooth=False):
|
||||
def set_points_smoothly(
|
||||
self,
|
||||
points: Iterable[np.ndarray],
|
||||
true_smooth: bool = False
|
||||
):
|
||||
self.set_points_as_corners(points)
|
||||
if true_smooth:
|
||||
self.make_smooth()
|
||||
@@ -450,7 +511,7 @@ class VMobject(Mobject):
|
||||
self.make_approximately_smooth()
|
||||
return self
|
||||
|
||||
def change_anchor_mode(self, mode):
|
||||
def change_anchor_mode(self, mode: str):
|
||||
assert(mode in ("jagged", "approx_smooth", "true_smooth"))
|
||||
nppc = self.n_points_per_curve
|
||||
for submob in self.family_members_with_points():
|
||||
@@ -495,12 +556,12 @@ class VMobject(Mobject):
|
||||
self.change_anchor_mode("jagged")
|
||||
return self
|
||||
|
||||
def add_subpath(self, points):
|
||||
def add_subpath(self, points: Iterable[np.ndarray]):
|
||||
assert(len(points) % self.n_points_per_curve == 0)
|
||||
self.append_points(points)
|
||||
return self
|
||||
|
||||
def append_vectorized_mobject(self, vectorized_mobject):
|
||||
def append_vectorized_mobject(self, vectorized_mobject: VMobject):
|
||||
new_points = list(vectorized_mobject.get_points())
|
||||
|
||||
if self.has_new_path_started():
|
||||
@@ -511,11 +572,11 @@ class VMobject(Mobject):
|
||||
return self
|
||||
|
||||
#
|
||||
def consider_points_equals(self, p0, p1):
|
||||
def consider_points_equals(self, p0: np.ndarray, p1: np.ndarray) -> bool:
|
||||
return get_norm(p1 - p0) < self.tolerance_for_point_equality
|
||||
|
||||
# Information about the curve
|
||||
def get_bezier_tuples_from_points(self, points):
|
||||
def get_bezier_tuples_from_points(self, points: Sequence[np.ndarray]):
|
||||
nppc = self.n_points_per_curve
|
||||
remainder = len(points) % nppc
|
||||
points = points[:len(points) - remainder]
|
||||
@@ -527,7 +588,10 @@ class VMobject(Mobject):
|
||||
def get_bezier_tuples(self):
|
||||
return self.get_bezier_tuples_from_points(self.get_points())
|
||||
|
||||
def get_subpaths_from_points(self, points):
|
||||
def get_subpaths_from_points(
|
||||
self,
|
||||
points: Sequence[np.ndarray]
|
||||
) -> list[Sequence[np.ndarray]]:
|
||||
nppc = self.n_points_per_curve
|
||||
diffs = points[nppc - 1:-1:nppc] - points[nppc::nppc]
|
||||
splits = (diffs * diffs).sum(1) > self.tolerance_for_point_equality
|
||||
@@ -544,28 +608,28 @@ class VMobject(Mobject):
|
||||
if (i2 - i1) >= nppc
|
||||
]
|
||||
|
||||
def get_subpaths(self):
|
||||
def get_subpaths(self) -> list[Sequence[np.ndarray]]:
|
||||
return self.get_subpaths_from_points(self.get_points())
|
||||
|
||||
def get_nth_curve_points(self, n):
|
||||
def get_nth_curve_points(self, n: int) -> np.ndarray:
|
||||
assert(n < self.get_num_curves())
|
||||
nppc = self.n_points_per_curve
|
||||
return self.get_points()[nppc * n:nppc * (n + 1)]
|
||||
|
||||
def get_nth_curve_function(self, n):
|
||||
def get_nth_curve_function(self, n: int) -> Callable[[float], np.ndarray]:
|
||||
return bezier(self.get_nth_curve_points(n))
|
||||
|
||||
def get_num_curves(self):
|
||||
def get_num_curves(self) -> int:
|
||||
return self.get_num_points() // self.n_points_per_curve
|
||||
|
||||
def quick_point_from_proportion(self, alpha):
|
||||
def quick_point_from_proportion(self, alpha: float) -> np.ndarray:
|
||||
# Assumes all curves have the same length, so is inaccurate
|
||||
num_curves = self.get_num_curves()
|
||||
n, residue = integer_interpolate(0, num_curves, alpha)
|
||||
curve_func = self.get_nth_curve_function(n)
|
||||
return curve_func(residue)
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
def point_from_proportion(self, alpha: float) -> np.ndarray:
|
||||
if alpha <= 0:
|
||||
return self.get_start()
|
||||
elif alpha >= 1:
|
||||
@@ -587,7 +651,7 @@ class VMobject(Mobject):
|
||||
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):
|
||||
def get_anchors_and_handles(self) -> list[np.ndarray]:
|
||||
"""
|
||||
returns anchors1, handles, anchors2,
|
||||
where (anchors1[i], handles[i], anchors2[i])
|
||||
@@ -601,14 +665,14 @@ class VMobject(Mobject):
|
||||
for i in range(nppc)
|
||||
]
|
||||
|
||||
def get_start_anchors(self):
|
||||
def get_start_anchors(self) -> np.ndarray:
|
||||
return self.get_points()[0::self.n_points_per_curve]
|
||||
|
||||
def get_end_anchors(self):
|
||||
def get_end_anchors(self) -> np.ndarray:
|
||||
nppc = self.n_points_per_curve
|
||||
return self.get_points()[nppc - 1::nppc]
|
||||
|
||||
def get_anchors(self):
|
||||
def get_anchors(self) -> np.ndarray:
|
||||
points = self.get_points()
|
||||
if len(points) == 1:
|
||||
return points
|
||||
@@ -617,7 +681,7 @@ class VMobject(Mobject):
|
||||
self.get_end_anchors(),
|
||||
))))
|
||||
|
||||
def get_points_without_null_curves(self, atol=1e-9):
|
||||
def get_points_without_null_curves(self, atol: float=1e-9) -> np.ndarray:
|
||||
nppc = self.n_points_per_curve
|
||||
points = self.get_points()
|
||||
distinct_curves = reduce(op.or_, [
|
||||
@@ -626,7 +690,7 @@ class VMobject(Mobject):
|
||||
])
|
||||
return points[distinct_curves.repeat(nppc)]
|
||||
|
||||
def get_arc_length(self, n_sample_points=None):
|
||||
def get_arc_length(self, n_sample_points: int | None = None) -> float:
|
||||
if n_sample_points is None:
|
||||
n_sample_points = 4 * self.get_num_curves() + 1
|
||||
points = np.array([
|
||||
@@ -637,7 +701,7 @@ class VMobject(Mobject):
|
||||
norms = np.array([get_norm(d) for d in diffs])
|
||||
return norms.sum()
|
||||
|
||||
def get_area_vector(self):
|
||||
def get_area_vector(self) -> np.ndarray:
|
||||
# Returns a vector whose length is the area bound by
|
||||
# the polygon formed by the anchor points, pointing
|
||||
# in a direction perpendicular to the polygon according
|
||||
@@ -657,7 +721,7 @@ class VMobject(Mobject):
|
||||
sum((p0[:, 0] + p1[:, 0]) * (p1[:, 1] - p0[:, 1])), # Add up (x1 + x2)*(y2 - y1)
|
||||
])
|
||||
|
||||
def get_unit_normal(self, recompute=False):
|
||||
def get_unit_normal(self, recompute: bool = False) -> np.ndarray:
|
||||
if not recompute:
|
||||
return self.data["unit_normal"][0]
|
||||
|
||||
@@ -683,7 +747,7 @@ class VMobject(Mobject):
|
||||
return self
|
||||
|
||||
# Alignment
|
||||
def align_points(self, vmobject):
|
||||
def align_points(self, vmobject: VMobject):
|
||||
if self.get_num_points() == len(vmobject.get_points()):
|
||||
return
|
||||
|
||||
@@ -726,7 +790,7 @@ class VMobject(Mobject):
|
||||
vmobject.set_points(np.vstack(new_subpaths2))
|
||||
return self
|
||||
|
||||
def insert_n_curves(self, n, recurse=True):
|
||||
def insert_n_curves(self, n: int, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
if mob.get_num_curves() > 0:
|
||||
new_points = mob.insert_n_curves_to_point_list(n, mob.get_points())
|
||||
@@ -736,7 +800,7 @@ class VMobject(Mobject):
|
||||
mob.set_points(new_points)
|
||||
return self
|
||||
|
||||
def insert_n_curves_to_point_list(self, n, points):
|
||||
def insert_n_curves_to_point_list(self, n: int, points: np.ndarray):
|
||||
nppc = self.n_points_per_curve
|
||||
if len(points) == 1:
|
||||
return np.repeat(points, nppc * n, 0)
|
||||
@@ -769,7 +833,13 @@ class VMobject(Mobject):
|
||||
new_points += partial_quadratic_bezier_points(group, a1, a2)
|
||||
return np.vstack(new_points)
|
||||
|
||||
def interpolate(self, mobject1, mobject2, alpha, *args, **kwargs):
|
||||
def interpolate(
|
||||
self,
|
||||
mobject1: VMobject,
|
||||
mobject2: VMobject,
|
||||
alpha: float,
|
||||
*args, **kwargs
|
||||
):
|
||||
super().interpolate(mobject1, mobject2, alpha, *args, **kwargs)
|
||||
if self.has_fill():
|
||||
tri1 = mobject1.get_triangulation()
|
||||
@@ -778,7 +848,7 @@ class VMobject(Mobject):
|
||||
self.refresh_triangulation()
|
||||
return self
|
||||
|
||||
def pointwise_become_partial(self, vmobject, a, b):
|
||||
def pointwise_become_partial(self, vmobject: VMobject, a: float, b: float):
|
||||
assert(isinstance(vmobject, VMobject))
|
||||
if a <= 0 and b >= 1:
|
||||
self.become(vmobject)
|
||||
@@ -820,7 +890,7 @@ class VMobject(Mobject):
|
||||
self.set_points(new_points)
|
||||
return self
|
||||
|
||||
def get_subcurve(self, a, b):
|
||||
def get_subcurve(self, a: float, b: float) -> VMobject:
|
||||
vmob = self.copy()
|
||||
vmob.pointwise_become_partial(self, a, b)
|
||||
return vmob
|
||||
@@ -832,7 +902,7 @@ class VMobject(Mobject):
|
||||
mob.needs_new_triangulation = True
|
||||
return self
|
||||
|
||||
def get_triangulation(self, normal_vector=None):
|
||||
def get_triangulation(self, normal_vector: np.ndarray | None = None):
|
||||
# Figure out how to triangulate the interior to know
|
||||
# how to send the points as to the vertex shader.
|
||||
# First triangles come directly from the points
|
||||
@@ -901,25 +971,30 @@ class VMobject(Mobject):
|
||||
return wrapper
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def set_points(self, points):
|
||||
def set_points(self, points: npt.ArrayLike):
|
||||
super().set_points(points)
|
||||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def set_data(self, data):
|
||||
def set_data(self, data: dict):
|
||||
super().set_data(data)
|
||||
return self
|
||||
|
||||
# TODO, how to be smart about tangents here?
|
||||
@triggers_refreshed_triangulation
|
||||
def apply_function(self, function, make_smooth=False, **kwargs):
|
||||
def apply_function(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
make_smooth: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
super().apply_function(function, **kwargs)
|
||||
if self.make_smooth_after_applying_functions or make_smooth:
|
||||
self.make_approximately_smooth()
|
||||
return self
|
||||
|
||||
def flip(self, *args, **kwargs):
|
||||
super().flip(*args, **kwargs)
|
||||
def flip(self, axis: np.ndarray = UP, **kwargs):
|
||||
super().flip(axis, **kwargs)
|
||||
self.refresh_unit_normal()
|
||||
self.refresh_triangulation()
|
||||
return self
|
||||
@@ -945,20 +1020,20 @@ class VMobject(Mobject):
|
||||
wrapper.refresh_id()
|
||||
return self
|
||||
|
||||
def get_fill_shader_wrapper(self):
|
||||
def get_fill_shader_wrapper(self) -> ShaderWrapper:
|
||||
self.fill_shader_wrapper.vert_data = self.get_fill_shader_data()
|
||||
self.fill_shader_wrapper.vert_indices = self.get_fill_shader_vert_indices()
|
||||
self.fill_shader_wrapper.uniforms = self.get_shader_uniforms()
|
||||
self.fill_shader_wrapper.depth_test = self.depth_test
|
||||
return self.fill_shader_wrapper
|
||||
|
||||
def get_stroke_shader_wrapper(self):
|
||||
def get_stroke_shader_wrapper(self) -> ShaderWrapper:
|
||||
self.stroke_shader_wrapper.vert_data = self.get_stroke_shader_data()
|
||||
self.stroke_shader_wrapper.uniforms = self.get_stroke_uniforms()
|
||||
self.stroke_shader_wrapper.depth_test = self.depth_test
|
||||
return self.stroke_shader_wrapper
|
||||
|
||||
def get_shader_wrapper_list(self):
|
||||
def get_shader_wrapper_list(self) -> list[ShaderWrapper]:
|
||||
# Build up data lists
|
||||
fill_shader_wrappers = []
|
||||
stroke_shader_wrappers = []
|
||||
@@ -987,13 +1062,13 @@ class VMobject(Mobject):
|
||||
result.append(wrapper)
|
||||
return result
|
||||
|
||||
def get_stroke_uniforms(self):
|
||||
def get_stroke_uniforms(self) -> dict[str, float]:
|
||||
result = dict(super().get_shader_uniforms())
|
||||
result["joint_type"] = JOINT_TYPE_MAP[self.joint_type]
|
||||
result["flat_stroke"] = float(self.flat_stroke)
|
||||
return result
|
||||
|
||||
def get_stroke_shader_data(self):
|
||||
def get_stroke_shader_data(self) -> np.ndarray:
|
||||
points = self.get_points()
|
||||
if len(self.stroke_data) != len(points):
|
||||
self.stroke_data = resize_array(self.stroke_data, len(points))
|
||||
@@ -1012,7 +1087,7 @@ class VMobject(Mobject):
|
||||
|
||||
return self.stroke_data
|
||||
|
||||
def get_fill_shader_data(self):
|
||||
def get_fill_shader_data(self) -> np.ndarray:
|
||||
points = self.get_points()
|
||||
if len(self.fill_data) != len(points):
|
||||
self.fill_data = resize_array(self.fill_data, len(points))
|
||||
@@ -1028,18 +1103,18 @@ class VMobject(Mobject):
|
||||
self.get_fill_shader_data()
|
||||
self.get_stroke_shader_data()
|
||||
|
||||
def get_fill_shader_vert_indices(self):
|
||||
def get_fill_shader_vert_indices(self) -> np.ndarray:
|
||||
return self.get_triangulation()
|
||||
|
||||
|
||||
class VGroup(VMobject):
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs):
|
||||
if not all([isinstance(m, VMobject) for m in vmobjects]):
|
||||
raise Exception("All submobjects must be of type VMobject")
|
||||
super().__init__(**kwargs)
|
||||
self.add(*vmobjects)
|
||||
|
||||
def __add__(self: 'VGroup', other: 'VMobject' or 'VGroup'):
|
||||
def __add__(self, other: VMobject | VGroup):
|
||||
assert(isinstance(other, VMobject))
|
||||
return self.add(other)
|
||||
|
||||
@@ -1053,14 +1128,14 @@ class VectorizedPoint(Point, VMobject):
|
||||
"artificial_height": 0.01,
|
||||
}
|
||||
|
||||
def __init__(self, location=ORIGIN, **kwargs):
|
||||
def __init__(self, location: np.ndarray = ORIGIN, **kwargs):
|
||||
Point.__init__(self, **kwargs)
|
||||
VMobject.__init__(self, **kwargs)
|
||||
self.set_points(np.array([location]))
|
||||
|
||||
|
||||
class CurvesAsSubmobjects(VGroup):
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
for tup in vmobject.get_bezier_tuples():
|
||||
part = VMobject()
|
||||
@@ -1076,7 +1151,7 @@ class DashedVMobject(VMobject):
|
||||
"color": WHITE
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
num_dashes = self.num_dashes
|
||||
ps_ratio = self.positive_space_ratio
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
@@ -15,11 +17,11 @@ class ValueTracker(Mobject):
|
||||
"value_type": np.float64,
|
||||
}
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
def __init__(self, value: float | complex = 0, **kwargs):
|
||||
self.value = value
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_data(self):
|
||||
def init_data(self) -> None:
|
||||
super().init_data()
|
||||
self.data["value"] = np.array(
|
||||
listify(self.value),
|
||||
@@ -27,17 +29,17 @@ class ValueTracker(Mobject):
|
||||
dtype=self.value_type,
|
||||
)
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> float | complex:
|
||||
result = self.data["value"][0, :]
|
||||
if len(result) == 1:
|
||||
return result[0]
|
||||
return result
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: float | complex):
|
||||
self.data["value"][0, :] = value
|
||||
return self
|
||||
|
||||
def increment_value(self, d_value):
|
||||
def increment_value(self, d_value: float | complex) -> None:
|
||||
self.set_value(self.get_value() + d_value)
|
||||
|
||||
|
||||
@@ -48,10 +50,10 @@ class ExponentialValueTracker(ValueTracker):
|
||||
behaves
|
||||
"""
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> float | complex:
|
||||
return np.exp(ValueTracker.get_value(self))
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: float | complex):
|
||||
return ValueTracker.set_value(self, np.log(value))
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
import random
|
||||
from typing import Sequence, TypeVar, Callable, Iterable
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.constants import *
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.animation.indication import VShowPassingFlash
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
@@ -18,8 +22,19 @@ from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.simple_functions import sigmoid
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
def get_vectorized_rgb_gradient_function(min_value, max_value, color_map):
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.coordinate_systems import CoordinateSystem
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def get_vectorized_rgb_gradient_function(
|
||||
min_value: T,
|
||||
max_value: T,
|
||||
color_map: str
|
||||
) -> Callable[[npt.ArrayLike], np.ndarray]:
|
||||
rgbs = np.array(get_colormap_list(color_map))
|
||||
|
||||
def func(values):
|
||||
@@ -37,12 +52,19 @@ def get_vectorized_rgb_gradient_function(min_value, max_value, color_map):
|
||||
return func
|
||||
|
||||
|
||||
def get_rgb_gradient_function(min_value, max_value, color_map):
|
||||
def get_rgb_gradient_function(
|
||||
min_value: T,
|
||||
max_value: T,
|
||||
color_map: str
|
||||
) -> Callable[[T], np.ndarray]:
|
||||
vectorized_func = get_vectorized_rgb_gradient_function(min_value, max_value, color_map)
|
||||
return lambda value: vectorized_func([value])[0]
|
||||
|
||||
|
||||
def move_along_vector_field(mobject, func):
|
||||
def move_along_vector_field(
|
||||
mobject: Mobject,
|
||||
func: Callable[[np.ndarray], np.ndarray]
|
||||
) -> Mobject:
|
||||
mobject.add_updater(
|
||||
lambda m, dt: m.shift(
|
||||
func(m.get_center()) * dt
|
||||
@@ -51,7 +73,10 @@ def move_along_vector_field(mobject, func):
|
||||
return mobject
|
||||
|
||||
|
||||
def move_submobjects_along_vector_field(mobject, func):
|
||||
def move_submobjects_along_vector_field(
|
||||
mobject: Mobject,
|
||||
func: Callable[[np.ndarray], np.ndarray]
|
||||
) -> Mobject:
|
||||
def apply_nudge(mob, dt):
|
||||
for submob in mob:
|
||||
x, y = submob.get_center()[:2]
|
||||
@@ -62,7 +87,11 @@ def move_submobjects_along_vector_field(mobject, func):
|
||||
return mobject
|
||||
|
||||
|
||||
def move_points_along_vector_field(mobject, func, coordinate_system):
|
||||
def move_points_along_vector_field(
|
||||
mobject: Mobject,
|
||||
func: Callable[[float, float], Iterable[float]],
|
||||
coordinate_system: CoordinateSystem
|
||||
) -> Mobject:
|
||||
cs = coordinate_system
|
||||
origin = cs.get_origin()
|
||||
|
||||
@@ -74,7 +103,10 @@ def move_points_along_vector_field(mobject, func, coordinate_system):
|
||||
return mobject
|
||||
|
||||
|
||||
def get_sample_points_from_coordinate_system(coordinate_system, step_multiple):
|
||||
def get_sample_points_from_coordinate_system(
|
||||
coordinate_system: CoordinateSystem,
|
||||
step_multiple: float
|
||||
) -> it.product[tuple[np.ndarray, ...]]:
|
||||
ranges = []
|
||||
for range_args in coordinate_system.get_all_ranges():
|
||||
_min, _max, step = range_args
|
||||
@@ -96,7 +128,12 @@ class VectorField(VGroup):
|
||||
"vector_config": {},
|
||||
}
|
||||
|
||||
def __init__(self, func, coordinate_system, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], Sequence[float]],
|
||||
coordinate_system: CoordinateSystem,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.func = func
|
||||
self.coordinate_system = coordinate_system
|
||||
@@ -112,7 +149,7 @@ class VectorField(VGroup):
|
||||
for coords in samples
|
||||
))
|
||||
|
||||
def get_vector(self, coords, **kwargs):
|
||||
def get_vector(self, coords: Iterable[float], **kwargs) -> Arrow:
|
||||
vector_config = merge_dicts_recursively(
|
||||
self.vector_config,
|
||||
kwargs
|
||||
@@ -157,19 +194,24 @@ class StreamLines(VGroup):
|
||||
"color_map": "3b1b_colormap",
|
||||
}
|
||||
|
||||
def __init__(self, func, coordinate_system, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], Sequence[float]],
|
||||
coordinate_system: CoordinateSystem,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.func = func
|
||||
self.coordinate_system = coordinate_system
|
||||
self.draw_lines()
|
||||
self.init_style()
|
||||
|
||||
def point_func(self, point):
|
||||
def point_func(self, point: np.ndarray) -> np.ndarray:
|
||||
in_coords = self.coordinate_system.p2c(point)
|
||||
out_coords = self.func(*in_coords)
|
||||
return self.coordinate_system.c2p(*out_coords)
|
||||
|
||||
def draw_lines(self):
|
||||
def draw_lines(self) -> None:
|
||||
lines = []
|
||||
origin = self.coordinate_system.get_origin()
|
||||
for point in self.get_start_points():
|
||||
@@ -194,7 +236,7 @@ class StreamLines(VGroup):
|
||||
lines.append(line)
|
||||
self.set_submobjects(lines)
|
||||
|
||||
def get_start_points(self):
|
||||
def get_start_points(self) -> np.ndarray:
|
||||
cs = self.coordinate_system
|
||||
sample_coords = get_sample_points_from_coordinate_system(
|
||||
cs, self.step_multiple,
|
||||
@@ -210,7 +252,7 @@ class StreamLines(VGroup):
|
||||
for coords in sample_coords
|
||||
])
|
||||
|
||||
def init_style(self):
|
||||
def init_style(self) -> None:
|
||||
if self.color_by_magnitude:
|
||||
values_to_rgbs = get_vectorized_rgb_gradient_function(
|
||||
*self.magnitude_range, self.color_map,
|
||||
@@ -247,7 +289,7 @@ class AnimatedStreamLines(VGroup):
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, stream_lines, **kwargs):
|
||||
def __init__(self, stream_lines: StreamLines, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.stream_lines = stream_lines
|
||||
for line in stream_lines:
|
||||
@@ -262,7 +304,7 @@ class AnimatedStreamLines(VGroup):
|
||||
|
||||
self.add_updater(lambda m, dt: m.update(dt))
|
||||
|
||||
def update(self, dt):
|
||||
def update(self, dt: float) -> None:
|
||||
stream_lines = self.stream_lines
|
||||
for line in stream_lines:
|
||||
line.time += dt
|
||||
@@ -278,7 +320,7 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
||||
"remover": True
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
def __init__(self, vmobject: VMobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
max_stroke_width = vmobject.get_stroke_width()
|
||||
max_time_width = kwargs.pop("time_width", self.time_width)
|
||||
|
||||
@@ -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(*[
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import inspect
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import random
|
||||
import inspect
|
||||
import platform
|
||||
import itertools as it
|
||||
from functools import wraps
|
||||
from typing import Iterable, Callable
|
||||
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
import numpy as np
|
||||
import time
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.animation.transform import MoveToTarget
|
||||
@@ -22,6 +26,12 @@ from manimlib.event_handler.event_type import EventType
|
||||
from manimlib.event_handler import EVENT_DISPATCHER
|
||||
from manimlib.logger import log
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL.Image import Image
|
||||
from manimlib.animation.animation import Animation
|
||||
|
||||
|
||||
class Scene(object):
|
||||
CONFIG = {
|
||||
@@ -36,7 +46,9 @@ class Scene(object):
|
||||
"end_at_animation_number": None,
|
||||
"leave_progress_bars": False,
|
||||
"preview": True,
|
||||
"presenter_mode": False,
|
||||
"linger_after_completion": True,
|
||||
"pan_sensitivity": 3,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -49,28 +61,29 @@ class Scene(object):
|
||||
else:
|
||||
self.window = None
|
||||
|
||||
self.camera = self.camera_class(**self.camera_config)
|
||||
self.camera: Camera = self.camera_class(**self.camera_config)
|
||||
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
|
||||
self.mobjects = [self.camera.frame]
|
||||
self.num_plays = 0
|
||||
self.time = 0
|
||||
self.skip_time = 0
|
||||
self.original_skipping_status = self.skip_animations
|
||||
self.mobjects: list[Mobject] = [self.camera.frame]
|
||||
self.num_plays: int = 0
|
||||
self.time: float = 0
|
||||
self.skip_time: float = 0
|
||||
self.original_skipping_status: bool = self.skip_animations
|
||||
if self.start_at_animation_number is not None:
|
||||
self.skip_animations = True
|
||||
|
||||
# Items associated with interaction
|
||||
self.mouse_point = Point()
|
||||
self.mouse_drag_point = Point()
|
||||
self.hold_on_wait = self.presenter_mode
|
||||
|
||||
# Much nicer to work with deterministic scenes
|
||||
if self.random_seed is not None:
|
||||
random.seed(self.random_seed)
|
||||
np.random.seed(self.random_seed)
|
||||
|
||||
def run(self):
|
||||
self.virtual_animation_start_time = 0
|
||||
self.real_animation_start_time = time.time()
|
||||
def run(self) -> None:
|
||||
self.virtual_animation_start_time: float = 0
|
||||
self.real_animation_start_time: float = time.time()
|
||||
self.file_writer.begin()
|
||||
|
||||
self.setup()
|
||||
@@ -80,7 +93,7 @@ class Scene(object):
|
||||
pass
|
||||
self.tear_down()
|
||||
|
||||
def setup(self):
|
||||
def setup(self) -> None:
|
||||
"""
|
||||
This is meant to be implement by any scenes which
|
||||
are comonly subclassed, and have some common setup
|
||||
@@ -88,18 +101,18 @@ class Scene(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def construct(self):
|
||||
def construct(self) -> None:
|
||||
# Where all the animation happens
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
|
||||
def tear_down(self):
|
||||
def tear_down(self) -> None:
|
||||
self.stop_skipping()
|
||||
self.file_writer.finish()
|
||||
if self.window and self.linger_after_completion:
|
||||
self.interact()
|
||||
|
||||
def interact(self):
|
||||
def interact(self) -> None:
|
||||
# If there is a window, enter a loop
|
||||
# which updates the frame while under
|
||||
# the hood calling the pyglet event loop
|
||||
@@ -114,7 +127,7 @@ class Scene(object):
|
||||
if self.quit_interaction:
|
||||
self.unlock_mobject_data()
|
||||
|
||||
def embed(self):
|
||||
def embed(self, close_scene_on_exit: bool = True) -> None:
|
||||
if not self.preview:
|
||||
# If the scene is just being
|
||||
# written, ignore embed calls
|
||||
@@ -139,21 +152,22 @@ class Scene(object):
|
||||
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
||||
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
||||
shell(local_ns=local_ns, stack_depth=2)
|
||||
# End scene when exiting an embed.
|
||||
raise EndSceneEarlyException()
|
||||
# End scene when exiting an embed
|
||||
if close_scene_on_exit:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
# Only these methods should touch the camera
|
||||
def get_image(self):
|
||||
def get_image(self) -> Image:
|
||||
return self.camera.get_image()
|
||||
|
||||
def show(self):
|
||||
def show(self) -> None:
|
||||
self.update_frame(ignore_skipping=True)
|
||||
self.get_image().show()
|
||||
|
||||
def update_frame(self, dt=0, ignore_skipping=False):
|
||||
def update_frame(self, dt: float = 0, ignore_skipping: bool = False) -> None:
|
||||
self.increment_time(dt)
|
||||
self.update_mobjects(dt)
|
||||
if self.skip_animations and not ignore_skipping:
|
||||
@@ -171,22 +185,22 @@ class Scene(object):
|
||||
if rt < vt:
|
||||
self.update_frame(0)
|
||||
|
||||
def emit_frame(self):
|
||||
def emit_frame(self) -> None:
|
||||
if not self.skip_animations:
|
||||
self.file_writer.write_frame(self.camera)
|
||||
|
||||
# Related to updating
|
||||
def update_mobjects(self, dt):
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
for mobject in self.mobjects:
|
||||
mobject.update(dt)
|
||||
|
||||
def should_update_mobjects(self):
|
||||
def should_update_mobjects(self) -> bool:
|
||||
return self.always_update_mobjects or any([
|
||||
len(mob.get_family_updaters()) > 0
|
||||
for mob in self.mobjects
|
||||
])
|
||||
|
||||
def has_time_based_updaters(self):
|
||||
def has_time_based_updaters(self) -> bool:
|
||||
return any([
|
||||
sm.has_time_based_updater()
|
||||
for mob in self.mobjects()
|
||||
@@ -194,14 +208,14 @@ class Scene(object):
|
||||
])
|
||||
|
||||
# Related to time
|
||||
def get_time(self):
|
||||
def get_time(self) -> float:
|
||||
return self.time
|
||||
|
||||
def increment_time(self, dt):
|
||||
def increment_time(self, dt: float) -> None:
|
||||
self.time += dt
|
||||
|
||||
# Related to internal mobject organization
|
||||
def get_top_level_mobjects(self):
|
||||
def get_top_level_mobjects(self) -> list[Mobject]:
|
||||
# Return only those which are not in the family
|
||||
# of another mobject from the scene
|
||||
mobjects = self.get_mobjects()
|
||||
@@ -215,10 +229,10 @@ class Scene(object):
|
||||
return num_families == 1
|
||||
return list(filter(is_top_level, mobjects))
|
||||
|
||||
def get_mobject_family_members(self):
|
||||
def get_mobject_family_members(self) -> list[Mobject]:
|
||||
return extract_mobject_family_members(self.mobjects)
|
||||
|
||||
def add(self, *new_mobjects):
|
||||
def add(self, *new_mobjects: Mobject):
|
||||
"""
|
||||
Mobjects will be displayed, from background to
|
||||
foreground in the order with which they are added.
|
||||
@@ -227,7 +241,7 @@ class Scene(object):
|
||||
self.mobjects += new_mobjects
|
||||
return self
|
||||
|
||||
def add_mobjects_among(self, values):
|
||||
def add_mobjects_among(self, values: Iterable):
|
||||
"""
|
||||
This is meant mostly for quick prototyping,
|
||||
e.g. to add all mobjects defined up to a point,
|
||||
@@ -239,17 +253,17 @@ class Scene(object):
|
||||
))
|
||||
return self
|
||||
|
||||
def remove(self, *mobjects_to_remove):
|
||||
def remove(self, *mobjects_to_remove: Mobject):
|
||||
self.mobjects = restructure_list_to_exclude_certain_family_members(
|
||||
self.mobjects, mobjects_to_remove
|
||||
)
|
||||
return self
|
||||
|
||||
def bring_to_front(self, *mobjects):
|
||||
def bring_to_front(self, *mobjects: Mobject):
|
||||
self.add(*mobjects)
|
||||
return self
|
||||
|
||||
def bring_to_back(self, *mobjects):
|
||||
def bring_to_back(self, *mobjects: Mobject):
|
||||
self.remove(*mobjects)
|
||||
self.mobjects = list(mobjects) + self.mobjects
|
||||
return self
|
||||
@@ -258,13 +272,18 @@ class Scene(object):
|
||||
self.mobjects = []
|
||||
return self
|
||||
|
||||
def get_mobjects(self):
|
||||
def get_mobjects(self) -> list[Mobject]:
|
||||
return list(self.mobjects)
|
||||
|
||||
def get_mobject_copies(self):
|
||||
def get_mobject_copies(self) -> list[Mobject]:
|
||||
return [m.copy() for m in self.mobjects]
|
||||
|
||||
def point_to_mobject(self, point, search_set=None, buff=0):
|
||||
def point_to_mobject(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
search_set: Iterable[Mobject] | None = None,
|
||||
buff: float = 0
|
||||
) -> Mobject | None:
|
||||
"""
|
||||
E.g. if clicking on the scene, this returns the top layer mobject
|
||||
under a given point
|
||||
@@ -277,7 +296,7 @@ class Scene(object):
|
||||
return None
|
||||
|
||||
# Related to skipping
|
||||
def update_skipping_status(self):
|
||||
def update_skipping_status(self) -> None:
|
||||
if self.start_at_animation_number is not None:
|
||||
if self.num_plays == self.start_at_animation_number:
|
||||
self.skip_time = self.time
|
||||
@@ -287,12 +306,18 @@ class Scene(object):
|
||||
if self.num_plays >= self.end_at_animation_number:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
def stop_skipping(self):
|
||||
def stop_skipping(self) -> None:
|
||||
self.virtual_animation_start_time = self.time
|
||||
self.skip_animations = False
|
||||
|
||||
# Methods associated with running animations
|
||||
def get_time_progression(self, run_time, n_iterations=None, desc="", override_skip_animations=False):
|
||||
def get_time_progression(
|
||||
self,
|
||||
run_time: float,
|
||||
n_iterations: int | None = None,
|
||||
desc: str = "",
|
||||
override_skip_animations: bool = False
|
||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
if self.skip_animations and not override_skip_animations:
|
||||
return [run_time]
|
||||
else:
|
||||
@@ -311,10 +336,13 @@ class Scene(object):
|
||||
desc=desc,
|
||||
)
|
||||
|
||||
def get_run_time(self, animations):
|
||||
def get_run_time(self, animations: Iterable[Animation]) -> float:
|
||||
return np.max([animation.run_time for animation in animations])
|
||||
|
||||
def get_animation_time_progression(self, animations):
|
||||
def get_animation_time_progression(
|
||||
self,
|
||||
animations: Iterable[Animation]
|
||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
run_time = self.get_run_time(animations)
|
||||
description = f"{self.num_plays} {animations[0]}"
|
||||
if len(animations) > 1:
|
||||
@@ -322,14 +350,18 @@ class Scene(object):
|
||||
time_progression = self.get_time_progression(run_time, desc=description)
|
||||
return time_progression
|
||||
|
||||
def get_wait_time_progression(self, duration, stop_condition=None):
|
||||
def get_wait_time_progression(
|
||||
self,
|
||||
duration: float,
|
||||
stop_condition: Callable[[], bool] | None = None
|
||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
kw = {"desc": f"{self.num_plays} Waiting"}
|
||||
if stop_condition is not None:
|
||||
kw["n_iterations"] = -1 # So it doesn't show % progress
|
||||
kw["override_skip_animations"] = True
|
||||
return self.get_time_progression(duration, **kw)
|
||||
|
||||
def anims_from_play_args(self, *args, **kwargs):
|
||||
def anims_from_play_args(self, *args, **kwargs) -> list[Animation]:
|
||||
"""
|
||||
Each arg can either be an animation, or a mobject method
|
||||
followed by that methods arguments (and potentially follow
|
||||
@@ -419,7 +451,7 @@ class Scene(object):
|
||||
self.num_plays += 1
|
||||
return wrapper
|
||||
|
||||
def lock_static_mobject_data(self, *animations):
|
||||
def lock_static_mobject_data(self, *animations: Animation) -> None:
|
||||
movers = list(it.chain(*[
|
||||
anim.mobject.get_family()
|
||||
for anim in animations
|
||||
@@ -429,10 +461,15 @@ class Scene(object):
|
||||
continue
|
||||
self.camera.set_mobjects_as_static(mobject)
|
||||
|
||||
def unlock_mobject_data(self):
|
||||
def unlock_mobject_data(self) -> None:
|
||||
self.camera.release_static_mobjects()
|
||||
|
||||
def begin_animations(self, animations):
|
||||
def refresh_locked_data(self):
|
||||
self.unlock_mobject_data()
|
||||
self.lock_static_mobject_data()
|
||||
return self
|
||||
|
||||
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
||||
for animation in animations:
|
||||
animation.begin()
|
||||
# Anything animated that's not already in the
|
||||
@@ -443,7 +480,7 @@ class Scene(object):
|
||||
if animation.mobject not in self.mobjects:
|
||||
self.add(animation.mobject)
|
||||
|
||||
def progress_through_animations(self, animations):
|
||||
def progress_through_animations(self, animations: Iterable[Animation]) -> None:
|
||||
last_t = 0
|
||||
for t in self.get_animation_time_progression(animations):
|
||||
dt = t - last_t
|
||||
@@ -455,7 +492,7 @@ class Scene(object):
|
||||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
|
||||
def finish_animations(self, animations):
|
||||
def finish_animations(self, animations: Iterable[Animation]) -> None:
|
||||
for animation in animations:
|
||||
animation.finish()
|
||||
animation.clean_up_from_scene(self)
|
||||
@@ -465,7 +502,7 @@ class Scene(object):
|
||||
self.update_mobjects(0)
|
||||
|
||||
@handle_play_like_call
|
||||
def play(self, *args, **kwargs):
|
||||
def play(self, *args, **kwargs) -> None:
|
||||
if len(args) == 0:
|
||||
log.warning("Called Scene.play with no animations")
|
||||
return
|
||||
@@ -477,23 +514,40 @@ class Scene(object):
|
||||
self.unlock_mobject_data()
|
||||
|
||||
@handle_play_like_call
|
||||
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
|
||||
def wait(
|
||||
self,
|
||||
duration: float = DEFAULT_WAIT_TIME,
|
||||
stop_condition: Callable[[], bool] = None,
|
||||
note: str = None,
|
||||
ignore_presenter_mode: bool = False
|
||||
):
|
||||
if note:
|
||||
log.info(note)
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
self.lock_static_mobject_data()
|
||||
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||
last_t = 0
|
||||
for t in time_progression:
|
||||
dt = t - last_t
|
||||
last_t = t
|
||||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
if stop_condition is not None and stop_condition():
|
||||
time_progression.close()
|
||||
break
|
||||
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:
|
||||
dt = t - last_t
|
||||
last_t = t
|
||||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
if stop_condition is not None and stop_condition():
|
||||
time_progression.close()
|
||||
break
|
||||
self.unlock_mobject_data()
|
||||
return self
|
||||
|
||||
def wait_until(self, stop_condition, max_time=60):
|
||||
def wait_until(
|
||||
self,
|
||||
stop_condition: Callable[[], bool],
|
||||
max_time: float = 60
|
||||
):
|
||||
self.wait(max_time, stop_condition=stop_condition)
|
||||
|
||||
def force_skipping(self):
|
||||
@@ -506,14 +560,20 @@ class Scene(object):
|
||||
self.skip_animations = self.original_skipping_status
|
||||
return self
|
||||
|
||||
def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs):
|
||||
def add_sound(
|
||||
self,
|
||||
sound_file: str,
|
||||
time_offset: float = 0,
|
||||
gain: float | None = None,
|
||||
gain_to_background: float | None = None
|
||||
):
|
||||
if self.skip_animations:
|
||||
return
|
||||
time = self.get_time() + time_offset
|
||||
self.file_writer.add_sound(sound_file, time, gain, **kwargs)
|
||||
self.file_writer.add_sound(sound_file, time, gain, gain_to_background)
|
||||
|
||||
# Helpers for interactive development
|
||||
def save_state(self):
|
||||
def save_state(self) -> None:
|
||||
self.saved_state = {
|
||||
"mobjects": self.mobjects,
|
||||
"mobject_states": [
|
||||
@@ -522,7 +582,7 @@ class Scene(object):
|
||||
],
|
||||
}
|
||||
|
||||
def restore(self):
|
||||
def restore(self) -> None:
|
||||
if not hasattr(self, "saved_state"):
|
||||
raise Exception("Trying to restore scene without having saved")
|
||||
mobjects = self.saved_state["mobjects"]
|
||||
@@ -533,7 +593,11 @@ class Scene(object):
|
||||
|
||||
# Event handling
|
||||
|
||||
def on_mouse_motion(self, point, d_point):
|
||||
def on_mouse_motion(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
d_point: np.ndarray
|
||||
) -> None:
|
||||
self.mouse_point.move_to(point)
|
||||
|
||||
event_data = {"point": point, "d_point": d_point}
|
||||
@@ -543,8 +607,8 @@ class Scene(object):
|
||||
|
||||
frame = self.camera.frame
|
||||
if self.window.is_key_pressed(ord("d")):
|
||||
frame.increment_theta(-d_point[0])
|
||||
frame.increment_phi(d_point[1])
|
||||
frame.increment_theta(-self.pan_sensitivity * d_point[0])
|
||||
frame.increment_phi(self.pan_sensitivity * d_point[1])
|
||||
elif self.window.is_key_pressed(ord("s")):
|
||||
shift = -d_point
|
||||
shift[0] *= frame.get_width() / 2
|
||||
@@ -553,7 +617,13 @@ class Scene(object):
|
||||
shift = np.dot(np.transpose(transform), shift)
|
||||
frame.shift(shift)
|
||||
|
||||
def on_mouse_drag(self, point, d_point, buttons, modifiers):
|
||||
def on_mouse_drag(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
d_point: np.ndarray,
|
||||
buttons: int,
|
||||
modifiers: int
|
||||
) -> None:
|
||||
self.mouse_drag_point.move_to(point)
|
||||
|
||||
event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers}
|
||||
@@ -561,19 +631,33 @@ class Scene(object):
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_mouse_press(self, point, button, mods):
|
||||
def on_mouse_press(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
button: int,
|
||||
mods: int
|
||||
) -> None:
|
||||
event_data = {"point": point, "button": button, "mods": mods}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MousePressEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_mouse_release(self, point, button, mods):
|
||||
def on_mouse_release(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
button: int,
|
||||
mods: int
|
||||
) -> None:
|
||||
event_data = {"point": point, "button": button, "mods": mods}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseReleaseEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_mouse_scroll(self, point, offset):
|
||||
def on_mouse_scroll(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
offset: np.ndarray
|
||||
) -> None:
|
||||
event_data = {"point": point, "offset": offset}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
@@ -588,13 +672,21 @@ class Scene(object):
|
||||
shift = np.dot(np.transpose(transform), offset)
|
||||
frame.shift(-20.0 * shift)
|
||||
|
||||
def on_key_release(self, symbol, modifiers):
|
||||
def on_key_release(
|
||||
self,
|
||||
symbol: int,
|
||||
modifiers: int
|
||||
) -> None:
|
||||
event_data = {"symbol": symbol, "modifiers": modifiers}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.KeyReleaseEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
def on_key_press(
|
||||
self,
|
||||
symbol: int,
|
||||
modifiers: int
|
||||
) -> None:
|
||||
try:
|
||||
char = chr(symbol)
|
||||
except OverflowError:
|
||||
@@ -610,17 +702,21 @@ class Scene(object):
|
||||
self.camera.frame.to_default_state()
|
||||
elif char == "q":
|
||||
self.quit_interaction = True
|
||||
elif char == " " or symbol == 65363: # Space or right arrow
|
||||
self.hold_on_wait = False
|
||||
elif char == "e" and modifiers == 3: # ctrl + shift + e
|
||||
self.embed(close_scene_on_exit=False)
|
||||
|
||||
def on_resize(self, width: int, height: int):
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
self.camera.reset_pixel_shape(width, height)
|
||||
|
||||
def on_show(self):
|
||||
def on_show(self) -> None:
|
||||
pass
|
||||
|
||||
def on_hide(self):
|
||||
def on_hide(self) -> None:
|
||||
pass
|
||||
|
||||
def on_close(self):
|
||||
def on_close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
import shutil
|
||||
import subprocess as sp
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import platform
|
||||
import subprocess as sp
|
||||
|
||||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
|
||||
from manimlib.constants import FFMPEG_BIN
|
||||
@@ -15,6 +18,13 @@ from manimlib.utils.file_ops import get_sorted_integer_files
|
||||
from manimlib.utils.sounds import get_full_sound_file_path
|
||||
from manimlib.logger import log
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.camera.camera import Camera
|
||||
from PIL.Image import Image
|
||||
|
||||
|
||||
class SceneFileWriter(object):
|
||||
CONFIG = {
|
||||
@@ -42,14 +52,14 @@ class SceneFileWriter(object):
|
||||
|
||||
def __init__(self, scene, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.scene = scene
|
||||
self.writing_process = None
|
||||
self.has_progress_display = False
|
||||
self.scene: Scene = scene
|
||||
self.writing_process: sp.Popen | None = None
|
||||
self.has_progress_display: bool = False
|
||||
self.init_output_directories()
|
||||
self.init_audio()
|
||||
|
||||
# Output directories and files
|
||||
def init_output_directories(self):
|
||||
def init_output_directories(self) -> None:
|
||||
out_dir = self.output_directory
|
||||
if self.mirror_module_path:
|
||||
module_dir = self.get_default_module_directory()
|
||||
@@ -69,13 +79,13 @@ class SceneFileWriter(object):
|
||||
movie_dir, "partial_movie_files", scene_name,
|
||||
))
|
||||
|
||||
def get_default_module_directory(self):
|
||||
def get_default_module_directory(self) -> str:
|
||||
path, _ = os.path.splitext(self.input_file_path)
|
||||
if path.startswith("_"):
|
||||
path = path[1:]
|
||||
return path
|
||||
|
||||
def get_default_scene_name(self):
|
||||
def get_default_scene_name(self) -> str:
|
||||
name = str(self.scene)
|
||||
saan = self.scene.start_at_animation_number
|
||||
eaan = self.scene.end_at_animation_number
|
||||
@@ -85,7 +95,7 @@ class SceneFileWriter(object):
|
||||
name += f"_{eaan}"
|
||||
return name
|
||||
|
||||
def get_resolution_directory(self):
|
||||
def get_resolution_directory(self) -> str:
|
||||
pixel_height = self.scene.camera.pixel_height
|
||||
frame_rate = self.scene.camera.frame_rate
|
||||
return "{}p{}".format(
|
||||
@@ -93,10 +103,10 @@ class SceneFileWriter(object):
|
||||
)
|
||||
|
||||
# Directory getters
|
||||
def get_image_file_path(self):
|
||||
def get_image_file_path(self) -> str:
|
||||
return self.image_file_path
|
||||
|
||||
def get_next_partial_movie_path(self):
|
||||
def get_next_partial_movie_path(self) -> str:
|
||||
result = os.path.join(
|
||||
self.partial_movie_directory,
|
||||
"{:05}{}".format(
|
||||
@@ -106,19 +116,22 @@ class SceneFileWriter(object):
|
||||
)
|
||||
return result
|
||||
|
||||
def get_movie_file_path(self):
|
||||
def get_movie_file_path(self) -> str:
|
||||
return self.movie_file_path
|
||||
|
||||
# Sound
|
||||
def init_audio(self):
|
||||
self.includes_sound = False
|
||||
def init_audio(self) -> None:
|
||||
self.includes_sound: bool = False
|
||||
|
||||
def create_audio_segment(self):
|
||||
def create_audio_segment(self) -> None:
|
||||
self.audio_segment = AudioSegment.silent()
|
||||
|
||||
def add_audio_segment(self, new_segment,
|
||||
time=None,
|
||||
gain_to_background=None):
|
||||
def add_audio_segment(
|
||||
self,
|
||||
new_segment: AudioSegment,
|
||||
time: float | None = None,
|
||||
gain_to_background: float | None = None
|
||||
) -> None:
|
||||
if not self.includes_sound:
|
||||
self.includes_sound = True
|
||||
self.create_audio_segment()
|
||||
@@ -142,27 +155,33 @@ class SceneFileWriter(object):
|
||||
gain_during_overlay=gain_to_background,
|
||||
)
|
||||
|
||||
def add_sound(self, sound_file, time=None, gain=None, **kwargs):
|
||||
def add_sound(
|
||||
self,
|
||||
sound_file: str,
|
||||
time: float | None = None,
|
||||
gain: float | None = None,
|
||||
gain_to_background: float | None = None
|
||||
) -> None:
|
||||
file_path = get_full_sound_file_path(sound_file)
|
||||
new_segment = AudioSegment.from_file(file_path)
|
||||
if gain:
|
||||
new_segment = new_segment.apply_gain(gain)
|
||||
self.add_audio_segment(new_segment, time, **kwargs)
|
||||
self.add_audio_segment(new_segment, time, gain_to_background)
|
||||
|
||||
# Writers
|
||||
def begin(self):
|
||||
def begin(self) -> None:
|
||||
if not self.break_into_partial_movies and self.write_to_movie:
|
||||
self.open_movie_pipe(self.get_movie_file_path())
|
||||
|
||||
def begin_animation(self):
|
||||
def begin_animation(self) -> None:
|
||||
if self.break_into_partial_movies and self.write_to_movie:
|
||||
self.open_movie_pipe(self.get_next_partial_movie_path())
|
||||
|
||||
def end_animation(self):
|
||||
def end_animation(self) -> None:
|
||||
if self.break_into_partial_movies and self.write_to_movie:
|
||||
self.close_movie_pipe()
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
if self.write_to_movie:
|
||||
if self.break_into_partial_movies:
|
||||
self.combine_movie_files()
|
||||
@@ -177,7 +196,7 @@ class SceneFileWriter(object):
|
||||
if self.should_open_file():
|
||||
self.open_file()
|
||||
|
||||
def open_movie_pipe(self, file_path):
|
||||
def open_movie_pipe(self, file_path: str) -> None:
|
||||
stem, ext = os.path.splitext(file_path)
|
||||
self.final_file_path = file_path
|
||||
self.temp_file_path = stem + "_temp" + ext
|
||||
@@ -223,7 +242,7 @@ class SceneFileWriter(object):
|
||||
)
|
||||
self.has_progress_display = True
|
||||
|
||||
def set_progress_display_subdescription(self, sub_desc):
|
||||
def set_progress_display_subdescription(self, sub_desc: str) -> None:
|
||||
desc_len = self.progress_description_len
|
||||
file = os.path.split(self.get_movie_file_path())[1]
|
||||
full_desc = f"Rendering {file} ({sub_desc})"
|
||||
@@ -233,14 +252,14 @@ class SceneFileWriter(object):
|
||||
full_desc += " " * (desc_len - len(full_desc))
|
||||
self.progress_display.set_description(full_desc)
|
||||
|
||||
def write_frame(self, camera):
|
||||
def write_frame(self, camera: Camera) -> None:
|
||||
if self.write_to_movie:
|
||||
raw_bytes = camera.get_raw_fbo_data()
|
||||
self.writing_process.stdin.write(raw_bytes)
|
||||
if self.has_progress_display:
|
||||
self.progress_display.update()
|
||||
|
||||
def close_movie_pipe(self):
|
||||
def close_movie_pipe(self) -> None:
|
||||
self.writing_process.stdin.close()
|
||||
self.writing_process.wait()
|
||||
self.writing_process.terminate()
|
||||
@@ -248,7 +267,7 @@ class SceneFileWriter(object):
|
||||
self.progress_display.close()
|
||||
shutil.move(self.temp_file_path, self.final_file_path)
|
||||
|
||||
def combine_movie_files(self):
|
||||
def combine_movie_files(self) -> None:
|
||||
kwargs = {
|
||||
"remove_non_integer_files": True,
|
||||
"extension": self.movie_file_extension,
|
||||
@@ -296,7 +315,7 @@ class SceneFileWriter(object):
|
||||
combine_process = sp.Popen(commands)
|
||||
combine_process.wait()
|
||||
|
||||
def add_sound_to_video(self):
|
||||
def add_sound_to_video(self) -> None:
|
||||
movie_file_path = self.get_movie_file_path()
|
||||
stem, ext = os.path.splitext(movie_file_path)
|
||||
sound_file_path = stem + ".wav"
|
||||
@@ -327,22 +346,22 @@ class SceneFileWriter(object):
|
||||
shutil.move(temp_file_path, movie_file_path)
|
||||
os.remove(sound_file_path)
|
||||
|
||||
def save_final_image(self, image):
|
||||
def save_final_image(self, image: Image) -> None:
|
||||
file_path = self.get_image_file_path()
|
||||
image.save(file_path)
|
||||
self.print_file_ready_message(file_path)
|
||||
|
||||
def print_file_ready_message(self, file_path):
|
||||
def print_file_ready_message(self, file_path: str) -> None:
|
||||
if not self.quiet:
|
||||
log.info(f"File ready at {file_path}")
|
||||
|
||||
def should_open_file(self):
|
||||
def should_open_file(self) -> bool:
|
||||
return any([
|
||||
self.show_file_location_upon_completion,
|
||||
self.open_file_upon_completion,
|
||||
])
|
||||
|
||||
def open_file(self):
|
||||
def open_file(self) -> None:
|
||||
if self.quiet:
|
||||
curr_stdout = sys.stdout
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
@@ -287,9 +287,6 @@ class LinearTransformationScene(VectorScene):
|
||||
},
|
||||
"background_plane_kwargs": {
|
||||
"color": GREY,
|
||||
"axis_config": {
|
||||
"stroke_color": GREY_B,
|
||||
},
|
||||
"axis_config": {
|
||||
"color": GREY,
|
||||
},
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import copy
|
||||
from typing import Iterable
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
import copy
|
||||
|
||||
from manimlib.utils.directories import get_shader_dir
|
||||
from manimlib.utils.file_ops import find_file
|
||||
@@ -15,15 +19,16 @@ from manimlib.utils.file_ops import find_file
|
||||
|
||||
|
||||
class ShaderWrapper(object):
|
||||
def __init__(self,
|
||||
vert_data=None,
|
||||
vert_indices=None,
|
||||
shader_folder=None,
|
||||
uniforms=None, # A dictionary mapping names of uniform variables
|
||||
texture_paths=None, # A dictionary mapping names to filepaths for textures.
|
||||
depth_test=False,
|
||||
render_primitive=moderngl.TRIANGLE_STRIP,
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
vert_data: np.ndarray | None = None,
|
||||
vert_indices: np.ndarray | None = None,
|
||||
shader_folder: str | None = None,
|
||||
uniforms: dict[str, float] | None = None, # A dictionary mapping names of uniform variables
|
||||
texture_paths: dict[str, str] | None = None, # A dictionary mapping names to filepaths for textures.
|
||||
depth_test: bool = False,
|
||||
render_primitive: int = moderngl.TRIANGLE_STRIP,
|
||||
):
|
||||
self.vert_data = vert_data
|
||||
self.vert_indices = vert_indices
|
||||
self.vert_attributes = vert_data.dtype.names
|
||||
@@ -46,20 +51,20 @@ class ShaderWrapper(object):
|
||||
result.texture_paths = dict(self.texture_paths)
|
||||
return result
|
||||
|
||||
def is_valid(self):
|
||||
def is_valid(self) -> bool:
|
||||
return all([
|
||||
self.vert_data is not None,
|
||||
self.program_code["vertex_shader"] is not None,
|
||||
self.program_code["fragment_shader"] is not None,
|
||||
])
|
||||
|
||||
def get_id(self):
|
||||
def get_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
def get_program_id(self):
|
||||
def get_program_id(self) -> int:
|
||||
return self.program_id
|
||||
|
||||
def create_id(self):
|
||||
def create_id(self) -> str:
|
||||
# A unique id for a shader
|
||||
return "|".join(map(str, [
|
||||
self.program_id,
|
||||
@@ -69,32 +74,32 @@ class ShaderWrapper(object):
|
||||
self.render_primitive,
|
||||
]))
|
||||
|
||||
def refresh_id(self):
|
||||
def refresh_id(self) -> None:
|
||||
self.program_id = self.create_program_id()
|
||||
self.id = self.create_id()
|
||||
|
||||
def create_program_id(self):
|
||||
def create_program_id(self) -> int:
|
||||
return hash("".join((
|
||||
self.program_code[f"{name}_shader"] or ""
|
||||
for name in ("vertex", "geometry", "fragment")
|
||||
)))
|
||||
|
||||
def init_program_code(self):
|
||||
def get_code(name):
|
||||
def init_program_code(self) -> None:
|
||||
def get_code(name: str) -> str | None:
|
||||
return get_shader_code_from_file(
|
||||
os.path.join(self.shader_folder, f"{name}.glsl")
|
||||
)
|
||||
|
||||
self.program_code = {
|
||||
self.program_code: dict[str, str | None] = {
|
||||
"vertex_shader": get_code("vert"),
|
||||
"geometry_shader": get_code("geom"),
|
||||
"fragment_shader": get_code("frag"),
|
||||
}
|
||||
|
||||
def get_program_code(self):
|
||||
def get_program_code(self) -> dict[str, str | None]:
|
||||
return self.program_code
|
||||
|
||||
def replace_code(self, old, new):
|
||||
def replace_code(self, old: str, new: str) -> None:
|
||||
code_map = self.program_code
|
||||
for (name, code) in code_map.items():
|
||||
if code_map[name] is None:
|
||||
@@ -102,7 +107,7 @@ class ShaderWrapper(object):
|
||||
code_map[name] = re.sub(old, new, code_map[name])
|
||||
self.refresh_id()
|
||||
|
||||
def combine_with(self, *shader_wrappers):
|
||||
def combine_with(self, *shader_wrappers: ShaderWrapper):
|
||||
# Assume they are of the same type
|
||||
if len(shader_wrappers) == 0:
|
||||
return
|
||||
@@ -122,10 +127,10 @@ class ShaderWrapper(object):
|
||||
|
||||
|
||||
# For caching
|
||||
filename_to_code_map = {}
|
||||
filename_to_code_map: dict[str, str] = {}
|
||||
|
||||
|
||||
def get_shader_code_from_file(filename):
|
||||
def get_shader_code_from_file(filename: str) -> str | None:
|
||||
if not filename:
|
||||
return None
|
||||
if filename in filename_to_code_map:
|
||||
@@ -157,7 +162,7 @@ def get_shader_code_from_file(filename):
|
||||
return result
|
||||
|
||||
|
||||
def get_colormap_code(rgb_list):
|
||||
def get_colormap_code(rgb_list: Iterable[float]) -> str:
|
||||
data = ",".join(
|
||||
"vec3({}, {}, {})".format(*rgb)
|
||||
for rgb in rgb_list
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Callable, TypeVar, Sequence
|
||||
|
||||
from scipy import linalg
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from manimlib.utils.simple_functions import choose
|
||||
from manimlib.utils.space_ops import find_intersection
|
||||
@@ -8,21 +13,27 @@ from manimlib.utils.space_ops import midpoint
|
||||
from manimlib.logger import log
|
||||
|
||||
CLOSED_THRESHOLD = 0.001
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def bezier(points):
|
||||
def bezier(
|
||||
points: Iterable[float | np.ndarray]
|
||||
) -> Callable[[float], float | np.ndarray]:
|
||||
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
|
||||
|
||||
|
||||
def partial_bezier_points(points, a, b):
|
||||
def partial_bezier_points(
|
||||
points: Sequence[np.ndarray],
|
||||
a: float,
|
||||
b: float
|
||||
) -> list[float]:
|
||||
"""
|
||||
Given an list of points which define
|
||||
a bezier curve, and two numbers 0<=a<b<=1,
|
||||
@@ -48,7 +59,11 @@ def partial_bezier_points(points, a, b):
|
||||
|
||||
# Shortened version of partial_bezier_points just for quadratics,
|
||||
# since this is called a fair amount
|
||||
def partial_quadratic_bezier_points(points, a, b):
|
||||
def partial_quadratic_bezier_points(
|
||||
points: Sequence[np.ndarray],
|
||||
a: float,
|
||||
b: float
|
||||
) -> list[float]:
|
||||
if a == 1:
|
||||
return 3 * [points[-1]]
|
||||
|
||||
@@ -65,7 +80,8 @@ def partial_quadratic_bezier_points(points, a, b):
|
||||
|
||||
# Linear interpolation variants
|
||||
|
||||
def interpolate(start, end, alpha):
|
||||
|
||||
def interpolate(start: T, end: T, alpha: np.ndarray | float) -> T:
|
||||
try:
|
||||
return (1 - alpha) * start + alpha * end
|
||||
except TypeError:
|
||||
@@ -76,12 +92,31 @@ def interpolate(start, end, alpha):
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def set_array_by_interpolation(arr, arr1, arr2, alpha, interp_func=interpolate):
|
||||
def outer_interpolate(
|
||||
start: np.ndarray | float,
|
||||
end: np.ndarray | float,
|
||||
alpha: np.ndarray | float,
|
||||
) -> T:
|
||||
result = np.outer(1 - alpha, start) + np.outer(alpha, end)
|
||||
return result.reshape((*np.shape(alpha), *np.shape(start)))
|
||||
|
||||
|
||||
def set_array_by_interpolation(
|
||||
arr: np.ndarray,
|
||||
arr1: np.ndarray,
|
||||
arr2: np.ndarray,
|
||||
alpha: float,
|
||||
interp_func: Callable[[np.ndarray, np.ndarray, float], np.ndarray] = interpolate
|
||||
) -> np.ndarray:
|
||||
arr[:] = interp_func(arr1, arr2, alpha)
|
||||
return arr
|
||||
|
||||
|
||||
def integer_interpolate(start, end, alpha):
|
||||
def integer_interpolate(
|
||||
start: T,
|
||||
end: T,
|
||||
alpha: float
|
||||
) -> tuple[int, float]:
|
||||
"""
|
||||
alpha is a float between 0 and 1. This returns
|
||||
an integer between start and end (inclusive) representing
|
||||
@@ -102,22 +137,30 @@ def integer_interpolate(start, end, alpha):
|
||||
return (value, residue)
|
||||
|
||||
|
||||
def mid(start, end):
|
||||
def mid(start: T, end: T) -> T:
|
||||
return (start + end) / 2.0
|
||||
|
||||
|
||||
def inverse_interpolate(start, end, value):
|
||||
def inverse_interpolate(start: T, end: T, value: T) -> float:
|
||||
return np.true_divide(value - start, end - start)
|
||||
|
||||
|
||||
def match_interpolate(new_start, new_end, old_start, old_end, old_value):
|
||||
def match_interpolate(
|
||||
new_start: T,
|
||||
new_end: T,
|
||||
old_start: T,
|
||||
old_end: T,
|
||||
old_value: T
|
||||
) -> T:
|
||||
return interpolate(
|
||||
new_start, new_end,
|
||||
inverse_interpolate(old_start, old_end, old_value)
|
||||
)
|
||||
|
||||
|
||||
def get_smooth_quadratic_bezier_handle_points(points):
|
||||
def get_smooth_quadratic_bezier_handle_points(
|
||||
points: Sequence[np.ndarray]
|
||||
) -> np.ndarray | list[np.ndarray]:
|
||||
"""
|
||||
Figuring out which bezier curves most smoothly connect a sequence of points.
|
||||
|
||||
@@ -149,7 +192,9 @@ def get_smooth_quadratic_bezier_handle_points(points):
|
||||
return handles
|
||||
|
||||
|
||||
def get_smooth_cubic_bezier_handle_points(points):
|
||||
def get_smooth_cubic_bezier_handle_points(
|
||||
points: npt.ArrayLike
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
points = np.array(points)
|
||||
num_handles = len(points) - 1
|
||||
dim = points.shape[1]
|
||||
@@ -207,7 +252,10 @@ def get_smooth_cubic_bezier_handle_points(points):
|
||||
return handle_pairs[0::2], handle_pairs[1::2]
|
||||
|
||||
|
||||
def diag_to_matrix(l_and_u, diag):
|
||||
def diag_to_matrix(
|
||||
l_and_u: tuple[int, int],
|
||||
diag: np.ndarray
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Converts array whose rows represent diagonal
|
||||
entries of a matrix into the matrix itself.
|
||||
@@ -224,13 +272,18 @@ def diag_to_matrix(l_and_u, diag):
|
||||
return matrix
|
||||
|
||||
|
||||
def is_closed(points):
|
||||
def is_closed(points: Sequence[np.ndarray]) -> bool:
|
||||
return np.allclose(points[0], points[-1])
|
||||
|
||||
|
||||
# Given 4 control points for a cubic bezier curve (or arrays of such)
|
||||
# return control points for 2 quadratics (or 2n quadratics) approximating them.
|
||||
def get_quadratic_approximation_of_cubic(a0, h0, h1, a1):
|
||||
def get_quadratic_approximation_of_cubic(
|
||||
a0: npt.ArrayLike,
|
||||
h0: npt.ArrayLike,
|
||||
h1: npt.ArrayLike,
|
||||
a1: npt.ArrayLike
|
||||
) -> np.ndarray:
|
||||
a0 = np.array(a0, ndmin=2)
|
||||
h0 = np.array(h0, ndmin=2)
|
||||
h1 = np.array(h1, ndmin=2)
|
||||
@@ -298,7 +351,9 @@ def get_quadratic_approximation_of_cubic(a0, h0, h1, a1):
|
||||
return result
|
||||
|
||||
|
||||
def get_smooth_quadratic_bezier_path_through(points):
|
||||
def get_smooth_quadratic_bezier_path_through(
|
||||
points: list[np.ndarray]
|
||||
) -> np.ndarray:
|
||||
# TODO
|
||||
h0, h1 = get_smooth_cubic_bezier_handle_points(points)
|
||||
a0 = points[:-1]
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
from typing import Callable
|
||||
|
||||
from manimlib.constants import BLACK
|
||||
from manimlib.mobject.numbers import Integer
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.logger import log
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
def print_family(mobject, n_tabs=0):
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
def print_family(mobject: Mobject, n_tabs: int = 0) -> None:
|
||||
"""For debugging purposes"""
|
||||
log.debug("\t" * n_tabs + str(mobject) + " " + str(id(mobject)))
|
||||
for submob in mobject.submobjects:
|
||||
print_family(submob, n_tabs + 1)
|
||||
|
||||
|
||||
def index_labels(mobject, label_height=0.15):
|
||||
def index_labels(
|
||||
mobject: Mobject | np.ndarray,
|
||||
label_height: float = 0.15
|
||||
) -> VGroup:
|
||||
labels = VGroup()
|
||||
for n, submob in enumerate(mobject):
|
||||
label = Integer(n)
|
||||
@@ -24,7 +36,7 @@ def index_labels(mobject, label_height=0.15):
|
||||
return labels
|
||||
|
||||
|
||||
def get_runtime(func):
|
||||
def get_runtime(func: Callable) -> float:
|
||||
now = time.time()
|
||||
func()
|
||||
return time.time() - now
|
||||
|
||||
@@ -1,48 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from manimlib.utils.file_ops import guarantee_existence
|
||||
from manimlib.utils.customization import get_customization
|
||||
|
||||
|
||||
def get_directories():
|
||||
def get_directories() -> dict[str, str]:
|
||||
return get_customization()["directories"]
|
||||
|
||||
|
||||
def get_temp_dir():
|
||||
def get_temp_dir() -> str:
|
||||
return get_directories()["temporary_storage"]
|
||||
|
||||
|
||||
def get_tex_dir():
|
||||
def get_tex_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "Tex"))
|
||||
|
||||
|
||||
def get_text_dir():
|
||||
def get_text_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "Text"))
|
||||
|
||||
|
||||
def get_mobject_data_dir():
|
||||
def get_mobject_data_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data"))
|
||||
|
||||
|
||||
def get_downloads_dir():
|
||||
def get_downloads_dir() -> str:
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads"))
|
||||
|
||||
|
||||
def get_output_dir():
|
||||
def get_output_dir() -> str:
|
||||
return guarantee_existence(get_directories()["output"])
|
||||
|
||||
|
||||
def get_raster_image_dir():
|
||||
def get_raster_image_dir() -> str:
|
||||
return get_directories()["raster_images"]
|
||||
|
||||
|
||||
def get_vector_image_dir():
|
||||
def get_vector_image_dir() -> str:
|
||||
return get_directories()["vector_images"]
|
||||
|
||||
|
||||
def get_sound_dir():
|
||||
def get_sound_dir() -> str:
|
||||
return get_directories()["sounds"]
|
||||
|
||||
|
||||
def get_shader_dir():
|
||||
def get_shader_dir() -> str:
|
||||
return get_directories()["shaders"]
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
from typing import Iterable
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
def extract_mobject_family_members(mobject_list, only_those_with_points=False):
|
||||
def extract_mobject_family_members(
|
||||
mobject_list: Iterable[Mobject],
|
||||
only_those_with_points: bool = False
|
||||
) -> list[Mobject]:
|
||||
result = list(it.chain(*[
|
||||
mob.get_family()
|
||||
for mob in mobject_list
|
||||
@@ -11,7 +22,10 @@ def extract_mobject_family_members(mobject_list, only_those_with_points=False):
|
||||
return result
|
||||
|
||||
|
||||
def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove):
|
||||
def restructure_list_to_exclude_certain_family_members(
|
||||
mobject_list: list[Mobject],
|
||||
to_remove: list[Mobject]
|
||||
) -> list[Mobject]:
|
||||
"""
|
||||
Removes anything in to_remove from mobject_list, but in the event that one of
|
||||
the items to be removed is a member of the family of an item in mobject_list,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Iterable
|
||||
|
||||
import numpy as np
|
||||
import validators
|
||||
|
||||
|
||||
def add_extension_if_not_present(file_name, extension):
|
||||
def add_extension_if_not_present(file_name: str, extension: str) -> str:
|
||||
# This could conceivably be smarter about handling existing differing extensions
|
||||
if(file_name[-len(extension):] != extension):
|
||||
return file_name + extension
|
||||
@@ -11,13 +15,17 @@ def add_extension_if_not_present(file_name, extension):
|
||||
return file_name
|
||||
|
||||
|
||||
def guarantee_existence(path):
|
||||
def guarantee_existence(path: str) -> str:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
return os.path.abspath(path)
|
||||
|
||||
|
||||
def find_file(file_name, directories=None, extensions=None):
|
||||
def find_file(
|
||||
file_name: str,
|
||||
directories: Iterable[str] | None = None,
|
||||
extensions: Iterable[str] | None = None
|
||||
) -> str:
|
||||
# Check if this is a file online first, and if so, download
|
||||
# it to a temporary directory
|
||||
if validators.url(file_name):
|
||||
@@ -47,13 +55,14 @@ def find_file(file_name, directories=None, extensions=None):
|
||||
raise IOError(f"{file_name} not Found")
|
||||
|
||||
|
||||
def get_sorted_integer_files(directory,
|
||||
min_index=0,
|
||||
max_index=np.inf,
|
||||
remove_non_integer_files=False,
|
||||
remove_indices_greater_than=None,
|
||||
extension=None,
|
||||
):
|
||||
def get_sorted_integer_files(
|
||||
directory: str,
|
||||
min_index: float = 0,
|
||||
max_index: float = np.inf,
|
||||
remove_non_integer_files: bool = False,
|
||||
remove_indices_greater_than: float | None = None,
|
||||
extension: str | None = None,
|
||||
) -> list[str]:
|
||||
indexed_files = []
|
||||
for file in os.listdir(directory):
|
||||
if '.' in file:
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from typing import Iterable
|
||||
|
||||
from manimlib.utils.file_ops import find_file
|
||||
from manimlib.utils.directories import get_raster_image_dir
|
||||
from manimlib.utils.directories import get_vector_image_dir
|
||||
|
||||
|
||||
def get_full_raster_image_path(image_file_name):
|
||||
def get_full_raster_image_path(image_file_name: str) -> str:
|
||||
return find_file(
|
||||
image_file_name,
|
||||
directories=[get_raster_image_dir()],
|
||||
@@ -14,7 +15,7 @@ def get_full_raster_image_path(image_file_name):
|
||||
)
|
||||
|
||||
|
||||
def get_full_vector_image_path(image_file_name):
|
||||
def get_full_vector_image_path(image_file_name: str) -> str:
|
||||
return find_file(
|
||||
image_file_name,
|
||||
directories=[get_vector_image_dir()],
|
||||
@@ -22,7 +23,7 @@ def get_full_vector_image_path(image_file_name):
|
||||
)
|
||||
|
||||
|
||||
def drag_pixels(frames):
|
||||
def drag_pixels(frames: Iterable) -> list:
|
||||
curr = frames[0]
|
||||
new_frames = []
|
||||
for frame in frames:
|
||||
@@ -31,7 +32,7 @@ def drag_pixels(frames):
|
||||
return new_frames
|
||||
|
||||
|
||||
def invert_image(image):
|
||||
def invert_image(image: Iterable) -> Image:
|
||||
arr = np.array(image)
|
||||
arr = (255 * np.ones(arr.shape)).astype(arr.dtype) - arr
|
||||
return Image.fromarray(arr)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import yaml
|
||||
import inspect
|
||||
import importlib
|
||||
import importlib
|
||||
from typing import Any
|
||||
|
||||
from rich import box
|
||||
from rich.rule import Rule
|
||||
@@ -10,13 +13,13 @@ from rich.console import Console
|
||||
from rich.prompt import Prompt, Confirm
|
||||
|
||||
|
||||
def get_manim_dir():
|
||||
def get_manim_dir() -> str:
|
||||
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):
|
||||
def remove_empty_value(dictionary: dict[str, Any]) -> None:
|
||||
for key in list(dictionary.keys()):
|
||||
if dictionary[key] == "":
|
||||
dictionary.pop(key)
|
||||
@@ -24,7 +27,7 @@ def remove_empty_value(dictionary):
|
||||
remove_empty_value(dictionary[key])
|
||||
|
||||
|
||||
def init_customization():
|
||||
def init_customization() -> None:
|
||||
configuration = {
|
||||
"directories": {
|
||||
"mirror_module_path": False,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import itertools as it
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Iterable, Sequence, TypeVar
|
||||
|
||||
import numpy as np
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
|
||||
def remove_list_redundancies(l):
|
||||
|
||||
def remove_list_redundancies(l: Iterable[T]) -> list[T]:
|
||||
"""
|
||||
Used instead of list(set(l)) to maintain order
|
||||
Keeps the last occurrence of each element
|
||||
@@ -17,7 +23,7 @@ def remove_list_redundancies(l):
|
||||
return reversed_result
|
||||
|
||||
|
||||
def list_update(l1, l2):
|
||||
def list_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]:
|
||||
"""
|
||||
Used instead of list(set(l1).update(l2)) to maintain order,
|
||||
making sure duplicates are removed from l1, not l2.
|
||||
@@ -25,26 +31,25 @@ def list_update(l1, l2):
|
||||
return [e for e in l1 if e not in l2] + list(l2)
|
||||
|
||||
|
||||
def list_difference_update(l1, l2):
|
||||
def list_difference_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]:
|
||||
return [e for e in l1 if e not in l2]
|
||||
|
||||
|
||||
def all_elements_are_instances(iterable, Class):
|
||||
return all([isinstance(e, Class) for e in iterable])
|
||||
|
||||
|
||||
def adjacent_n_tuples(objects, n):
|
||||
def adjacent_n_tuples(objects: Iterable[T], n: int) -> zip[tuple[T, T]]:
|
||||
return zip(*[
|
||||
[*objects[k:], *objects[:k]]
|
||||
for k in range(n)
|
||||
])
|
||||
|
||||
|
||||
def adjacent_pairs(objects):
|
||||
def adjacent_pairs(objects: Iterable[T]) -> zip[tuple[T, T]]:
|
||||
return adjacent_n_tuples(objects, 2)
|
||||
|
||||
|
||||
def batch_by_property(items, property_func):
|
||||
def batch_by_property(
|
||||
items: Iterable[T],
|
||||
property_func: Callable[[T], S]
|
||||
) -> list[tuple[T, S]]:
|
||||
"""
|
||||
Takes in a list, and returns a list of tuples, (batch, prop)
|
||||
such that all items in a batch have the same output when
|
||||
@@ -71,7 +76,7 @@ def batch_by_property(items, property_func):
|
||||
return batch_prop_pairs
|
||||
|
||||
|
||||
def listify(obj):
|
||||
def listify(obj) -> list:
|
||||
if isinstance(obj, str):
|
||||
return [obj]
|
||||
try:
|
||||
@@ -80,13 +85,13 @@ def listify(obj):
|
||||
return [obj]
|
||||
|
||||
|
||||
def resize_array(nparray, length):
|
||||
def resize_array(nparray: np.ndarray, length: int) -> np.ndarray:
|
||||
if len(nparray) == length:
|
||||
return nparray
|
||||
return np.resize(nparray, (length, *nparray.shape[1:]))
|
||||
|
||||
|
||||
def resize_preserving_order(nparray, length):
|
||||
def resize_preserving_order(nparray: np.ndarray, length: int) -> np.ndarray:
|
||||
if len(nparray) == 0:
|
||||
return np.zeros((length, *nparray.shape[1:]))
|
||||
if len(nparray) == length:
|
||||
@@ -95,7 +100,7 @@ def resize_preserving_order(nparray, length):
|
||||
return nparray[indices]
|
||||
|
||||
|
||||
def resize_with_interpolation(nparray, length):
|
||||
def resize_with_interpolation(nparray: np.ndarray, length: int) -> np.ndarray:
|
||||
if len(nparray) == length:
|
||||
return nparray
|
||||
if length == 0:
|
||||
@@ -108,7 +113,10 @@ def resize_with_interpolation(nparray, length):
|
||||
])
|
||||
|
||||
|
||||
def make_even(iterable_1, iterable_2):
|
||||
def make_even(
|
||||
iterable_1: Sequence[T],
|
||||
iterable_2: Sequence[S]
|
||||
) -> tuple[list[T], list[S]]:
|
||||
len1 = len(iterable_1)
|
||||
len2 = len(iterable_2)
|
||||
if len1 == len2:
|
||||
@@ -120,22 +128,12 @@ def make_even(iterable_1, iterable_2):
|
||||
)
|
||||
|
||||
|
||||
def make_even_by_cycling(iterable_1, iterable_2):
|
||||
length = max(len(iterable_1), len(iterable_2))
|
||||
cycle1 = it.cycle(iterable_1)
|
||||
cycle2 = it.cycle(iterable_2)
|
||||
return (
|
||||
[next(cycle1) for x in range(length)],
|
||||
[next(cycle2) for x in range(length)]
|
||||
)
|
||||
def hash_obj(obj: object) -> int:
|
||||
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))
|
||||
|
||||
def remove_nones(sequence):
|
||||
return [x for x in sequence if x]
|
||||
|
||||
|
||||
# Note this is redundant with it.chain
|
||||
|
||||
|
||||
def concatenate_lists(*list_of_lists):
|
||||
return [item for l in list_of_lists for item in l]
|
||||
return hash(obj)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import numpy as np
|
||||
import math
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import OUT
|
||||
from manimlib.utils.bezier import interpolate
|
||||
@@ -9,7 +11,11 @@ from manimlib.utils.space_ops import rotation_matrix_transpose
|
||||
STRAIGHT_PATH_THRESHOLD = 0.01
|
||||
|
||||
|
||||
def straight_path(start_points, end_points, alpha):
|
||||
def straight_path(
|
||||
start_points: np.ndarray,
|
||||
end_points: np.ndarray,
|
||||
alpha: float
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Same function as interpolate, but renamed to reflect
|
||||
intent of being used to determine how a set of points move
|
||||
@@ -19,7 +25,10 @@ def straight_path(start_points, end_points, alpha):
|
||||
return interpolate(start_points, end_points, alpha)
|
||||
|
||||
|
||||
def path_along_arc(arc_angle, axis=OUT):
|
||||
def path_along_arc(
|
||||
arc_angle: float,
|
||||
axis: np.ndarray = OUT
|
||||
) -> Callable[[np.ndarray, np.ndarray, float], np.ndarray]:
|
||||
"""
|
||||
If vect is vector from start to end, [vect[:,1], -vect[:,0]] is
|
||||
perpendicular to vect in the left direction.
|
||||
@@ -41,9 +50,9 @@ def path_along_arc(arc_angle, axis=OUT):
|
||||
return path
|
||||
|
||||
|
||||
def clockwise_path():
|
||||
def clockwise_path() -> Callable[[np.ndarray, np.ndarray, float], np.ndarray]:
|
||||
return path_along_arc(-np.pi)
|
||||
|
||||
|
||||
def counterclockwise_path():
|
||||
def counterclockwise_path() -> Callable[[np.ndarray, np.ndarray, float], np.ndarray]:
|
||||
return path_along_arc(np.pi)
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.utils.bezier import bezier
|
||||
|
||||
|
||||
def linear(t):
|
||||
def linear(t: float) -> float:
|
||||
return t
|
||||
|
||||
|
||||
def smooth(t):
|
||||
def smooth(t: float) -> float:
|
||||
# Zero first and second derivatives at t=0 and t=1.
|
||||
# Equivalent to bezier([0, 0, 0, 1, 1, 1])
|
||||
s = 1 - t
|
||||
return (t**3) * (10 * s * s + 5 * s * t + t * t)
|
||||
|
||||
|
||||
def rush_into(t):
|
||||
def rush_into(t: float) -> float:
|
||||
return 2 * smooth(0.5 * t)
|
||||
|
||||
|
||||
def rush_from(t):
|
||||
def rush_from(t: float) -> float:
|
||||
return 2 * smooth(0.5 * (t + 1)) - 1
|
||||
|
||||
|
||||
def slow_into(t):
|
||||
def slow_into(t: float) -> float:
|
||||
return np.sqrt(1 - (1 - t) * (1 - t))
|
||||
|
||||
|
||||
def double_smooth(t):
|
||||
def double_smooth(t: float) -> float:
|
||||
if t < 0.5:
|
||||
return 0.5 * smooth(2 * t)
|
||||
else:
|
||||
return 0.5 * (1 + smooth(2 * t - 1))
|
||||
|
||||
|
||||
def there_and_back(t):
|
||||
def there_and_back(t: float) -> float:
|
||||
new_t = 2 * t if t < 0.5 else 2 * (1 - t)
|
||||
return smooth(new_t)
|
||||
|
||||
|
||||
def there_and_back_with_pause(t, pause_ratio=1. / 3):
|
||||
def there_and_back_with_pause(t: float, pause_ratio: float = 1. / 3) -> float:
|
||||
a = 1. / pause_ratio
|
||||
if t < 0.5 - pause_ratio / 2:
|
||||
return smooth(a * t)
|
||||
@@ -48,21 +50,28 @@ def there_and_back_with_pause(t, pause_ratio=1. / 3):
|
||||
return smooth(a - a * t)
|
||||
|
||||
|
||||
def running_start(t, pull_factor=-0.5):
|
||||
def running_start(t: float, pull_factor: float = -0.5) -> float:
|
||||
return bezier([0, 0, pull_factor, pull_factor, 1, 1, 1])(t)
|
||||
|
||||
|
||||
def not_quite_there(func=smooth, proportion=0.7):
|
||||
def not_quite_there(
|
||||
func: Callable[[float], float] = smooth,
|
||||
proportion: float = 0.7
|
||||
) -> Callable[[float], float]:
|
||||
def result(t):
|
||||
return proportion * func(t)
|
||||
return result
|
||||
|
||||
|
||||
def wiggle(t, wiggles=2):
|
||||
def wiggle(t: float, wiggles: float = 2) -> float:
|
||||
return there_and_back(t) * np.sin(wiggles * np.pi * t)
|
||||
|
||||
|
||||
def squish_rate_func(func, a=0.4, b=0.6):
|
||||
def squish_rate_func(
|
||||
func: Callable[[float], float],
|
||||
a: float = 0.4,
|
||||
b: float = 0.6
|
||||
) -> Callable[[float], float]:
|
||||
def result(t):
|
||||
if a == b:
|
||||
return a
|
||||
@@ -81,11 +90,11 @@ def squish_rate_func(func, a=0.4, b=0.6):
|
||||
# "lingering", different from squish_rate_func's default params
|
||||
|
||||
|
||||
def lingering(t):
|
||||
def lingering(t: float) -> float:
|
||||
return squish_rate_func(lambda t: t, 0, 0.8)(t)
|
||||
|
||||
|
||||
def exponential_decay(t, half_life=0.1):
|
||||
def exponential_decay(t: float, half_life: float = 0.1) -> float:
|
||||
# The half-life should be rather small to minimize
|
||||
# the cut-off error at the end
|
||||
return 1 - np.exp(-t / half_life)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import inspect
|
||||
import numpy as np
|
||||
from scipy import special
|
||||
import math
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ def sigmoid(x):
|
||||
|
||||
@lru_cache(maxsize=10)
|
||||
def choose(n, k):
|
||||
return special.comb(n, k, exact=True)
|
||||
return math.comb(n, k)
|
||||
|
||||
|
||||
def gen_choose(n, r):
|
||||
return np.prod(np.arange(n, n - r, -1)) / math.factorial(r)
|
||||
|
||||
|
||||
def get_num_args(function):
|
||||
|
||||
@@ -2,7 +2,7 @@ from manimlib.utils.file_ops import find_file
|
||||
from manimlib.utils.directories import get_sound_dir
|
||||
|
||||
|
||||
def get_full_sound_file_path(sound_file_name):
|
||||
def get_full_sound_file_path(sound_file_name) -> str:
|
||||
return find_file(
|
||||
sound_file_name,
|
||||
directories=[get_sound_dir()],
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import numpy as np
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import operator as op
|
||||
from functools import reduce
|
||||
import math
|
||||
from typing import Callable, Iterable, Sequence
|
||||
import platform
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from mapbox_earcut import triangulate_float32 as earcut
|
||||
from scipy.spatial.transform import Rotation
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
|
||||
from manimlib.constants import RIGHT
|
||||
from manimlib.constants import DOWN
|
||||
@@ -13,7 +21,7 @@ from manimlib.utils.iterables import adjacent_pairs
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
|
||||
def cross(v1, v2):
|
||||
def cross(v1: np.ndarray, v2: np.ndarray) -> list[np.ndarray]:
|
||||
return [
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
@@ -21,171 +29,11 @@ def cross(v1, v2):
|
||||
]
|
||||
|
||||
|
||||
def get_norm(vect):
|
||||
def get_norm(vect: Iterable) -> float:
|
||||
return sum((x**2 for x in vect))**0.5
|
||||
|
||||
|
||||
# Quaternions
|
||||
# TODO, implement quaternion type
|
||||
|
||||
|
||||
def quaternion_mult(*quats):
|
||||
if len(quats) == 0:
|
||||
return [1, 0, 0, 0]
|
||||
result = quats[0]
|
||||
for next_quat in quats[1:]:
|
||||
w1, x1, y1, z1 = result
|
||||
w2, x2, y2, z2 = next_quat
|
||||
result = [
|
||||
w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
|
||||
w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
|
||||
w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2,
|
||||
w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2,
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
def quaternion_from_angle_axis(angle, axis, axis_normalized=False):
|
||||
if not axis_normalized:
|
||||
axis = normalize(axis)
|
||||
return [math.cos(angle / 2), *(math.sin(angle / 2) * axis)]
|
||||
|
||||
|
||||
def angle_axis_from_quaternion(quaternion):
|
||||
axis = normalize(
|
||||
quaternion[1:],
|
||||
fall_back=[1, 0, 0]
|
||||
)
|
||||
angle = 2 * np.arccos(quaternion[0])
|
||||
if angle > TAU / 2:
|
||||
angle = TAU - angle
|
||||
return angle, axis
|
||||
|
||||
|
||||
def quaternion_conjugate(quaternion):
|
||||
result = list(quaternion)
|
||||
for i in range(1, len(result)):
|
||||
result[i] *= -1
|
||||
return result
|
||||
|
||||
|
||||
def rotate_vector(vector, angle, axis=OUT):
|
||||
if len(vector) == 2:
|
||||
# Use complex numbers...because why not
|
||||
z = complex(*vector) * np.exp(complex(0, angle))
|
||||
result = [z.real, z.imag]
|
||||
elif len(vector) == 3:
|
||||
# Use quaternions...because why not
|
||||
quat = quaternion_from_angle_axis(angle, axis)
|
||||
quat_inv = quaternion_conjugate(quat)
|
||||
product = quaternion_mult(quat, [0, *vector], quat_inv)
|
||||
result = product[1:]
|
||||
else:
|
||||
raise Exception("vector must be of dimension 2 or 3")
|
||||
|
||||
if isinstance(vector, np.ndarray):
|
||||
return np.array(result)
|
||||
return result
|
||||
|
||||
|
||||
def thick_diagonal(dim, thickness=2):
|
||||
row_indices = np.arange(dim).repeat(dim).reshape((dim, dim))
|
||||
col_indices = np.transpose(row_indices)
|
||||
return (np.abs(row_indices - col_indices) < thickness).astype('uint8')
|
||||
|
||||
|
||||
def rotation_matrix_transpose_from_quaternion(quat):
|
||||
quat_inv = quaternion_conjugate(quat)
|
||||
return [
|
||||
quaternion_mult(quat, [0, *basis], quat_inv)[1:]
|
||||
for basis in [
|
||||
[1, 0, 0],
|
||||
[0, 1, 0],
|
||||
[0, 0, 1],
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def rotation_matrix_from_quaternion(quat):
|
||||
return np.transpose(rotation_matrix_transpose_from_quaternion(quat))
|
||||
|
||||
|
||||
def rotation_matrix_transpose(angle, axis):
|
||||
if axis[0] == 0 and axis[1] == 0:
|
||||
# axis = [0, 0, z] case is common enough it's worth
|
||||
# having a shortcut
|
||||
sgn = 1 if axis[2] > 0 else -1
|
||||
cos_a = math.cos(angle)
|
||||
sin_a = math.sin(angle) * sgn
|
||||
return [
|
||||
[cos_a, sin_a, 0],
|
||||
[-sin_a, cos_a, 0],
|
||||
[0, 0, 1],
|
||||
]
|
||||
quat = quaternion_from_angle_axis(angle, axis)
|
||||
return rotation_matrix_transpose_from_quaternion(quat)
|
||||
|
||||
|
||||
def rotation_matrix(angle, axis):
|
||||
"""
|
||||
Rotation in R^3 about a specified axis of rotation.
|
||||
"""
|
||||
return np.transpose(rotation_matrix_transpose(angle, axis))
|
||||
|
||||
|
||||
def rotation_about_z(angle):
|
||||
return [
|
||||
[math.cos(angle), -math.sin(angle), 0],
|
||||
[math.sin(angle), math.cos(angle), 0],
|
||||
[0, 0, 1]
|
||||
]
|
||||
|
||||
|
||||
def z_to_vector(vector):
|
||||
"""
|
||||
Returns some matrix in SO(3) which takes the z-axis to the
|
||||
(normalized) vector provided as an argument
|
||||
"""
|
||||
axis = cross(OUT, vector)
|
||||
if get_norm(axis) == 0:
|
||||
if vector[2] > 0:
|
||||
return np.identity(3)
|
||||
else:
|
||||
return rotation_matrix(PI, RIGHT)
|
||||
angle = np.arccos(np.dot(OUT, normalize(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
|
||||
"""
|
||||
return np.angle(complex(*vector[:2]))
|
||||
|
||||
|
||||
def angle_between_vectors(v1, v2):
|
||||
"""
|
||||
Returns the angle between two 3D vectors.
|
||||
This angle will always be btw 0 and pi
|
||||
"""
|
||||
return math.acos(clip(np.dot(normalize(v1), normalize(v2)), -1, 1))
|
||||
|
||||
|
||||
def project_along_vector(point, vector):
|
||||
matrix = np.identity(3) - np.outer(vector, vector)
|
||||
return np.dot(point, matrix.T)
|
||||
|
||||
|
||||
def normalize(vect, fall_back=None):
|
||||
def normalize(vect: np.ndarray, fall_back: np.ndarray | None = None) -> np.ndarray:
|
||||
norm = get_norm(vect)
|
||||
if norm > 0:
|
||||
return np.array(vect) / norm
|
||||
@@ -195,7 +43,128 @@ def normalize(vect, fall_back=None):
|
||||
return np.zeros(len(vect))
|
||||
|
||||
|
||||
def normalize_along_axis(array, axis, fall_back=None):
|
||||
# Operations related to rotation
|
||||
|
||||
|
||||
def quaternion_mult(*quats: Sequence[float]) -> list[float]:
|
||||
# Real part is last entry, which is bizzare, but fits scipy Rotation convention
|
||||
if len(quats) == 0:
|
||||
return [0, 0, 0, 1]
|
||||
result = quats[0]
|
||||
for next_quat in quats[1:]:
|
||||
x1, y1, z1, w1 = result
|
||||
x2, y2, z2, w2 = next_quat
|
||||
result = [
|
||||
w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
|
||||
w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2,
|
||||
w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2,
|
||||
w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
def quaternion_from_angle_axis(
|
||||
angle: float,
|
||||
axis: np.ndarray,
|
||||
) -> list[float]:
|
||||
return Rotation.from_rotvec(angle * normalize(axis)).as_quat()
|
||||
|
||||
|
||||
def angle_axis_from_quaternion(quat: Sequence[float]) -> tuple[float, np.ndarray]:
|
||||
rot_vec = Rotation.from_quat(quat).as_rotvec()
|
||||
norm = get_norm(rot_vec)
|
||||
return norm, rot_vec / norm
|
||||
|
||||
|
||||
def quaternion_conjugate(quaternion: Iterable) -> list:
|
||||
result = list(quaternion)
|
||||
for i in range(3):
|
||||
result[i] *= -1
|
||||
return result
|
||||
|
||||
|
||||
def rotate_vector(
|
||||
vector: Iterable,
|
||||
angle: float,
|
||||
axis: np.ndarray = OUT
|
||||
) -> np.ndarray | list[float]:
|
||||
rot = Rotation.from_rotvec(angle * normalize(axis))
|
||||
return np.dot(vector, rot.as_matrix().T)
|
||||
|
||||
|
||||
def rotate_vector_2d(vector: Iterable, angle: float):
|
||||
# Use complex numbers...because why not
|
||||
z = complex(*vector) * np.exp(complex(0, angle))
|
||||
return np.array([z.real, z.imag])
|
||||
|
||||
|
||||
def rotation_matrix_transpose_from_quaternion(quat: Iterable) -> np.ndarray:
|
||||
return Rotation.from_quat(quat).as_matrix()
|
||||
|
||||
|
||||
def rotation_matrix_from_quaternion(quat: Iterable) -> np.ndarray:
|
||||
return np.transpose(rotation_matrix_transpose_from_quaternion(quat))
|
||||
|
||||
|
||||
def rotation_matrix(angle: float, axis: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Rotation in R^3 about a specified axis of rotation.
|
||||
"""
|
||||
return Rotation.from_rotvec(angle * normalize(axis)).as_matrix()
|
||||
|
||||
|
||||
def rotation_matrix_transpose(angle: float, axis: np.ndarray) -> np.ndarray:
|
||||
return rotation_matrix(angle, axis).T
|
||||
|
||||
|
||||
def rotation_about_z(angle: float) -> list[list[float]]:
|
||||
return [
|
||||
[math.cos(angle), -math.sin(angle), 0],
|
||||
[math.sin(angle), math.cos(angle), 0],
|
||||
[0, 0, 1]
|
||||
]
|
||||
|
||||
|
||||
def rotation_between_vectors(v1, v2) -> np.ndarray:
|
||||
if np.all(np.isclose(v1, v2)):
|
||||
return np.identity(3)
|
||||
return rotation_matrix(
|
||||
angle=angle_between_vectors(v1, v2),
|
||||
axis=np.cross(v1, v2)
|
||||
)
|
||||
|
||||
|
||||
def z_to_vector(vector: np.ndarray) -> np.ndarray:
|
||||
return rotation_between_vectors(OUT, vector)
|
||||
|
||||
|
||||
def angle_of_vector(vector: Sequence[float]) -> float:
|
||||
"""
|
||||
Returns polar coordinate theta when vector is project on xy plane
|
||||
"""
|
||||
return np.angle(complex(*vector[:2]))
|
||||
|
||||
|
||||
def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
|
||||
"""
|
||||
Returns the angle between two 3D vectors.
|
||||
This angle will always be btw 0 and pi
|
||||
"""
|
||||
n1 = get_norm(v1)
|
||||
n2 = get_norm(v2)
|
||||
cos_angle = np.dot(v1, v2) / np.float64(n1 * n2)
|
||||
return math.acos(clip(cos_angle, -1, 1))
|
||||
|
||||
|
||||
def project_along_vector(point: np.ndarray, vector: np.ndarray) -> np.ndarray:
|
||||
matrix = np.identity(3) - np.outer(vector, vector)
|
||||
return np.dot(point, matrix.T)
|
||||
|
||||
|
||||
def normalize_along_axis(
|
||||
array: np.ndarray,
|
||||
axis: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
norms = np.sqrt((array * array).sum(axis))
|
||||
norms[norms == 0] = 1
|
||||
buffed_norms = np.repeat(norms, array.shape[axis]).reshape(array.shape)
|
||||
@@ -203,7 +172,11 @@ def normalize_along_axis(array, axis, fall_back=None):
|
||||
return array
|
||||
|
||||
|
||||
def get_unit_normal(v1, v2, tol=1e-6):
|
||||
def get_unit_normal(
|
||||
v1: np.ndarray,
|
||||
v2: np.ndarray,
|
||||
tol: float = 1e-6
|
||||
) -> np.ndarray:
|
||||
v1 = normalize(v1)
|
||||
v2 = normalize(v2)
|
||||
cp = cross(v1, v2)
|
||||
@@ -221,7 +194,13 @@ def get_unit_normal(v1, v2, tol=1e-6):
|
||||
###
|
||||
|
||||
|
||||
def compass_directions(n=4, start_vect=RIGHT):
|
||||
def thick_diagonal(dim: int, thickness: int = 2) -> np.ndarray:
|
||||
row_indices = np.arange(dim).repeat(dim).reshape((dim, dim))
|
||||
col_indices = np.transpose(row_indices)
|
||||
return (np.abs(row_indices - col_indices) < thickness).astype('uint8')
|
||||
|
||||
|
||||
def compass_directions(n: int = 4, start_vect: np.ndarray = RIGHT) -> np.ndarray:
|
||||
angle = TAU / n
|
||||
return np.array([
|
||||
rotate_vector(start_vect, k * angle)
|
||||
@@ -229,28 +208,36 @@ def compass_directions(n=4, start_vect=RIGHT):
|
||||
])
|
||||
|
||||
|
||||
def complex_to_R3(complex_num):
|
||||
def complex_to_R3(complex_num: complex) -> np.ndarray:
|
||||
return np.array((complex_num.real, complex_num.imag, 0))
|
||||
|
||||
|
||||
def R3_to_complex(point):
|
||||
def R3_to_complex(point: Sequence[float]) -> complex:
|
||||
return complex(*point[:2])
|
||||
|
||||
|
||||
def complex_func_to_R3_func(complex_func):
|
||||
def complex_func_to_R3_func(
|
||||
complex_func: Callable[[complex], complex]
|
||||
) -> Callable[[np.ndarray], np.ndarray]:
|
||||
return lambda p: complex_to_R3(complex_func(R3_to_complex(p)))
|
||||
|
||||
|
||||
def center_of_mass(points):
|
||||
def center_of_mass(points: Iterable[npt.ArrayLike]) -> np.ndarray:
|
||||
points = [np.array(point).astype("float") for point in points]
|
||||
return sum(points) / len(points)
|
||||
|
||||
|
||||
def midpoint(point1, point2):
|
||||
def midpoint(
|
||||
point1: Sequence[float],
|
||||
point2: Sequence[float]
|
||||
) -> np.ndarray:
|
||||
return center_of_mass([point1, point2])
|
||||
|
||||
|
||||
def line_intersection(line1, line2):
|
||||
def line_intersection(
|
||||
line1: Sequence[Sequence[float]],
|
||||
line2: Sequence[Sequence[float]]
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
return intersection point of two lines,
|
||||
each defined with a pair of vectors determining
|
||||
@@ -271,7 +258,13 @@ def line_intersection(line1, line2):
|
||||
return np.array([x, y, 0])
|
||||
|
||||
|
||||
def find_intersection(p0, v0, p1, v1, threshold=1e-5):
|
||||
def find_intersection(
|
||||
p0: npt.ArrayLike,
|
||||
v0: npt.ArrayLike,
|
||||
p1: npt.ArrayLike,
|
||||
v1: npt.ArrayLike,
|
||||
threshold: float = 1e-5
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Return the intersection of a line passing through p0 in direction v0
|
||||
with one passing through p1 in direction v1. (Or array of intersections
|
||||
@@ -300,7 +293,11 @@ def find_intersection(p0, v0, p1, v1, threshold=1e-5):
|
||||
return p0 + ratio * v0
|
||||
|
||||
|
||||
def get_closest_point_on_line(a, b, p):
|
||||
def get_closest_point_on_line(
|
||||
a: np.ndarray,
|
||||
b: np.ndarray,
|
||||
p: np.ndarray
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
It returns point x such that
|
||||
x is on line ab and xp is perpendicular to ab.
|
||||
@@ -315,7 +312,7 @@ def get_closest_point_on_line(a, b, p):
|
||||
return ((t * a) + ((1 - t) * b))
|
||||
|
||||
|
||||
def get_winding_number(points):
|
||||
def get_winding_number(points: Iterable[float]) -> float:
|
||||
total_angle = 0
|
||||
for p1, p2 in adjacent_pairs(points):
|
||||
d_angle = angle_of_vector(p2) - angle_of_vector(p1)
|
||||
@@ -326,14 +323,18 @@ def get_winding_number(points):
|
||||
|
||||
##
|
||||
|
||||
def cross2d(a, b):
|
||||
def cross2d(a: np.ndarray, b: np.ndarray) -> np.ndarray:
|
||||
if len(a.shape) == 2:
|
||||
return a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0]
|
||||
else:
|
||||
return a[0] * b[1] - b[0] * a[1]
|
||||
|
||||
|
||||
def tri_area(a, b, c):
|
||||
def tri_area(
|
||||
a: Sequence[float],
|
||||
b: Sequence[float],
|
||||
c: Sequence[float]
|
||||
) -> float:
|
||||
return 0.5 * abs(
|
||||
a[0] * (b[1] - c[1]) +
|
||||
b[0] * (c[1] - a[1]) +
|
||||
@@ -341,7 +342,12 @@ def tri_area(a, b, c):
|
||||
)
|
||||
|
||||
|
||||
def is_inside_triangle(p, a, b, c):
|
||||
def is_inside_triangle(
|
||||
p: np.ndarray,
|
||||
a: np.ndarray,
|
||||
b: np.ndarray,
|
||||
c: np.ndarray
|
||||
) -> bool:
|
||||
"""
|
||||
Test if point p is inside triangle abc
|
||||
"""
|
||||
@@ -353,12 +359,12 @@ def is_inside_triangle(p, a, b, c):
|
||||
return np.all(crosses > 0) or np.all(crosses < 0)
|
||||
|
||||
|
||||
def norm_squared(v):
|
||||
def norm_squared(v: Sequence[float]) -> float:
|
||||
return v[0] * v[0] + v[1] * v[1] + v[2] * v[2]
|
||||
|
||||
|
||||
# TODO, fails for polygons drawn over themselves
|
||||
def earclip_triangulation(verts, ring_ends):
|
||||
def earclip_triangulation(verts: np.ndarray, ring_ends: list[int]) -> list:
|
||||
"""
|
||||
Returns a list of indices giving a triangulation
|
||||
of a polygon, potentially with holes
|
||||
@@ -410,7 +416,16 @@ def earclip_triangulation(verts, ring_ends):
|
||||
))
|
||||
|
||||
chilren = [[] for i in rings]
|
||||
for idx, i in enumerate(rings_sorted):
|
||||
ringenum = ProgressDisplay(
|
||||
enumerate(rings_sorted),
|
||||
total=len(rings),
|
||||
leave=False,
|
||||
ascii=True if platform.system() == 'Windows' else None,
|
||||
dynamic_ncols=True,
|
||||
desc="SVG Triangulation",
|
||||
delay=3,
|
||||
)
|
||||
for idx, i in ringenum:
|
||||
for j in rings_sorted[:idx][::-1]:
|
||||
if is_in_fast(i, j):
|
||||
chilren[j].append(i)
|
||||
|
||||
@@ -32,7 +32,7 @@ def get_tex_config():
|
||||
get_manim_dir(), "manimlib", "tex_templates",
|
||||
SAVED_TEX_CONFIG["template_file"],
|
||||
)
|
||||
with open(template_filename, "r") as file:
|
||||
with open(template_filename, "r", encoding="utf-8") as file:
|
||||
SAVED_TEX_CONFIG["tex_body"] = file.read()
|
||||
return SAVED_TEX_CONFIG
|
||||
|
||||
@@ -88,7 +88,7 @@ def tex_to_dvi(tex_file):
|
||||
if exit_code != 0:
|
||||
log_file = tex_file.replace(".tex", ".log")
|
||||
log.error("LaTeX Error! Not a worry, it happens to the best of us.")
|
||||
with open(log_file, "r") as file:
|
||||
with open(log_file, "r", encoding="utf-8") as file:
|
||||
for line in file.readlines():
|
||||
if line.startswith("!"):
|
||||
log.debug(f"The error could be: `{line[2:-1]}`")
|
||||
@@ -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,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import moderngl_window as mglw
|
||||
from moderngl_window.context.pyglet.window import Window as PygletWindow
|
||||
@@ -7,6 +9,11 @@ from screeninfo import get_monitors
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.customization import get_customization
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.scene.scene import Scene
|
||||
|
||||
|
||||
class Window(PygletWindow):
|
||||
fullscreen = False
|
||||
@@ -15,7 +22,12 @@ class Window(PygletWindow):
|
||||
vsync = True
|
||||
cursor = True
|
||||
|
||||
def __init__(self, scene, size=(1280, 720), **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
scene: Scene,
|
||||
size: tuple[int, int] = (1280, 720),
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(size=size)
|
||||
digest_config(self, kwargs)
|
||||
|
||||
@@ -37,7 +49,7 @@ class Window(PygletWindow):
|
||||
self.position = initial_position
|
||||
self.position = initial_position
|
||||
|
||||
def find_initial_position(self, size):
|
||||
def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]:
|
||||
custom_position = get_customization()["window_position"]
|
||||
monitors = get_monitors()
|
||||
mon_index = get_customization()["window_monitor"]
|
||||
@@ -59,7 +71,12 @@ class Window(PygletWindow):
|
||||
)
|
||||
|
||||
# Delegate event handling to scene
|
||||
def pixel_coords_to_space_coords(self, px, py, relative=False):
|
||||
def pixel_coords_to_space_coords(
|
||||
self,
|
||||
px: int,
|
||||
py: int,
|
||||
relative: bool = False
|
||||
) -> np.ndarray:
|
||||
pw, ph = self.size
|
||||
fw, fh = self.scene.camera.get_frame_shape()
|
||||
fc = self.scene.camera.get_frame_center()
|
||||
@@ -72,59 +89,59 @@ class Window(PygletWindow):
|
||||
0
|
||||
])
|
||||
|
||||
def on_mouse_motion(self, x, y, dx, dy):
|
||||
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
|
||||
super().on_mouse_motion(x, y, dx, dy)
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||
self.scene.on_mouse_motion(point, d_point)
|
||||
|
||||
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
|
||||
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None:
|
||||
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||
self.scene.on_mouse_drag(point, d_point, buttons, modifiers)
|
||||
|
||||
def on_mouse_press(self, x: int, y: int, button, mods):
|
||||
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
|
||||
super().on_mouse_press(x, y, button, mods)
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
self.scene.on_mouse_press(point, button, mods)
|
||||
|
||||
def on_mouse_release(self, x: int, y: int, button, mods):
|
||||
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
|
||||
super().on_mouse_release(x, y, button, mods)
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
self.scene.on_mouse_release(point, button, mods)
|
||||
|
||||
def on_mouse_scroll(self, x, y, x_offset: float, y_offset: float):
|
||||
def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None:
|
||||
super().on_mouse_scroll(x, y, x_offset, y_offset)
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
|
||||
self.scene.on_mouse_scroll(point, offset)
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
self.pressed_keys.add(symbol) # Modifiers?
|
||||
super().on_key_press(symbol, modifiers)
|
||||
self.scene.on_key_press(symbol, modifiers)
|
||||
|
||||
def on_key_release(self, symbol, modifiers):
|
||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||
self.pressed_keys.difference_update({symbol}) # Modifiers?
|
||||
super().on_key_release(symbol, modifiers)
|
||||
self.scene.on_key_release(symbol, modifiers)
|
||||
|
||||
def on_resize(self, width: int, height: int):
|
||||
def on_resize(self, width: int, height: int) -> None:
|
||||
super().on_resize(width, height)
|
||||
self.scene.on_resize(width, height)
|
||||
|
||||
def on_show(self):
|
||||
def on_show(self) -> None:
|
||||
super().on_show()
|
||||
self.scene.on_show()
|
||||
|
||||
def on_hide(self):
|
||||
def on_hide(self) -> None:
|
||||
super().on_hide()
|
||||
self.scene.on_hide()
|
||||
|
||||
def on_close(self):
|
||||
def on_close(self) -> None:
|
||||
super().on_close()
|
||||
self.scene.on_close()
|
||||
|
||||
def is_key_pressed(self, symbol):
|
||||
def is_key_pressed(self, symbol: int) -> bool:
|
||||
return (symbol in self.pressed_keys)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
argparse
|
||||
colour
|
||||
numpy
|
||||
Pillow
|
||||
@@ -15,9 +14,9 @@ pygments
|
||||
pyyaml
|
||||
rich
|
||||
screeninfo
|
||||
pyreadline; sys_platform == 'win32'
|
||||
validators
|
||||
ipython
|
||||
PyOpenGL
|
||||
manimpango>=0.2.0,<0.4.0
|
||||
cssselect2
|
||||
manimpango>=0.4.0.post0,<0.5.0
|
||||
isosurfaces
|
||||
svgelements
|
||||
|
||||
25
setup.cfg
25
setup.cfg
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = manimgl
|
||||
version = 1.4.0
|
||||
version = 1.6.1
|
||||
author = Grant Sanderson
|
||||
author_email= grant@3blue1brown.com
|
||||
description = Animation engine for explanatory math videos
|
||||
@@ -12,12 +12,23 @@ 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.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
|
||||
@@ -34,12 +45,12 @@ install_requires =
|
||||
pyyaml
|
||||
rich
|
||||
screeninfo
|
||||
pyreadline; sys_platform == 'win32'
|
||||
validators
|
||||
ipython
|
||||
PyOpenGL
|
||||
manimpango>=0.2.0,<0.4.0
|
||||
cssselect2
|
||||
manimpango>=0.4.0.post0,<0.5.0
|
||||
isosurfaces
|
||||
svgelements
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
|
||||
Reference in New Issue
Block a user