377 Commits

Author SHA1 Message Date
TonyCrane
9d7db7aacd release: ready to release v1.6.0 2022-04-07 11:00:43 +08:00
TonyCrane
e8430b38b2 docs: update changelog for #1781 2022-04-07 10:57:21 +08:00
鹤翔万里
1f32a9e674 Some fix (#1781)
* fix: reduce warning from numpy

* fix: fix ControlsExample
2022-04-07 10:50:18 +08:00
TonyCrane
d31f3df5af docs: update changelog for #1779 #1780 2022-04-07 10:05:04 +08:00
鹤翔万里
e9bf13882e Merge pull request #1780 from YishiMichael/master
Add support for `substring` and `case_sensitive` parameters
2022-04-07 09:58:39 +08:00
YishiMichael
3550108ff7 Handle out-of-bound spans 2022-04-07 09:48:44 +08:00
YishiMichael
557707ea75 Support substring and case_sensitive parameters 2022-04-07 00:46:41 +08:00
Grant Sanderson
13c731e166 Merge pull request #1779 from YishiMichael/master
Refactor `LabelledString` and relevant classes
2022-04-06 08:53:23 -07:00
YishiMichael
d349c9283d Merge branch 'master' of https://github.com/YishiMichael/manim 2022-04-06 23:17:19 +08:00
YishiMichael
18963fb9fe Some refactors on LabelledString 2022-04-06 23:16:59 +08:00
YishiMichael
a69c9887f9 Merge branch '3b1b:master' into master 2022-04-06 22:39:26 +08:00
YishiMichael
93f8d3f1ca Some refactors on LabelledString 2022-04-06 22:38:33 +08:00
TonyCrane
e4ccbdfba9 docs: update changelog since v1.5.0 2022-04-06 11:14:45 +08:00
YishiMichael
fc97bfb647 Merge branch '3b1b:master' into master 2022-04-05 22:28:39 +08:00
YishiMichael
f9d8a76767 Remove unnecessary raise statement (#1778)
* Fix typo

* Remove unnecessary raise statement
2022-04-05 22:22:59 +08:00
YishiMichael
55a91a2354 Remove unnecessary raise statement 2022-04-05 22:16:26 +08:00
YishiMichael
50ffcbc5c7 Merge branch '3b1b:master' into master 2022-04-05 21:10:45 +08:00
YishiMichael
b764791258 Fix typo (#1777) 2022-04-05 14:04:26 +08:00
YishiMichael
7f616987a3 Fix typo 2022-04-05 14:01:07 +08:00
Grant Sanderson
648855dae0 Merge pull request #1772 from YishiMichael/master
Construct LabelledString base class for MarkupText and MTex
2022-04-04 10:01:13 -07:00
YishiMichael
974d9d5ab0 Avoid empty spans 2022-04-04 14:53:40 +08:00
YishiMichael
3c3264d7d6 Support passing in spans directly 2022-04-02 22:42:19 +08:00
鹤翔万里
39673a80d7 fix: add missing import of annotations 2022-04-02 22:00:02 +08:00
YishiMichael
84c56b3624 Fix typo 2022-03-31 18:11:37 +08:00
YishiMichael
dc816c9f8d Improve algorithm 2022-03-31 18:08:10 +08:00
YishiMichael
d5ab9a91c4 Reorganize files 2022-03-31 16:15:58 +08:00
YishiMichael
106f2a3837 Fix shallow copying bug 2022-03-31 11:36:50 +08:00
YishiMichael
724a500cc6 Fix shallow copying bug 2022-03-31 11:20:42 +08:00
YishiMichael
461500637e Fix type bug 2022-03-31 10:57:25 +08:00
YishiMichael
fc4f649570 Fix bugs brought by empty strings 2022-03-31 10:36:14 +08:00
YishiMichael
852da9ac2a Merge branch 'master' of https://github.com/YishiMichael/manim 2022-03-30 22:09:54 +08:00
YishiMichael
637d779190 Fix empty zipping bug 2022-03-30 22:09:26 +08:00
YishiMichael
9bbbed3a83 Remove comment 2022-03-30 22:04:10 +08:00
YishiMichael
1cde28838f Merge branch '3b1b:master' into master 2022-03-30 22:02:23 +08:00
YishiMichael
a8039d803e Rename file 2022-03-30 21:58:27 +08:00
YishiMichael
0add9b6e3a Rename file 2022-03-30 21:57:27 +08:00
YishiMichael
c5ec47b0e9 Refactor LabelledString 2022-03-30 21:53:00 +08:00
Grant Sanderson
769a4bbaf9 Merge pull request #1770 from 3b1b/video-work
Small camera/3d updates
2022-03-29 20:35:33 -07:00
Grant Sanderson
0f8d7ed597 Add VPrism, and refactor VCube 2022-03-29 20:34:14 -07:00
Grant Sanderson
2a7a7ac518 Add getter and setter for joint_type 2022-03-29 20:28:48 -07:00
Grant Sanderson
0610f331a4 Add get/set field_of_view for camera frame 2022-03-29 20:20:41 -07:00
Grant Sanderson
a0ba9c8b30 Fix CameraFrame.get_euler_angles to match conventions with set_euler_angles 2022-03-29 19:21:12 -07:00
YishiMichael
7e8b3a4c6b Refactor LabelledString 2022-03-29 23:38:06 +08:00
Grant Sanderson
393f77cb03 Merge pull request #1766 from sunkisser/SVGfeedback
Give the user feedback for SVGs that take a while
2022-03-28 09:30:00 -07:00
YishiMichael
82c972b946 Remove saxutils.unescape process 2022-03-28 19:31:19 +08:00
YishiMichael
89e139009b Remove an error raising 2022-03-28 19:17:40 +08:00
YishiMichael
45faa9063b Add items for hash_seed 2022-03-28 19:02:50 +08:00
YishiMichael
0e31ff12e2 Tiny fix for TransformMatchingString 2022-03-28 18:54:43 +08:00
YishiMichael
473aaea399 Construct LabelledString 2022-03-28 17:55:50 +08:00
Sunkisser
c4ea794107 use tqdm to display progress bar for long running SVG triangulations 2022-03-28 03:30:10 +00:00
Sunkisser
cfba6c431f revert previous changes - we will refactor using tqdm 2022-03-28 02:55:21 +00:00
鹤翔万里
e11c5def63 style: some style fixes 2022-03-28 08:05:41 +08:00
Sunkisser
a3e4246938 use log.debug and display idx+1 2022-03-27 21:59:49 +00:00
YishiMichael
305c6e6ee9 Resolve conflict for #1765 2022-03-27 15:33:51 +08:00
YishiMichael
3b01ec48e6 Refactor MTex 2022-03-27 14:44:50 +08:00
Sunkisser
969aa82f04 user feedback for SVGs that take a while 2022-03-26 23:43:11 +00:00
YishiMichael
e44a2fc8c6 Refactor MTex 2022-03-27 00:29:22 +08:00
YishiMichael
9ac1805e7e Refactor MTex 2022-03-26 20:52:28 +08:00
YishiMichael
6ad8636fab Adjust some typings (#1765)
* Adjust some typings

* Adjust typings
2022-03-23 14:17:34 +08:00
YishiMichael
4a03d196a6 Adjust typings 2022-03-23 13:34:30 +08:00
YishiMichael
519e2f4f1e Adjust some typings 2022-03-23 12:21:40 +08:00
Grant Sanderson
aefde2969f Merge pull request #1764 from 3b1b/video-work
Video work
2022-03-22 11:41:24 -07:00
Grant Sanderson
b7a3201fb3 Reorder imports 2022-03-22 11:31:52 -07:00
Grant Sanderson
a9349057ad Merge branch 'master' of github.com:3b1b/manim into video-work 2022-03-22 11:30:25 -07:00
Grant Sanderson
e812b99594 Re-add necessary imports 2022-03-22 11:07:26 -07:00
Grant Sanderson
0c8b333a42 Merge pull request #1736 from TonyCrane/master
Add type hints according to PEP 484 and PEP 604
2022-03-22 11:05:10 -07:00
Grant Sanderson
f690164087 Merge branch 'master' into master 2022-03-22 11:00:33 -07:00
Grant Sanderson
9d0cc810c5 Make panning more sensitive to mouse movements 2022-03-22 10:36:48 -07:00
Grant Sanderson
8b1f0a8749 Refactor Mobject.set_rgba_array_by_color 2022-03-22 10:35:49 -07:00
Grant Sanderson
c0b7b55e49 Use stroke_color to init arrow 2022-03-22 10:35:34 -07:00
Grant Sanderson
41b52c6117 Merge pull request #1751 from YishiMichael/master
Refactor Text with the latest manimpango
2022-03-22 09:50:43 -07:00
YishiMichael
e5ce0ca286 Reorganize methods 2022-03-22 20:46:35 +08:00
YishiMichael
a8c2a9fa3f Clean up code 2022-03-21 23:11:37 +08:00
YishiMichael
cabc1322d6 Clean up code 2022-03-21 23:06:47 +08:00
YishiMichael
c51811d2f1 Except IndexError for MTex.get_part_by_tex 2022-03-21 22:45:06 +08:00
Grant Sanderson
7bf3615bb1 Refactor rotation methods to use scipy.spatial.transform.Rotation 2022-03-18 17:11:08 -07:00
Grant Sanderson
1872b0516b Normalize rotation axis 2022-03-18 17:10:16 -07:00
Grant Sanderson
625460467f Refactor CameraFrame to use scipy.spatial.transform.Rotation 2022-03-18 16:06:15 -07:00
Grant Sanderson
e19f35585d Add GlowDots, analogous to GlowDot 2022-03-17 12:00:49 -07:00
Grant Sanderson
66819f5dbc Add 2d and 3d pianos 2022-03-17 12:00:29 -07:00
Grant Sanderson
f249da95fb Add a basic Prismify to turn a flat VMobject into something with depth 2022-03-17 12:00:10 -07:00
Grant Sanderson
bb3bd41605 Merge pull request #1762 from widcardw/patch-1
Fix the width of riemann rectangles
2022-03-17 09:06:42 -07:00
widcardw
67f8007764 Fix the width of riemann rectangles 2022-03-17 14:10:30 +08:00
YishiMichael
2a0709664d Add explicit return statement 2022-03-17 11:33:53 +08:00
YishiMichael
de46df78dc Modify warning message 2022-03-17 10:58:41 +08:00
Grant Sanderson
bd6c731e67 Allow CoordinateSystem.coords_to_point to work on arrays of coords 2022-03-16 12:24:22 -07:00
Grant Sanderson
c3e13fff05 Allow Numberline.number_to_point to work on an array of numbers 2022-03-16 12:24:03 -07:00
Grant Sanderson
bf2d9edfe6 Allow interpolate to work on an array of alpha values 2022-03-16 12:23:51 -07:00
Grant Sanderson
fa38b56fd8 Bug fix in cases where empty array is passed to shader 2022-03-16 12:23:11 -07:00
Grant Sanderson
dfbbb34035 Merge pull request #1757 from TurkeyBilly/patch-7
Reorganize getters for ParametricCurve
2022-03-06 09:27:22 -08:00
Bill Xi
0cef9a1e61 Reorganize getters for ParametricCurve 2022-03-06 13:54:42 +08:00
YishiMichael
2d764e12f4 fix char escaping bug 2022-03-03 21:09:05 +08:00
YishiMichael
d744311f15 add warning for slicing methods 2022-03-03 20:47:44 +08:00
YishiMichael
11af9508f2 add back get_parts_by_text, get_part_by_text methods 2022-03-03 20:38:15 +08:00
YishiMichael
a227ffde05 PEP8: reorder imports 2022-03-02 20:28:26 +08:00
YishiMichael
e0b0ae280e Allow passing strings to local_configs 2022-03-02 19:59:14 +08:00
YishiMichael
fce38fd8a5 Modify default value of apply_space_chars 2022-03-02 19:52:45 +08:00
YishiMichael
52a99a0c49 Add global_config, local_configs params 2022-03-02 19:34:56 +08:00
YishiMichael
956e3a69c7 Refactor Text 2022-03-02 18:38:24 +08:00
YishiMichael
95a3ac6876 Refactor Text 2022-02-26 20:36:32 +08:00
YishiMichael
b06a5d3f23 Refactor Text 2022-02-26 20:31:26 +08:00
YishiMichael
fa8962e024 Refactor Text 2022-02-20 23:35:48 +08:00
YishiMichael
0a4c4d5849 Merge branch '3b1b:master' into master 2022-02-20 23:34:10 +08:00
YishiMichael
e879da32d5 Specify UTF-8 encoding for tex files (#1748) 2022-02-17 19:09:55 +08:00
YishiMichael
6b12bc2f5e Merge branch '3b1b:master' into master 2022-02-17 19:06:17 +08:00
YishiMichael
4aeccd7769 Specify UTF-8 encoding for tex files 2022-02-17 19:03:45 +08:00
TonyCrane
4fbe948b63 style: insert an empty line after import 2022-02-16 21:08:25 +08:00
TonyCrane
05bee011d2 chore: update type hint of SVGMobject 2022-02-16 20:37:07 +08:00
鹤翔万里
37b548395c Merge branch 'master' into master 2022-02-16 20:30:53 +08:00
TonyCrane
4356c42e00 release: ready to release v1.5.0 2022-02-16 12:01:03 +08:00
TonyCrane
aea79be6cc workflow: only build wheels for python 3.6+ 2022-02-16 11:59:33 +08:00
TonyCrane
a08e9b01de Merge branch 'update' 2022-02-16 11:47:46 +08:00
TonyCrane
9f3b404df6 resolve conflict and add type hints for it 2022-02-16 11:46:55 +08:00
TonyCrane
8ef42fae24 Merge branch 'master' of https://github.com/3b1b/manim 2022-02-16 11:21:20 +08:00
TonyCrane
6be6bd3075 docs: change the style of changelog 2022-02-16 11:20:08 +08:00
TonyCrane
a33eac7aa8 docs: update changelog for #1742 #1744 #1745 #1746 2022-02-16 11:17:37 +08:00
Grant Sanderson
9d6a28bc29 Merge pull request #1746 from 3b1b/video-work
Change interaction-to-embed keybinding
2022-02-15 10:14:18 -08:00
Grant Sanderson
06405d5758 Merge branch 'master' of github.com:3b1b/manim into video-work 2022-02-15 10:11:35 -08:00
Grant Sanderson
46e356e791 Change keyboard shortcut to drop into an embedding to be ctrl+shift+e 2022-02-15 10:10:57 -08:00
Grant Sanderson
97ca42d454 Merge pull request #1745 from YishiMichael/master
Reorganize inheriting order and refactor SVGMobject
2022-02-15 10:05:53 -08:00
Grant Sanderson
a4eee6f44c Merge pull request #1744 from TurkeyBilly/patch-3
Add text_config for DecimalNumber
2022-02-15 09:59:46 -08:00
YishiMichael
8cac16b452 Update display_during_execution 2022-02-15 21:59:09 +08:00
YishiMichael
719cd8cde3 Remove redundant brackets 2022-02-15 21:54:56 +08:00
Bill Xi
0bb9216c14 Update hash_obj method 2022-02-15 21:50:14 +08:00
YishiMichael
6f9df8db26 Improve hashing algorithm 2022-02-15 21:38:22 +08:00
YishiMichael
3756605a45 Update display_during_execution 2022-02-15 20:55:44 +08:00
TonyCrane
0e4d4155a3 workflow: only build wheels for python 3.7+ 2022-02-15 20:23:59 +08:00
YishiMichael
0cab23b2ba Reorganize inheriting order of SVGMobject 2022-02-15 20:16:15 +08:00
TonyCrane
854f7cd2bf fix: remove type alias import in indication.py 2022-02-15 18:47:17 +08:00
TonyCrane
41c4023986 chore: add type hints to manimlib.animation 2022-02-15 18:39:45 +08:00
TonyCrane
d19e0cb9ab fix: remove import before future 2022-02-15 14:56:00 +08:00
TonyCrane
f085e6c2dd chore: add type hints to manimlib.window 2022-02-15 14:55:35 +08:00
TonyCrane
91ffdeb2d4 chore: add type hints to manimlib.shader_wrapper 2022-02-15 14:49:02 +08:00
TonyCrane
db71ed1ae9 fix: fix type hint of remove_empty_value 2022-02-15 14:38:55 +08:00
TonyCrane
4c16bfc2c0 chore: add type hints to manimlib.mobject 2022-02-15 14:37:15 +08:00
Bill Xi
aef02bfcf9 changed hashing 2022-02-15 11:45:17 +08:00
TonyCrane
3744844efa fix: fix type hint of set_array_by_interpolation 2022-02-15 11:35:22 +08:00
Bill Xi
9d04e287d7 Removed init_colors 2022-02-15 10:20:06 +08:00
Bill Xi
97c0f4857b Update numbers.py
Added config passing for text
2022-02-15 09:35:10 +08:00
Grant Sanderson
7f9b0a7eac Merge pull request #1742 from 3b1b/video-work
Presenter mode bug fix
2022-02-14 07:58:55 -08:00
Grant Sanderson
133724d29a Allow for using right arrow in presenter mode 2022-02-14 07:56:26 -08:00
Grant Sanderson
559b96e7ce Small bug fix for presenter mode 2022-02-14 07:52:06 -08:00
TonyCrane
773e013af9 chore: add type hints to manimlib.mobject.svg 2022-02-14 22:55:41 +08:00
TonyCrane
61c70b426c remove unnecessary import 2022-02-14 21:43:22 +08:00
TonyCrane
9bdcc8b635 style: remove quotes of annotations according to PEP 563 2022-02-14 21:41:45 +08:00
TonyCrane
66caf0c1ad chore: only import some classes when type checking 2022-02-14 21:34:56 +08:00
TonyCrane
62cab9feaf chore: re-add type hint for EventListener 2022-02-14 21:25:46 +08:00
TonyCrane
be5de32d70 chore: add type hints to manimlib.scene 2022-02-14 21:22:18 +08:00
鹤翔万里
09ce4717aa Merge branch '3b1b:master' into master 2022-02-14 20:02:50 +08:00
TonyCrane
7fb6f352c4 fix: fix some bugs caused by type hints and imports 2022-02-14 20:02:24 +08:00
TonyCrane
f29ef87bba style/docs: fix argument help style and update docs for it 2022-02-14 19:50:30 +08:00
TonyCrane
e39f81ccff Merge branch '3b1b-master' 2022-02-14 14:12:21 +08:00
TonyCrane
a0ed9edb42 resolve conflict 2022-02-14 14:12:06 +08:00
TonyCrane
fc1e916f42 docs: update changelog for #1725 #1727 #1728 #1731 #1739 #1740 2022-02-14 14:03:51 +08:00
Grant Sanderson
b3b7d214ad Fix Write bug (#1740)
* Avoid division by zero error for calling Write on null objects
2022-02-13 20:04:05 -08:00
Grant Sanderson
602809758e Video work (#1739)
* Enable setting points to a null list, and adding one point at a time.

* Add refresh_locked_data

* Add presenter mode to scenes with -p option

* Allow for an embed by hitting e during interaction

* Add set_min_height, etc.

* Make sure null parametric curve has at least one point

* Account for edge case where \{ is used in Tex

* Allow for logging notes in wait calls, useful for presenter mode

* Simplify choose, and add gen_choose for fractional amounts

* Default to no top on axes

* Allow match_x, match_y, etc. to take in a point

* Allow wait calls to ignore presenter mode

* Just use math.combo, no caching with choose(n, r)

* Use generator instead of list in bezier

* Bubble init_colors should override

* Account for "px" values read in from an svg

* Stop displaying when writing is happening

* Update the way Bubble override SVG colors
2022-02-13 15:16:16 -08:00
TonyCrane
960463d143 docs: remove support for python 3.6 2022-02-13 20:47:04 +08:00
TonyCrane
9a8aee481d chore: add type hints to manimlib.event_handler 2022-02-13 20:03:05 +08:00
TonyCrane
1064e2bb30 chore: add type hints to manimlib.camera 2022-02-13 19:32:53 +08:00
TonyCrane
992e61ddf2 style: rename Color type to ManimColor 2022-02-13 19:02:28 +08:00
TonyCrane
19187ead06 chore: add type hints to manimlib.mobject.types 2022-02-13 18:56:50 +08:00
TonyCrane
7f8216bb09 chore: replace some iterable with npt.ArrayLike 2022-02-13 15:18:04 +08:00
TonyCrane
e78113373a chore: add type hints to manimlib.mobject.mobject 2022-02-13 15:11:35 +08:00
TonyCrane
35025631eb chore: fix type hint of bezier 2022-02-13 12:56:03 +08:00
Elisha Hollander
f9351536e4 minor fixes (#1737) 2022-02-13 11:12:41 +08:00
TonyCrane
6e292daf58 chore: add type hints to manimlib.utils 2022-02-12 23:47:23 +08:00
YishiMichael
67f5b10626 Attempt to refactor SVGMobject with svgelements (#1731)
* Some small refactors

* Refactor MTex

* Implement TransformMatchingMTex

* Some refactors

* Some refactors

* Some small refactors

* Strip strings before matching

* Implement get_submob_tex

* Use RGB color mode

* Some small refactors

* Try refactoring SVGMobject with svglib

* Refactor SVGMobject using svgelements

* Refactor SVGMobject using svgelements

* Use functions instead of func names as dict values

* style: modify import order to conform to PEP8

* Set default values to None

* modify import order

* Remove unused import

Co-authored-by: TonyCrane <tonycrane@foxmail.com>
2022-02-11 07:53:21 -08:00
YishiMichael
baba6929df Implement ImplicitFunction (#1727) 2022-02-07 08:24:40 -08:00
YishiMichael
d6b20a7306 Refactor MTex and implement TransformMatchingMTex (#1725)
* Some small refactors

* Refactor MTex

* Implement TransformMatchingMTex

* Some refactors

* Some refactors

* Some small refactors

* Strip strings before matching

* Implement get_submob_tex

* Use RGB color mode

* Some small refactors
2022-02-07 08:21:53 -08:00
鹤翔万里
4c3ba7f674 Clean dependencies (#1728)
* clean dependencies

* add classifiers to metadata
2022-02-05 22:13:34 +08:00
TonyCrane
3883f57bf8 release: ready to release v1.4.1 2022-02-04 11:03:37 +08:00
TonyCrane
d2e0811285 import Iterable from collections.abc instead of collections 2022-02-04 10:55:59 +08:00
Grant Sanderson
1e2a6ffb8a Merge pull request #1724 from TurkeyBilly/patch-2
Temporarily fix boolean operation bug
2022-01-31 08:06:27 -08:00
Bill Xi
56e5696163 Update boolean_ops.py 2022-01-31 23:29:36 +08:00
TonyCrane
1ec00629a5 release: ready to release v1.4.0 2022-01-30 13:06:22 +08:00
TonyCrane
aa6335cd90 docs: update changelog for #1719 #1720 #1721 and #1723 2022-01-30 13:00:57 +08:00
YishiMichael
7093f7d02d Some small refactors to MTex (#1723) 2022-01-30 12:42:35 +08:00
Grant Sanderson
fad9ed2df7 Merge pull request #1720 from YishiMichael/master
Handle explicit color-related commands
2022-01-29 08:01:48 -08:00
YishiMichael
725155409b Some small refactors 2022-01-29 21:06:54 +08:00
YishiMichael
a6675eb043 Some small refactors 2022-01-29 14:35:27 +08:00
YishiMichael
5d2dcec307 Fix color-related bugs 2022-01-29 14:05:52 +08:00
Grant Sanderson
f60dc7cd07 Merge pull request #1719 from 3b1b/parse-style
Parse style from <style> tag and Add support to <line> tag
2022-01-27 13:33:14 -08:00
YishiMichael
6c39cac62b Remove redundant attribute 2022-01-28 01:19:02 +08:00
鹤翔万里
2bd25a55fa add back override parameter to init_colors 2022-01-28 00:20:13 +08:00
鹤翔万里
0e4edfdd79 improve config helper (#1721) 2022-01-28 00:16:19 +08:00
YishiMichael
277256a407 Merge branch '3b1b:master' into master 2022-01-27 23:11:19 +08:00
YishiMichael
831b7d455c Handle explicit color-related commands 2022-01-27 23:09:05 +08:00
TonyCrane
1d14a23af9 docs: update changelog for #1712 #1717 and #1716 2022-01-27 22:21:40 +08:00
TonyCrane
dffa70ea15 docs: update changelog for #1704 and #1709 2022-01-27 22:09:57 +08:00
TonyCrane
31976063df add dependency cssselect2 2022-01-27 17:31:14 +08:00
TonyCrane
aa135280ac support <line> tag in SVG 2022-01-27 17:23:58 +08:00
TonyCrane
f0160822ba fix bug of ref map 2022-01-27 17:17:19 +08:00
TonyCrane
48e07d1817 parse style attribute using tinycss 2022-01-27 17:16:52 +08:00
TonyCrane
3ef5899a24 some cleanups 2022-01-27 16:43:45 +08:00
TonyCrane
f895455264 add parser for <style> tag of SVG 2022-01-27 16:37:51 +08:00
Grant Sanderson
3baa14103e Merge pull request #1716 from YishiMichael/master
Refactor MTex and some clean-ups
2022-01-26 08:56:44 -08:00
Grant Sanderson
c315300ff1 Merge branch 'master' into master 2022-01-26 08:54:18 -08:00
Grant Sanderson
3b17d6d0eb Merge pull request #1718 from 3b1b/text-fix
Text fix
2022-01-26 08:21:36 -08:00
Grant Sanderson
8a29de5ef0 Override style for Text 2022-01-26 08:21:00 -08:00
Grant Sanderson
ecb729850a Small style fixes 2022-01-26 08:20:45 -08:00
Grant Sanderson
a770291053 Include style in MTex.get_mobjects_from 2022-01-26 08:20:38 -08:00
Grant Sanderson
27c666fab5 Merge pull request #1717 from 3b1b/svg-style
Parse and generate style for SVG
2022-01-26 07:56:03 -08:00
YishiMichael
942a7e71b8 Update MTex 2022-01-26 23:46:13 +08:00
TonyCrane
ebb75d1235 cached SVGMobject in SingleStringTex with default color 2022-01-26 20:37:44 +08:00
TonyCrane
9af23415a2 synchronize SingleStringTex's color to SVGMobject 2022-01-26 20:20:48 +08:00
TonyCrane
19778e405a some cleanups 2022-01-26 19:55:47 +08:00
TonyCrane
833e40c2d4 fix default style 2022-01-26 19:50:27 +08:00
TonyCrane
9df53b8a18 fix the bug of M command with more than 2 args 2022-01-26 14:05:01 +08:00
TonyCrane
ff86b0e378 fix the bug of outdated relative_point after command Z 2022-01-26 13:56:42 +08:00
TonyCrane
92adcd75d4 add style support to svg 2022-01-26 13:53:53 +08:00
YishiMichael
240f5020b4 Add back default_config.yml 2022-01-26 13:21:27 +08:00
YishiMichael
e8205a5049 Some refactors for MTex 2022-01-26 13:03:14 +08:00
TonyCrane
6c8dd14adc some clean 2022-01-26 11:00:57 +08:00
Grant Sanderson
07f84e2676 Merge pull request #1712 from 3b1b/fix-svg
Improve handling of SVG transform and Some refactors
2022-01-25 13:26:40 -08:00
TonyCrane
8db1164ece some refactors 2022-01-25 21:48:04 +08:00
TonyCrane
790bf0a104 fix typo 2022-01-25 20:25:30 +08:00
TonyCrane
8205edcc4c fix a small bug 2022-01-25 20:13:20 +08:00
TonyCrane
05b3c9852e fix add_smooth_cubic_curve_to when have only one point 2022-01-25 20:06:00 +08:00
TonyCrane
925f2e123f add comments 2022-01-25 19:54:19 +08:00
TonyCrane
565763a2ff reconstruct path parser 2022-01-25 19:44:42 +08:00
TonyCrane
6a74c241b8 fix bug of node which is not an element 2022-01-25 16:28:23 +08:00
TonyCrane
416cc8e6d5 add warning for unsupported element type 2022-01-25 14:41:11 +08:00
TonyCrane
d694aed452 add support for skewX and skewY transform 2022-01-25 14:40:02 +08:00
TonyCrane
11379283aa add support for rotate transform 2022-01-25 14:29:47 +08:00
TonyCrane
dd13559b11 replace warnings.warn with log.warning 2022-01-25 14:09:05 +08:00
TonyCrane
1658438fef allow Mobject.scale receive iterable scale_factor 2022-01-25 14:05:32 +08:00
TonyCrane
f4eb2724c5 refactor SVGMobject.handle_transforms 2022-01-25 14:04:35 +08:00
TonyCrane
33f720c73a fix typo 2022-01-25 13:15:53 +08:00
TonyCrane
bbb4fa155c fix the depth of svg tag 2022-01-25 13:14:19 +08:00
Grant Sanderson
2318c9e716 Merge pull request #1709 from TurkeyBilly/patch-4
Fix "Code is unreachable Pylance" warning for NumberPlane
2022-01-17 08:56:08 -08:00
Bill Xi
e80dd243f1 Added abstract method decorator and override 2022-01-17 20:27:34 +08:00
Grant Sanderson
3ffe300f96 Merge pull request #1704 from TurkeyBilly/patch-2
Adding "label_buff" config parameter for Brace
2022-01-03 08:53:34 -08:00
Bill Xi
24e3caa072 fix no "import copy" bug
added import copy
2022-01-03 16:49:00 +08:00
Bill Xi
9efd02c500 Remove spelling mistake
I misspelled "label"
2022-01-03 16:37:26 +08:00
Bill Xi
0a318486c5 Adding "lable_buff" config parameter for Brace 2022-01-03 14:57:16 +08:00
鹤翔万里
919133c6bf Merge pull request #1702 from Suji04/patch-2
removed extra 'all' from comments
2021-12-31 18:18:50 +08:00
Sujan Dutta
066a2ed5dc removed extra 'all' from comments 2021-12-31 00:10:57 -05:00
TonyCrane
09ced7ce9a docs: update changelog for #1694 and #1697 2021-12-23 10:34:15 +08:00
Grant Sanderson
505b229117 Merge pull request #1697 from 3b1b/video-work
Video work
2021-12-21 10:59:50 -08:00
Grant Sanderson
5aa8d15d85 Use FFMPEG_BIN instead of "ffmpeg" for sound incorporation 2021-12-21 10:58:58 -08:00
Grant Sanderson
7aa05572ab Remove unnecessary import 2021-12-21 10:58:41 -08:00
Grant Sanderson
f1996f8479 Small hack for the lightbulb, needs to be fixed properly later 2021-12-21 10:58:33 -08:00
Grant Sanderson
37b63ca956 Merge pull request #1694 from DangGiaChi/BarChart_modified
Add option to add ticks on x-axis in BarChart()
2021-12-17 09:30:53 -08:00
DangGiaChi
84fd657d9b Change variables names: x_tick, x_ticks, y_tick, y_ticks 2021-12-17 15:02:10 +07:00
DangGiaChi
b489490f41 Fixed things as suggestions 2021-12-17 07:14:37 +07:00
DangGiaChi
bbf45f95c6 Add option to add ticks on x-axis in BarChart() 2021-12-16 22:03:29 +07:00
TonyCrane
b61f1473a5 release: ready to release v1.3.0 2021-12-14 13:41:44 +08:00
TonyCrane
e3d5b49a55 docs: remove deprecated config usage example 2021-12-14 13:35:50 +08:00
TonyCrane
4d6a0db1e1 docs: update changelog for #1691 and #1678 2021-12-14 13:31:44 +08:00
TonyCrane
0af46e149d add metavar LINENO for --embed option 2021-12-14 12:14:04 +08:00
TonyCrane
896b011d76 docs: update changelog for #1688 2021-12-14 12:11:25 +08:00
Grant Sanderson
3adaf8e325 Merge pull request #1678 from YishiMichael/master
Construct `MTex`
2021-12-13 16:09:55 -08:00
Grant Sanderson
8762177df5 Merge pull request #1691 from 3b1b/video-work
Video work
2021-12-13 16:07:42 -08:00
Grant Sanderson
a1d51474ea Add GlowDot 2021-12-13 16:03:57 -08:00
Grant Sanderson
83841ae415 Add Dodecahedron 2021-12-13 16:03:46 -08:00
Grant Sanderson
b81f244c3c Inserted "self.embed" line should match passed in line number 2021-12-13 16:03:36 -08:00
Grant Sanderson
7023548ec6 Fix TransformMatchingParts bug 2021-12-13 16:03:12 -08:00
Grant Sanderson
758f329a06 Use array copy when checking need for refreshing triangulation 2021-12-13 16:02:47 -08:00
Grant Sanderson
8f1dfabff0 VectorizedPoint should call __init__ for both super classes 2021-12-13 16:02:10 -08:00
Grant Sanderson
7fa01d5de8 Small formatting change 2021-12-13 16:01:54 -08:00
Michael W
0de303d5e0 Some refactors
- Split out `_TexParser` class
- Replace `math_mode` parameter with `tex_environment`
- Fix the bug that braces following even number of backslashes aren't matched
2021-12-13 21:01:27 +08:00
Michael W
155839bde9 Add unbreakable_commands parameter 2021-12-13 12:46:29 +08:00
Grant Sanderson
3a1e5e1bcf Remove old implementation for SurfaceMesh 2021-12-07 10:07:49 -08:00
Grant Sanderson
264f7b1172 Add Circle.get_radius 2021-12-07 10:07:25 -08:00
Grant Sanderson
85e90a1488 Don't print info for pre-run scene 2021-12-07 10:07:15 -08:00
Grant Sanderson
f8e6e7df3c Update progress display for full scene render 2021-12-07 10:06:48 -08:00
Grant Sanderson
5dd7cce67f Have Scene.wait only go through full progression during skipping when there are time-based updaters 2021-12-07 10:05:33 -08:00
Grant Sanderson
f21a4a4696 Only stop skipping if the scene wasn't originally meant to be 2021-12-07 10:04:28 -08:00
Grant Sanderson
98b0d266d2 Make sure skip_animations and start_at_animation_number play well together 2021-12-07 10:03:10 -08:00
Michael W
6821a7c20e Handle empty strings 2021-12-07 14:12:08 +08:00
Michael W
00f72da493 Some small refactor 2021-12-07 13:17:48 +08:00
Michael W
744916507c Add a debugging method 2021-12-07 12:55:52 +08:00
Michael W
88d863c1d7 Support get_tex() for submobjects of MTex 2021-12-07 00:34:07 +08:00
Michael W
d7dcc9d76f Recover file 2021-12-07 00:32:12 +08:00
Michael W
4631508b7d Add get_tex() method 2021-12-06 13:48:17 +08:00
Michael W
8803088121 Fix bugs concerned with child environments 2021-12-06 09:44:59 +08:00
Michael W
1d466cb299 Add Exception for indices_of_part() 2021-12-05 22:17:09 +08:00
Michael W
5a1f00b1cb Add TransformMatchingMTex 2021-12-05 11:46:15 +08:00
Michael W
17d31045b2 Add TransformMatchingMTex 2021-12-05 11:45:42 +08:00
Michael W
950466c1da Some refactors 2021-12-05 10:21:55 +08:00
Michael W
62151e52f1 Merge branch '3b1b:master' into master 2021-12-01 08:42:17 +08:00
Grant Sanderson
b4ce0b910c Merge pull request #1688 from 3b1b/video-work
Video work
2021-11-30 11:45:57 -08:00
Grant Sanderson
9dd1f47dab Create single progress display for full scene render
When a scene is written to file, it will now do a preliminary run of a copy of the scene with skip_animations turned on to count the total frames, which has the added benefit of catching runtime errors early, and allowing an quicker preview of the last frame to be sure everything will render as expected.

The Progress display bars for individual animations are replaced with a more global progress display bar showing the full render time for the scene.

This has the downside that all the non-rendering computations in a scene are run twice, so any scene with slow computations unrelated to rendering will take longer. But those are rarer, so the benefits seem worth it.
2021-11-30 11:41:33 -08:00
Grant Sanderson
49743daf32 Add Mobject.insert_submobject method 2021-11-30 11:30:50 -08:00
Grant Sanderson
ba23fbe71e Make sure Mobject.is_fixed_in_frame stays updated with uniforms 2021-11-30 11:30:34 -08:00
Grant Sanderson
ee1594a3cb Match fix_in_frame status for FlashAround mobject 2021-11-30 11:29:12 -08:00
Grant Sanderson
e9afb0ee33 Fix tiny PEP errors 2021-11-30 11:28:26 -08:00
Michael W
8b1715379d Some small refactors 2021-11-29 09:48:00 +08:00
Michael W
2501fac32f Some small refactors 2021-11-29 09:38:48 +08:00
Michael W
1aec0462ec Some small refactors 2021-11-29 01:43:48 +08:00
Michael W
83c70a59d8 Sort superscripts and subscripts in submobjects 2021-11-29 01:15:38 +08:00
Michael W
9b8a6e7ff8 Merge branch '3b1b:master' into master 2021-11-28 23:38:23 +08:00
Michael W
758f2ec236 Some small refactor 2021-11-28 23:38:12 +08:00
TonyCrane
d9cac38618 update changelog 2021-11-28 18:50:57 +08:00
Michael W
e8ebfa312b Prevent infinite loops from unexpected inputs 2021-11-28 13:26:54 +08:00
Michael W
dae24891fa Add get_all_isolated_substrings method 2021-11-28 13:03:33 +08:00
Michael W
a4f9de1ca1 Fix bugs concerned with coloring 2021-11-28 12:14:29 +08:00
Michael W
697028cd4c Add slicing and indexing methods 2021-11-27 23:07:46 +08:00
Michael W
c84acc0023 Remove disabled methods 2021-11-27 19:53:52 +08:00
Michael W
b1d869cd11 Update __init__.py to include mtex_mobject 2021-11-27 16:21:06 +08:00
Michael W
13a5f6d6ff Add MTex 2021-11-27 16:19:01 +08:00
Michael W
e3f87d835b Recover files 2021-11-27 16:17:22 +08:00
Michael W
7ffab788b7 Recover numbers.py 2021-11-27 16:16:18 +08:00
Grant Sanderson
bcd09906be Fix bug in ShowSubmobjectsOneByOne 2021-11-22 08:05:59 -08:00
Grant Sanderson
407c53f97c Have rotation_between_vectors handle identical/similar vectors 2021-11-18 17:52:48 -08:00
Grant Sanderson
eea3c6b294 Better align SurfaceMesh to the corresponding surface polygons 2021-11-18 17:52:17 -08:00
Grant Sanderson
d2182b9112 Make sure set_length returns self 2021-11-18 17:51:56 -08:00
Grant Sanderson
fbc329d7ce Small bug fix for angle_between_vectors 2021-11-17 12:49:53 -08:00
Grant Sanderson
25045143a1 Have mobject uniforms supercede camera uniforms 2021-11-17 12:49:08 -08:00
Grant Sanderson
e899604a2d Add getter methods for specific euler angles 2021-11-17 12:48:17 -08:00
Grant Sanderson
0b898a5594 Add always_sort_to_camera for surfaces 2021-11-16 17:38:43 -08:00
Grant Sanderson
ee2f68cd49 Exchange gloss for reflectiveness 2021-11-16 17:38:30 -08:00
Grant Sanderson
2cce4ccdd7 Exchange gloss for reflectiveness 2021-11-16 17:38:08 -08:00
Grant Sanderson
f3ecebee43 Remove unnecessary import 2021-11-16 17:37:45 -08:00
Grant Sanderson
e764da3c3a use quick_point_from_proportion for graph points 2021-11-16 17:37:27 -08:00
Grant Sanderson
fbbea47d11 Change temp embed file name 2021-11-16 17:37:01 -08:00
Grant Sanderson
781a9934fd Add shortcut for setting black background stroke 2021-11-16 17:29:24 -08:00
Grant Sanderson
a7173142bf Fix VMobject.fade 2021-11-16 17:29:10 -08:00
Grant Sanderson
0e78027186 Improve point_from_proportion to account for arc length 2021-11-16 17:28:48 -08:00
Grant Sanderson
82bd02d21f Fix angle_between_vectors, add rotation_between_vectors 2021-11-16 17:08:35 -08:00
Grant Sanderson
d065e1973d Add option to insert embed line from the command line (mildly hacky) 2021-11-14 12:31:56 -08:00
Grant Sanderson
7070777408 Tiny formatting change 2021-11-12 15:47:23 -08:00
TonyCrane
5c2a9f2129 style: change CRLF to LF
Change the line ending characters from CRLF to LF
2021-11-12 21:49:56 +08:00
Michael W
1b695e1c19 Refactor Tex 2021-11-12 21:22:42 +08:00
Michael W
da1cc44d90 Remove SingleStringTex 2021-11-12 21:21:44 +08:00
Grant Sanderson
3bbb759112 Merge branch 'master' of github.com:3b1b/manim into video-work 2021-11-09 09:18:56 -08:00
Grant Sanderson
41c6cbcb59 Merge pull request #1675 from YishiMichael/master
Add boolean operations for mobjects
2021-11-09 09:18:38 -08:00
Grant Sanderson
5930e6a176 Refresh unit normal when reversing points 2021-11-09 09:15:15 -08:00
Grant Sanderson
8f3ff91165 Add reflectiveness to style and default to fill for VMobject.get_color 2021-11-09 09:15:00 -08:00
Michael W
b12677bc1a Add files via upload 2021-11-10 00:35:09 +08:00
Michael W
cdec64e3f1 Add boolean operations for mobjects 2021-11-10 00:23:40 +08:00
Michael W
2dc8bc9b9c Add boolean operations for mobjects 2021-11-10 00:23:04 +08:00
Michael W
94f0bf557a Add skia-pathops package 2021-11-10 00:21:04 +08:00
Grant Sanderson
e20690b7c1 Don't necessarily remove anti_alias on ThreeDScene 2021-11-08 21:48:42 -08:00
Grant Sanderson
2c7689ed9e Enable glow_factor on dots 2021-11-08 21:47:48 -08:00
Grant Sanderson
c73d507c76 Fix SurfaceMesh to be evenly spaced 2021-11-08 21:47:26 -08:00
Grant Sanderson
317a5d6226 Make it possible to set full screen preview as a default 2021-11-08 21:47:02 -08:00
Grant Sanderson
4339f97c56 Small refactor and added functionality 2021-11-08 21:46:35 -08:00
Grant Sanderson
81c3ae3037 Have separate notions of gloss and reflectiveness 2021-11-08 21:46:09 -08:00
Grant Sanderson
61b04079f5 Merge branch 'master' of github.com:3b1b/manim into video-work 2021-11-01 13:18:09 -07:00
Grant Sanderson
5a0e5a16ea Merge pull request #1667 from TurkeyBilly/master
Overridden add operations for mobjects
2021-11-01 13:17:16 -07:00
Grant Sanderson
f0b5181694 Update manimlib/mobject/mobject.py
Small bug fix to Mobject.__add__
2021-11-01 13:16:50 -07:00
Grant Sanderson
185782a2e7 Remove stray brace 2021-11-01 13:05:13 -07:00
Grant Sanderson
8ab95ebe9d Change where unit_normal data gets updated 2021-11-01 13:04:53 -07:00
Bill Xi
77159eea2e Update mobject.py 2021-11-01 10:19:06 +08:00
Bill Xi
6766e459f2 Update vectorized_mobject.py 2021-10-31 20:03:04 +08:00
Bill Xi
01f4ef3e5d Create mobject.py 2021-10-31 20:02:30 +08:00
Bill Xi
b531c82bc4 Update mobject.py 2021-10-31 20:01:16 +08:00
Bill Xi
5d942d5ac0 Update vectorized_mobject.py 2021-10-31 18:42:14 +08:00
Bill Xi
b285ca7c22 Update vectorized_mobject.py 2021-10-31 18:38:23 +08:00
Bill Xi
82540edae9 Update mobject.py 2021-10-31 18:37:12 +08:00
Bill Xi
f9a6fa7036 Update mobject.py 2021-10-31 18:35:28 +08:00
Bill Xi
4eabaecfc8 Update mobject.py 2021-10-31 18:34:23 +08:00
Grant Sanderson
b881e55fca Merge branch 'master' of github.com:3b1b/manim into video-work 2021-10-24 09:48:56 -07:00
Grant Sanderson
f1c50640a3 Merge pull request #1662 from YishiMichael/master
Refactor command handling in `svg_mobject.py`
2021-10-24 09:47:54 -07:00
Grant Sanderson
deb1311e48 Fix VideoIcon 2021-10-24 09:28:52 -07:00
Grant Sanderson
82fa6ab125 Temporary hack to fix a bug I don't understand 2021-10-24 09:28:39 -07:00
Michael W
4d91ff3f2f Update balance_braces method 2021-10-24 23:21:49 +08:00
Michael W
b6f9da87d0 Refactor command handling in svg_mobject.py 2021-10-24 22:30:18 +08:00
BillyLikesHacking
c60e97ebf9 Update vectorized_mobject.py 2021-10-22 20:58:19 +08:00
BillyLikesHacking
b1ed16e81a Update mobject.py 2021-10-22 20:46:47 +08:00
BillyLikesHacking
c94ebaa260 Update vectorized_mobject.py 2021-10-22 20:03:58 +08:00
BillyLikesHacking
030fb52018 Update vectorized_mobject.py 2021-10-22 20:03:05 +08:00
BillyLikesHacking
487f582302 Update mobject.py 2021-10-22 20:02:05 +08:00
BillyLikesHacking
6d0c55d2ba Update mobject.py 2021-10-22 20:00:27 +08:00
鹤翔万里
c82f60e29e Merge pull request #1658 from 050644zf/master
Update the link of the Chinese Ver. Docs
2021-10-20 17:25:28 +08:00
Nightsky
c03279d626 Update the link of the Chinese Ver. Docs 2021-10-20 17:20:52 +08:00
Grant Sanderson
7b72fa8ca1 Merge branch 'master' of github.com:3b1b/manim into video-work 2021-10-18 07:12:10 -07:00
Grant Sanderson
8b454fbe93 Slight tweaks to how saturation_factor works on newton-fractal 2021-10-18 07:12:05 -07:00
Grant Sanderson
f77e25ff86 Merge pull request #1655 from widcardw/master
Fix the bug of rotating camera
2021-10-18 07:01:58 -07:00
widcardw
872ef67cf7 Fix bug of rotating camera 2021-10-18 21:00:25 +08:00
widcardw
305ca72ebe Fix the bug of rotating camera 2021-10-18 19:05:05 +08:00
TonyCrane
4d81d3678b update changelog 2021-10-16 21:07:53 +08:00
鹤翔万里
55e968e174 Merge pull request #1653 from YishiMichael/master
Fix parameter typo
2021-10-16 21:06:32 +08:00
TonyCrane
97d1609849 update changelog 2021-10-16 21:03:36 +08:00
TonyCrane
e10f850d0d add cli flag to specify log level 2021-10-16 21:01:39 +08:00
Michael W
b8584fe5ab Fix parameter typo 2021-10-16 20:59:31 +08:00
97 changed files with 5964 additions and 2964 deletions

View File

@@ -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/*

View File

@@ -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).

View File

@@ -1,63 +1,257 @@
Changelog
=========
v1.6.0
------
Breaking changes
^^^^^^^^^^^^^^^^
- **Python 3.6 is no longer supported** (`#1736 <https://github.com/3b1b/manim/pull/1736>`__)
Fixed bugs
^^^^^^^^^^
- Fixed the width of riemann rectangles (`#1762 <https://github.com/3b1b/manim/pull/1762>`__)
- Bug fixed in cases where empty array is passed to shader (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/fa38b56fd87f713657c7f778f39dca7faf15baa8>`__)
- Fixed ``AddTextWordByWord`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
- Fixed ``ControlsExample`` (`#1781 <https://github.com/3b1b/manim/pull/1781>`__)
New features
^^^^^^^^^^^^
- Added more functions to ``Text`` (details: `#1751 <https://github.com/3b1b/manim/pull/1751>`__)
- Allowed ``interpolate`` to work on an array of alpha values (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/bf2d9edfe67c7e63ac0107d1d713df7ae7c3fb8f>`__)
- Allowed ``Numberline.number_to_point`` and ``CoordinateSystem.coords_to_point`` to work on an array of inputs (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/c3e13fff0587d3bb007e71923af7eaf9e4926560>`__)
- Added a basic ``Prismify`` to turn a flat ``VMobject`` into something with depth (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/f249da95fb65ed5495cd1db1f12ece7e90061af6>`__)
- Added ``GlowDots``, analogous to ``GlowDot`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/e19f35585d817e74b40bc30b1ab7cee84b24da05>`__)
- Added ``TransformMatchingStrings`` which is compatible with ``Text`` and ``MTex`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
- Added support for ``substring`` and ``case_sensitive`` parameters for ``LabelledString.get_parts_by_string`` (`#1780 <https://github.com/3b1b/manim/pull/1780>`__)
Refactor
^^^^^^^^
- Added type hints (`#1736 <https://github.com/3b1b/manim/pull/1736>`__)
- Specifid UTF-8 encoding for tex files (`#1748 <https://github.com/3b1b/manim/pull/1748>`__)
- Refactored ``Text`` with the latest manimpango (`#1751 <https://github.com/3b1b/manim/pull/1751>`__)
- Reorganized getters for ``ParametricCurve`` (`#1757 <https://github.com/3b1b/manim/pull/1757>`__)
- Refactored ``CameraFrame`` to use ``scipy.spatial.transform.Rotation`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/625460467fdc01fc1b6621cbb3d2612195daedb9>`__)
- Refactored rotation methods to use ``scipy.spatial.transform.Rotation`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/7bf3615bb15cc6d15506d48ac800a23313054c8e>`__)
- Used ``stroke_color`` to init ``Arrow`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/c0b7b55e49f06b75ae133b5a810bebc28c212cd6>`__)
- Refactored ``Mobject.set_rgba_array_by_color`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/8b1f0a8749d91eeda4b674ed156cbc7f8e1e48a8>`__)
- Made panning more sensitive to mouse movements (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/9d0cc810c5fcb4252990e706c6bf880d571cb1a2>`__)
- Added loading progress for large SVGs (`#1766 <https://github.com/3b1b/manim/pull/1766>`__)
- Added getter/setter of ``field_of_view`` for ``CameraFrame`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0610f331a4f7a126a3aae34f8a2a86eabcb692f4>`__)
- Renamed ``focal_distance`` to ``focal_dist_to_height`` and added getter/setter (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0610f331a4f7a126a3aae34f8a2a86eabcb692f4>`__)
- Added getter and setter for ``VMobject.joint_type`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/2a7a7ac5189a14170f883533137e8a2ae09aac41>`__)
- Refactored ``VCube`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0f8d7ed59751d42d5011813ba5694ecb506082f7>`__)
- Refactored ``Prism`` to receive ``width height depth`` instead of ``dimensions`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0f8d7ed59751d42d5011813ba5694ecb506082f7>`__)
- Refactored ``Text``, ``MarkupText`` and ``MTex`` based on ``LabelledString`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
- Refactored ``LabelledString`` and relevant classes (`#1779 <https://github.com/3b1b/manim/pull/1779>`__)
v1.5.0
------
Fixed bugs
^^^^^^^^^^
- Bug fix for the case of calling ``Write`` on a null object (`#1740 <https://github.com/3b1b/manim/pull/1740>`__)
New features
^^^^^^^^^^^^
- Added ``TransformMatchingMTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
- Added ``ImplicitFunction`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
- Added ``Polyline`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
- Allowed ``Mobject.set_points`` to take in an empty list, and added ``Mobject.add_point`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/a64259158538eae6043566aaf3d3329ff4ac394b>`__)
- Added ``Scene.refresh_locked_data`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/33d2894c167c577a15fdadbaf26488ff1f5bff87>`__)
- Added presenter mode to scenes with ``-p`` option (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9a9cc8bdacb7541b7cd4a52ad705abc21f3e27fe>`__ and `#1742 <https://github.com/3b1b/manim/pull/1742>`__)
- Allowed for an embed by hitting ``ctrl+shift+e`` during interaction (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9df12fcb7d8360e51cd7021d6877ca1a5c31835e>`__ and `#1746 <https://github.com/3b1b/manim/pull/1746>`__)
- Added ``Mobject.set_min_width/height/depth`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2798d15591a0375ae6bb9135473e6f5328267323>`__)
- Allowed ``Mobject.match_coord/x/y/z`` to take in a point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/29a4d3e82ba94c007c996b2d1d0f923941452698>`__)
- Added ``text_config`` to ``DecimalNumber`` (`#1744 <https://github.com/3b1b/manim/pull/1744>`__)
Refactor
^^^^^^^^
- Refactored ``MTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
- Refactored ``SVGMobject`` with svgelements (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
- Made sure ``ParametricCurve`` has at least one point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2488b9e866fb1ecb842a27dd9f4956ec167e3dee>`__)
- Set default to no tips on ``Axes`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/6c6d387a210756c38feca7d34838aa9ac99bb58a>`__)
- Stopped displaying when writing tex string is happening (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/58e06e8f6b7c5059ff315d51fd0018fec5cfbb05>`__)
- Reorganize inheriting order and refactor SVGMobject (`#1745 <https://github.com/3b1b/manim/pull/1745>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on ``isosurfaces`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
- Removed dependency on ``argparse`` since it's a built-in module (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
- Removed dependency on ``pyreadline`` (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
- Removed dependency on ``cssselect2`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
- Added dependency on ``svgelements`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
v1.4.1
------
Fixed bugs
^^^^^^^^^^
- Temporarily fixed boolean operations' bug (`#1724 <https://github.com/3b1b/manim/pull/1724>`__)
- Import ``Iterable`` from ``collections.abc`` instead of ``collections`` which is deprecated since python 3.9 (`d2e0811 <https://github.com/3b1b/manim/commit/d2e0811285f7908e71a65e664fec88b1af1c6144>`__)
v1.4.0
------
Fixed bugs
^^^^^^^^^^
- Temporarily fixed ``Lightbulb`` (`f1996f8 <https://github.com/3b1b/manim/pull/1697/commits/f1996f8479f9e33d626b3b66e9eb6995ce231d86>`__)
- Fixed some bugs of ``SVGMobject`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Fixed some bugs of SVG path string parser (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
- Fixed some bugs of ``MTex`` (`#1720 <https://github.com/3b1b/manim/pull/1720>`__)
New features
^^^^^^^^^^^^
- Added option to add ticks on x-axis in ``BarChart`` (`#1694 <https://github.com/3b1b/manim/pull/1694>`__)
- Added ``lable_buff`` config parameter for ``Brace`` (`#1704 <https://github.com/3b1b/manim/pull/1704>`__)
- Added support for ``rotate skewX skewY`` transform in SVG (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Added style support to ``SVGMobject`` (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
- Added parser to <style> element of SVG (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
- Added support for <line> element in ``SVGMobject`` (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
Refactor
^^^^^^^^
- Used ``FFMPEG_BIN`` instead of ``"ffmpeg"`` for sound incorporation (`5aa8d15 <https://github.com/3b1b/manim/pull/1697/commits/5aa8d15d85797f68a8f169ca69fd90d441a3abbe>`__)
- Decorated ``CoordinateSystem.get_axes`` and ``.get_all_ranges`` as abstract method (`#1709 <https://github.com/3b1b/manim/pull/1709>`__)
- Refactored SVG path string parser (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Allowed ``Mobject.scale`` to receive iterable ``scale_factor`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Refactored ``MTex`` (`#1716 <https://github.com/3b1b/manim/pull/1716>`__)
- Improved config helper (``manimgl --config``) (`#1721 <https://github.com/3b1b/manim/pull/1721>`__)
- Refactored ``MTex`` (`#1723 <https://github.com/3b1b/manim/pull/1723>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on python package `cssselect2 <https://github.com/Kozea/cssselect2>`__ (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
v1.3.0
------
Fixed bugs
^^^^^^^^^^
- Fixed ``Mobject.stretch_to_fit_depth`` (`#1653 <https://github.com/3b1b/manim/pull/1653>`__)
- Fixed the bug of rotating camera (`#1655 <https://github.com/3b1b/manim/pull/1655>`__)
- Fixed ``SurfaceMesh`` to be evenly spaced (`c73d507 <https://github.com/3b1b/manim/pull/1688/commits/c73d507c76af5c8602d4118bc7538ba04c03ebae>`__)
- Fixed ``angle_between_vectors`` add ``rotation_between_vectors`` (`82bd02d <https://github.com/3b1b/manim/pull/1688/commits/82bd02d21fbd89b71baa21e077e143f440df9014>`__)
- Fixed ``VMobject.fade`` (`a717314 <https://github.com/3b1b/manim/pull/1688/commits/a7173142bf93fd309def0cc10f3c56f5e6972332>`__)
- Fixed ``angle_between_vectors`` (`fbc329d <https://github.com/3b1b/manim/pull/1688/commits/fbc329d7ce3b11821d47adf6052d932f7eff724a>`__)
- Fixed bug in ``ShowSubmobjectsOneByOne`` (`bcd0990 <https://github.com/3b1b/manim/pull/1688/commits/bcd09906bea5eaaa5352e7bee8f3153f434cf606>`__)
- Fixed bug in ``TransformMatchingParts`` (`7023548 <https://github.com/3b1b/manim/pull/1691/commits/7023548ec62c4adb2f371aab6a8c7f62deb7c33c>`__)
New features
^^^^^^^^^^^^
- Added CLI flag ``--log-level`` to specify log level (`e10f850 <https://github.com/3b1b/manim/commit/e10f850d0d9f971931cc85d44befe67dc842af6d>`__)
- Added operations (``+`` and ``*``) for ``Mobject`` (`#1667 <https://github.com/3b1b/manim/pull/1667>`__)
- Added 4 boolean operations for ``VMobject`` in ``manimlib/mobject/boolean_ops.py`` (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
- ``Union(*vmobjects, **kwargs)``
- ``Difference(subject, clip, **kwargs)``
- ``Intersection(*vmobjects, **kwargs)``
- ``Exclusion(*vmobjects, **kwargs)``
- Added reflectiveness (`81c3ae3 <https://github.com/3b1b/manim/pull/1688/commits/81c3ae30372e288dc772633dbd17def6e603753e>`__)
- Enabled ``glow_factor`` on ``DotCloud`` (`2c7689e <https://github.com/3b1b/manim/pull/1688/commits/2c7689ed9e81229ce87c648f97f26267956c0bc9>`__)
- Added option ``-e`` to insert embed line from the command line (`d065e19 <https://github.com/3b1b/manim/pull/1688/commits/d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772>`__)
- Improved ``point_from_proportion`` to account for arc length (`0e78027 <https://github.com/3b1b/manim/pull/1688/commits/0e78027186a976f7e5fa8d586f586bf6e6baab8d>`__)
- Added shortcut ``set_backstroke`` for setting black background stroke (`781a993 <https://github.com/3b1b/manim/pull/1688/commits/781a9934fda6ba11f22ba32e8ccddcb3ba78592e>`__)
- Added ``Suface.always_sort_to_camera`` (`0b898a5 <https://github.com/3b1b/manim/pull/1688/commits/0b898a5594203668ed9cad38b490ab49ba233bd4>`__)
- Added getter methods for specific euler angles (`e899604 <https://github.com/3b1b/manim/pull/1688/commits/e899604a2d05f78202fcb3b9824ec34647237eae>`__)
- Hade ``rotation_between_vectors`` handle identical/similar vectors (`407c53f <https://github.com/3b1b/manim/pull/1688/commits/407c53f97c061bfd8a53beacd88af4c786f9e9ee>`__)
- Added ``Mobject.insert_submobject`` method (`49743da <https://github.com/3b1b/manim/pull/1688/commits/49743daf3244bfa11a427040bdde8e2bb79589e8>`__)
- Created single progress display for full scene render (`9dd1f47 <https://github.com/3b1b/manim/pull/1688/commits/9dd1f47dabca1580d6102e34e44574b0cba556e7>`__)
- Added ``Circle.get_radius`` (`264f7b1 <https://github.com/3b1b/manim/pull/1691/commits/264f7b11726e9e736f0fe472f66e38539f74e848>`__)
- Added ``Dodecahedron`` (`83841ae <https://github.com/3b1b/manim/pull/1691/commits/83841ae41568a9c9dff44cd163106c19a74ac281>`__)
- Added ``GlowDot`` (`a1d5147 <https://github.com/3b1b/manim/pull/1691/commits/a1d51474ea1ce3b7aa3efbe4c5e221be70ee2f5b>`__)
- Added ``MTex`` , see `#1678 <https://github.com/3b1b/manim/pull/1678>`__ for details (`#1678 <https://github.com/3b1b/manim/pull/1678>`__)
Refactor
^^^^^^^^
- Refactored support for command ``A`` in path of SVG (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
- Refactored ``SingleStringTex.balance_braces`` (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
- Slight tweaks to how saturation_factor works on newton-fractal (`8b454fb <https://github.com/3b1b/manim/pull/1688/commits/8b454fbe9335a7011e947093230b07a74ba9c653>`__)
- Made it possible to set full screen preview as a default (`317a5d6 <https://github.com/3b1b/manim/pull/1688/commits/317a5d6226475b6b54a78db7116c373ef84ea923>`__)
- Used ``quick_point_from_proportion`` for graph points (`e764da3 <https://github.com/3b1b/manim/pull/1688/commits/e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc>`__)
- Made sure ``Line.set_length`` returns self (`d2182b9 <https://github.com/3b1b/manim/pull/1688/commits/d2182b9112300558b6c074cefd685f97c10b3898>`__)
- Better align ``SurfaceMesh`` to the corresponding surface polygons (`eea3c6b <https://github.com/3b1b/manim/pull/1688/commits/eea3c6b29438f9e9325329c4355e76b9f635e97a>`__)
- Match ``fix_in_frame`` status for ``FlashAround`` mobject (`ee1594a <https://github.com/3b1b/manim/pull/1688/commits/ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3>`__)
- Made sure ``Mobject.is_fixed_in_frame`` stays updated with uniforms (`ba23fbe <https://github.com/3b1b/manim/pull/1688/commits/ba23fbe71e4a038201cd7df1d200514ed1c13bc2>`__)
- Made sure ``skip_animations`` and ``start_at_animation_number`` play well together (`98b0d26 <https://github.com/3b1b/manim/pull/1691/commits/98b0d266d2475926a606331923cca3dc1dea97ad>`__)
- Updated progress display for full scene render (`f8e6e7d <https://github.com/3b1b/manim/pull/1691/commits/f8e6e7df3ceb6f3d845ced4b690a85b35e0b8d00>`__)
- ``VectorizedPoint`` should call ``__init__`` for both super classes (`8f1dfab <https://github.com/3b1b/manim/pull/1691/commits/8f1dfabff04a8456f5c4df75b0f97d50b2755003>`__)
- Used array copy when checking need for refreshing triangulation (`758f329 <https://github.com/3b1b/manim/pull/1691/commits/758f329a06a0c198b27a48c577575d94554305bf>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__ (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
v1.2.0
------
Fixed bugs
^^^^^^^^^^
- `#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
@@ -83,7 +277,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`

View File

@@ -84,8 +84,6 @@ Text
.. code-block:: python
START_X = 30
START_Y = 20
NORMAL = "NORMAL"
ITALIC = "ITALIC"
OBLIQUE = "OBLIQUE"

View File

@@ -83,22 +83,3 @@ Its value is a dictionary, passed in as ``kwargs`` when initializing the ``Camer
to modify the value of the properties of the ``Camera`` class.
So the nesting of the ``CONFIG`` dictionary **essentially** passes in the value as ``kwargs``.
Common usage
------------
When writing a class by yourself, you can add attributes or modify the attributes
of the parent class through ``CONFIG``.
The most commonly used is to modify the properties of the camera when writing a ``Scene``:
.. code-block:: python
class YourScene(Scene):
CONFIG = {
"camera_config": {
"background_color": WHITE,
},
}
For example, the above dictionary will change the background color to white, etc.

View File

@@ -43,6 +43,7 @@ flag abbr function
``--hd`` Render at a 1080p quality
``--uhd`` Render at a 4k quality
``--full_screen`` ``-f`` Show window in full screen
``--presenter_mode`` ``-p`` Scene will stay paused during wait calls until space bar or right arrow is hit, like a slide show
``--save_pngs`` ``-g`` Save each frame as a png
``--save_as_gif`` ``-i`` Save the video as gif
``--transparent`` ``-t`` Render to a movie file with an alpha channel
@@ -53,11 +54,12 @@ flag abbr function
``--config`` Guide for automatic configuration
``--file_name FILE_NAME`` Name for the movie or image file
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value.
``--embed LINENO`` ``-e`` Takes a line number as an argument, and results in the scene being called as if the line ``self.embed()`` was inserted into the scene code at that line number
``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080"
``--frame_rate FRAME_RATE`` Frame rate, as an integer
``--color COLOR`` ``-c`` Background color
``--leave_progress_bars`` Leave progress bars displayed in terminal
``--video_dir VIDEO_DIR`` directory to write video
``--video_dir VIDEO_DIR`` Directory to write video
``--config_file CONFIG_FILE`` Path to the custom configuration file
========================================================== ====== =================================================================================================================================================================================================

View 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

View File

@@ -6,7 +6,7 @@ Manim's documentation
Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as seen in the videos
at `3Blue1Brown <https://www.3blue1brown.com/>`_.
And here is a Chinese version of this documentation: https://docs.manim.org.cn/shaders
And here is a Chinese version of this documentation: https://docs.manim.org.cn/
.. toctree::
:maxdepth: 2

View File

@@ -274,7 +274,7 @@ class UpdatersExample(Scene):
square = Square()
square.set_fill(BLUE_E, 1)
# On all all frames, the constructor Brace(square, UP) will
# On all frames, the constructor Brace(square, UP) will
# be called, and the mobject brace will set its data to match
# that of the newly constructed object
brace = always_redraw(Brace, square, UP)
@@ -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():

View File

@@ -22,6 +22,7 @@ from manimlib.camera.camera import *
from manimlib.window import *
from manimlib.mobject.boolean_ops import *
from manimlib.mobject.coordinate_systems import *
from manimlib.mobject.changing import *
from manimlib.mobject.frame import *
@@ -36,6 +37,8 @@ from manimlib.mobject.probability import *
from manimlib.mobject.shape_matchers import *
from manimlib.mobject.svg.brace import *
from manimlib.mobject.svg.drawings import *
from manimlib.mobject.svg.labelled_string import *
from manimlib.mobject.svg.mtex_mobject import *
from manimlib.mobject.svg.svg_mobject import *
from manimlib.mobject.svg.tex_mobject import *
from manimlib.mobject.svg.text_mobject import *
@@ -66,4 +69,3 @@ from manimlib.utils.rate_functions import *
from manimlib.utils.simple_functions import *
from manimlib.utils.sounds import *
from manimlib.utils.space_ops import *
from manimlib.utils.strings import *

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python
import manimlib.config
import manimlib.logger
import manimlib.extract_scene
import manimlib.utils.init_config
from manimlib import __version__
@@ -9,8 +10,10 @@ def main():
print(f"ManimGL \033[32mv{__version__}\033[0m")
args = manimlib.config.parse_cli()
if args.version and args.file == None:
if args.version and args.file is None:
return
if args.log_level:
manimlib.logger.log.setLevel(args.log_level)
if args.config:
manimlib.utils.init_config.init_customization()
@@ -21,5 +24,6 @@ def main():
for scene in scenes:
scene.run()
if __name__ == "__main__":
main()

View File

@@ -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()

View File

@@ -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:

View File

@@ -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,35 +195,27 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
"int_func": np.ceil,
}
def __init__(self, group, **kwargs):
new_group = Group(*group)
super().__init__(new_group, **kwargs)
def update_submobject_list(self, index):
def update_submobject_list(self, index: int) -> None:
# N = len(self.all_submobs)
if index == 0:
self.mobject.set_submobjects([])
else:
self.mobject.set_submobjects(self.all_submobs[index - 1])
self.mobject.set_submobjects([self.all_submobs[index - 1]])
# TODO, this is broken...
class AddTextWordByWord(Succession):
class AddTextWordByWord(ShowIncreasingSubsets):
CONFIG = {
# If given a value for run_time, it will
# override the time_per_char
# override the time_per_word
"run_time": None,
"time_per_char": 0.06,
"time_per_word": 0.2,
"rate_func": linear,
}
def __init__(self, text_mobject, **kwargs):
def __init__(self, string_mobject, **kwargs):
assert isinstance(string_mobject, LabelledString)
grouped_mobject = string_mobject.get_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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,20 +242,22 @@ 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:
path.fix_in_frame()
path.insert_n_curves(self.n_inserted_curves)
path.set_points(path.get_points_without_null_curves())
path.set_stroke(self.color, self.stroke_width)
super().__init__(path, **kwargs)
def get_path(self, mobject):
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)
@@ -250,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),
@@ -267,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")
@@ -280,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
@@ -312,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]
@@ -337,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)),
@@ -362,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()
@@ -371,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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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)

View File

@@ -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):

View File

@@ -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)

View File

@@ -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))
@@ -68,10 +80,10 @@ class TransformMatchingParts(AnimationGroup):
anims.append(FadeTransformPieces(fade_source, fade_target, **kwargs))
else:
anims.append(FadeOutToPoint(
fade_source, fade_target.get_center(), **kwargs
fade_source, target_mobject.get_center(), **kwargs
))
anims.append(FadeInFromPoint(
fade_target.copy(), fade_source.get_center(), **kwargs
fade_target.copy(), mobject.get_center(), **kwargs
))
super().__init__(*anims)
@@ -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,132 @@ 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_mobject: LabelledString,
target_mobject: LabelledString,
**kwargs
):
digest_config(self, kwargs)
assert isinstance(source_mobject, LabelledString)
assert isinstance(target_mobject, LabelledString)
anims = []
rest_source_indices = list(range(len(source_mobject.submobjects)))
rest_target_indices = list(range(len(target_mobject.submobjects)))
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_mobject, source_arg)
target_parts = func(target_mobject, target_arg)
source_indices_lists = source_mobject.indices_lists_of_parts(
source_parts
)
target_indices_lists = target_mobject.indices_lists_of_parts(
target_parts
)
filtered_source_indices_lists = list(filter(
lambda indices_list: all([
index in rest_source_indices
for index in indices_list
]), source_indices_lists
))
filtered_target_indices_lists = list(filter(
lambda indices_list: all([
index in rest_target_indices
for index in indices_list
]), target_indices_lists
))
if not all([
filtered_source_indices_lists,
filtered_target_indices_lists
]):
continue
anims.append(anim_class(source_parts, target_parts, **kwargs))
for index in it.chain(*filtered_source_indices_lists):
rest_source_indices.remove(index)
for index in it.chain(*filtered_target_indices_lists):
rest_target_indices.remove(index)
def get_common_substrs(func):
return sorted([
substr for substr in func(source_mobject)
if substr and substr in func(target_mobject)
], key=len, reverse=True)
def get_parts_from_keys(mobject, keys):
if not isinstance(keys, tuple):
keys = (keys,)
indices = []
for key in keys:
if isinstance(key, int):
indices.append(key)
elif isinstance(key, range):
indices.extend(key)
elif isinstance(key, str):
all_parts = mobject.get_parts_by_string(key)
indices.extend(it.chain(*[
mobject.indices_of_part(part) for part in all_parts
]))
else:
raise TypeError(key)
return VGroup(VGroup(*[
mobject[index] for index in remove_list_redundancies(indices)
]))
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(LabelledString.get_specified_substrs)
)
add_anims_from(
FadeTransformPieces,
LabelledString.get_parts_by_group_substr,
get_common_substrs(LabelledString.get_group_substrs)
)
fade_source = VGroup(*[
source_mobject[index]
for index in rest_source_indices
])
fade_target = VGroup(*[
target_mobject[index]
for index in rest_target_indices
])
if self.transform_mismatches:
anims.append(ReplacementTransform(
fade_source,
fade_target,
**kwargs
))
else:
anims.append(FadeOutToPoint(
fade_source,
target_mobject.get_center(),
**kwargs
))
anims.append(FadeInFromPoint(
fade_target,
source_mobject.get_center(),
**kwargs
))
super().__init__(*anims)

View File

@@ -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)

View File

@@ -1,93 +1,93 @@
from __future__ import annotations
import moderngl
from colour import Color
import OpenGL.GL as gl
import math
from PIL import Image
import numpy as np
import itertools as it
import numpy as np
from scipy.spatial.transform import Rotation
from PIL import Image
from manimlib.constants import *
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Point
from manimlib.utils.config_ops import digest_config
from manimlib.utils.simple_functions import fdiv
from manimlib.utils.simple_functions import clip
from manimlib.utils.space_ops import angle_of_vector
from manimlib.utils.space_ops import rotation_matrix_transpose_from_quaternion
from manimlib.utils.space_ops import rotation_matrix_transpose
from manimlib.utils.space_ops import quaternion_from_angle_axis
from manimlib.utils.space_ops import quaternion_mult
from manimlib.utils.space_ops import normalize
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.shader_wrapper import ShaderWrapper
class CameraFrame(Mobject):
CONFIG = {
"frame_shape": (FRAME_WIDTH, FRAME_HEIGHT),
"center_point": ORIGIN,
# Theta, phi, gamma
"euler_angles": [0, 0, 0],
"focal_distance": 2,
"focal_dist_to_height": 2,
}
def init_data(self):
super().init_data()
self.data["euler_angles"] = np.array(self.euler_angles, dtype=float)
self.refresh_rotation_matrix()
def init_uniforms(self) -> None:
super().init_uniforms()
# As a quaternion
self.uniforms["orientation"] = Rotation.identity().as_quat()
self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height
def init_points(self):
def init_points(self) -> None:
self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP])
self.set_width(self.frame_shape[0], stretch=True)
self.set_height(self.frame_shape[1], stretch=True)
self.move_to(self.center_point)
def set_orientation(self, rotation: Rotation):
self.uniforms["orientation"][:] = rotation.as_quat()
return self
def get_orientation(self):
return Rotation.from_quat(self.uniforms["orientation"])
def to_default_state(self):
self.center()
self.set_height(FRAME_HEIGHT)
self.set_width(FRAME_WIDTH)
self.set_euler_angles(0, 0, 0)
self.set_orientation(Rotation.identity())
return self
def get_euler_angles(self):
return self.data["euler_angles"]
return self.get_orientation().as_euler("zxz")[::-1]
def get_inverse_camera_rotation_matrix(self):
return self.inverse_camera_rotation_matrix
return self.get_orientation().as_matrix().T
def refresh_rotation_matrix(self):
# Rotate based on camera orientation
theta, phi, gamma = self.get_euler_angles()
quat = quaternion_mult(
quaternion_from_angle_axis(theta, OUT, axis_normalized=True),
quaternion_from_angle_axis(phi, RIGHT, axis_normalized=True),
quaternion_from_angle_axis(gamma, OUT, axis_normalized=True),
)
self.inverse_camera_rotation_matrix = rotation_matrix_transpose_from_quaternion(quat)
def rotate(self, angle, axis=OUT, **kwargs):
curr_rot_T = self.get_inverse_camera_rotation_matrix()
added_rot_T = rotation_matrix_transpose(angle, axis)
new_rot_T = np.dot(curr_rot_T, added_rot_T)
Fz = new_rot_T[2]
phi = np.arccos(Fz[2])
theta = angle_of_vector(Fz[:2]) + PI / 2
partial_rot_T = np.dot(
rotation_matrix_transpose(phi, RIGHT),
rotation_matrix_transpose(theta, OUT),
)
gamma = angle_of_vector(np.dot(partial_rot_T, new_rot_T.T)[:, 0])
self.set_euler_angles(theta, phi, gamma)
def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
rot = Rotation.from_rotvec(angle * normalize(axis))
self.set_orientation(rot * self.get_orientation())
return self
def set_euler_angles(self, theta=None, phi=None, gamma=None, 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
@@ -95,53 +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 set_focal_distance(self, focal_distance: float):
self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height()
return self
def set_field_of_view(self, field_of_view: float):
self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2)
return self
def get_shape(self):
return (self.get_width(), self.get_height())
def get_center(self):
def get_center(self) -> np.ndarray:
# Assumes first point is at the center
return self.get_points()[0]
def get_width(self):
def get_width(self) -> float:
points = self.get_points()
return points[2, 0] - points[1, 0]
def get_height(self):
def get_height(self) -> float:
points = self.get_points()
return points[4, 1] - points[3, 1]
def get_focal_distance(self):
return self.focal_distance * self.get_height()
def get_focal_distance(self) -> float:
return self.uniforms["focal_dist_to_height"] * self.get_height()
def interpolate(self, *args, **kwargs):
super().interpolate(*args, **kwargs)
self.refresh_rotation_matrix()
def get_field_of_view(self) -> float:
return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2)
def get_implied_camera_location(self) -> np.ndarray:
to_camera = self.get_inverse_camera_rotation_matrix()[2]
dist = self.get_focal_distance()
return self.get_center() + dist * to_camera
class Camera(object):
@@ -170,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
]
@@ -185,35 +192,49 @@ class Camera(object):
self.refresh_perspective_uniforms()
self.static_mobject_to_render_group_list = {}
def init_frame(self):
def init_frame(self) -> None:
self.frame = CameraFrame(**self.frame_config)
def init_context(self, ctx=None):
def init_context(self, ctx: moderngl.Context | None = None) -> None:
if ctx is None:
ctx = moderngl.create_standalone_context()
fbo = self.get_fbo(ctx, 0)
else:
fbo = ctx.detect_framebuffer()
self.ctx = ctx
self.fbo = fbo
self.set_ctx_blending()
# For multisample antialiasing
fbo_msaa = self.get_fbo(ctx, self.samples)
fbo_msaa.use()
ctx.enable(moderngl.BLEND)
ctx.blend_func = (
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
moderngl.ONE, moderngl.ONE
)
self.ctx = ctx
self.fbo = fbo
self.fbo_msaa = fbo_msaa
def init_light_source(self):
def set_ctx_blending(self, enable: bool = True) -> None:
if enable:
self.ctx.enable(moderngl.BLEND)
else:
self.ctx.disable(moderngl.BLEND)
self.ctx.blend_func = (
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
# moderngl.ONE, moderngl.ONE
)
def set_ctx_depth_test(self, enable: bool = True) -> None:
if enable:
self.ctx.enable(moderngl.DEPTH_TEST)
else:
self.ctx.disable(moderngl.DEPTH_TEST)
def init_light_source(self) -> None:
self.light_source = Point(self.light_source_position)
# Methods associated with the frame buffer
def get_fbo(self, ctx, samples=0):
def get_fbo(
self,
ctx: moderngl.Context,
samples: int = 0
) -> moderngl.Framebuffer:
pw = self.pixel_width
ph = self.pixel_height
return ctx.framebuffer(
@@ -228,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)
@@ -249,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(),
@@ -257,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])
@@ -265,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,
@@ -275,29 +296,32 @@ class Camera(object):
return texture
# Getting camera attributes
def get_pixel_shape(self):
def get_pixel_shape(self) -> tuple[int, int]:
return self.fbo.viewport[2:4]
# return (self.pixel_width, self.pixel_height)
def get_pixel_width(self):
def get_pixel_width(self) -> int:
return self.get_pixel_shape()[0]
def get_pixel_height(self):
def get_pixel_height(self) -> int:
return self.get_pixel_shape()[1]
def get_frame_height(self):
def get_frame_height(self) -> float:
return self.frame.get_height()
def get_frame_width(self):
def get_frame_width(self) -> float:
return self.frame.get_width()
def get_frame_shape(self):
def get_frame_shape(self) -> tuple[float, float]:
return (self.get_frame_width(), self.get_frame_height())
def get_frame_center(self):
def get_frame_center(self) -> np.ndarray:
return self.frame.get_center()
def resize_frame_shape(self, fixed_dimension=0):
def get_location(self) -> tuple[float, float, float]:
return self.frame.get_implied_camera_location()
def resize_frame_shape(self, fixed_dimension: bool = False) -> None:
"""
Changes frame_shape to match the aspect ratio
of the pixels, where fixed_dimension determines
@@ -309,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
@@ -317,34 +341,32 @@ 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)
self.update_depth_test(shader_wrapper)
self.set_ctx_depth_test(shader_wrapper.depth_test)
render_group["vao"].render(int(shader_wrapper.render_primitive))
if render_group["single_use"]:
self.release_render_group(render_group)
def update_depth_test(self, shader_wrapper):
if shader_wrapper.depth_test:
self.ctx.enable(moderngl.DEPTH_TEST)
else:
self.ctx.disable(moderngl.DEPTH_TEST)
def get_render_group_list(self, mobject):
def get_render_group_list(self, mobject: Mobject) -> list[dict[str]] | map[dict[str]]:
try:
return self.static_mobject_to_render_group_list[id(mobject)]
except KeyError:
return map(self.get_render_group, mobject.get_shader_wrapper_list())
def get_render_group(self, shader_wrapper, single_use=True):
def get_render_group(
self,
shader_wrapper: ShaderWrapper,
single_use: bool = True
) -> dict[str]:
# Data buffers
vbo = self.ctx.buffer(shader_wrapper.vert_data.tobytes())
if shader_wrapper.vert_indices is None:
@@ -372,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)] = [
@@ -385,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
@@ -406,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(shader_wrapper.uniforms.items(), self.perspective_uniforms.items()):
for name, value in it.chain(self.perspective_uniforms.items(), shader_wrapper.uniforms.items()):
try:
if isinstance(value, np.ndarray):
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()
@@ -427,24 +458,32 @@ class Camera(object):
anti_alias_width = self.anti_alias_width / (ph / fh)
# Orient light
rotation = frame.get_inverse_camera_rotation_matrix()
light_pos = self.light_source.get_location()
light_pos = np.dot(rotation, light_pos)
offset = frame.get_center()
light_pos = np.dot(
rotation, self.light_source.get_location() + offset
)
cam_pos = self.frame.get_implied_camera_location() # TODO
self.perspective_uniforms = {
"frame_shape": frame.get_shape(),
"anti_alias_width": anti_alias_width,
"camera_center": tuple(frame.get_center()),
"camera_offset": tuple(offset),
"camera_rotation": tuple(np.array(rotation).T.flatten()),
"camera_position": tuple(cam_pos),
"light_source_position": tuple(light_pos),
"focal_distance": frame.get_focal_distance(),
}
def init_textures(self):
self.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
tid = self.n_textures
self.n_textures += 1
im = Image.open(path).convert("RGBA")
@@ -457,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()
@@ -468,4 +507,5 @@ class Camera(object):
class ThreeDCamera(Camera):
CONFIG = {
"samples": 4,
"anti_alias_width": 0,
}

View File

@@ -5,6 +5,7 @@ import importlib
import os
import sys
import yaml
from contextlib import contextmanager
from screeninfo import get_monitors
from manimlib.utils.config_ops import merge_dicts_recursively
@@ -12,6 +13,9 @@ from manimlib.utils.init_config import init_customization
from manimlib.logger import log
__config_file__ = "custom_config.yml"
def parse_cli():
try:
parser = argparse.ArgumentParser()
@@ -19,7 +23,7 @@ def parse_cli():
module_location.add_argument(
"file",
nargs="?",
help="path to file holding the python code for the scene",
help="Path to file holding the python code for the scene",
)
parser.add_argument(
"scene_names",
@@ -61,6 +65,12 @@ def parse_cli():
action="store_true",
help="Show window in full screen",
)
parser.add_argument(
"-p", "--presenter_mode",
action="store_true",
help="Scene will stay paused during wait calls until "
"space bar or right arrow is hit, like a slide show"
)
parser.add_argument(
"-g", "--save_pngs",
action="store_true",
@@ -112,6 +122,12 @@ def parse_cli():
"in two comma separated values, e.g. \"3,6\", it will end"
"the rendering at the second value",
)
parser.add_argument(
"-e", "--embed", metavar="LINENO",
help="Takes a line number as an argument, and results"
"in the scene being called as if the line `self.embed()`"
"was inserted into the scene code at that line number."
)
parser.add_argument(
"-r", "--resolution",
help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"",
@@ -142,6 +158,10 @@ def parse_cli():
action="store_true",
help="Display the version of manimgl"
)
parser.add_argument(
"--log-level",
help="Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL"
)
args = parser.parse_args()
return args
except argparse.ArgumentError as err:
@@ -158,14 +178,30 @@ def get_manim_dir():
def get_module(file_name):
if file_name is None:
return None
else:
module_name = file_name.replace(os.sep, ".").replace(".py", "")
spec = importlib.util.spec_from_file_location(module_name, file_name)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
module_name = file_name.replace(os.sep, ".").replace(".py", "")
spec = importlib.util.spec_from_file_location(module_name, file_name)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@contextmanager
def insert_embed_line(file_name, lineno):
with open(file_name, 'r') as fp:
lines = fp.readlines()
line = lines[lineno - 1]
n_spaces = len(line) - len(line.lstrip())
lines.insert(lineno, " " * n_spaces + "self.embed()\n")
alt_file = file_name.replace(".py", "_inserted_embed.py")
with open(alt_file, 'w') as fp:
fp.writelines(lines)
try:
yield alt_file
finally:
os.remove(alt_file)
__config_file__ = "custom_config.yml"
def get_custom_config():
global __config_file__
@@ -193,9 +229,11 @@ def get_custom_config():
def check_temporary_storage(config):
if config["directories"]["temporary_storage"] == "" and sys.platform == "win32":
log.warning("You may be using Windows platform and have not specified the path of"
log.warning(
"You may be using Windows platform and have not specified the path of"
" `temporary_storage`, which may cause OSError. So it is recommended"
" to specify the `temporary_storage` in the config file (.yml)")
" to specify the `temporary_storage` in the config file (.yml)"
)
def get_configuration(args):
@@ -225,8 +263,10 @@ def get_configuration(args):
elif not os.path.exists(__config_file__):
log.info(f"Using the default configuration file, which you can modify in `{global_defaults_file}`")
log.info("If you want to create a local configuration file, you can create a file named"
f" `{__config_file__}`, or run `manimgl --config`")
log.info(
"If you want to create a local configuration file, you can create a file named"
f" `{__config_file__}`, or run `manimgl --config`"
)
custom_config = get_custom_config()
check_temporary_storage(custom_config)
@@ -256,16 +296,23 @@ def get_configuration(args):
"quiet": args.quiet,
}
module = get_module(args.file)
if args.embed is None:
module = get_module(args.file)
else:
with insert_embed_line(args.file, int(args.embed)) as alt_file:
module = get_module(alt_file)
config = {
"module": module,
"scene_names": args.scene_names,
"file_writer_config": file_writer_config,
"quiet": args.quiet or args.write_all,
"write_all": args.write_all,
"skip_animations": args.skip_animations,
"start_at_animation_number": args.start_at_animation_number,
"preview": not write_file,
"end_at_animation_number": None,
"preview": not write_file,
"presenter_mode": args.presenter_mode,
"leave_progress_bars": args.leave_progress_bars,
}
@@ -278,7 +325,7 @@ def get_configuration(args):
mon_index = custom_config["window_monitor"]
monitor = monitors[min(mon_index, len(monitors) - 1)]
window_width = monitor.width
if not args.full_screen:
if not (args.full_screen or custom_config["full_screen"]):
window_width //= 2
window_height = window_width * 9 // 16
config["window_config"] = {
@@ -295,10 +342,6 @@ def get_configuration(args):
else:
config["start_at_animation_number"] = int(stan)
config["skip_animations"] = any([
args.skip_animations,
args.start_at_animation_number,
])
return config

View File

@@ -64,8 +64,6 @@ JOINT_TYPE_MAP = {
}
# Related to Text
START_X = 30
START_Y = 20
NORMAL = "NORMAL"
ITALIC = "ITALIC"
OBLIQUE = "OBLIQUE"

View File

@@ -34,6 +34,7 @@ style:
# the window on the monitor, e.g. "960,540"
window_position: UR
window_monitor: 0
full_screen: False
# If break_into_partial_movies is set to True, then many small
# files will be written corresponding to each Scene.play and
# Scene.wait call, and these files will then be combined

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,6 @@
import inspect
import sys
import copy
from manimlib.scene.scene import Scene
from manimlib.config import get_custom_config
@@ -38,7 +39,7 @@ def prompt_user_for_choice(scene_classes):
"\nScene Name or Number: "
)
return [
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str)-1]
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str) - 1]
for split_str in user_input.replace(" ", "").split(",")
]
except IndexError:
@@ -63,10 +64,31 @@ def get_scene_config(config):
"end_at_animation_number",
"leave_progress_bars",
"preview",
"presenter_mode",
]
])
def compute_total_frames(scene_class, scene_config):
"""
When a scene is being written to file, a copy of the scene is run with
skip_animations set to true so as to count how many frames it will require.
This allows for a total progress bar on rendering, and also allows runtime
errors to be exposed preemptively for long running scenes. The final frame
is saved by default, so that one can more quickly check that the last frame
looks as expected.
"""
pre_config = copy.deepcopy(scene_config)
pre_config["file_writer_config"]["write_to_movie"] = False
pre_config["file_writer_config"]["save_last_frame"] = True
pre_config["file_writer_config"]["quiet"] = True
pre_config["skip_animations"] = True
pre_scene = scene_class(**pre_config)
pre_scene.run()
total_time = pre_scene.time - pre_scene.skip_time
return int(total_time * scene_config["camera_config"]["frame_rate"])
def get_scenes_to_render(scene_classes, scene_config, config):
if config["write_all"]:
return [sc(**scene_config) for sc in scene_classes]
@@ -76,6 +98,9 @@ def get_scenes_to_render(scene_classes, scene_config, config):
found = False
for scene_class in scene_classes:
if scene_class.__name__ == scene_name:
fw_config = scene_config["file_writer_config"]
if fw_config["write_to_movie"]:
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
scene = scene_class(**scene_config)
result.append(scene)
found = True

View File

@@ -6,7 +6,8 @@ __all__ = ["log"]
FORMAT = "%(message)s"
logging.basicConfig(
level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
level=logging.WARNING, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
)
log = logging.getLogger("rich")
log = logging.getLogger("manimgl")
log.setLevel("DEBUG")

View File

@@ -0,0 +1,121 @@
from __future__ import annotations
import numpy as np
import pathops
from manimlib.mobject.types.vectorized_mobject import VMobject
# Boolean operations between 2D mobjects
# Borrowed from from https://github.com/ManimCommunity/manim/
def _convert_vmobject_to_skia_path(vmobject: VMobject) -> pathops.Path:
path = pathops.Path()
subpaths = vmobject.get_subpaths_from_points(vmobject.get_all_points())
for subpath in subpaths:
quads = vmobject.get_bezier_tuples_from_points(subpath)
start = subpath[0]
path.moveTo(*start[:2])
for p0, p1, p2 in quads:
path.quadTo(*p1[:2], *p2[:2])
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
path.close()
return path
def _convert_skia_path_to_vmobject(
path: pathops.Path,
vmobject: VMobject
) -> VMobject:
PathVerb = pathops.PathVerb
current_path_start = np.array([0.0, 0.0, 0.0])
for path_verb, points in path:
if path_verb == PathVerb.CLOSE:
vmobject.add_line_to(current_path_start)
else:
points = np.hstack((np.array(points), np.zeros((len(points), 1))))
if path_verb == PathVerb.MOVE:
for point in points:
current_path_start = point
vmobject.start_new_path(point)
elif path_verb == PathVerb.CUBIC:
vmobject.add_cubic_bezier_curve_to(*points)
elif path_verb == PathVerb.LINE:
vmobject.add_line_to(points[0])
elif path_verb == PathVerb.QUAD:
vmobject.add_quadratic_bezier_curve_to(*points)
else:
raise Exception(f"Unsupported: {path_verb}")
return vmobject.reverse_points()
class Union(VMobject):
def __init__(self, *vmobjects: VMobject, **kwargs):
if len(vmobjects) < 2:
raise ValueError("At least 2 mobjects needed for Union.")
super().__init__(**kwargs)
outpen = pathops.Path()
paths = [
_convert_vmobject_to_skia_path(vmobject)
for vmobject in vmobjects
]
pathops.union(paths, outpen.getPen())
_convert_skia_path_to_vmobject(outpen, self)
class Difference(VMobject):
def __init__(self, subject: VMobject, clip: VMobject, **kwargs):
super().__init__(**kwargs)
outpen = pathops.Path()
pathops.difference(
[_convert_vmobject_to_skia_path(subject)],
[_convert_vmobject_to_skia_path(clip)],
outpen.getPen(),
)
_convert_skia_path_to_vmobject(outpen, self)
class Intersection(VMobject):
def __init__(self, *vmobjects: VMobject, **kwargs):
if len(vmobjects) < 2:
raise ValueError("At least 2 mobjects needed for Intersection.")
super().__init__(**kwargs)
outpen = pathops.Path()
pathops.intersection(
[_convert_vmobject_to_skia_path(vmobjects[0])],
[_convert_vmobject_to_skia_path(vmobjects[1])],
outpen.getPen(),
)
new_outpen = outpen
for _i in range(2, len(vmobjects)):
new_outpen = pathops.Path()
pathops.intersection(
[outpen],
[_convert_vmobject_to_skia_path(vmobjects[_i])],
new_outpen.getPen(),
)
outpen = new_outpen
_convert_skia_path_to_vmobject(outpen, self)
class Exclusion(VMobject):
def __init__(self, *vmobjects: VMobject, **kwargs):
if len(vmobjects) < 2:
raise ValueError("At least 2 mobjects needed for Exclusion.")
super().__init__(**kwargs)
outpen = pathops.Path()
pathops.xor(
[_convert_vmobject_to_skia_path(vmobjects[0])],
[_convert_vmobject_to_skia_path(vmobjects[1])],
outpen.getPen(),
)
new_outpen = outpen
for _i in range(2, len(vmobjects)):
new_outpen = pathops.Path()
pathops.xor(
[outpen],
[_convert_vmobject_to_skia_path(vmobjects[_i])],
new_outpen.getPen(),
)
outpen = new_outpen
_convert_skia_path_to_vmobject(outpen, self)

View File

@@ -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:

View File

@@ -1,5 +1,10 @@
import numpy as np
from __future__ import annotations
import numbers
from abc import abstractmethod
from typing import Type, TypeVar, Union, Callable, Iterable, Sequence
import numpy as np
from manimlib.constants import *
from manimlib.mobject.functions import ParametricCurve
@@ -17,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
@@ -38,54 +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)
def get_axes(self):
@abstractmethod
def get_axes(self) -> VGroup:
raise Exception("Not implemented")
def get_all_ranges(self):
@abstractmethod
def get_all_ranges(self) -> list[np.ndarray]:
raise Exception("Not implemented")
def get_axis(self, index):
def get_axis(self, index: int) -> NumberLine:
return self.get_axes()[index]
def get_x_axis(self):
def get_x_axis(self) -> NumberLine:
return self.get_axis(0)
def get_y_axis(self):
def get_y_axis(self) -> NumberLine:
return self.get_axis(1)
def get_z_axis(self):
def get_z_axis(self) -> NumberLine:
return self.get_axis(2)
def get_x_axis_label(self, label_tex, edge=RIGHT, direction=DL, **kwargs):
def get_x_axis_label(
self,
label_tex: str,
edge: 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,
@@ -94,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
@@ -136,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]),
@@ -145,36 +199,42 @@ class CoordinateSystem():
graph.underlying_function = function
return graph
def input_to_graph_point(self, x, graph):
def input_to_graph_point(
self,
x: float,
graph: ParametricCurve
) -> np.ndarray | None:
if hasattr(graph, "underlying_function"):
return self.coords_to_point(x, graph.underlying_function(x))
else:
alpha = binary_search(
function=lambda a: self.point_to_coords(
graph.point_from_proportion(a)
graph.quick_point_from_proportion(a)
)[0],
target=x,
lower_bound=self.x_range[0],
upper_bound=self.x_range[1],
)
if alpha is not None:
return graph.point_from_proportion(alpha)
return graph.quick_point_from_proportion(alpha)
else:
return None
def i2gp(self, x, graph):
def i2gp(self, x: float, graph: ParametricCurve) -> np.ndarray | None:
"""
Alias for input_to_graph_point
"""
return self.input_to_graph_point(x, graph)
def get_graph_label(self,
graph,
label="f(x)",
x=None,
direction=RIGHT,
buff=MED_SMALL_BUFF,
color=None):
def get_graph_label(
self,
graph: ParametricCurve,
label: str | Mobject = "f(x)",
x: float | None = None,
direction: np.ndarray = RIGHT,
buff: float = MED_SMALL_BUFF,
color: ManimColor | None = None
) -> Tex | Mobject:
if isinstance(label, str):
label = Tex(label)
if color is None:
@@ -201,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:
@@ -254,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)
@@ -263,6 +343,7 @@ class CoordinateSystem():
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_opacity=fill_opacity,
stroke_background=stroke_background
)
return result
@@ -274,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": {},
@@ -285,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)
@@ -311,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]):
@@ -364,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
@@ -387,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]
@@ -417,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
@@ -439,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()
@@ -449,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())
@@ -468,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:
@@ -496,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)

View File

@@ -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

View File

@@ -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,12 +337,15 @@ class Circle(Arc):
self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0)
self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1)
def point_at_angle(self, angle):
def point_at_angle(self, angle: float) -> np.ndarray:
start_angle = self.get_start_angle()
return self.point_from_proportion(
(angle - start_angle) / TAU
)
def get_radius(self) -> float:
return get_norm(self.get_start() - self.get_center())
class Dot(Circle):
CONFIG = {
@@ -314,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)
@@ -398,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):
@@ -435,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)
@@ -451,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.
@@ -468,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
@@ -476,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
"""
@@ -493,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(
@@ -505,8 +561,9 @@ 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
class DashedLine(Line):
@@ -528,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]
@@ -566,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)
@@ -590,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,
@@ -599,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):
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:
@@ -651,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()
@@ -675,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)
@@ -744,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
@@ -761,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
@@ -777,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)
@@ -790,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):
@@ -845,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
@@ -889,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())
@@ -914,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:
@@ -927,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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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
)

View File

@@ -1,3 +1,7 @@
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
@@ -38,7 +42,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 +52,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 +75,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 +92,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 +101,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)
def number_to_point(self, number: float | np.ndarray) -> np.ndarray:
alpha = (number - self.x_min) / (self.x_max - self.x_min)
return 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 +119,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 +156,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()

View File

@@ -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()))

View File

@@ -1,3 +1,8 @@
from __future__ import annotations
from typing import Iterable, Union, Sequence
import colour
from manimlib.constants import *
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
@@ -9,6 +14,8 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.color import color_gradient
from manimlib.utils.iterables import listify
ManimColor = Union[str, colour.Color, Sequence[float]]
EPSILON = 0.0001
@@ -24,7 +31,11 @@ class SampleSpace(Rectangle):
"default_label_scale_val": 1,
}
def add_title(self, title="Sample space", buff=MED_SMALL_BUFF):
def add_title(
self,
title: str = "Sample space",
buff: float = MED_SMALL_BUFF
) -> None:
# TODO, should this really exist in SampleSpaceScene
title_mob = TexText(title)
if title_mob.get_width() > self.get_width():
@@ -33,17 +44,23 @@ class SampleSpace(Rectangle):
self.title = title_mob
self.add(title_mob)
def add_label(self, label):
def add_label(self, label: str) -> None:
self.label = label
def complete_p_list(self, p_list):
def complete_p_list(self, p_list: list[float]) -> list[float]:
new_p_list = listify(p_list)
remainder = 1.0 - sum(new_p_list)
if abs(remainder) > EPSILON:
new_p_list.append(remainder)
return new_p_list
def get_division_along_dimension(self, p_list, dim, colors, vect):
def get_division_along_dimension(
self,
p_list: list[float],
dim: int,
colors: Iterable[ManimColor],
vect: np.ndarray
) -> VGroup:
p_list = self.complete_p_list(p_list)
colors = color_gradient(colors, len(p_list))
@@ -60,38 +77,41 @@ class SampleSpace(Rectangle):
return parts
def get_horizontal_division(
self, p_list,
colors=[GREEN_E, BLUE_E],
vect=DOWN
):
self,
p_list: list[float],
colors: Iterable[ManimColor] = [GREEN_E, BLUE_E],
vect: np.ndarray = DOWN
) -> VGroup:
return self.get_division_along_dimension(p_list, 1, colors, vect)
def get_vertical_division(
self, p_list,
colors=[MAROON_B, YELLOW],
vect=RIGHT
):
self,
p_list: list[float],
colors: Iterable[ManimColor] = [MAROON_B, YELLOW],
vect: np.ndarray = RIGHT
) -> VGroup:
return self.get_division_along_dimension(p_list, 0, colors, vect)
def divide_horizontally(self, *args, **kwargs):
def divide_horizontally(self, *args, **kwargs) -> None:
self.horizontal_parts = self.get_horizontal_division(*args, **kwargs)
self.add(self.horizontal_parts)
def divide_vertically(self, *args, **kwargs):
def divide_vertically(self, *args, **kwargs) -> None:
self.vertical_parts = self.get_vertical_division(*args, **kwargs)
self.add(self.vertical_parts)
def get_subdivision_braces_and_labels(
self, parts, labels, direction,
buff=SMALL_BUFF,
min_num_quads=1
):
self,
parts: VGroup,
labels: str,
direction: np.ndarray,
buff: float = SMALL_BUFF,
) -> VGroup:
label_mobs = VGroup()
braces = VGroup()
for label, part in zip(labels, parts):
brace = Brace(
part, direction,
min_num_quads=min_num_quads,
buff=buff
)
if isinstance(label, Mobject):
@@ -112,22 +132,35 @@ class SampleSpace(Rectangle):
}
return VGroup(parts.braces, parts.labels)
def get_side_braces_and_labels(self, labels, direction=LEFT, **kwargs):
def get_side_braces_and_labels(
self,
labels: str,
direction: np.ndarray = LEFT,
**kwargs
) -> VGroup:
assert(hasattr(self, "horizontal_parts"))
parts = self.horizontal_parts
return self.get_subdivision_braces_and_labels(parts, labels, direction, **kwargs)
def get_top_braces_and_labels(self, labels, **kwargs):
def get_top_braces_and_labels(
self,
labels: str,
**kwargs
) -> VGroup:
assert(hasattr(self, "vertical_parts"))
parts = self.vertical_parts
return self.get_subdivision_braces_and_labels(parts, labels, UP, **kwargs)
def get_bottom_braces_and_labels(self, labels, **kwargs):
def get_bottom_braces_and_labels(
self,
labels: str,
**kwargs
) -> VGroup:
assert(hasattr(self, "vertical_parts"))
parts = self.vertical_parts
return self.get_subdivision_braces_and_labels(parts, labels, DOWN, **kwargs)
def add_braces_and_labels(self):
def add_braces_and_labels(self) -> None:
for attr in "horizontal_parts", "vertical_parts":
if not hasattr(self, attr):
continue
@@ -136,7 +169,7 @@ class SampleSpace(Rectangle):
if hasattr(parts, subattr):
self.add(getattr(parts, subattr))
def __getitem__(self, index):
def __getitem__(self, index: int | slice) -> VGroup:
if hasattr(self, "horizontal_parts"):
return self.horizontal_parts[index]
elif hasattr(self, "vertical_parts"):
@@ -149,7 +182,9 @@ class BarChart(VGroup):
"height": 4,
"width": 6,
"n_ticks": 4,
"include_x_ticks": False,
"tick_width": 0.2,
"tick_height": 0.15,
"label_y_axis": True,
"y_axis_label_height": 0.25,
"max_value": 1,
@@ -160,43 +195,55 @@ class BarChart(VGroup):
"bar_label_scale_val": 0.75,
}
def __init__(self, values, **kwargs):
def __init__(self, values: Iterable[float], **kwargs):
VGroup.__init__(self, **kwargs)
if self.max_value is None:
self.max_value = max(values)
self.n_ticks_x = len(values)
self.add_axes()
self.add_bars(values)
self.center()
def add_axes(self):
def add_axes(self) -> None:
x_axis = Line(self.tick_width * LEFT / 2, self.width * RIGHT)
y_axis = Line(MED_LARGE_BUFF * DOWN, self.height * UP)
ticks = VGroup()
y_ticks = VGroup()
heights = np.linspace(0, self.height, self.n_ticks + 1)
values = np.linspace(0, self.max_value, self.n_ticks + 1)
for y, value in zip(heights, values):
tick = Line(LEFT, RIGHT)
tick.set_width(self.tick_width)
tick.move_to(y * UP)
ticks.add(tick)
y_axis.add(ticks)
y_tick = Line(LEFT, RIGHT)
y_tick.set_width(self.tick_width)
y_tick.move_to(y * UP)
y_ticks.add(y_tick)
y_axis.add(y_ticks)
if self.include_x_ticks == True:
x_ticks = VGroup()
widths = np.linspace(0, self.width, self.n_ticks_x + 1)
label_values = np.linspace(0, len(self.bar_names), self.n_ticks_x + 1)
for x, value in zip(widths, label_values):
x_tick = Line(UP, DOWN)
x_tick.set_height(self.tick_height)
x_tick.move_to(x * RIGHT)
x_ticks.add(x_tick)
x_axis.add(x_ticks)
self.add(x_axis, y_axis)
self.x_axis, self.y_axis = x_axis, y_axis
if self.label_y_axis:
labels = VGroup()
for tick, value in zip(ticks, values):
for y_tick, value in zip(y_ticks, values):
label = Tex(str(np.round(value, 2)))
label.set_height(self.y_axis_label_height)
label.next_to(tick, LEFT, SMALL_BUFF)
label.next_to(y_tick, LEFT, SMALL_BUFF)
labels.add(label)
self.y_axis_labels = labels
self.add(labels)
def add_bars(self, values):
buff = float(self.width) / (2 * len(values) + 1)
def add_bars(self, values: Iterable[float]) -> None:
buff = float(self.width) / (2 * len(values))
bars = VGroup()
for i, value in enumerate(values):
bar = Rectangle(
@@ -205,7 +252,7 @@ class BarChart(VGroup):
stroke_width=self.bar_stroke_width,
fill_opacity=self.bar_fill_opacity,
)
bar.move_to((2 * i + 1) * buff * RIGHT, DOWN + LEFT)
bar.move_to((2 * i + 0.5) * buff * RIGHT, DOWN + LEFT * 5)
bars.add(bar)
bars.set_color_by_gradient(*self.bar_colors)
@@ -220,7 +267,7 @@ class BarChart(VGroup):
self.bars = bars
self.bar_labels = bar_labels
def change_bar_values(self, values):
def change_bar_values(self, values: Iterable[float]) -> None:
for bar, value in zip(self.bars, values):
bar_bottom = bar.get_bottom()
bar.stretch_to_fit_height(

View File

@@ -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)

View File

@@ -1,10 +1,15 @@
import numpy as np
import math
from __future__ import annotations
import math
import copy
from typing import Iterable
import numpy as np
from manimlib.animation.composition import AnimationGroup
from manimlib.constants import *
from manimlib.animation.fading import FadeIn
from manimlib.animation.growing import GrowFromCenter
from manimlib.animation.composition import AnimationGroup
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import SingleStringTex
from manimlib.mobject.svg.tex_mobject import TexText
@@ -13,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 = {
@@ -20,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)
@@ -35,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)]:
@@ -48,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(),
@@ -62,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)
@@ -88,29 +108,40 @@ class BraceLabel(VMobject):
CONFIG = {
"label_constructor": Tex,
"label_scale": 1,
"label_buff": DEFAULT_MOBJECT_TO_MOBJECT_BUFFER
}
def __init__(self, obj, text, brace_direction=DOWN, **kwargs):
def __init__(
self,
obj: list[VMobject] | Mobject,
text: Iterable[str] | str,
brace_direction: np.ndarray = DOWN,
**kwargs
) -> None:
VMobject.__init__(self, **kwargs)
self.brace_direction = brace_direction
if isinstance(obj, list):
obj = VMobject(*obj)
self.brace = Brace(obj, brace_direction, **kwargs)
if isinstance(text, tuple) or isinstance(text, list):
if isinstance(text, Iterable):
self.label = self.label_constructor(*text, **kwargs)
else:
self.label = self.label_constructor(str(text))
if self.label_scale != 1:
self.label.scale(self.label_scale)
self.brace.put_at_tip(self.label)
self.brace.put_at_tip(self.label, buff=self.label_buff)
self.set_submobjects([self.brace, self.label])
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):
def creation_anim(
self,
label_anim: Animation = FadeIn,
brace_anim: Animation=GrowFromCenter
) -> AnimationGroup:
return AnimationGroup(brace_anim(self.brace), label_anim(self.label))
def shift_brace(self, obj, **kwargs):
def shift_brace(self, obj: list[VMobject] | Mobject, **kwargs):
if isinstance(obj, list):
obj = VMobject(*obj)
self.brace = Brace(obj, self.brace_direction, **kwargs)
@@ -118,7 +149,7 @@ class BraceLabel(VMobject):
self.submobjects[0] = self.brace
return self
def change_label(self, *text, **kwargs):
def change_label(self, *text: str, **kwargs):
self.label = self.label_constructor(*text, **kwargs)
if self.label_scale != 1:
self.label.scale(self.label_scale)
@@ -127,7 +158,7 @@ class BraceLabel(VMobject):
self.submobjects[1] = self.label
return self
def change_brace_label(self, obj, *text):
def change_brace_label(self, obj: list[VMobject] | Mobject, *text: str):
self.shift_brace(obj)
self.change_label(*text)
return self

View File

@@ -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):
@@ -50,6 +53,7 @@ class Lightbulb(SVGMobject):
def __init__(self, **kwargs):
super().__init__("lightbulb", **kwargs)
self.insert_n_curves(25)
class Speedometer(VMobject):
@@ -200,12 +204,11 @@ class Laptop(VGroup):
class VideoIcon(SVGMobject):
CONFIG = {
"file_name": "video_icon",
"width": FRAME_WIDTH / 12.,
}
def __init__(self, **kwargs):
SVGMobject.__init__(self, **kwargs)
super().__init__(file_name="video_icon", **kwargs)
self.center()
self.set_width(self.width)
self.set_stroke(color=WHITE, width=0)
@@ -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)

View File

@@ -0,0 +1,540 @@
from __future__ import annotations
import re
import colour
import itertools as it
from typing import Iterable, Union, Sequence
from abc import abstractmethod
from manimlib.constants import WHITE
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.color import color_to_int_rgb
from manimlib.utils.config_ops import digest_config
from manimlib.utils.iterables import 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):
"""
An abstract base class for `MTex` and `MarkupText`
"""
CONFIG = {
"base_color": None,
"use_plain_file": False,
"isolate": [],
}
def __init__(self, string: str, **kwargs):
self.string = string
reserved_svg_default = kwargs.pop("svg_default", {})
digest_config(self, kwargs)
self.reserved_svg_default = reserved_svg_default
self.base_color = self.base_color \
or reserved_svg_default.get("color", None) \
or reserved_svg_default.get("fill_color", None) \
or WHITE
self.pre_parse()
self.parse()
super().__init__(**kwargs)
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_decorated_string(use_plain_file=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 any([
self.use_plain_file,
self.reserved_svg_default,
self.has_predefined_colors
]):
file_path = self.get_file_path_(use_plain_file=True)
plain_svg = _StringSVG(
file_path,
svg_default=self.reserved_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.ignored_spans = self.get_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()
# 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:
return re.compile(pattern, flags).search(self.string, **kwargs)
def match(self, pattern: str, flags: int = 0, **kwargs) -> re.Match:
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 get_span_replacement_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 color_to_label(color: ManimColor) -> int:
rgb_tuple = color_to_int_rgb(color)
rgb = LabelledString.rgb_to_int(rgb_tuple)
if rgb == 16777215: # white
return -1
return rgb
@abstractmethod
def get_begin_color_command_str(int_rgb: int) -> str:
return ""
@abstractmethod
def get_end_color_command_str() -> str:
return ""
# 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]
def get_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.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 = [
self.full_span,
*self.internal_specified_spans,
*self.external_specified_spans,
*self.find_substrs(self.isolate)
]
shrinked_spans = list(filter(
lambda span: span[0] < span[1],
[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_inserted_string_pairs(
self, use_plain_file: bool
) -> list[tuple[Span, tuple[str, str]]]:
return []
@abstractmethod
def get_other_repl_items(
self, use_plain_file: bool
) -> list[tuple[Span, str]]:
return []
def get_decorated_string(self, use_plain_file: bool) -> str:
span_repl_dict = self.get_span_replacement_dict(
self.get_inserted_string_pairs(use_plain_file),
self.get_other_repl_items(use_plain_file)
)
result = self.get_replaced_substr(self.full_span, span_repl_dict)
if not use_plain_file:
return result
return "".join([
self.get_begin_color_command_str(
self.rgb_to_int(color_to_int_rgb(self.base_color))
),
result,
self.get_end_color_command_str()
])
@abstractmethod
def has_predefined_colors(self) -> bool:
return False
# Post-parsing
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_span_items(self) -> tuple[list[int], list[Span]]:
submob_labels = [submob.label for submob in self.submobjects]
if not submob_labels:
return [], []
return tuple(zip(*self.compress_neighbours(submob_labels)))
def get_group_substrs(self) -> list[str]:
group_labels, _ = self.get_group_span_items()
if not group_labels:
return []
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])
)
]
return [
self.get_cleaned_substr(span) if span[0] < span[1] else ""
for span in shrinked_spans
]
def get_submob_groups(self) -> VGroup:
_, submob_spans = self.get_group_span_items()
return VGroup(*[
VGroup(*self.submobjects[slice(*submob_span)])
for submob_span in submob_spans
])
# 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_parts_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(*filter(
lambda submob: submob.label in labels,
self.submobjects
))
def get_parts_by_string(
self, substr: str, case_sensitive: bool = True, **kwargs
) -> VGroup:
flags = 0
if not case_sensitive:
flags |= re.I
return VGroup(*[
self.get_parts_by_custom_span(span, **kwargs)
for span in self.find_substr(substr, flags=flags)
])
def get_parts_by_group_substr(self, substr: str) -> VGroup:
return VGroup(*[
group
for group, group_substr in zip(
self.get_submob_groups(), self.get_group_substrs()
)
if group_substr == substr
])
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 indices_of_part(self, part: Iterable[VMobject]) -> list[int]:
return [self.submobjects.index(submob) for submob in part]
def indices_lists_of_parts(
self, parts: Iterable[Iterable[VMobject]]
) -> list[list[int]]:
return [self.indices_of_part(part) for part in parts]
def get_string(self) -> str:
return self.string

View File

@@ -0,0 +1,340 @@
from __future__ import annotations
import colour
from typing import Union, Sequence
from manimlib.mobject.svg.labelled_string import LabelledString
from manimlib.utils.tex_file_writing import tex_to_svg_file
from manimlib.utils.tex_file_writing import get_tex_config
from manimlib.utils.tex_file_writing import display_during_execution
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
ManimColor = Union[str, colour.Color, Sequence[float]]
Span = tuple[int, int]
SCALE_FACTOR_PER_FONT_POINT = 0.001
class MTex(LabelledString):
CONFIG = {
"font_size": 48,
"alignment": "\\centering",
"tex_environment": "align*",
"tex_to_color_map": {},
}
def __init__(self, tex_string: str, **kwargs):
# Prevent from passing an empty string.
if not tex_string:
tex_string = "\\quad"
self.tex_string = tex_string
super().__init__(tex_string, **kwargs)
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
@property
def hash_seed(self) -> tuple:
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.base_color,
self.use_plain_file,
self.isolate,
self.tex_string,
self.alignment,
self.tex_environment,
self.tex_to_color_map
)
def get_file_path_by_content(self, content: str) -> str:
full_tex = self.get_tex_file_body(content)
with display_during_execution(f"Writing \"{self.tex_string}\""):
file_path = self.tex_to_svg_file_path(full_tex)
return file_path
def get_tex_file_body(self, content: str) -> str:
if self.tex_environment:
content = "\n".join([
f"\\begin{{{self.tex_environment}}}",
content,
f"\\end{{{self.tex_environment}}}"
])
if self.alignment:
content = "\n".join([self.alignment, content])
tex_config = get_tex_config()
return tex_config["tex_body"].replace(
tex_config["text_to_replace"],
content
)
@staticmethod
def tex_to_svg_file_path(tex_file_content: str) -> str:
return tex_to_svg_file(tex_file_content)
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_begin_color_command_str(rgb_int: int) -> str:
rgb_tuple = MTex.int_to_rgb(rgb_int)
return "".join([
"{{",
"\\color[RGB]",
"{",
",".join(map(str, rgb_tuple)),
"}"
])
@staticmethod
def get_end_color_command_str() -> str:
return "}}"
# Pre-parsing
def get_backslash_indices(self) -> list[int]:
# Newlines (`\\`) don't count.
return [
span[1] - 1
for span in self.find_spans(r"\\+")
if (span[1] - span[0]) % 2 == 1
]
def get_unescaped_char_spans(self, chars: str):
return sorted(filter(
lambda span: span[0] - 1 not in self.backslash_indices,
self.find_substrs(list(chars))
))
def get_brace_index_pairs(self) -> list[Span]:
left_brace_indices = []
right_brace_indices = []
left_brace_indices_stack = []
for span in self.get_unescaped_char_spans("{}"):
index = span[0]
if self.get_substr(span) == "{":
left_brace_indices_stack.append(index)
else:
if not left_brace_indices_stack:
raise ValueError("Missing '{' inserted")
left_brace_index = left_brace_indices_stack.pop()
left_brace_indices.append(left_brace_index)
right_brace_indices.append(index)
if left_brace_indices_stack:
raise ValueError("Missing '}' inserted")
return list(zip(left_brace_indices, right_brace_indices))
def get_script_char_spans(self) -> list[int]:
return self.get_unescaped_char_spans("_^")
def get_script_content_spans(self) -> list[Span]:
result = []
brace_indices_dict = dict(self.brace_index_pairs)
script_pattern = r"[a-zA-Z0-9]|\\[a-zA-Z]+"
for script_char_span in self.script_char_spans:
span_begin = self.match(r"\s*", pos=script_char_span[1]).end()
if span_begin in brace_indices_dict.keys():
span_end = brace_indices_dict[span_begin] + 1
else:
match_obj = self.match(script_pattern, pos=span_begin)
if not match_obj:
script_name = {
"_": "subscript",
"^": "superscript"
}[script_char]
raise ValueError(
f"Unclear {script_name} detected while parsing. "
"Please use braces to clarify"
)
span_end = match_obj.end()
result.append((span_begin, span_end))
return result
def get_script_spans(self) -> list[Span]:
return [
(
self.search(r"\s*$", endpos=script_char_span[0]).start(),
script_content_span[1]
)
for script_char_span, script_content_span in zip(
self.script_char_spans, self.script_content_spans
)
]
# Parsing
def get_command_repl_items(self) -> list[tuple[Span, str]]:
color_related_command_dict = {
"color": (1, False),
"textcolor": (1, False),
"pagecolor": (1, True),
"colorbox": (1, True),
"fcolorbox": (2, True),
}
result = []
backslash_indices = self.backslash_indices
right_brace_indices = [
right_index
for left_index, right_index in self.brace_index_pairs
]
pattern = "".join([
r"\\",
"(",
"|".join(color_related_command_dict.keys()),
")",
r"(?![a-zA-Z])"
])
for match_obj in self.finditer(pattern):
span_begin, cmd_end = match_obj.span()
if span_begin not in backslash_indices:
continue
cmd_name = match_obj.group(1)
n_braces, substitute_cmd = color_related_command_dict[cmd_name]
span_end = self.take_nearest_value(
right_brace_indices, cmd_end, n_braces
) + 1
if substitute_cmd:
repl_str = "\\" + cmd_name + n_braces * "{white}"
else:
repl_str = ""
result.append(((span_begin, span_end), repl_str))
return result
def get_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_inserted_string_pairs(
self, use_plain_file: bool
) -> list[tuple[Span, tuple[str, str]]]:
if use_plain_file:
return []
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
]
return [
(span, (
self.get_begin_color_command_str(label),
self.get_end_color_command_str()
))
for label, span in enumerate(extended_label_span_list)
]
def get_other_repl_items(
self, use_plain_file: bool
) -> list[tuple[Span, str]]:
if use_plain_file:
return []
return self.command_repl_items.copy()
@property
def has_predefined_colors(self) -> bool:
return bool(self.command_repl_items)
# Post-parsing
def get_cleaned_substr(self, span: Span) -> str:
substr = super().get_cleaned_substr(span)
if not self.brace_index_pairs:
return substr
# Balance braces.
left_brace_indices, right_brace_indices = zip(*self.brace_index_pairs)
unclosed_left_braces = 0
unclosed_right_braces = 0
for index in range(*span):
if index in left_brace_indices:
unclosed_left_braces += 1
elif index in right_brace_indices:
if unclosed_left_braces == 0:
unclosed_right_braces += 1
else:
unclosed_left_braces -= 1
return "".join([
unclosed_right_braces * "{",
substr,
unclosed_left_braces * "}"
])
# Method alias
def get_parts_by_tex(self, tex: str, **kwargs) -> VGroup:
return self.get_parts_by_string(tex, **kwargs)
def get_part_by_tex(self, tex: str, **kwargs) -> VMobject:
return self.get_part_by_string(tex, **kwargs)
def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
return self.set_color_by_string(tex, color, **kwargs)
def set_color_by_tex_to_color_map(
self, tex_to_color_map: dict[str, ManimColor], **kwargs
):
return self.set_color_by_string_to_color_map(
tex_to_color_map, **kwargs
)
def get_tex(self) -> str:
return self.get_string()
class MTexText(MTex):
CONFIG = {
"tex_environment": None,
}

View File

@@ -1,38 +1,34 @@
import itertools as it
import re
import string
import warnings
from __future__ import annotations
import os
import hashlib
import itertools as it
from typing import Callable
from xml.etree import ElementTree as ET
from xml.dom import minidom
from manimlib.constants import DEFAULT_STROKE_WIDTH
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT
from manimlib.constants import BLACK
from manimlib.constants import WHITE
from manimlib.constants import DEGREES, PI
import svgelements as se
import numpy as np
from manimlib.constants import RIGHT
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Polygon
from manimlib.mobject.geometry import Polyline
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import RoundedRectangle
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.color import *
from manimlib.utils.config_ops import digest_config
from manimlib.utils.directories import get_mobject_data_dir
from manimlib.utils.images import get_full_vector_image_path
from manimlib.utils.simple_functions import clip
from manimlib.utils.iterables import hash_obj
from manimlib.logger import log
def string_to_numbers(num_string):
num_string = num_string.replace("-", ",-")
num_string = num_string.replace("e,-", "e-")
return [
float(s)
for s in re.split("[ ,]", num_string)
if s != ""
]
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
return np.array([x, y, 0.0])
class SVGMobject(VMobject):
@@ -40,26 +36,249 @@ class SVGMobject(VMobject):
"should_center": True,
"height": 2,
"width": None,
# Must be filled in in a subclass, or when called
"file_name": None,
"unpack_groups": True, # if False, creates a hierarchy of VGroups
# TODO, style components should be read in, not defaulted
"stroke_width": DEFAULT_STROKE_WIDTH,
"fill_opacity": 1.0,
"path_string_config": {}
# Style that overrides the original svg
"color": None,
"opacity": None,
"fill_color": None,
"fill_opacity": None,
"stroke_width": None,
"stroke_color": None,
"stroke_opacity": None,
# Style that fills only when not specified
# If None, regarded as default values from svg standard
"svg_default": {
"color": None,
"opacity": None,
"fill_color": None,
"fill_opacity": None,
"stroke_width": None,
"stroke_color": None,
"stroke_opacity": None,
},
"path_string_config": {},
}
def __init__(self, file_name=None, **kwargs):
digest_config(self, kwargs)
self.file_name = file_name or self.file_name
if file_name is None:
raise Exception("Must specify file for SVGMobject")
self.file_path = get_full_vector_image_path(file_name)
def __init__(self, file_name: str | None = None, **kwargs):
super().__init__(**kwargs)
self.file_name = file_name or self.file_name
self.init_svg_mobject()
self.init_colors()
self.move_into_position()
def move_into_position(self):
def init_svg_mobject(self) -> None:
hash_val = hash_obj(self.hash_seed)
if hash_val in SVG_HASH_TO_MOB_MAP:
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
self.add(*mob)
return
self.generate_mobject()
SVG_HASH_TO_MOB_MAP[hash_val] = self.copy()
@property
def hash_seed(self) -> tuple:
# Returns data which can uniquely represent the result of `init_points`.
# The hashed value of it is stored as a key in `SVG_HASH_TO_MOB_MAP`.
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.file_name
)
def generate_mobject(self) -> None:
file_path = self.get_file_path()
element_tree = ET.parse(file_path)
new_tree = self.modify_xml_tree(element_tree)
# Create a temporary svg file to dump modified svg to be parsed
root, ext = os.path.splitext(file_path)
modified_file_path = root + "_" + ext
new_tree.write(modified_file_path)
svg = se.SVG.parse(modified_file_path)
os.remove(modified_file_path)
mobjects = self.get_mobjects_from(svg)
self.add(*mobjects)
self.flip(RIGHT) # Flip y
def get_file_path(self) -> str:
if self.file_name is None:
raise Exception("Must specify file for SVGMobject")
return get_full_vector_image_path(self.file_name)
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
config_style_dict = self.generate_config_style_dict()
style_keys = (
"fill",
"fill-opacity",
"stroke",
"stroke-opacity",
"stroke-width",
"style"
)
root = element_tree.getroot()
root_style_dict = {
k: v for k, v in root.attrib.items()
if k in style_keys
}
new_root = ET.Element("svg", {})
config_style_node = ET.SubElement(new_root, "g", config_style_dict)
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
root_style_node.extend(root)
return ET.ElementTree(new_root)
def generate_config_style_dict(self) -> dict[str, str]:
keys_converting_dict = {
"fill": ("color", "fill_color"),
"fill-opacity": ("opacity", "fill_opacity"),
"stroke": ("color", "stroke_color"),
"stroke-opacity": ("opacity", "stroke_opacity"),
"stroke-width": ("stroke_width",)
}
svg_default_dict = self.svg_default
result = {}
for svg_key, style_keys in keys_converting_dict.items():
for style_key in style_keys:
if svg_default_dict[style_key] is None:
continue
result[svg_key] = str(svg_default_dict[style_key])
return result
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
result = []
for shape in svg.elements():
if isinstance(shape, se.Group):
continue
elif isinstance(shape, se.Path):
mob = self.path_to_mobject(shape)
elif isinstance(shape, se.SimpleLine):
mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape)
elif isinstance(shape, se.Circle):
mob = self.circle_to_mobject(shape)
elif isinstance(shape, se.Ellipse):
mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape)
elif isinstance(shape, se.Polyline):
mob = self.polyline_to_mobject(shape)
# elif isinstance(shape, se.Text):
# mob = self.text_to_mobject(shape)
elif type(shape) == se.SVGElement:
continue
else:
log.warning(f"Unsupported element type: {type(shape)}")
continue
if not mob.has_points():
continue
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.Transformable) and shape.apply:
self.handle_transform(mob, shape.transform)
result.append(mob)
return result
@staticmethod
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:
mat = np.array([
[matrix.a, matrix.c],
[matrix.b, matrix.d]
])
vec = np.array([matrix.e, matrix.f, 0.0])
mob.apply_matrix(mat)
mob.shift(vec)
return mob
@staticmethod
def apply_style_to_mobject(
mob: VMobject,
shape: se.GraphicObject
) -> VMobject:
mob.set_style(
stroke_width=shape.stroke_width,
stroke_color=shape.stroke.hex,
stroke_opacity=shape.stroke.opacity,
fill_color=shape.fill.hex,
fill_opacity=shape.fill.opacity
)
return mob
@staticmethod
def handle_transform(mob, matrix):
mat = np.array([
[matrix.a, matrix.c],
[matrix.b, matrix.d]
])
vec = np.array([matrix.e, matrix.f, 0.0])
mob.apply_matrix(mat)
mob.shift(vec)
return mob
def path_to_mobject(self, path: se.Path) -> VMobjectFromSVGPath:
return VMobjectFromSVGPath(path, **self.path_string_config)
def line_to_mobject(self, line: se.Line) -> Line:
return Line(
start=_convert_point_to_3d(line.x1, line.y1),
end=_convert_point_to_3d(line.x2, line.y2)
)
def rect_to_mobject(self, rect: se.Rect) -> Rectangle:
if rect.rx == 0 or rect.ry == 0:
mob = Rectangle(
width=rect.width,
height=rect.height,
)
else:
mob = RoundedRectangle(
width=rect.width,
height=rect.height * rect.rx / rect.ry,
corner_radius=rect.rx
)
mob.stretch_to_fit_height(rect.height)
mob.shift(_convert_point_to_3d(
rect.x + rect.width / 2,
rect.y + rect.height / 2
))
return mob
def circle_to_mobject(self, circle: se.Circle) -> Circle:
# svgelements supports `rx` & `ry` but `r`
mob = Circle(radius=circle.rx)
mob.shift(_convert_point_to_3d(
circle.cx, circle.cy
))
return mob
def ellipse_to_mobject(self, ellipse: se.Ellipse) -> Circle:
mob = Circle(radius=ellipse.rx)
mob.stretch_to_fit_height(2 * ellipse.ry)
mob.shift(_convert_point_to_3d(
ellipse.cx, ellipse.cy
))
return mob
def polygon_to_mobject(self, polygon: se.Polygon) -> Polygon:
points = [
_convert_point_to_3d(*point)
for point in polygon
]
return Polygon(*points)
def polyline_to_mobject(self, polyline: se.Polyline) -> Polyline:
points = [
_convert_point_to_3d(*point)
for point in polyline
]
return Polyline(*points)
def text_to_mobject(self, text: se.Text):
pass
def move_into_position(self) -> None:
if self.should_center:
self.center()
if self.height is not None:
@@ -67,275 +286,26 @@ class SVGMobject(VMobject):
if self.width is not None:
self.set_width(self.width)
def init_points(self):
doc = minidom.parse(self.file_path)
self.ref_to_element = {}
for svg in doc.getElementsByTagName("svg"):
mobjects = self.get_mobjects_from(svg)
if self.unpack_groups:
self.add(*mobjects)
else:
self.add(*mobjects[0].submobjects)
doc.unlink()
def get_mobjects_from(self, element):
result = []
if not isinstance(element, minidom.Element):
return result
if element.tagName == 'defs':
self.update_ref_to_element(element)
elif element.tagName == 'style':
pass # TODO, handle style
elif element.tagName in ['g', 'svg', 'symbol']:
result += it.chain(*(
self.get_mobjects_from(child)
for child in element.childNodes
))
elif element.tagName == 'path':
result.append(self.path_string_to_mobject(
element.getAttribute('d')
))
elif element.tagName == 'use':
result += self.use_to_mobjects(element)
elif element.tagName == 'rect':
result.append(self.rect_to_mobject(element))
elif element.tagName == 'circle':
result.append(self.circle_to_mobject(element))
elif element.tagName == 'ellipse':
result.append(self.ellipse_to_mobject(element))
elif element.tagName in ['polygon', 'polyline']:
result.append(self.polygon_to_mobject(element))
else:
pass # TODO
# warnings.warn("Unknown element type: " + element.tagName)
result = [m for m in result if m is not None]
self.handle_transforms(element, VGroup(*result))
if len(result) > 1 and not self.unpack_groups:
result = [VGroup(*result)]
return result
def g_to_mobjects(self, g_element):
mob = VGroup(*self.get_mobjects_from(g_element))
self.handle_transforms(g_element, mob)
return mob.submobjects
def path_string_to_mobject(self, path_string):
return VMobjectFromSVGPathstring(
path_string,
**self.path_string_config,
)
def use_to_mobjects(self, use_element):
# Remove initial "#" character
ref = use_element.getAttribute("xlink:href")[1:]
if ref not in self.ref_to_element:
warnings.warn(f"{ref} not recognized")
return VGroup()
return self.get_mobjects_from(
self.ref_to_element[ref]
)
def attribute_to_float(self, attr):
stripped_attr = "".join([
char for char in attr
if char in string.digits + "." + "-"
])
return float(stripped_attr)
def polygon_to_mobject(self, polygon_element):
path_string = polygon_element.getAttribute("points")
for digit in string.digits:
path_string = path_string.replace(f" {digit}", f"L {digit}")
path_string = path_string.replace("L", "M", 1)
return self.path_string_to_mobject(path_string)
def circle_to_mobject(self, circle_element):
x, y, r = [
self.attribute_to_float(
circle_element.getAttribute(key)
)
if circle_element.hasAttribute(key)
else 0.0
for key in ("cx", "cy", "r")
]
return Circle(radius=r).shift(x * RIGHT + y * DOWN)
def ellipse_to_mobject(self, circle_element):
x, y, rx, ry = [
self.attribute_to_float(
circle_element.getAttribute(key)
)
if circle_element.hasAttribute(key)
else 0.0
for key in ("cx", "cy", "rx", "ry")
]
result = Circle()
result.stretch(rx, 0)
result.stretch(ry, 1)
result.shift(x * RIGHT + y * DOWN)
return result
def rect_to_mobject(self, rect_element):
fill_color = rect_element.getAttribute("fill")
stroke_color = rect_element.getAttribute("stroke")
stroke_width = rect_element.getAttribute("stroke-width")
corner_radius = rect_element.getAttribute("rx")
# input preprocessing
fill_opacity = 1
if fill_color in ["", "none", "#FFF", "#FFFFFF"] or Color(fill_color) == Color(WHITE):
fill_opacity = 0
fill_color = BLACK # shdn't be necessary but avoids error msgs
if fill_color in ["#000", "#000000"]:
fill_color = WHITE
if stroke_color in ["", "none", "#FFF", "#FFFFFF"] or Color(stroke_color) == Color(WHITE):
stroke_width = 0
stroke_color = BLACK
if stroke_color in ["#000", "#000000"]:
stroke_color = WHITE
if stroke_width in ["", "none", "0"]:
stroke_width = 0
if corner_radius in ["", "0", "none"]:
corner_radius = 0
corner_radius = float(corner_radius)
if corner_radius == 0:
mob = Rectangle(
width=self.attribute_to_float(
rect_element.getAttribute("width")
),
height=self.attribute_to_float(
rect_element.getAttribute("height")
),
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_color=fill_color,
fill_opacity=fill_opacity
)
else:
mob = RoundedRectangle(
width=self.attribute_to_float(
rect_element.getAttribute("width")
),
height=self.attribute_to_float(
rect_element.getAttribute("height")
),
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_color=fill_color,
fill_opacity=opacity,
corner_radius=corner_radius
)
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
return mob
def handle_transforms(self, element, mobject):
# TODO, this could use some cleaning...
x, y = 0, 0
try:
x = self.attribute_to_float(element.getAttribute('x'))
# Flip y
y = -self.attribute_to_float(element.getAttribute('y'))
mobject.shift([x, y, 0])
except Exception:
pass
transform = element.getAttribute('transform')
try: # transform matrix
prefix = "matrix("
suffix = ")"
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
transform = string_to_numbers(transform)
transform = np.array(transform).reshape([3, 2])
x = transform[2][0]
y = -transform[2][1]
matrix = np.identity(self.dim)
matrix[:2, :2] = transform[:2, :]
matrix[1] *= -1
matrix[:, 1] *= -1
for mob in mobject.family_members_with_points():
mob.apply_matrix(matrix.T)
mobject.shift(x * RIGHT + y * UP)
except:
pass
try: # transform scale
prefix = "scale("
suffix = ")"
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
scale_values = string_to_numbers(transform)
if len(scale_values) == 2:
scale_x, scale_y = scale_values
mobject.scale(np.array([scale_x, scale_y, 1]), about_point=ORIGIN)
elif len(scale_values) == 1:
scale = scale_values[0]
mobject.scale(np.array([scale, scale, 1]), about_point=ORIGIN)
except:
pass
try: # transform translate
prefix = "translate("
suffix = ")"
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
x, y = string_to_numbers(transform)
mobject.shift(x * RIGHT + y * DOWN)
except:
pass
# TODO, ...
def flatten(self, input_list):
output_list = []
for i in input_list:
if isinstance(i, list):
output_list.extend(self.flatten(i))
else:
output_list.append(i)
return output_list
def get_all_childNodes_have_id(self, element):
all_childNodes_have_id = []
if not isinstance(element, minidom.Element):
return
if element.hasAttribute('id'):
return [element]
for e in element.childNodes:
all_childNodes_have_id.append(self.get_all_childNodes_have_id(e))
return self.flatten([e for e in all_childNodes_have_id if e])
def update_ref_to_element(self, defs):
new_refs = dict([(e.getAttribute('id'), e) for e in self.get_all_childNodes_have_id(defs)])
self.ref_to_element.update(new_refs)
class VMobjectFromSVGPathstring(VMobject):
class VMobjectFromSVGPath(VMobject):
CONFIG = {
"long_lines": False,
"should_subdivide_sharp_curves": False,
"should_remove_null_curves": False,
}
def __init__(self, path_string, **kwargs):
self.path_string = path_string
def __init__(self, path_obj: 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")
@@ -345,211 +315,34 @@ class VMobjectFromSVGPathstring(VMobject):
self.triangulation = np.load(tris_filepath)
self.needs_new_triangulation = False
else:
self.relative_point = np.array(ORIGIN)
for command, coord_string in self.get_commands_and_coord_strings():
new_points = self.string_to_points(command, coord_string)
self.handle_command(command, new_points)
self.handle_commands()
if self.should_subdivide_sharp_curves:
# For a healthy triangulation later
self.subdivide_sharp_curves()
if self.should_remove_null_curves:
# Get rid of any null curves
self.set_points(self.get_points_without_null_curves())
# SVG treats y-coordinate differently
self.stretch(-1, 1, about_point=ORIGIN)
# Save to a file for future use
np.save(points_filepath, self.get_points())
np.save(tris_filepath, self.get_triangulation())
def get_commands_and_coord_strings(self):
all_commands = list(self.get_command_to_function_map().keys())
all_commands += [c.lower() for c in all_commands]
pattern = "[{}]".format("".join(all_commands))
return zip(
re.findall(pattern, self.path_string),
re.split(pattern, self.path_string)[1:]
)
def handle_command(self, command, new_points):
if command.islower():
# Treat it as a relative command
if command == "a":
# Only the last `self.dim` columns refer to points
new_points[:, -self.dim:] += self.relative_point
else:
new_points += self.relative_point
func, n_points = self.command_to_function(command)
command_points = new_points[:n_points]
if command.upper() == "A":
func(*command_points[0][:-self.dim], np.array(command_points[0][-self.dim:]))
else:
func(*command_points)
leftover_points = new_points[n_points:]
# Recursively handle the rest of the points
if len(leftover_points) > 0:
if command.upper() == "M":
# Treat following points as relative line coordinates
command = "l"
if command.islower():
if command == "a":
leftover_points[:, -self.dim:] -= self.relative_point
else:
leftover_points -= self.relative_point
self.relative_point = self.get_last_point()
self.handle_command(command, leftover_points)
else:
# Command is over, reset for future relative commands
self.relative_point = self.get_last_point()
def string_to_points(self, command, coord_string):
numbers = string_to_numbers(coord_string)
if command.upper() == "A":
# Only the last `self.dim` columns refer to points
# Each "point" returned here has a size of `(5 + self.dim)`
params = np.array(numbers).reshape((-1, 7))
result = np.zeros((params.shape[0], 5 + self.dim))
result[:, :7] = params
return result
if command.upper() in ["H", "V"]:
i = {"H": 0, "V": 1}[command.upper()]
xy = np.zeros((len(numbers), 2))
xy[:, i] = numbers
if command.isupper():
xy[:, 1 - i] = self.relative_point[1 - i]
else:
xy = np.array(numbers).reshape((-1, 2))
result = np.zeros((xy.shape[0], self.dim))
result[:, :2] = xy
return result
def add_elliptical_arc_to(self, rx, ry, x_axis_rotation, large_arc_flag, sweep_flag, point):
"""
In fact, this method only suits 2d VMobjects.
"""
def close_to_zero(a, threshold=1e-5):
return abs(a) < threshold
def solve_2d_linear_equation(a, b, c):
"""
Using Crammer's rule to solve the linear equation `[a b]x = c`
where `a`, `b` and `c` are all 2d vectors.
"""
def det(a, b):
return a[0] * b[1] - a[1] * b[0]
d = det(a, b)
if close_to_zero(d):
raise Exception("Cannot handle 0 determinant.")
return [det(c, b) / d, det(a, c) / d]
def get_arc_center_and_angles(x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, x1, y1):
"""
The parameter functions of an ellipse rotated `phi` radians counterclockwise is (on `alpha`):
x = cx + rx * cos(alpha) * cos(phi) + ry * sin(alpha) * sin(phi),
y = cy + rx * cos(alpha) * sin(phi) - ry * sin(alpha) * cos(phi).
Now we have two points sitting on the ellipse: `(x0, y0)`, `(x1, y1)`, corresponding to 4 equations,
and we want to hunt for 4 variables: `cx`, `cy`, `alpha0` and `alpha_1`.
Let `d_alpha = alpha1 - alpha0`, then:
if `sweep_flag = 0` and `large_arc_flag = 1`, then `PI <= d_alpha < 2 * PI`;
if `sweep_flag = 0` and `large_arc_flag = 0`, then `0 < d_alpha <= PI`;
if `sweep_flag = 1` and `large_arc_flag = 0`, then `-PI <= d_alpha < 0`;
if `sweep_flag = 1` and `large_arc_flag = 1`, then `-2 * PI < d_alpha <= -PI`.
"""
xd = x1 - x0
yd = y1 - y0
if close_to_zero(xd) and close_to_zero(yd):
raise Exception("Cannot find arc center since the start point and the end point meet.")
# Find `p = cos(alpha1) - cos(alpha0)`, `q = sin(alpha1) - sin(alpha0)`
eq0 = [rx * np.cos(phi), ry * np.sin(phi), xd]
eq1 = [rx * np.sin(phi), -ry * np.cos(phi), yd]
p, q = solve_2d_linear_equation(*zip(eq0, eq1))
# Find `s = (alpha1 - alpha0) / 2`, `t = (alpha1 + alpha0) / 2`
# If `sin(s) = 0`, this requires `p = q = 0`,
# implying `xd = yd = 0`, which is impossible.
sin_s = (p ** 2 + q ** 2) ** 0.5 / 2
if sweep_flag:
sin_s = -sin_s
sin_s = clip(sin_s, -1, 1)
s = np.arcsin(sin_s)
if large_arc_flag:
if not sweep_flag:
s = PI - s
else:
s = -PI - s
sin_t = -p / (2 * sin_s)
cos_t = q / (2 * sin_s)
cos_t = clip(cos_t, -1, 1)
t = np.arccos(cos_t)
if sin_t <= 0:
t = -t
# We can make sure `0 < abs(s) < PI`, `-PI <= t < PI`.
alpha0 = t - s
alpha_1 = t + s
cx = x0 - rx * np.cos(alpha0) * np.cos(phi) - ry * np.sin(alpha0) * np.sin(phi)
cy = y0 - rx * np.cos(alpha0) * np.sin(phi) + ry * np.sin(alpha0) * np.cos(phi)
return cx, cy, alpha0, alpha_1
def get_point_on_ellipse(cx, cy, rx, ry, phi, angle):
return np.array([
cx + rx * np.cos(angle) * np.cos(phi) + ry * np.sin(angle) * np.sin(phi),
cy + rx * np.cos(angle) * np.sin(phi) - ry * np.sin(angle) * np.cos(phi),
0
])
def convert_elliptical_arc_to_quadratic_bezier_curve(
cx, cy, rx, ry, phi, start_angle, end_angle, n_components=8
):
theta = (end_angle - start_angle) / n_components / 2
handles = np.array([
get_point_on_ellipse(cx, cy, rx / np.cos(theta), ry / np.cos(theta), phi, a)
for a in np.linspace(
start_angle + theta,
end_angle - theta,
n_components,
)
])
anchors = np.array([
get_point_on_ellipse(cx, cy, rx, ry, phi, a)
for a in np.linspace(
start_angle + theta * 2,
end_angle,
n_components,
)
])
return handles, anchors
phi = x_axis_rotation * DEGREES
x0, y0 = self.get_last_point()[:2]
cx, cy, start_angle, end_angle = get_arc_center_and_angles(
x0, y0, rx, ry, phi, large_arc_flag, sweep_flag, point[0], point[1]
)
handles, anchors = convert_elliptical_arc_to_quadratic_bezier_curve(
cx, cy, rx, ry, phi, start_angle, end_angle
)
for handle, anchor in zip(handles, anchors):
self.add_quadratic_bezier_curve_to(handle, anchor)
def command_to_function(self, command):
return self.get_command_to_function_map()[command.upper()]
def get_command_to_function_map(self):
"""
Associates svg command to VMobject function, and
the number of arguments it takes in
"""
return {
"M": (self.start_new_path, 1),
"L": (self.add_line_to, 1),
"H": (self.add_line_to, 1),
"V": (self.add_line_to, 1),
"C": (self.add_cubic_bezier_curve_to, 3),
"S": (self.add_smooth_cubic_curve_to, 2),
"Q": (self.add_quadratic_bezier_curve_to, 2),
"T": (self.add_smooth_curve_to, 1),
"A": (self.add_elliptical_arc_to, 1),
"Z": (self.close_path, 0),
def handle_commands(self) -> None:
segment_class_to_func_map = {
se.Move: (self.start_new_path, ("end",)),
se.Close: (self.close_path, ()),
se.Line: (self.add_line_to, ("end",)),
se.QuadraticBezier: (self.add_quadratic_bezier_curve_to, ("control", "end")),
se.CubicBezier: (self.add_cubic_bezier_curve_to, ("control1", "control2", "end"))
}
for segment in self.path_obj:
segment_class = segment.__class__
func, attr_names = segment_class_to_func_map[segment_class]
points = [
_convert_point_to_3d(*segment.__getattribute__(attr_name))
for attr_name in attr_names
]
func(*points)
def get_original_path_string(self):
return self.path_string
# Get rid of the side effect of trailing "Z M" commands.
if self.has_new_path_started():
self.resize_points(self.get_num_points() - 1)

View File

@@ -1,65 +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_to_mob_map = {}
class SingleStringTex(VMobject):
class SingleStringTex(SVGMobject):
CONFIG = {
"height": None,
"fill_opacity": 1.0,
"stroke_width": 0,
"should_center": True,
"svg_default": {
"color": WHITE,
},
"path_string_config": {
"should_subdivide_sharp_curves": True,
"should_remove_null_curves": True,
},
"font_size": 48,
"height": None,
"organize_left_to_right": False,
"alignment": "\\centering",
"math_mode": True,
"organize_left_to_right": False,
}
def __init__(self, tex_string, **kwargs):
super().__init__(**kwargs)
assert(isinstance(tex_string, str))
def __init__(self, tex_string: str, **kwargs):
assert isinstance(tex_string, str)
self.tex_string = tex_string
if tex_string not in tex_string_to_mob_map:
with display_during_execution(f" Writing \"{tex_string}\""):
full_tex = self.get_tex_file_body(tex_string)
filename = tex_to_svg_file(full_tex)
svg_mob = SVGMobject(
filename,
height=None,
path_string_config={
"should_subdivide_sharp_curves": True,
"should_remove_null_curves": True,
}
)
tex_string_to_mob_map[tex_string] = svg_mob
self.add(*(
sm.copy()
for sm in tex_string_to_mob_map[tex_string]
))
self.init_colors()
super().__init__(**kwargs)
if self.height is None:
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
def get_tex_file_body(self, tex_string):
@property
def hash_seed(self) -> tuple:
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.tex_string,
self.alignment,
self.math_mode
)
def get_file_path(self) -> str:
full_tex = self.get_tex_file_body(self.tex_string)
with display_during_execution(f"Writing \"{self.tex_string}\""):
file_path = tex_to_svg_file(full_tex)
return file_path
def get_tex_file_body(self, tex_string: str) -> str:
new_tex = self.get_modified_expression(tex_string)
if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
@@ -72,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
@@ -127,20 +134,27 @@ class SingleStringTex(VMobject):
tex = ""
return tex
def balance_braces(self, tex):
def balance_braces(self, tex: str) -> str:
"""
Makes Tex resiliant to unmatched { at start
Makes Tex resiliant to unmatched braces
"""
num_lefts, num_rights = [tex.count(char) for char in "{}"]
while num_rights > num_lefts:
tex = "{" + tex
num_lefts += 1
while num_lefts > num_rights:
tex = tex + "}"
num_rights += 1
num_unclosed_brackets = 0
for i in range(len(tex)):
if i > 0 and tex[i - 1] == "\\":
# So as to not count '\{' type expressions
continue
char = tex[i]
if char == "{":
num_unclosed_brackets += 1
elif char == "}":
if num_unclosed_brackets == 0:
tex = "{" + tex
else:
num_unclosed_brackets -= 1
tex += num_unclosed_brackets * "}"
return tex
def get_tex(self):
def get_tex(self) -> str:
return self.tex_string
def organize_submobjects_left_to_right(self):
@@ -155,7 +169,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)
@@ -166,7 +180,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()]
@@ -214,7 +228,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()
@@ -229,27 +248,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:
@@ -261,10 +289,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
@@ -283,7 +311,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:
@@ -296,7 +324,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)
@@ -334,7 +362,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

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import math
from manimlib.constants import *
@@ -6,7 +8,10 @@ from manimlib.mobject.types.surface import SGroup
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.geometry import Square
from manimlib.mobject.geometry import Polygon
from manimlib.utils.bezier import interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import z_to_vector
from manimlib.utils.space_ops import compass_directions
@@ -14,26 +19,29 @@ from manimlib.utils.space_ops import compass_directions
class SurfaceMesh(VGroup):
CONFIG = {
"resolution": (21, 21),
"resolution": (21, 11),
"stroke_width": 1,
"normal_nudge": 1e-3,
"normal_nudge": 1e-2,
"depth_test": True,
"flat_stroke": False,
}
def __init__(self, uv_surface, **kwargs):
def __init__(self, uv_surface: Surface, **kwargs):
if not isinstance(uv_surface, Surface):
raise Exception("uv_surface must be of type Surface")
self.uv_surface = uv_surface
super().__init__(**kwargs)
def init_points(self):
def init_points(self) -> None:
uv_surface = self.uv_surface
full_nu, full_nv = uv_surface.resolution
part_nu, part_nv = self.resolution
u_indices = np.linspace(0, full_nu, part_nu).astype(int)
v_indices = np.linspace(0, full_nv, part_nv).astype(int)
# 'indices' are treated as floats. Later, there will be
# an interpolation between the floor and ceiling of these
# indices
u_indices = np.linspace(0, full_nu - 1, part_nu)
v_indices = np.linspace(0, full_nv - 1, part_nv)
points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points()
normals = uv_surface.get_unit_normals()
@@ -42,12 +50,21 @@ class SurfaceMesh(VGroup):
for ui in u_indices:
path = VMobject()
full_ui = full_nv * ui
path.set_points_smoothly(nudged_points[full_ui:full_ui + full_nv])
low_ui = full_nv * int(math.floor(ui))
high_ui = full_nv * int(math.ceil(ui))
path.set_points_smoothly(interpolate(
nudged_points[low_ui:low_ui + full_nv],
nudged_points[high_ui:high_ui + full_nv],
ui % 1
))
self.add(path)
for vi in v_indices:
path = VMobject()
path.set_points_smoothly(nudged_points[vi::full_nv])
path.set_points_smoothly(interpolate(
nudged_points[int(math.floor(vi))::full_nv],
nudged_points[int(math.ceil(vi))::full_nv],
vi % 1
))
self.add(path)
@@ -61,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),
@@ -77,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
@@ -99,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):
@@ -109,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__(
@@ -128,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):
@@ -148,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):
@@ -166,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,
@@ -174,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]
@@ -185,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,
@@ -196,24 +220,86 @@ 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 Prism(Cube):
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 = {
"dimensions": [3, 2, 1]
"fill_color": BLUE_E,
"fill_opacity": 1,
"stroke_width": 1,
"reflectiveness": 0.2,
"gloss": 0.3,
"shadow": 0.2,
"depth_test": True,
}
def init_points(self):
Cube.init_points(self)
for dim, value in enumerate(self.dimensions):
self.rescale_to_fit(value, dim, stretch=True)
def init_points(self) -> None:
# Star by creating two of the pentagons, meeting
# back to back on the positive x-axis
phi = (1 + math.sqrt(5)) / 2
x, y, z = np.identity(3)
pentagon1 = Polygon(
[phi, 1 / phi, 0],
[1, 1, 1],
[1 / phi, 0, phi],
[1, -1, 1],
[phi, -1 / phi, 0],
)
pentagon2 = pentagon1.copy().stretch(-1, 2, about_point=ORIGIN)
pentagon2.reverse_points()
x_pair = VGroup(pentagon1, pentagon2)
z_pair = x_pair.copy().apply_matrix(np.array([z, -x, -y]).T)
y_pair = x_pair.copy().apply_matrix(np.array([y, z, x]).T)
self.add(*x_pair, *y_pair, *z_pair)
for pentagon in list(self):
pc = pentagon.copy()
pc.apply_function(lambda p: -p)
pc.reverse_points()
self.add(pc)
# # Rotate those two pentagons by all the axis permuations to fill
# # out the dodecahedron
# Id = np.identity(3)
# for i in range(3):
# perm = [j % 3 for j in range(i, i + 3)]
# for b in [1, -1]:
# matrix = b * np.array([Id[0][perm], Id[1][perm], Id[2][perm]])
# self.add(pentagon1.copy().apply_matrix(matrix, about_point=ORIGIN))
# self.add(pentagon2.copy().apply_matrix(matrix, about_point=ORIGIN))
class Prismify(VGroup):
CONFIG = {
"apply_depth_test": True,
}
def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):
# At the moment, this assume stright edges
super().__init__(**kwargs)
vect = depth * direction
self.add(vmobject.copy())
points = vmobject.get_points()[::vmobject.n_points_per_curve]
for p1, p2 in adjacent_pairs(points):
wall = VMobject()
wall.match_style(vmobject)
wall.set_points_as_corners([p1, p2, p2 + vect, p1 + vect])
self.add(wall)
self.add(vmobject.copy().shift(vect).reverse_points())

View File

@@ -1,13 +1,18 @@
from __future__ import annotations
import numpy as np
import numpy.typing as npt
import moderngl
from manimlib.constants import GREY_C
from manimlib.constants import YELLOW
from manimlib.constants import ORIGIN
from manimlib.mobject.types.point_cloud_mobject import PMobject
from manimlib.utils.iterables import resize_preserving_order
DEFAULT_DOT_RADIUS = 0.05
DEFAULT_GLOW_DOT_RADIUS = 0.2
DEFAULT_GRID_HEIGHT = 6
DEFAULT_BUFF_RATIO = 0.5
@@ -17,6 +22,7 @@ class DotCloud(PMobject):
"color": GREY_C,
"opacity": 1,
"radius": DEFAULT_DOT_RADIUS,
"glow_factor": 0,
"shader_folder": "true_dot",
"render_primitive": moderngl.POINTS,
"shader_dtype": [
@@ -26,23 +32,31 @@ class DotCloud(PMobject):
],
}
def __init__(self, points=None, **kwargs):
def __init__(self, points: npt.ArrayLike = None, **kwargs):
super().__init__(**kwargs)
if points is not None:
self.set_points(points)
def init_data(self):
def init_data(self) -> None:
super().init_data()
self.data["radii"] = np.zeros((1, 1))
self.set_radius(self.radius)
def to_grid(self, n_rows, n_cols, n_layers=1,
buff_ratio=None,
h_buff_ratio=1.0,
v_buff_ratio=1.0,
d_buff_ratio=1.0,
height=DEFAULT_GRID_HEIGHT,
):
def init_uniforms(self) -> None:
super().init_uniforms()
self.uniforms["glow_factor"] = self.glow_factor
def to_grid(
self,
n_rows: int,
n_cols: int,
n_layers: int = 1,
buff_ratio: float | None = None,
h_buff_ratio: float = 1.0,
v_buff_ratio: float = 1.0,
d_buff_ratio: float = 1.0,
height: float = DEFAULT_GRID_HEIGHT,
):
n_points = n_rows * n_cols * n_layers
points = np.repeat(range(n_points), 3, axis=0).reshape((n_points, 3))
points[:, 0] = points[:, 0] % n_cols
@@ -67,44 +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 compute_bounding_box(self):
def set_glow_factor(self, glow_factor: float) -> None:
self.uniforms["glow_factor"] = glow_factor
def get_glow_factor(self) -> float:
return self.uniforms["glow_factor"]
def compute_bounding_box(self) -> np.ndarray:
bb = super().compute_bounding_box()
radius = self.get_radius()
bb[0] += np.full((3,), -radius)
bb[2] += np.full((3,), radius)
return bb
def scale(self, scale_factor, scale_radii=True, **kwargs):
def scale(
self,
scale_factor: float | npt.ArrayLike,
scale_radii: bool = True,
**kwargs
):
super().scale(scale_factor, **kwargs)
if scale_radii:
self.set_radii(scale_factor * self.get_radii())
return self
def make_3d(self, gloss=0.5, shadow=0.2):
self.set_gloss(gloss)
def make_3d(self, reflectiveness: float = 0.5, shadow: float = 0.2):
self.set_reflectiveness(reflectiveness)
self.set_shadow(shadow)
self.apply_depth_test()
return self
def get_shader_data(self):
def get_shader_data(self) -> np.ndarray:
shader_data = super().get_shader_data()
self.read_data_to_shader(shader_data, "radius", "radii")
self.read_data_to_shader(shader_data, "color", "rgbas")
@@ -112,5 +137,17 @@ class DotCloud(PMobject):
class TrueDot(DotCloud):
def __init__(self, center=ORIGIN, radius=DEFAULT_DOT_RADIUS, **kwargs):
super().__init__(points=[center], radius=radius, **kwargs)
def __init__(self, center: np.ndarray = ORIGIN, **kwargs):
super().__init__(points=[center], **kwargs)
class GlowDots(DotCloud):
CONFIG = {
"glow_factor": 2,
"radius": DEFAULT_GLOW_DOT_RADIUS,
"color": YELLOW,
}
class GlowDot(GlowDots, TrueDot):
pass

View File

@@ -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,33 @@ 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 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 +64,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")

View File

@@ -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])

View File

@@ -1,5 +1,10 @@
import numpy as np
from __future__ import annotations
from typing import Iterable, Callable
import moderngl
import numpy as np
import numpy.typing as npt
from manimlib.constants import *
from manimlib.mobject.mobject import Mobject
@@ -9,6 +14,11 @@ from manimlib.utils.images import get_full_raster_image_path
from manimlib.utils.iterables import listify
from manimlib.utils.space_ops import normalize_along_axis
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.camera.camera import Camera
class Surface(Mobject):
CONFIG = {
@@ -20,7 +30,8 @@ class Surface(Mobject):
"resolution": (101, 101),
"color": GREY,
"opacity": 1.0,
"gloss": 0.3,
"reflectiveness": 0.3,
"gloss": 0.1,
"shadow": 0.4,
"prefered_creation_axis": 1,
# For du and dv steps. Much smaller and numerical error
@@ -41,7 +52,7 @@ class Surface(Mobject):
super().__init__(**kwargs)
self.compute_triangle_indices()
def uv_func(self, u, v):
def uv_func(self, u: float, v: float) -> tuple[float, float, float]:
# To be implemented in subclasses
return (u, v, 0.0)
@@ -84,15 +95,17 @@ class Surface(Mobject):
indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right
self.triangle_indices = indices
def get_triangle_indices(self):
def get_triangle_indices(self) -> np.ndarray:
return self.triangle_indices
def get_surface_points_and_nudged_points(self):
def get_surface_points_and_nudged_points(
self
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
points = self.get_points()
k = len(points) // 3
return points[:k], points[k:2 * k], points[2 * k:]
def get_unit_normals(self):
def get_unit_normals(self) -> np.ndarray:
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
normals = np.cross(
(du_points - s_points) / self.epsilon,
@@ -100,7 +113,13 @@ class Surface(Mobject):
)
return normalize_along_axis(normals, 1)
def pointwise_become_partial(self, smobject, a, b, axis=None):
def pointwise_become_partial(
self,
smobject: "Surface",
a: float,
b: float,
axis: np.ndarray | None = None
):
assert(isinstance(smobject, Surface))
if axis is None:
axis = self.prefered_creation_axis
@@ -115,7 +134,14 @@ class Surface(Mobject):
]))
return self
def get_partial_points_array(self, points, a, b, resolution, axis):
def get_partial_points_array(
self,
points: np.ndarray,
a: float,
b: float,
resolution: npt.ArrayLike,
axis: int
) -> np.ndarray:
if len(points) == 0:
return points
nu, nv = resolution[:2]
@@ -148,7 +174,7 @@ class Surface(Mobject):
).reshape(shape)
return points.reshape((nu * nv, *resolution[2:]))
def sort_faces_back_to_front(self, vect=OUT):
def sort_faces_back_to_front(self, vect: np.ndarray = OUT):
tri_is = self.triangle_indices
indices = list(range(len(tri_is) // 3))
points = self.get_points()
@@ -161,8 +187,13 @@ class Surface(Mobject):
tri_is[k::3] = tri_is[k::3][indices]
return self
def always_sort_to_camera(self, camera: Camera):
self.add_updater(lambda m: m.sort_faces_back_to_front(
camera.get_location() - self.get_center()
))
# For shaders
def get_shader_data(self):
def get_shader_data(self) -> np.ndarray:
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
shader_data = self.get_resized_shader_data_array(len(s_points))
if "points" not in self.locked_data_keys:
@@ -172,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)
@@ -194,7 +231,7 @@ class SGroup(Surface):
"resolution": (0, 0),
}
def __init__(self, *parametric_surfaces, **kwargs):
def __init__(self, *parametric_surfaces: Surface, **kwargs):
super().__init__(uv_func=None, **kwargs)
self.add(*parametric_surfaces)
@@ -214,7 +251,13 @@ class TexturedSurface(Surface):
]
}
def __init__(self, uv_surface, image_file, dark_image_file=None, **kwargs):
def __init__(
self,
uv_surface: Surface,
image_file: str,
dark_image_file: str | None = None,
**kwargs
):
if not isinstance(uv_surface, Surface):
raise Exception("uv_surface must be of type Surface")
# Set texture information
@@ -230,10 +273,10 @@ class TexturedSurface(Surface):
self.uv_surface = uv_surface
self.uv_func = uv_surface.uv_func
self.u_range = uv_surface.u_range
self.v_range = uv_surface.v_range
self.resolution = uv_surface.resolution
self.gloss = self.uv_surface.gloss
self.u_range: tuple[float, float] = uv_surface.u_range
self.v_range: tuple[float, float] = uv_surface.v_range
self.resolution: tuple[float, float] = uv_surface.resolution
self.gloss: float = self.uv_surface.gloss
super().__init__(**kwargs)
def init_data(self):
@@ -257,12 +300,18 @@ class TexturedSurface(Surface):
def init_colors(self):
self.data["opacity"] = np.array([self.uv_surface.data["rgbas"][:, 3]])
def set_opacity(self, opacity, recurse=True):
def set_opacity(self, opacity: float, recurse: bool = True):
for mob in self.get_family(recurse):
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
return self
def pointwise_become_partial(self, tsmobject, a, b, axis=1):
def pointwise_become_partial(
self,
tsmobject: "TexturedSurface",
a: float,
b: float,
axis: int = 1
):
super().pointwise_become_partial(tsmobject, a, b, axis)
im_coords = self.data["im_coords"]
im_coords[:] = tsmobject.data["im_coords"]
@@ -274,7 +323,7 @@ class TexturedSurface(Surface):
)
return self
def fill_in_shader_color_info(self, shader_data):
def fill_in_shader_color_info(self, shader_data: np.ndarray) -> np.ndarray:
self.read_data_to_shader(shader_data, "opacity", "opacity")
self.read_data_to_shader(shader_data, "im_coords", "im_coords")
return shader_data

View File

@@ -1,8 +1,13 @@
import itertools as it
import operator as op
import moderngl
from __future__ import annotations
import operator as op
import itertools as it
from functools import reduce, wraps
from typing import Iterable, Sequence, Callable, Union
import colour
import moderngl
import numpy.typing as npt
from manimlib.constants import *
from manimlib.mobject.mobject import Mobject
@@ -12,6 +17,7 @@ from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points
from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points
from manimlib.utils.bezier import get_quadratic_approximation_of_cubic
from manimlib.utils.bezier import interpolate
from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.bezier import partial_quadratic_bezier_points
from manimlib.utils.color import rgb_to_hex
@@ -28,6 +34,9 @@ from manimlib.utils.space_ops import z_to_vector
from manimlib.shader_wrapper import ShaderWrapper
ManimColor = Union[str, colour.Color, Sequence[float]]
class VMobject(Mobject):
CONFIG = {
"fill_color": None,
@@ -74,7 +83,6 @@ class VMobject(Mobject):
self.needs_new_triangulation = True
self.triangulation = np.zeros(0, dtype='i4')
super().__init__(**kwargs)
self.refresh_unit_normal()
def get_group_class(self):
return VGroup
@@ -105,7 +113,12 @@ class VMobject(Mobject):
self.set_flat_stroke(self.flat_stroke)
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:
@@ -115,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:
@@ -135,24 +160,36 @@ class VMobject(Mobject):
mob.draw_stroke_behind_fill = background
return self
def align_stroke_width_data_to_points(self, recurse=True):
def set_backstroke(
self,
color: ManimColor = BLACK,
width: float | npt.ArrayLike = 3,
background: bool = True
):
self.set_stroke(color, width, background=background)
return self
def align_stroke_width_data_to_points(self, recurse: bool = True) -> None:
for mob in self.get_family(recurse):
mob.data["stroke_width"] = resize_with_interpolation(
mob.data["stroke_width"], len(mob.get_points())
)
def set_style(self,
fill_color=None,
fill_opacity=None,
fill_rgba=None,
stroke_color=None,
stroke_opacity=None,
stroke_rgba=None,
stroke_width=None,
stroke_background=True,
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:
@@ -177,6 +214,8 @@ class VMobject(Mobject):
background=stroke_background,
)
if reflectiveness is not None:
self.set_reflectiveness(reflectiveness, recurse=recurse)
if gloss is not None:
self.set_gloss(gloss, recurse=recurse)
if shadow is not None:
@@ -185,15 +224,16 @@ class VMobject(Mobject):
def get_style(self):
return {
"fill_rgba": self.data['fill_rgba'],
"stroke_rgba": self.data['stroke_rgba'],
"stroke_width": self.data['stroke_width'],
"fill_rgba": self.data['fill_rgba'].copy(),
"stroke_rgba": self.data['stroke_rgba'].copy(),
"stroke_width": self.data['stroke_width'].copy(),
"stroke_background": self.draw_stroke_behind_fill,
"reflectiveness": self.get_reflectiveness(),
"gloss": self.get_gloss(),
"shadow": self.get_shadow(),
}
def match_style(self, vmobject, recurse=True):
def match_style(self, vmobject: VMobject, recurse: bool = True):
self.set_style(**vmobject.get_style(), recurse=False)
if recurse:
# Does its best to match up submobject lists, and
@@ -207,101 +247,115 @@ class VMobject(Mobject):
sm1.match_style(sm2)
return self
def set_color(self, color, recurse=True):
def set_color(self, color: ManimColor, recurse: bool = True):
self.set_fill(color, recurse=recurse)
self.set_stroke(color, recurse=recurse)
return self
def set_opacity(self, opacity, recurse=True):
def set_opacity(self, opacity: float, recurse: bool = True):
self.set_fill(opacity=opacity, recurse=recurse)
self.set_stroke(opacity=opacity, recurse=recurse)
return self
def fade(self, darkness=0.5, recurse=True):
factor = 1.0 - darkness
self.set_fill(
opacity=factor * self.get_fill_opacity(),
recurse=False,
)
self.set_stroke(
opacity=factor * self.get_stroke_opacity(),
recurse=False,
)
super().fade(darkness, recurse)
def fade(self, darkness: float = 0.5, recurse: bool = True):
mobs = self.get_family() if recurse else [self]
for mob in mobs:
factor = 1.0 - darkness
mob.set_fill(
opacity=factor * mob.get_fill_opacity(),
recurse=False,
)
mob.set_stroke(
opacity=factor * mob.get_stroke_opacity(),
recurse=False,
)
return self
def get_fill_colors(self):
def get_fill_colors(self) -> list[str]:
return [
rgb_to_hex(rgba[:3])
for rgba in self.data['fill_rgba']
]
def get_fill_opacities(self):
def get_fill_opacities(self) -> np.ndarray:
return self.data['fill_rgba'][:, 3]
def get_stroke_colors(self):
def get_stroke_colors(self) -> list[str]:
return [
rgb_to_hex(rgba[:3])
for rgba in self.data['stroke_rgba']
]
def get_stroke_opacities(self):
def get_stroke_opacities(self) -> np.ndarray:
return self.data['stroke_rgba'][:, 3]
def get_stroke_widths(self):
def get_stroke_widths(self) -> np.ndarray:
return self.data['stroke_width'][:, 0]
# TODO, it's weird for these to return the first of various lists
# rather than the full information
def get_fill_color(self):
def get_fill_color(self) -> str:
"""
If there are multiple colors (for gradient)
this returns the first one
"""
return self.get_fill_colors()[0]
def get_fill_opacity(self):
def get_fill_opacity(self) -> float:
"""
If there are multiple opacities, this returns the
first
"""
return self.get_fill_opacities()[0]
def get_stroke_color(self):
def get_stroke_color(self) -> str:
return self.get_stroke_colors()[0]
def get_stroke_width(self):
def get_stroke_width(self) -> float | np.ndarray:
return self.get_stroke_widths()[0]
def get_stroke_opacity(self):
def get_stroke_opacity(self) -> float:
return self.get_stroke_opacities()[0]
def get_color(self):
if self.has_stroke():
return self.get_stroke_color()
return self.get_fill_color()
def get_color(self) -> str:
if self.has_fill():
return self.get_fill_color()
return self.get_stroke_color()
def has_stroke(self):
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))
@@ -311,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.
"""
@@ -333,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:
@@ -362,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:
@@ -371,18 +436,21 @@ class VMobject(Mobject):
self.add_quadratic_bezier_curve_to(new_handle, point)
return self
def add_smooth_cubic_curve_to(self, handle, point):
def add_smooth_cubic_curve_to(self, handle: np.ndarray, point: np.ndarray):
self.throw_error_if_no_points()
new_handle = self.get_reflection_of_last_handle()
if self.get_num_points() == 1:
new_handle = self.get_points()[-1]
else:
new_handle = self.get_reflection_of_last_handle()
self.add_cubic_bezier_curve_to(new_handle, handle, point)
def has_new_path_started(self):
def has_new_path_started(self) -> bool:
return self.get_num_points() % self.n_points_per_curve == 1
def get_last_point(self):
def get_last_point(self) -> np.ndarray:
return self.get_points()[-1]
def get_reflection_of_last_handle(self):
def get_reflection_of_last_handle(self) -> np.ndarray:
points = self.get_points()
return 2 * points[-1] - points[-2]
@@ -390,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 = []
@@ -413,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(*[
@@ -427,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()
@@ -435,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():
@@ -480,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():
@@ -496,23 +572,26 @@ class VMobject(Mobject):
return self
#
def consider_points_equals(self, p0, p1):
def consider_points_equals(self, p0: np.ndarray, p1: np.ndarray) -> bool:
return get_norm(p1 - p0) < self.tolerance_for_point_equality
# Information about the curve
def get_bezier_tuples_from_points(self, points):
def get_bezier_tuples_from_points(self, points: Sequence[np.ndarray]):
nppc = self.n_points_per_curve
remainder = len(points) % nppc
points = points[:len(points) - remainder]
return [
return (
points[i:i + nppc]
for i in range(0, len(points), nppc)
]
)
def get_bezier_tuples(self):
return self.get_bezier_tuples_from_points(self.get_points())
def get_subpaths_from_points(self, points):
def get_subpaths_from_points(
self,
points: Sequence[np.ndarray]
) -> list[Sequence[np.ndarray]]:
nppc = self.n_points_per_curve
diffs = points[nppc - 1:-1:nppc] - points[nppc::nppc]
splits = (diffs * diffs).sum(1) > self.tolerance_for_point_equality
@@ -529,27 +608,50 @@ class VMobject(Mobject):
if (i2 - i1) >= nppc
]
def get_subpaths(self):
def get_subpaths(self) -> list[Sequence[np.ndarray]]:
return self.get_subpaths_from_points(self.get_points())
def get_nth_curve_points(self, n):
def get_nth_curve_points(self, n: int) -> np.ndarray:
assert(n < self.get_num_curves())
nppc = self.n_points_per_curve
return self.get_points()[nppc * n:nppc * (n + 1)]
def get_nth_curve_function(self, n):
def get_nth_curve_function(self, n: int) -> Callable[[float], np.ndarray]:
return bezier(self.get_nth_curve_points(n))
def get_num_curves(self):
def get_num_curves(self) -> int:
return self.get_num_points() // self.n_points_per_curve
def point_from_proportion(self, alpha):
def quick_point_from_proportion(self, alpha: float) -> np.ndarray:
# Assumes all curves have the same length, so is inaccurate
num_curves = self.get_num_curves()
n, residue = integer_interpolate(0, num_curves, alpha)
curve_func = self.get_nth_curve_function(n)
return curve_func(residue)
def get_anchors_and_handles(self):
def point_from_proportion(self, alpha: float) -> np.ndarray:
if alpha <= 0:
return self.get_start()
elif alpha >= 1:
return self.get_end()
partials = [0]
for tup in self.get_bezier_tuples():
# Approximate length with straight line from start to end
arclen = get_norm(tup[0] - tup[-1])
partials.append(partials[-1] + arclen)
full = partials[-1]
if full == 0:
return self.get_start()
# First index where the partial lenth is more alpha times the full length
i = next(
(i for i, x in enumerate(partials) if x >= full * alpha),
len(partials) # Default
)
residue = inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha)
return self.get_nth_curve_function(i - 1)(residue)
def get_anchors_and_handles(self) -> list[np.ndarray]:
"""
returns anchors1, handles, anchors2,
where (anchors1[i], handles[i], anchors2[i])
@@ -563,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
@@ -579,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_, [
@@ -588,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([
@@ -599,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
@@ -619,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]
@@ -629,21 +731,23 @@ class VMobject(Mobject):
area_vect = self.get_area_vector()
area = get_norm(area_vect)
if area > 0:
return area_vect / area
normal = area_vect / area
else:
points = self.get_points()
return get_unit_normal(
normal = get_unit_normal(
points[1] - points[0],
points[2] - points[1],
)
self.data["unit_normal"][:] = normal
return normal
def refresh_unit_normal(self):
for mob in self.get_family():
mob.data["unit_normal"][:] = mob.get_unit_normal(recompute=True)
mob.get_unit_normal(recompute=True)
return self
# Alignment
def align_points(self, vmobject):
def align_points(self, vmobject: VMobject):
if self.get_num_points() == len(vmobject.get_points()):
return
@@ -686,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())
@@ -696,12 +800,12 @@ class VMobject(Mobject):
mob.set_points(new_points)
return self
def insert_n_curves_to_point_list(self, n, points):
def insert_n_curves_to_point_list(self, n: int, points: np.ndarray):
nppc = self.n_points_per_curve
if len(points) == 1:
return np.repeat(points, nppc * n, 0)
bezier_groups = self.get_bezier_tuples_from_points(points)
bezier_groups = list(self.get_bezier_tuples_from_points(points))
norms = np.array([
get_norm(bg[nppc - 1] - bg[0])
for bg in bezier_groups
@@ -729,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()
@@ -738,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)
@@ -780,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
@@ -792,12 +902,12 @@ class VMobject(Mobject):
mob.needs_new_triangulation = True
return self
def get_triangulation(self, normal_vector=None):
def get_triangulation(self, normal_vector: np.ndarray | None = None):
# Figure out how to triangulate the interior to know
# how to send the points as to the vertex shader.
# First triangles come directly from the points
if normal_vector is None:
normal_vector = self.get_unit_normal()
normal_vector = self.get_unit_normal(recompute=True)
if not self.needs_new_triangulation:
return self.triangulation
@@ -853,7 +963,7 @@ class VMobject(Mobject):
def triggers_refreshed_triangulation(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
old_points = self.get_points()
old_points = self.get_points().copy()
func(self, *args, **kwargs)
if not np.all(self.get_points() == old_points):
self.refresh_unit_normal()
@@ -861,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
@@ -905,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 = []
@@ -947,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))
@@ -972,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))
@@ -988,17 +1103,21 @@ class VMobject(Mobject):
self.get_fill_shader_data()
self.get_stroke_shader_data()
def get_fill_shader_vert_indices(self):
def get_fill_shader_vert_indices(self) -> np.ndarray:
return self.get_triangulation()
class VGroup(VMobject):
def __init__(self, *vmobjects, **kwargs):
def __init__(self, *vmobjects: VMobject, **kwargs):
if not all([isinstance(m, VMobject) for m in vmobjects]):
raise Exception("All submobjects must be of type VMobject")
super().__init__(**kwargs)
self.add(*vmobjects)
def __add__(self, other: VMobject | VGroup):
assert(isinstance(other, VMobject))
return self.add(other)
class VectorizedPoint(Point, VMobject):
CONFIG = {
@@ -1009,13 +1128,14 @@ class VectorizedPoint(Point, VMobject):
"artificial_height": 0.01,
}
def __init__(self, location=ORIGIN, **kwargs):
super().__init__(**kwargs)
def __init__(self, location: np.ndarray = ORIGIN, **kwargs):
Point.__init__(self, **kwargs)
VMobject.__init__(self, **kwargs)
self.set_points(np.array([location]))
class CurvesAsSubmobjects(VGroup):
def __init__(self, vmobject, **kwargs):
def __init__(self, vmobject: VMobject, **kwargs):
super().__init__(**kwargs)
for tup in vmobject.get_bezier_tuples():
part = VMobject()
@@ -1031,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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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(*[

View File

@@ -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,26 +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()
@@ -78,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
@@ -86,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
@@ -112,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
@@ -135,23 +150,24 @@ class Scene(object):
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
local_ns[term] = getattr(self, term)
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
shell(local_ns=local_ns, stack_depth=2)
# End scene when exiting an embed.
raise EndSceneEarlyException()
# End scene when exiting an embed
if close_scene_on_exit:
raise EndSceneEarlyException()
def __str__(self):
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:
@@ -169,30 +185,37 @@ class Scene(object):
if rt < vt:
self.update_frame(0)
def emit_frame(self):
def emit_frame(self) -> None:
if not self.skip_animations:
self.file_writer.write_frame(self.camera)
# Related to updating
def update_mobjects(self, dt):
def update_mobjects(self, dt: float) -> None:
for mobject in self.mobjects:
mobject.update(dt)
def should_update_mobjects(self):
def should_update_mobjects(self) -> bool:
return self.always_update_mobjects or any([
len(mob.get_family_updaters()) > 0
for mob in self.mobjects
])
def has_time_based_updaters(self) -> bool:
return any([
sm.has_time_based_updater()
for mob in self.mobjects()
for sm in mob.get_family()
])
# Related to time
def get_time(self):
def get_time(self) -> float:
return self.time
def increment_time(self, dt):
def increment_time(self, dt: float) -> None:
self.time += dt
# Related to internal mobject organization
def get_top_level_mobjects(self):
def get_top_level_mobjects(self) -> list[Mobject]:
# Return only those which are not in the family
# of another mobject from the scene
mobjects = self.get_mobjects()
@@ -206,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.
@@ -218,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,
@@ -230,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
@@ -249,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
@@ -268,64 +296,72 @@ 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.stop_skipping()
self.skip_time = self.time
if not self.original_skipping_status:
self.stop_skipping()
if self.end_at_animation_number is not None:
if self.num_plays >= self.end_at_animation_number:
raise EndSceneEarlyException()
def stop_skipping(self):
if self.skip_animations:
self.skip_animations = False
self.skip_time += self.time
def stop_skipping(self) -> None:
self.virtual_animation_start_time = self.time
self.skip_animations = False
# Methods associated with running animations
def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False):
def get_time_progression(
self,
run_time: float,
n_iterations: int | None = None,
desc: str = "",
override_skip_animations: bool = False
) -> list[float] | np.ndarray | ProgressDisplay:
if self.skip_animations and not override_skip_animations:
times = [run_time]
return [run_time]
else:
step = 1 / self.camera.frame_rate
times = np.arange(0, run_time, step)
time_progression = ProgressDisplay(
if self.file_writer.has_progress_display:
self.file_writer.set_progress_display_subdescription(desc)
return times
return ProgressDisplay(
times,
total=n_iterations,
leave=self.leave_progress_bars,
ascii=True if platform.system() == 'Windows' else None
ascii=True if platform.system() == 'Windows' else None,
desc=desc,
)
return time_progression
def get_run_time(self, animations):
def get_run_time(self, animations: Iterable[Animation]) -> float:
return np.max([animation.run_time for animation in animations])
def get_animation_time_progression(self, animations):
def get_animation_time_progression(
self,
animations: Iterable[Animation]
) -> list[float] | np.ndarray | ProgressDisplay:
run_time = self.get_run_time(animations)
time_progression = self.get_time_progression(run_time)
time_progression.set_description("".join([
f"Animation {self.num_plays}: {animations[0]}",
", etc." if len(animations) > 1 else "",
]))
description = f"{self.num_plays} {animations[0]}"
if len(animations) > 1:
description += ", etc."
time_progression = self.get_time_progression(run_time, desc=description)
return time_progression
def get_wait_time_progression(self, duration, stop_condition):
def get_wait_time_progression(
self,
duration: float,
stop_condition: Callable[[], bool] | None = None
) -> list[float] | np.ndarray | ProgressDisplay:
kw = {"desc": f"{self.num_plays} Waiting"}
if stop_condition is not None:
time_progression = self.get_time_progression(
duration,
n_iterations=-1, # So it doesn't show % progress
override_skip_animations=True
)
time_progression.set_description(
"Waiting for {}".format(stop_condition.__name__)
)
else:
time_progression = self.get_time_progression(duration)
time_progression.set_description(
"Waiting {}".format(self.num_plays)
)
return time_progression
kw["n_iterations"] = -1 # So it doesn't show % progress
kw["override_skip_animations"] = True
return self.get_time_progression(duration, **kw)
def anims_from_play_args(self, *args, **kwargs):
def anims_from_play_args(self, *args, **kwargs) -> list[Animation]:
"""
Each arg can either be an animation, or a mobject method
followed by that methods arguments (and potentially follow
@@ -415,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
@@ -425,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
@@ -439,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
@@ -451,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)
@@ -461,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
@@ -473,10 +514,22 @@ class Scene(object):
self.unlock_mobject_data()
@handle_play_like_call
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
def wait(
self,
duration: float = DEFAULT_WAIT_TIME,
stop_condition: Callable[[], bool] = None,
note: str = None,
ignore_presenter_mode: bool = False
):
if note:
log.info(note)
self.update_mobjects(dt=0) # Any problems with this?
if self.should_update_mobjects():
self.lock_static_mobject_data()
self.lock_static_mobject_data()
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
while self.hold_on_wait:
self.update_frame(dt=1 / self.camera.frame_rate)
self.hold_on_wait = True
else:
time_progression = self.get_wait_time_progression(duration, stop_condition)
last_t = 0
for t in time_progression:
@@ -487,18 +540,14 @@ class Scene(object):
if stop_condition is not None and stop_condition():
time_progression.close()
break
self.unlock_mobject_data()
elif self.skip_animations:
# Do nothing
return self
else:
self.update_frame(duration)
n_frames = int(duration * self.camera.frame_rate)
for n in range(n_frames):
self.emit_frame()
self.unlock_mobject_data()
return self
def wait_until(self, stop_condition, max_time=60):
def wait_until(
self,
stop_condition: Callable[[], bool],
max_time: float = 60
):
self.wait(max_time, stop_condition=stop_condition)
def force_skipping(self):
@@ -511,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": [
@@ -527,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"]
@@ -538,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}
@@ -548,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
@@ -558,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}
@@ -566,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:
@@ -593,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:
@@ -615,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

View File

@@ -1,10 +1,14 @@
import numpy as np
from pydub import AudioSegment
import shutil
import subprocess as sp
from __future__ import annotations
import os
import sys
import shutil
import platform
import subprocess as sp
import numpy as np
from pydub import AudioSegment
from tqdm import tqdm as ProgressDisplay
from manimlib.constants import FFMPEG_BIN
from manimlib.utils.config_ops import digest_config
@@ -14,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 = {
@@ -35,17 +46,20 @@ class SceneFileWriter(object):
"open_file_upon_completion": False,
"show_file_location_upon_completion": False,
"quiet": False,
"total_frames": 0,
"progress_description_len": 60,
}
def __init__(self, scene, **kwargs):
digest_config(self, kwargs)
self.scene = scene
self.writing_process = None
self.scene: Scene = scene
self.writing_process: sp.Popen | None = None
self.has_progress_display: bool = False
self.init_output_directories()
self.init_audio()
# Output directories and files
def init_output_directories(self):
def init_output_directories(self) -> None:
out_dir = self.output_directory
if self.mirror_module_path:
module_dir = self.get_default_module_directory()
@@ -65,19 +79,23 @@ class SceneFileWriter(object):
movie_dir, "partial_movie_files", scene_name,
))
def get_default_module_directory(self):
def get_default_module_directory(self) -> str:
path, _ = os.path.splitext(self.input_file_path)
if path.startswith("_"):
path = path[1:]
return path
def get_default_scene_name(self):
if self.file_name is None:
return self.scene.__class__.__name__
else:
return self.file_name
def get_default_scene_name(self) -> str:
name = str(self.scene)
saan = self.scene.start_at_animation_number
eaan = self.scene.end_at_animation_number
if saan is not None:
name += f"_{saan}"
if eaan is not None:
name += f"_{eaan}"
return name
def get_resolution_directory(self):
def get_resolution_directory(self) -> str:
pixel_height = self.scene.camera.pixel_height
frame_rate = self.scene.camera.frame_rate
return "{}p{}".format(
@@ -85,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(
@@ -98,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()
@@ -134,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()
@@ -169,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
@@ -205,18 +232,42 @@ class SceneFileWriter(object):
command += [self.temp_file_path]
self.writing_process = sp.Popen(command, stdin=sp.PIPE)
def write_frame(self, camera):
if self.total_frames > 0:
self.progress_display = ProgressDisplay(
range(self.total_frames),
# bar_format="{l_bar}{bar}|{n_fmt}/{total_fmt}",
leave=False,
ascii=True if platform.system() == 'Windows' else None,
dynamic_ncols=True,
)
self.has_progress_display = True
def set_progress_display_subdescription(self, sub_desc: str) -> None:
desc_len = self.progress_description_len
file = os.path.split(self.get_movie_file_path())[1]
full_desc = f"Rendering {file} ({sub_desc})"
if len(full_desc) > desc_len:
full_desc = full_desc[:desc_len - 4] + "...)"
else:
full_desc += " " * (desc_len - len(full_desc))
self.progress_display.set_description(full_desc)
def write_frame(self, camera: Camera) -> None:
if self.write_to_movie:
raw_bytes = camera.get_raw_fbo_data()
self.writing_process.stdin.write(raw_bytes)
if self.has_progress_display:
self.progress_display.update()
def close_movie_pipe(self):
def close_movie_pipe(self) -> None:
self.writing_process.stdin.close()
self.writing_process.wait()
self.writing_process.terminate()
if self.has_progress_display:
self.progress_display.close()
shutil.move(self.temp_file_path, self.final_file_path)
def combine_movie_files(self):
def combine_movie_files(self) -> None:
kwargs = {
"remove_non_integer_files": True,
"extension": self.movie_file_extension,
@@ -264,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"
@@ -276,7 +327,7 @@ class SceneFileWriter(object):
)
temp_file_path = stem + "_temp" + ext
commands = [
"ffmpeg",
FFMPEG_BIN,
"-i", movie_file_path,
"-i", sound_file_path,
'-y', # overwrite output file if it exists
@@ -295,21 +346,22 @@ class SceneFileWriter(object):
shutil.move(temp_file_path, movie_file_path)
os.remove(sound_file_path)
def save_final_image(self, image):
def save_final_image(self, image: Image) -> None:
file_path = self.get_image_file_path()
image.save(file_path)
self.print_file_ready_message(file_path)
def print_file_ready_message(self, file_path):
log.info(f"File ready at {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")

View File

@@ -5,7 +5,6 @@ class ThreeDScene(Scene):
CONFIG = {
"camera_config": {
"samples": 4,
"anti_alias_width": 0,
}
}

View File

@@ -287,9 +287,6 @@ class LinearTransformationScene(VectorScene):
},
"background_plane_kwargs": {
"color": GREY,
"axis_config": {
"stroke_color": GREY_B,
},
"axis_config": {
"color": GREY,
},

View File

@@ -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

View File

@@ -1,6 +1,6 @@
uniform vec2 frame_shape;
uniform float anti_alias_width;
uniform vec3 camera_center;
uniform vec3 camera_offset;
uniform mat3 camera_rotation;
uniform float is_fixed_in_frame;
uniform float focal_distance;

View File

@@ -13,39 +13,56 @@ vec4 add_light(vec4 color,
vec3 point,
vec3 unit_normal,
vec3 light_coords,
vec3 cam_coords,
float reflectiveness,
float gloss,
float shadow){
if(gloss == 0.0 && shadow == 0.0) return color;
if(reflectiveness == 0.0 && gloss == 0.0 && shadow == 0.0) return color;
float camera_distance = focal_distance;
vec4 result = color;
// Assume everything has already been rotated such that camera is in the z-direction
vec3 to_camera = vec3(0, 0, camera_distance) - point;
vec3 to_light = light_coords - point;
// cam_coords = vec3(0, 0, focal_distance);
vec3 to_camera = normalize(cam_coords - point);
vec3 to_light = normalize(light_coords - point);
// TODO, do we actually want this? It effectively treats surfaces as two-sided
if(dot(to_camera,unit_normal) < 0){
unit_normal *= -1;
}
// Note, this effectively treats surfaces as two-sided
// if(dot(to_camera, unit_normal) < 0) unit_normal *= -1;
float light_to_normal = dot(to_light, unit_normal);
// When unit normal points towards light, brighten
float bright_factor = max(light_to_normal, 0) * reflectiveness;
// For glossy surface, add extra shine if light beam go towards camera
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
float dot_prod = dot(normalize(light_reflection), normalize(to_camera));
float shine = gloss * exp(-3 * pow(1 - dot_prod, 2));
float dp2 = dot(normalize(to_light), unit_normal);
float darkening = mix(1, max(dp2, 0), shadow);
return vec4(
darkening * mix(color.rgb, vec3(1.0), shine),
color.a
);
float light_to_cam = dot(light_reflection, to_camera);
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));
bright_factor += shine;
result.rgb = mix(result.rgb, vec3(1.0), bright_factor);
if (light_to_normal < 0){
// Darken
result.rgb = mix(result.rgb, vec3(0.0), -light_to_normal * shadow);
}
// float darkening = mix(1, max(light_to_normal, 0), shadow);
// return vec4(
// darkening * mix(color.rgb, vec3(1.0), shine),
// color.a
// );
return result;
}
vec4 finalize_color(vec4 color,
vec3 point,
vec3 unit_normal,
vec3 light_coords,
vec3 cam_coords,
float reflectiveness,
float gloss,
float shadow){
///// INSERT COLOR FUNCTION HERE /////
// The line above may be replaced by arbitrary code snippets, as per
// the method Mobject.set_color_by_code
return add_light(color, point, unit_normal, light_coords, gloss, shadow);
return add_light(
color, point, unit_normal, light_coords, cam_coords,
reflectiveness, gloss, shadow
);
}

View File

@@ -1,5 +1,5 @@
// Assumes the following uniforms exist in the surrounding context:
// uniform vec3 camera_center;
// uniform vec3 camera_offset;
// uniform mat3 camera_rotation;
vec3 get_rotated_surface_unit_normal_vector(vec3 point, vec3 du_point, vec3 dv_point){

View File

@@ -1,6 +1,6 @@
// Assumes the following uniforms exist in the surrounding context:
// uniform float is_fixed_in_frame;
// uniform vec3 camera_center;
// uniform vec3 camera_offset;
// uniform mat3 camera_rotation;
vec3 rotate_point_into_frame(vec3 point){
@@ -15,5 +15,5 @@ vec3 position_point_into_frame(vec3 point){
if(bool(is_fixed_in_frame)){
return point;
}
return rotate_point_into_frame(point - camera_center);
return rotate_point_into_frame(point - camera_offset);
}

View File

@@ -1,6 +1,8 @@
#version 330
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float focal_distance;
@@ -71,6 +73,8 @@ void main() {
xyz_coords,
vec3(0.0, 0.0, 1.0),
light_source_position,
camera_position,
reflectiveness,
gloss,
shadow
);

View File

@@ -1,6 +1,8 @@
#version 330
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float focal_distance;
@@ -75,7 +77,7 @@ vec2 seek_root(vec2 z, vec2[MAX_DEGREE + 1] coefs, int max_steps, out float n_it
}
z = z - step;
}
n_iters -= clamp((threshold - curr_len) / (last_len - curr_len), 0.0, 1.0);
n_iters -= log(curr_len) / log(threshold);
return z;
}
@@ -118,7 +120,7 @@ void main() {
color = colors[i];
}
}
color *= 1.0 + (0.01 * saturation_factor) * (n_iters - 5 * saturation_factor);
color *= 1.0 + (0.01 * saturation_factor) * (n_iters - 2 * saturation_factor);
if(black_for_cycles > 0 && min_dist > CLOSE_ENOUGH){
color = vec4(0.0, 0.0, 0.0, 1.0);
@@ -151,6 +153,8 @@ void main() {
xyz_coords,
vec3(0.0, 0.0, 1.0),
light_source_position,
camera_position,
reflectiveness,
gloss,
shadow
);

View File

@@ -3,7 +3,7 @@
#INSERT camera_uniform_declarations.glsl
in vec4 color;
in float fill_all; // Either 0 or 1e
in float fill_all; // Either 0 or 1
in float uv_anti_alias_width;
in vec3 xyz_coords;

View File

@@ -11,6 +11,8 @@ uniform float focal_distance;
uniform float is_fixed_in_frame;
// Needed for finalize_color
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
@@ -44,6 +46,8 @@ void emit_vertex_wrapper(vec3 point, int index){
point,
v_global_unit_normal[index],
light_source_position,
camera_position,
reflectiveness,
gloss,
shadow
);

View File

@@ -13,7 +13,9 @@ uniform float flat_stroke;
//Needed for lighting
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float joint_type;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
@@ -259,6 +261,8 @@ void main() {
xyz_coords,
v_global_unit_normal[index_map[i]],
light_source_position,
camera_position,
reflectiveness,
gloss,
shadow
);

View File

@@ -1,6 +1,8 @@
#version 330
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float focal_distance;
@@ -19,6 +21,8 @@ void main() {
xyz_coords,
normalize(v_normal),
light_source_position,
camera_position,
reflectiveness,
gloss,
shadow
);

View File

@@ -4,6 +4,8 @@ uniform sampler2D LightTexture;
uniform sampler2D DarkTexture;
uniform float num_textures;
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float focal_distance;
@@ -36,6 +38,8 @@ void main() {
xyz_coords,
normalize(v_normal),
light_source_position,
camera_position,
reflectiveness,
gloss,
shadow
);

View File

@@ -1,10 +1,13 @@
#version 330
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float anti_alias_width;
uniform float focal_distance;
uniform float glow_factor;
in vec4 color;
in float radius;
@@ -22,14 +25,23 @@ void main() {
if (signed_dist > 0.5 * anti_alias_width){
discard;
}
vec3 normal = vec3(diff / radius, sqrt(1 - (dist * dist) / (radius * radius)));
frag_color = finalize_color(
color,
vec3(point.xy, 0.0),
normal,
light_source_position,
gloss,
shadow
);
frag_color = color;
if(gloss > 0 || shadow > 0){
vec3 normal = vec3(diff / radius, sqrt(1 - (dist * dist) / (radius * radius)));
frag_color = finalize_color(
frag_color,
vec3(point.xy, 0.0),
normal,
light_source_position,
camera_position,
reflectiveness,
gloss,
shadow
);
}
if(glow_factor > 0){
frag_color.a *= pow(1 - dist / radius, glow_factor);
}
frag_color.a *= smoothstep(0.5, -0.5, signed_dist / anti_alias_width);
}

View File

@@ -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,9 +80,15 @@ def partial_quadratic_bezier_points(points, a, b):
# Linear interpolation variants
def interpolate(start, end, alpha):
def interpolate(start: T, end: T, alpha: float) -> T:
try:
return (1 - alpha) * start + alpha * end
if isinstance(alpha, float):
return (1 - alpha) * start + alpha * end
# Otherwise, assume alpha is a list or array, and return
# an appropriated shaped array of all corresponding
# interpolations
result = np.outer(1 - alpha, start) + np.outer(alpha, end)
return result.reshape((*np.shape(alpha), *np.shape(start)))
except TypeError:
log.debug(f"`start` parameter with type `{type(start)}` and dtype `{start.dtype}`")
log.debug(f"`end` parameter with type `{type(end)}` and dtype `{end.dtype}`")
@@ -76,12 +97,22 @@ def interpolate(start, end, alpha):
sys.exit(2)
def set_array_by_interpolation(arr, arr1, arr2, alpha, interp_func=interpolate):
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 +133,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 +188,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 +248,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 +268,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 +347,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]

View File

@@ -7,8 +7,6 @@ from manimlib.constants import WHITE
from manimlib.constants import COLORMAP_3B1B
from manimlib.utils.bezier import interpolate
from manimlib.utils.iterables import resize_with_interpolation
from manimlib.utils.simple_functions import clip_in_place
from manimlib.utils.space_ops import normalize
def color_to_rgb(color):
@@ -105,16 +103,6 @@ def random_color():
return Color(rgb=(random.random() for i in range(3)))
def get_shaded_rgb(rgb, point, unit_normal_vect, light_source):
to_sun = normalize(light_source - point)
factor = 0.5 * np.dot(unit_normal_vect, to_sun)**3
if factor < 0:
factor *= 0.5
result = rgb + factor
clip_in_place(rgb + factor, 0, 1)
return result
def get_colormap_list(map_name="viridis", n_colors=9):
"""
Options for map_name:

View File

@@ -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

View File

@@ -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"]

View File

@@ -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,

View File

@@ -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:

View 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)

View File

@@ -1,7 +1,33 @@
import yaml
import os
from __future__ import annotations
def init_customization():
import os
import yaml
import inspect
import importlib
from typing import Any
from rich import box
from rich.rule import Rule
from rich.table import Table
from rich.console import Console
from rich.prompt import Prompt, Confirm
def get_manim_dir() -> 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: dict[str, Any]) -> None:
for key in list(dictionary.keys()):
if dictionary[key] == "":
dictionary.pop(key)
elif isinstance(dictionary[key], dict):
remove_empty_value(dictionary[key])
def init_customization() -> None:
configuration = {
"directories": {
"mirror_module_path": False,
@@ -24,6 +50,7 @@ def init_customization():
},
"window_position": "UR",
"window_monitor": 0,
"full_screen": False,
"break_into_partial_movies": False,
"camera_qualities": {
"low": {
@@ -46,41 +73,103 @@ def init_customization():
}
}
print("Initialize configuration")
scope = input(" Please select the scope of the configuration [global/local]: ")
if scope == "global":
from manimlib.config import get_manim_dir
file_name = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
else:
file_name = os.path.join(os.getcwd(), "custom_config.yml")
console = Console()
console.print(Rule("[bold]Configuration Guide[/bold]"))
# print("Initialize configuration")
try:
scope = Prompt.ask(
" Select the scope of the configuration",
choices=["global", "local"],
default="local"
)
print("\n directories:")
configuration["directories"]["output"] = input(" [1/8] Where should manim output video and image files place: ")
configuration["directories"]["raster_images"] = input(" [2/8] Which folder should manim find raster images (.jpg .png .gif) in (optional): ")
configuration["directories"]["vector_images"] = input(" [3/8] Which folder should manim find vector images (.svg .xdv) in (optional): ")
configuration["directories"]["sounds"] = input(" [4/8] Which folder should manim find sound files (.mp3 .wav) in (optional): ")
configuration["directories"]["temporary_storage"] = input(" [5/8] Which folder should manim storage temporary files: ")
console.print("[bold]Directories:[/bold]")
dir_config = configuration["directories"]
dir_config["output"] = Prompt.ask(
" Where should manim [bold]output[/bold] video and image files place [prompt.default](optional, default is none)",
default="",
show_default=False
)
dir_config["raster_images"] = Prompt.ask(
" Which folder should manim find [bold]raster images[/bold] (.jpg .png .gif) in "
"[prompt.default](optional, default is none)",
default="",
show_default=False
)
dir_config["vector_images"] = Prompt.ask(
" Which folder should manim find [bold]vector images[/bold] (.svg .xdv) in "
"[prompt.default](optional, default is none)",
default="",
show_default=False
)
dir_config["sounds"] = Prompt.ask(
" Which folder should manim find [bold]sound files[/bold] (.mp3 .wav) in "
"[prompt.default](optional, default is none)",
default="",
show_default=False
)
dir_config["temporary_storage"] = Prompt.ask(
" Which folder should manim storage [bold]temporary files[/bold] "
"[prompt.default](recommended, use system temporary folder by default)",
default="",
show_default=False
)
print("\n tex:")
tex = input(" [6/8] Which executable file to use to compile [latex/xelatex]: ")
if tex == "latex":
configuration["tex"]["executable"] = "latex"
configuration["tex"]["template_file"] = "tex_template.tex"
configuration["tex"]["intermediate_filetype"] = "dvi"
else:
configuration["tex"]["executable"] = "xelatex -no-pdf"
configuration["tex"]["template_file"] = "ctex_template.tex"
configuration["tex"]["intermediate_filetype"] = "xdv"
console.print("[bold]LaTeX:[/bold]")
tex_config = configuration["tex"]
tex = Prompt.ask(
" Select an executable program to use to compile a LaTeX source file",
choices=["latex", "xelatex"],
default="latex"
)
if tex == "latex":
tex_config["executable"] = "latex"
tex_config["template_file"] = "tex_template.tex"
tex_config["intermediate_filetype"] = "dvi"
else:
tex_config["executable"] = "xelatex -no-pdf"
tex_config["template_file"] = "ctex_template.tex"
tex_config["intermediate_filetype"] = "xdv"
console.print("[bold]Styles:[/bold]")
configuration["style"]["background_color"] = Prompt.ask(
" Which [bold]background color[/bold] do you want [italic](hex code)",
default="#333333"
)
print("\n style:")
configuration["style"]["background_color"] = input(" [7/8] Which background color do you want (hex code): ")
console.print("[bold]Camera qualities:[/bold]")
table = Table(
"low", "medium", "high", "ultra_high",
title="Four defined qualities",
box=box.ROUNDED
)
table.add_row("480p15", "720p30", "1080p60", "2160p60")
console.print(table)
configuration["camera_qualities"]["default_quality"] = Prompt.ask(
" Which one to choose as the default rendering quality",
choices=["low", "medium", "high", "ultra_high"],
default="high"
)
print("\n camera_qualities:")
print(" Four defined qualities: low: 480p15 medium: 720p30 high: 1080p60 ultra_high: 2160p60")
configuration["camera_qualities"]["default_quality"] = input(" [8/8] Which one to choose as the default rendering quality [low/medium/high/ultra_high]: ")
write_to_file = Confirm.ask(
"\n[bold]Are you sure to write these configs to file?[/bold]",
default=True
)
if not write_to_file:
raise KeyboardInterrupt
with open(file_name, 'w', encoding="utf_8") as file:
yaml.dump(configuration, file)
global_file_name = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
if scope == "global":
file_name = global_file_name
else:
if os.path.exists(global_file_name):
remove_empty_value(configuration)
file_name = os.path.join(os.getcwd(), "custom_config.yml")
with open(file_name, "w", encoding="utf-8") as f:
yaml.dump(configuration, f)
console.print(f"\n:rocket: You have successfully set up a {scope} configuration file!")
console.print(f"You can manually modify it in: [cyan]`{file_name}`[/cyan]")
print(f"\nYou have set up a {scope} configuration file")
print(f"You can manually modify it again in: {file_name}\n")
except KeyboardInterrupt:
console.print("\n[green]Exit configuration guide[/green]")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,34 +1,20 @@
from functools import reduce
import inspect
import numpy as np
import operator as op
import math
from functools import lru_cache
def sigmoid(x):
return 1.0 / (1 + np.exp(-x))
CHOOSE_CACHE = {}
@lru_cache(maxsize=10)
def choose(n, k):
return math.comb(n, k)
def choose_using_cache(n, r):
if n not in CHOOSE_CACHE:
CHOOSE_CACHE[n] = {}
if r not in CHOOSE_CACHE[n]:
CHOOSE_CACHE[n][r] = choose(n, r, use_cache=False)
return CHOOSE_CACHE[n][r]
def choose(n, r, use_cache=True):
if use_cache:
return choose_using_cache(n, r)
if n < r:
return 0
if r == 0:
return 1
denom = reduce(op.mul, range(1, r + 1), 1)
numer = reduce(op.mul, range(n, n - r, -1), 1)
return numer // denom
def gen_choose(n, r):
return np.prod(np.arange(n, n - r, -1)) / math.factorial(r)
def get_num_args(function):
@@ -53,14 +39,6 @@ def clip(a, min_a, max_a):
return a
def clip_in_place(array, min_val=None, max_val=None):
if max_val is not None:
array[array > max_val] = max_val
if min_val is not None:
array[array < min_val] = min_val
return array
def fdiv(a, b, zero_over_zero_value=None):
if zero_over_zero_value is not None:
out = np.full_like(a, zero_over_zero_value)

View File

@@ -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()],

View File

@@ -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
@@ -10,165 +18,22 @@ from manimlib.constants import OUT
from manimlib.constants import PI
from manimlib.constants import TAU
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.simple_functions import clip
def get_norm(vect):
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],
v1[0] * v2[1] - v1[1] * v2[0]
]
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 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
"""
diff = (angle_of_vector(v2) - angle_of_vector(v1)) % TAU
return min(diff, TAU - diff)
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
@@ -178,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)
@@ -186,15 +172,11 @@ def normalize_along_axis(array, axis, fall_back=None):
return array
def cross(v1, v2):
return np.array([
v1[1] * v2[2] - v1[2] * v2[1],
v1[2] * v2[0] - v1[0] * v2[2],
v1[0] * v2[1] - v1[1] * v2[0]
])
def get_unit_normal(v1, v2, tol=1e-6):
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)
@@ -212,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)
@@ -220,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
@@ -262,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
@@ -291,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.
@@ -306,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)
@@ -317,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]) +
@@ -332,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
"""
@@ -344,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
@@ -401,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)

View File

@@ -1,42 +0,0 @@
import re
import string
def to_camel_case(name):
return "".join([
[c for c in part if c not in string.punctuation + string.whitespace].capitalize()
for part in name.split("_")
])
def initials(name, sep_values=[" ", "_"]):
return "".join([
(s[0] if s else "")
for s in re.split("|".join(sep_values), name)
])
def camel_case_initials(name):
return [c for c in name if c.isupper()]
def complex_string(complex_num):
return [c for c in str(complex_num) if c not in "()"]
def split_string_to_isolate_substrings(full_string, *isolate):
"""
Given a string, and an arbitrary number of possible substrings,
to isolate, this returns a list of strings which would concatenate
to make the full string, and in which these special substrings
appear as their own elements.
For example,split_string_to_isolate_substrings("to be or not to be", "to", "be")
would return ["to", " ", "be", " or not ", "to", " ", "be"]
"""
pattern = "|".join(*(
"({})".format(re.escape(ss))
for ss in isolate
))
pieces = re.split(pattern, full_string)
return list(filter(lambda s: s, pieces))

View File

@@ -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

View File

@@ -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)

View File

@@ -1,4 +1,3 @@
argparse
colour
numpy
Pillow
@@ -9,13 +8,15 @@ mapbox-earcut
matplotlib
moderngl
moderngl_window
skia-pathops
pydub
pygments
pyyaml
rich
screeninfo
pyreadline; sys_platform == 'win32'
validators
ipython
PyOpenGL
manimpango>=0.2.0,<0.4.0
manimpango>=0.4.0.post0,<0.5.0
isosurfaces
svgelements

View File

@@ -1,6 +1,6 @@
[metadata]
name = manimgl
version = 1.2.0
version = 1.6.0
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
@@ -28,16 +39,18 @@ install_requires =
matplotlib
moderngl
moderngl_window
skia-pathops
pydub
pygments
pyyaml
rich
screeninfo
pyreadline; sys_platform == 'win32'
validators
ipython
PyOpenGL
manimpango>=0.2.0,<0.4.0
manimpango>=0.4.0.post0,<0.5.0
isosurfaces
svgelements
[options.entry_points]
console_scripts =