238 Commits

Author SHA1 Message Date
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
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
YishiMichael
0cab23b2ba Reorganize inheriting order of SVGMobject 2022-02-15 20:16:15 +08:00
Bill Xi
aef02bfcf9 changed hashing 2022-02-15 11:45:17 +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
f29ef87bba style/docs: fix argument help style and update docs for it 2022-02-14 19:50:30 +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
Elisha Hollander
f9351536e4 minor fixes (#1737) 2022-02-13 11:12:41 +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
63 changed files with 2216 additions and 962 deletions

View File

@@ -8,6 +8,11 @@ jobs:
deploy:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ["py36", "py37", "py38", "py39", "py310"]
steps:
- uses: actions/checkout@v2
@@ -20,11 +25,13 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine build
- name: Build and publish
- name: Build wheels
run: python setup.py bdist_wheel --python-tag ${{ matrix.python }}
- name: Upload wheels
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python -m build
twine upload dist/*

View File

@@ -1,63 +1,210 @@
Changelog
=========
v1.5.0
------
Fixed bugs
^^^^^^^^^^
- Bug fix for the case of calling ``Write`` on a null object (`#1740 <https://github.com/3b1b/manim/pull/1740>`__)
New Features
^^^^^^^^^^^^
- Added ``TransformMatchingMTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
- Added ``ImplicitFunction`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
- Added ``Polyline`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
- Allowed ``Mobject.set_points`` to take in an empty list, and added ``Mobject.add_point`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/a64259158538eae6043566aaf3d3329ff4ac394b>`__)
- Added ``Scene.refresh_locked_data`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/33d2894c167c577a15fdadbaf26488ff1f5bff87>`__)
- Added presenter mode to scenes with ``-p`` option (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9a9cc8bdacb7541b7cd4a52ad705abc21f3e27fe>`__ and `#1742 <https://github.com/3b1b/manim/pull/1742>`__)
- Allowed for an embed by hitting ``ctrl+shift+e`` during interaction (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9df12fcb7d8360e51cd7021d6877ca1a5c31835e>`__ and `#1746 <https://github.com/3b1b/manim/pull/1746>`__)
- Added ``Mobject.set_min_width/height/depth`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2798d15591a0375ae6bb9135473e6f5328267323>`__)
- Allowed ``Mobject.match_coord/x/y/z`` to take in a point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/29a4d3e82ba94c007c996b2d1d0f923941452698>`__)
- Added ``text_config`` to ``DecimalNumber`` (`#1744 <https://github.com/3b1b/manim/pull/1744>`__)
Refactor
^^^^^^^^
- Refactored ``MTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
- Refactored ``SVGMobject`` with svgelements (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
- Made sure ``ParametricCurve`` has at least one point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2488b9e866fb1ecb842a27dd9f4956ec167e3dee>`__)
- Set default to no tips on ``Axes`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/6c6d387a210756c38feca7d34838aa9ac99bb58a>`__)
- Stopped displaying when writing tex string is happening (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/58e06e8f6b7c5059ff315d51fd0018fec5cfbb05>`__)
- Reorganize inheriting order and refactor SVGMobject (`#1745 <https://github.com/3b1b/manim/pull/1745>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on ``isosurfaces`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
- Removed dependency on ``argparse`` since it's a built-in module (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
- Removed dependency on ``pyreadline`` (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
- Removed dependency on ``cssselect2`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
- Added dependency on ``svgelements`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
v1.4.1
------
Fixed bugs
^^^^^^^^^^
- Temporarily fixed boolean operations' bug (`#1724 <https://github.com/3b1b/manim/pull/1724>`__)
- Import ``Iterable`` from ``collections.abc`` instead of ``collections`` which is deprecated since python 3.9 (`d2e0811 <https://github.com/3b1b/manim/commit/d2e0811285f7908e71a65e664fec88b1af1c6144>`__)
v1.4.0
------
Fixed bugs
^^^^^^^^^^
- Temporarily fixed ``Lightbulb`` (`f1996f8 <https://github.com/3b1b/manim/pull/1697/commits/f1996f8479f9e33d626b3b66e9eb6995ce231d86>`__)
- Fixed some bugs of ``SVGMobject`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Fixed some bugs of SVG path string parser (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
- Fixed some bugs of ``MTex`` (`#1720 <https://github.com/3b1b/manim/pull/1720>`__)
New Features
^^^^^^^^^^^^
- Added option to add ticks on x-axis in ``BarChart`` (`#1694 <https://github.com/3b1b/manim/pull/1694>`__)
- Added ``lable_buff`` config parameter for ``Brace`` (`#1704 <https://github.com/3b1b/manim/pull/1704>`__)
- Added support for ``rotate skewX skewY`` transform in SVG (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Added style support to ``SVGMobject`` (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
- Added parser to <style> element of SVG (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
- Added support for <line> element in ``SVGMobject`` (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
Refactor
^^^^^^^^
- Used ``FFMPEG_BIN`` instead of ``"ffmpeg"`` for sound incorporation (`5aa8d15 <https://github.com/3b1b/manim/pull/1697/commits/5aa8d15d85797f68a8f169ca69fd90d441a3abbe>`__)
- Decorated ``CoordinateSystem.get_axes`` and ``.get_all_ranges`` as abstract method (`#1709 <https://github.com/3b1b/manim/pull/1709>`__)
- Refactored SVG path string parser (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Allowed ``Mobject.scale`` to receive iterable ``scale_factor`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Refactored ``MTex`` (`#1716 <https://github.com/3b1b/manim/pull/1716>`__)
- Improved config helper (``manimgl --config``) (`#1721 <https://github.com/3b1b/manim/pull/1721>`__)
- Refactored ``MTex`` (`#1723 <https://github.com/3b1b/manim/pull/1723>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on python package `cssselect2 <https://github.com/Kozea/cssselect2>`__ (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
v1.3.0
------
Fixed bugs
^^^^^^^^^^
- Fixed ``Mobject.stretch_to_fit_depth`` (`#1653 <https://github.com/3b1b/manim/pull/1653>`__)
- Fixed the bug of rotating camera (`#1655 <https://github.com/3b1b/manim/pull/1655>`__)
- Fixed ``SurfaceMesh`` to be evenly spaced (`c73d507 <https://github.com/3b1b/manim/pull/1688/commits/c73d507c76af5c8602d4118bc7538ba04c03ebae>`__)
- Fixed ``angle_between_vectors`` add ``rotation_between_vectors`` (`82bd02d <https://github.com/3b1b/manim/pull/1688/commits/82bd02d21fbd89b71baa21e077e143f440df9014>`__)
- Fixed ``VMobject.fade`` (`a717314 <https://github.com/3b1b/manim/pull/1688/commits/a7173142bf93fd309def0cc10f3c56f5e6972332>`__)
- Fixed ``angle_between_vectors`` (`fbc329d <https://github.com/3b1b/manim/pull/1688/commits/fbc329d7ce3b11821d47adf6052d932f7eff724a>`__)
- Fixed bug in ``ShowSubmobjectsOneByOne`` (`bcd0990 <https://github.com/3b1b/manim/pull/1688/commits/bcd09906bea5eaaa5352e7bee8f3153f434cf606>`__)
- Fixed bug in ``TransformMatchingParts`` (`7023548 <https://github.com/3b1b/manim/pull/1691/commits/7023548ec62c4adb2f371aab6a8c7f62deb7c33c>`__)
New Features
^^^^^^^^^^^^
- Added CLI flag ``--log-level`` to specify log level (`e10f850 <https://github.com/3b1b/manim/commit/e10f850d0d9f971931cc85d44befe67dc842af6d>`__)
- Added operations (``+`` and ``*``) for ``Mobject`` (`#1667 <https://github.com/3b1b/manim/pull/1667>`__)
- Added 4 boolean operations for ``VMobject`` in ``manimlib/mobject/boolean_ops.py`` (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
- ``Union(*vmobjects, **kwargs)``
- ``Difference(subject, clip, **kwargs)``
- ``Intersection(*vmobjects, **kwargs)``
- ``Exclusion(*vmobjects, **kwargs)``
- Added reflectiveness (`81c3ae3 <https://github.com/3b1b/manim/pull/1688/commits/81c3ae30372e288dc772633dbd17def6e603753e>`__)
- Enabled ``glow_factor`` on ``DotCloud`` (`2c7689e <https://github.com/3b1b/manim/pull/1688/commits/2c7689ed9e81229ce87c648f97f26267956c0bc9>`__)
- Added option ``-e`` to insert embed line from the command line (`d065e19 <https://github.com/3b1b/manim/pull/1688/commits/d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772>`__)
- Improved ``point_from_proportion`` to account for arc length (`0e78027 <https://github.com/3b1b/manim/pull/1688/commits/0e78027186a976f7e5fa8d586f586bf6e6baab8d>`__)
- Added shortcut ``set_backstroke`` for setting black background stroke (`781a993 <https://github.com/3b1b/manim/pull/1688/commits/781a9934fda6ba11f22ba32e8ccddcb3ba78592e>`__)
- Added ``Suface.always_sort_to_camera`` (`0b898a5 <https://github.com/3b1b/manim/pull/1688/commits/0b898a5594203668ed9cad38b490ab49ba233bd4>`__)
- Added getter methods for specific euler angles (`e899604 <https://github.com/3b1b/manim/pull/1688/commits/e899604a2d05f78202fcb3b9824ec34647237eae>`__)
- Hade ``rotation_between_vectors`` handle identical/similar vectors (`407c53f <https://github.com/3b1b/manim/pull/1688/commits/407c53f97c061bfd8a53beacd88af4c786f9e9ee>`__)
- Added ``Mobject.insert_submobject`` method (`49743da <https://github.com/3b1b/manim/pull/1688/commits/49743daf3244bfa11a427040bdde8e2bb79589e8>`__)
- Created single progress display for full scene render (`9dd1f47 <https://github.com/3b1b/manim/pull/1688/commits/9dd1f47dabca1580d6102e34e44574b0cba556e7>`__)
- Added ``Circle.get_radius`` (`264f7b1 <https://github.com/3b1b/manim/pull/1691/commits/264f7b11726e9e736f0fe472f66e38539f74e848>`__)
- Added ``Dodecahedron`` (`83841ae <https://github.com/3b1b/manim/pull/1691/commits/83841ae41568a9c9dff44cd163106c19a74ac281>`__)
- Added ``GlowDot`` (`a1d5147 <https://github.com/3b1b/manim/pull/1691/commits/a1d51474ea1ce3b7aa3efbe4c5e221be70ee2f5b>`__)
- Added ``MTex`` , see `#1678 <https://github.com/3b1b/manim/pull/1678>`__ for details (`#1678 <https://github.com/3b1b/manim/pull/1678>`__)
Refactor
^^^^^^^^
- Refactored support for command ``A`` in path of SVG (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
- Refactored ``SingleStringTex.balance_braces`` (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
- Slight tweaks to how saturation_factor works on newton-fractal (`8b454fb <https://github.com/3b1b/manim/pull/1688/commits/8b454fbe9335a7011e947093230b07a74ba9c653>`__)
- Made it possible to set full screen preview as a default (`317a5d6 <https://github.com/3b1b/manim/pull/1688/commits/317a5d6226475b6b54a78db7116c373ef84ea923>`__)
- Used ``quick_point_from_proportion`` for graph points (`e764da3 <https://github.com/3b1b/manim/pull/1688/commits/e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc>`__)
- Made sure ``Line.set_length`` returns self (`d2182b9 <https://github.com/3b1b/manim/pull/1688/commits/d2182b9112300558b6c074cefd685f97c10b3898>`__)
- Better align ``SurfaceMesh`` to the corresponding surface polygons (`eea3c6b <https://github.com/3b1b/manim/pull/1688/commits/eea3c6b29438f9e9325329c4355e76b9f635e97a>`__)
- Match ``fix_in_frame`` status for ``FlashAround`` mobject (`ee1594a <https://github.com/3b1b/manim/pull/1688/commits/ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3>`__)
- Made sure ``Mobject.is_fixed_in_frame`` stays updated with uniforms (`ba23fbe <https://github.com/3b1b/manim/pull/1688/commits/ba23fbe71e4a038201cd7df1d200514ed1c13bc2>`__)
- Made sure ``skip_animations`` and ``start_at_animation_number`` play well together (`98b0d26 <https://github.com/3b1b/manim/pull/1691/commits/98b0d266d2475926a606331923cca3dc1dea97ad>`__)
- Updated progress display for full scene render (`f8e6e7d <https://github.com/3b1b/manim/pull/1691/commits/f8e6e7df3ceb6f3d845ced4b690a85b35e0b8d00>`__)
- ``VectorizedPoint`` should call ``__init__`` for both super classes (`8f1dfab <https://github.com/3b1b/manim/pull/1691/commits/8f1dfabff04a8456f5c4df75b0f97d50b2755003>`__)
- Used array copy when checking need for refreshing triangulation (`758f329 <https://github.com/3b1b/manim/pull/1691/commits/758f329a06a0c198b27a48c577575d94554305bf>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__ (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
v1.2.0
------
Fixed bugs
^^^^^^^^^^
- `#1592 <https://github.com/3b1b/manim/pull/1592>`__: Fixed ``put_start_and_end_on`` in 3D
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Fixed ``DecimalNumber``'s scaling issue
- `56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__: Fixed bug with common range array used for all coordinate systems
- `8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__: Fixed ``CoordinateSystem`` init bug
- `0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__: Fixed bug for single-valued ``ValueTracker``
- `54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__: Fixed bug with SVG rectangles
- `d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__: Fixed ``DotCloud.set_radii``
- `b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__: Temporarily fixed bug for ``PMobject`` array resizing
- `5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__: Fixed ``match_style``
- `719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__: Fixed negative ``path_arc`` case
- `c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__: Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis``
- `7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__: Fixed ``ComplexPlane`` -i display bug
- Fixed ``put_start_and_end_on`` in 3D (`#1592 <https://github.com/3b1b/manim/pull/1592>`__)
- Fixed ``DecimalNumber``'s scaling issue (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
- Fixed bug with common range array used for all coordinate systems (`56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__)
- Fixed ``CoordinateSystem`` init bug (`8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__)
- Fixed bug for single-valued ``ValueTracker`` (`0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__)
- Fixed bug with SVG rectangles (`54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__)
- Fixed ``DotCloud.set_radii`` (`d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__)
- Temporarily fixed bug for ``PMobject`` array resizing (`b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__)
- Fixed ``match_style`` (`5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__)
- Fixed negative ``path_arc`` case (`719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__)
- Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis`` (`c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__)
- Fixed ``ComplexPlane`` -i display bug (`7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__)
New Features
^^^^^^^^^^^^
- `#1598 <https://github.com/3b1b/manim/pull/1598>`__: Supported the elliptical arc command ``A`` for ``SVGMobject``
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Added ``FlashyFadeIn``
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Save triangulation
- `#1625 <https://github.com/3b1b/manim/pull/1625>`__: Added new ``Code`` mobject
- `#1637 <https://github.com/3b1b/manim/pull/1637>`__: Add warnings and use rich to display log
- `bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__: Added ``VCube``
- `6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__: Supported ``ValueTracker`` to track vectors
- `3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__: Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject``
- `a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__: Added ``TracgTail``
- `acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__: Added ``Scene.point_to_mobject``
- `f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__: Added poly_fractal shader
- `b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__: Added kwargs to ``TipableVMobject.set_length``
- `17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__: Added ``Mobject.replicate``
- `33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__: Added mandelbrot_fractal shader
- `f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__: Saved state before each embed
- `e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__: Allowed releasing of Textures
- `14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__: Consolidated and renamed newton_fractal shader
- `6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__: Hade ``ImageMoject`` remember the filepath to the Image
- Supported the elliptical arc command ``A`` for ``SVGMobject`` (`#1598 <https://github.com/3b1b/manim/pull/1598>`__)
- Added ``FlashyFadeIn`` (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
- Save triangulation (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
- Added new ``Code`` mobject (`#1625 <https://github.com/3b1b/manim/pull/1625>`__)
- Add warnings and use rich to display log (`#1637 <https://github.com/3b1b/manim/pull/1637>`__)
- Added ``VCube`` (`bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__)
- Supported ``ValueTracker`` to track vectors (`6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__)
- Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject`` (`3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__)
- Added ``TracgTail`` (`a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__)
- Added ``Scene.point_to_mobject`` (`acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__)
- Added poly_fractal shader (`f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__)
- Added kwargs to ``TipableVMobject.set_length`` (`b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__)
- Added ``Mobject.replicate`` (`17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__)
- Added mandelbrot_fractal shader (`33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__)
- Saved state before each embed (`f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__)
- Allowed releasing of Textures (`e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__)
- Consolidated and renamed newton_fractal shader (`14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__)
- Hade ``ImageMoject`` remember the filepath to the Image (`6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__)
Refactor
^^^^^^^^
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Changed back to simpler ``Mobject.scale`` implementation
- `b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__: Simplified ``Square``
- `40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__: Removed unused parameter ``triangulation_locked``
- `8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__: Reimplemented ``Arrow``
- `d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__: Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default
- `7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__: Refactored to call ``_handle_scale_side_effects`` after scaling takes place
- `7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__: Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end``
- `0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__: Made sure framerate is 30 for previewed scenes
- `c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__: Pushed ``pixel_coords_to_space_coords`` to ``Window``
- `d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__: Refactored to pass tuples and not arrays to uniforms
- `9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__: Refactored to copy uniform arrays in ``Mobject.copy``
- `ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__: Added ``bounding_box`` as exceptional key to point_cloud mobject
- `329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__: Made sure stroke width is always a float
- Changed back to simpler ``Mobject.scale`` implementation (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
- Simplified ``Square`` (`b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__)
- Removed unused parameter ``triangulation_locked`` (`40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__)
- Reimplemented ``Arrow`` (`8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__)
- Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default (`d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__)
- Refactored to call ``_handle_scale_side_effects`` after scaling takes place (`7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__)
- Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end`` (`7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__)
- Made sure framerate is 30 for previewed scenes (`0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__)
- Pushed ``pixel_coords_to_space_coords`` to ``Window`` (`c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__)
- Refactored to pass tuples and not arrays to uniforms (`d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__)
- Refactored to copy uniform arrays in ``Mobject.copy`` (`9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__)
- Added ``bounding_box`` as exceptional key to point_cloud mobject (`ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__)
- Made sure stroke width is always a float (`329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__)
v1.1.0

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

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

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,7 @@ from manimlib.mobject.probability import *
from manimlib.mobject.shape_matchers import *
from manimlib.mobject.svg.brace import *
from manimlib.mobject.svg.drawings import *
from manimlib.mobject.svg.mtex_mobject import *
from manimlib.mobject.svg.svg_mobject import *
from manimlib.mobject.svg.tex_mobject import *
from manimlib.mobject.svg.text_mobject import *
@@ -66,4 +68,3 @@ from manimlib.utils.rate_functions import *
from manimlib.utils.simple_functions import *
from manimlib.utils.sounds import *
from manimlib.utils.space_ops import *
from manimlib.utils.strings import *

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,7 +1,6 @@
from manimlib.animation.animation import Animation
from manimlib.animation.composition import Succession
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.mobject import Group
from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.rate_functions import linear
@@ -147,7 +146,7 @@ class Write(DrawBorderThenFill):
else:
self.run_time = 2
if self.lag_ratio is None:
self.lag_ratio = min(4.0 / length, 0.2)
self.lag_ratio = min(4.0 / (length + 1.0), 0.2)
class ShowIncreasingSubsets(Animation):
@@ -174,16 +173,12 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
"int_func": np.ceil,
}
def __init__(self, group, **kwargs):
new_group = Group(*group)
super().__init__(new_group, **kwargs)
def update_submobject_list(self, index):
# N = len(self.all_submobs)
if index == 0:
self.mobject.set_submobjects([])
else:
self.mobject.set_submobjects(self.all_submobs[index - 1])
self.mobject.set_submobjects([self.all_submobs[index - 1]])
# TODO, this is broken...

View File

@@ -224,6 +224,8 @@ class FlashAround(VShowPassingFlash):
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
path = self.get_path(mobject)
if mobject.is_fixed_in_frame:
path.fix_in_frame()
path.insert_n_curves(self.n_inserted_curves)
path.set_points(path.get_points_without_null_curves())
path.set_stroke(self.color, self.stroke_width)

View File

@@ -17,7 +17,6 @@ class Broadcast(LaggedStart):
"remover": True,
"lag_ratio": 0.2,
"run_time": 3,
"remover": True,
}
def __init__(self, focal_point, **kwargs):

View File

@@ -1,16 +1,20 @@
import numpy as np
import itertools as it
from manimlib.animation.composition import AnimationGroup
from manimlib.animation.fading import FadeTransformPieces
from manimlib.animation.fading import FadeInFromPoint
from manimlib.animation.fading import FadeOutToPoint
from manimlib.animation.transform import ReplacementTransform
from manimlib.animation.transform import Transform
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Group
from manimlib.mobject.svg.mtex_mobject import MTex
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config
from manimlib.utils.iterables import remove_list_redundancies
class TransformMatchingParts(AnimationGroup):
@@ -68,10 +72,10 @@ class TransformMatchingParts(AnimationGroup):
anims.append(FadeTransformPieces(fade_source, fade_target, **kwargs))
else:
anims.append(FadeOutToPoint(
fade_source, fade_target.get_center(), **kwargs
fade_source, target_mobject.get_center(), **kwargs
))
anims.append(FadeInFromPoint(
fade_target.copy(), fade_source.get_center(), **kwargs
fade_target.copy(), mobject.get_center(), **kwargs
))
super().__init__(*anims)
@@ -139,3 +143,108 @@ class TransformMatchingTex(TransformMatchingParts):
@staticmethod
def get_mobject_key(mobject):
return mobject.get_tex()
class TransformMatchingMTex(AnimationGroup):
CONFIG = {
"key_map": dict(),
}
def __init__(self, source_mobject, target_mobject, **kwargs):
digest_config(self, kwargs)
assert isinstance(source_mobject, MTex)
assert isinstance(target_mobject, MTex)
anims = []
rest_source_submobs = source_mobject.submobjects.copy()
rest_target_submobs = target_mobject.submobjects.copy()
def add_anim_from(anim_class, func, source_attr, target_attr=None):
if target_attr is None:
target_attr = source_attr
source_parts = func(source_mobject, source_attr)
target_parts = func(target_mobject, target_attr)
filtered_source_parts = [
submob_part for submob_part in source_parts
if all([
submob in rest_source_submobs
for submob in submob_part
])
]
filtered_target_parts = [
submob_part for submob_part in target_parts
if all([
submob in rest_target_submobs
for submob in submob_part
])
]
if not (filtered_source_parts and filtered_target_parts):
return
anims.append(anim_class(
VGroup(*filtered_source_parts),
VGroup(*filtered_target_parts),
**kwargs
))
for submob in it.chain(*filtered_source_parts):
rest_source_submobs.remove(submob)
for submob in it.chain(*filtered_target_parts):
rest_target_submobs.remove(submob)
def get_submobs_from_keys(mobject, keys):
if not isinstance(keys, tuple):
keys = (keys,)
indices = []
for key in keys:
if isinstance(key, int):
indices.append(key)
elif isinstance(key, range):
indices.extend(key)
elif isinstance(key, str):
all_parts = mobject.get_parts_by_tex(key)
indices.extend(it.chain(*[
mobject.indices_of_part(part) for part in all_parts
]))
else:
raise TypeError(key)
return VGroup(VGroup(*[
mobject[i] for i in remove_list_redundancies(indices)
]))
for source_key, target_key in self.key_map.items():
add_anim_from(
ReplacementTransform, get_submobs_from_keys,
source_key, target_key
)
common_specified_substrings = sorted(list(
set(source_mobject.get_specified_substrings()).intersection(
target_mobject.get_specified_substrings()
)
), key=len, reverse=True)
for part_tex_string in common_specified_substrings:
add_anim_from(
FadeTransformPieces, MTex.get_parts_by_tex, part_tex_string
)
common_submob_tex_strings = {
source_submob.get_tex() for source_submob in source_mobject
}.intersection({
target_submob.get_tex() for target_submob in target_mobject
})
for tex_string in common_submob_tex_strings:
add_anim_from(
FadeTransformPieces,
lambda mobject, attr: VGroup(*[
VGroup(mob) for mob in mobject
if mob.get_tex() == attr
]),
tex_string
)
anims.append(FadeOutToPoint(
VGroup(*rest_source_submobs), target_mobject.get_center(), **kwargs
))
anims.append(FadeInFromPoint(
VGroup(*rest_target_submobs), source_mobject.get_center(), **kwargs
))
super().__init__(*anims)

View File

@@ -1,4 +1,5 @@
import moderngl
import math
from colour import Color
import OpenGL.GL as gl
@@ -67,7 +68,7 @@ class CameraFrame(Mobject):
added_rot_T = rotation_matrix_transpose(angle, axis)
new_rot_T = np.dot(curr_rot_T, added_rot_T)
Fz = new_rot_T[2]
phi = np.arccos(Fz[2])
phi = np.arccos(clip(Fz[2], -1, 1))
theta = angle_of_vector(Fz[:2]) + PI / 2
partial_rot_T = np.dot(
rotation_matrix_transpose(phi, RIGHT),
@@ -121,6 +122,15 @@ class CameraFrame(Mobject):
self.refresh_rotation_matrix()
return self
def get_theta(self):
return self.data["euler_angles"][0]
def get_phi(self):
return self.data["euler_angles"][1]
def get_gamma(self):
return self.data["euler_angles"][2]
def get_shape(self):
return (self.get_width(), self.get_height())
@@ -139,6 +149,16 @@ class CameraFrame(Mobject):
def get_focal_distance(self):
return self.focal_distance * self.get_height()
def get_implied_camera_location(self):
theta, phi, gamma = self.get_euler_angles()
dist = self.get_focal_distance()
x, y, z = self.get_center()
return (
x + dist * math.sin(theta) * math.sin(phi),
y - dist * math.cos(theta) * math.sin(phi),
z + dist * math.cos(phi)
)
def interpolate(self, *args, **kwargs):
super().interpolate(*args, **kwargs)
self.refresh_rotation_matrix()
@@ -194,20 +214,30 @@ class Camera(object):
fbo = self.get_fbo(ctx, 0)
else:
fbo = ctx.detect_framebuffer()
self.ctx = ctx
self.fbo = fbo
self.set_ctx_blending()
# For multisample antialiasing
fbo_msaa = self.get_fbo(ctx, self.samples)
fbo_msaa.use()
self.fbo_msaa = fbo_msaa
ctx.enable(moderngl.BLEND)
ctx.blend_func = (
def set_ctx_blending(self, enable=True):
if enable:
self.ctx.enable(moderngl.BLEND)
else:
self.ctx.disable(moderngl.BLEND)
self.ctx.blend_func = (
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
moderngl.ONE, moderngl.ONE
# moderngl.ONE, moderngl.ONE
)
self.ctx = ctx
self.fbo = fbo
self.fbo_msaa = fbo_msaa
def set_ctx_depth_test(self, enable=True):
if enable:
self.ctx.enable(moderngl.DEPTH_TEST)
else:
self.ctx.disable(moderngl.DEPTH_TEST)
def init_light_source(self):
self.light_source = Point(self.light_source_position)
@@ -297,6 +327,9 @@ class Camera(object):
def get_frame_center(self):
return self.frame.get_center()
def get_location(self):
return self.frame.get_implied_camera_location()
def resize_frame_shape(self, fixed_dimension=0):
"""
Changes frame_shape to match the aspect ratio
@@ -327,17 +360,11 @@ class Camera(object):
shader_wrapper = render_group["shader_wrapper"]
shader_program = render_group["prog"]
self.set_shader_uniforms(shader_program, shader_wrapper)
self.update_depth_test(shader_wrapper)
self.set_ctx_depth_test(shader_wrapper.depth_test)
render_group["vao"].render(int(shader_wrapper.render_primitive))
if render_group["single_use"]:
self.release_render_group(render_group)
def update_depth_test(self, shader_wrapper):
if shader_wrapper.depth_test:
self.ctx.enable(moderngl.DEPTH_TEST)
else:
self.ctx.disable(moderngl.DEPTH_TEST)
def get_render_group_list(self, mobject):
try:
return self.static_mobject_to_render_group_list[id(mobject)]
@@ -410,7 +437,7 @@ class Camera(object):
for name, path in shader_wrapper.texture_paths.items():
tid = self.get_texture_id(path)
shader[name].value = tid
for name, value in it.chain(shader_wrapper.uniforms.items(), self.perspective_uniforms.items()):
for name, value in it.chain(self.perspective_uniforms.items(), shader_wrapper.uniforms.items()):
try:
if isinstance(value, np.ndarray):
value = tuple(value)
@@ -427,14 +454,18 @@ class Camera(object):
anti_alias_width = self.anti_alias_width / (ph / fh)
# Orient light
rotation = frame.get_inverse_camera_rotation_matrix()
light_pos = self.light_source.get_location()
light_pos = np.dot(rotation, light_pos)
offset = frame.get_center()
light_pos = np.dot(
rotation, self.light_source.get_location() + offset
)
cam_pos = self.frame.get_implied_camera_location() # TODO
self.perspective_uniforms = {
"frame_shape": frame.get_shape(),
"anti_alias_width": anti_alias_width,
"camera_center": tuple(frame.get_center()),
"camera_offset": tuple(offset),
"camera_rotation": tuple(np.array(rotation).T.flatten()),
"camera_position": tuple(cam_pos),
"light_source_position": tuple(light_pos),
"focal_distance": frame.get_focal_distance(),
}
@@ -445,6 +476,8 @@ class Camera(object):
def get_texture_id(self, path):
if path not in self.path_to_texture:
if self.n_textures == 15: # I have no clue why this is needed
self.n_textures += 1
tid = self.n_textures
self.n_textures += 1
im = Image.open(path).convert("RGBA")
@@ -468,4 +501,5 @@ class Camera(object):
class ThreeDCamera(Camera):
CONFIG = {
"samples": 4,
"anti_alias_width": 0,
}

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

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

View File

@@ -1,3 +1,4 @@
from abc import abstractmethod
import numpy as np
import numbers
@@ -55,9 +56,11 @@ class CoordinateSystem():
def get_origin(self):
return self.c2p(*[0] * self.dimension)
@abstractmethod
def get_axes(self):
raise Exception("Not implemented")
@abstractmethod
def get_all_ranges(self):
raise Exception("Not implemented")
@@ -151,14 +154,14 @@ class CoordinateSystem():
else:
alpha = binary_search(
function=lambda a: self.point_to_coords(
graph.point_from_proportion(a)
graph.quick_point_from_proportion(a)
)[0],
target=x,
lower_bound=self.x_range[0],
upper_bound=self.x_range[1],
)
if alpha is not None:
return graph.point_from_proportion(alpha)
return graph.quick_point_from_proportion(alpha)
else:
return None
@@ -274,7 +277,7 @@ class CoordinateSystem():
class Axes(VGroup, CoordinateSystem):
CONFIG = {
"axis_config": {
"include_tip": True,
"include_tip": False,
"numbers_to_exclude": [0],
},
"x_axis_config": {},

View File

@@ -1,3 +1,5 @@
from isosurfaces import plot_isoline
from manimlib.constants import *
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config
@@ -42,6 +44,8 @@ class ParametricCurve(VMobject):
self.add_points_as_corners(points[1:])
if self.use_smoothing:
self.make_approximately_smooth()
if not self.has_points():
self.set_points([self.t_func(t_min)])
return self
@@ -68,3 +72,40 @@ class FunctionGraph(ParametricCurve):
def get_point_from_function(self, x):
return self.t_func(x)
class ImplicitFunction(VMobject):
CONFIG = {
"x_range": [-FRAME_X_RADIUS, FRAME_X_RADIUS],
"y_range": [-FRAME_Y_RADIUS, FRAME_Y_RADIUS],
"min_depth": 5,
"max_quads": 1500,
"use_smoothing": True
}
def __init__(self, func, x_range=None, y_range=None, **kwargs):
digest_config(self, kwargs)
self.function = func
super().__init__(**kwargs)
def init_points(self):
p_min, p_max = (
np.array([self.x_range[0], self.y_range[0]]),
np.array([self.x_range[1], self.y_range[1]]),
)
curves = plot_isoline(
fn=lambda u: self.function(u[0], u[1]),
pmin=p_min,
pmax=p_max,
min_depth=self.min_depth,
max_quads=self.max_quads,
) # returns a list of lists of 2D points
curves = [
np.pad(curve, [(0, 0), (0, 1)]) for curve in curves if curve != []
] # add z coord as 0
for curve in curves:
self.start_new_path(curve[0])
self.add_points_as_corners(curve[1:])
if self.use_smoothing:
self.make_smooth()
return self

View File

@@ -305,6 +305,9 @@ class Circle(Arc):
(angle - start_angle) / TAU
)
def get_radius(self):
return get_norm(self.get_start() - self.get_center())
class Dot(Circle):
CONFIG = {
@@ -507,6 +510,7 @@ class Line(TipableVMobject):
def set_length(self, length, **kwargs):
self.scale(length / self.get_length(), **kwargs)
return self
class DashedLine(Line):
@@ -845,6 +849,11 @@ class Polygon(VMobject):
return self
class Polyline(Polygon):
def init_points(self):
self.set_points_as_corners(self.vertices)
class RegularPolygon(Polygon):
CONFIG = {
"start_angle": None,

View File

@@ -112,7 +112,7 @@ class Matrix(VMobject):
"\\left[",
"\\begin{array}{c}",
*height * ["\\quad \\\\"],
"\\end{array}"
"\\end{array}",
"\\right]",
]))[0]
bracket_pair.set_height(

View File

@@ -4,6 +4,7 @@ import random
import sys
import moderngl
from functools import wraps
from collections.abc import Iterable
import numpy as np
@@ -43,10 +44,13 @@ class Mobject(object):
"opacity": 1,
"dim": 3, # TODO, get rid of this
# Lighting parameters
# Positive gloss up to 1 makes it reflect the light.
"gloss": 0.0,
# Positive shadow up to 1 makes a side opposite the light darker
# ...
# Larger reflectiveness makes things brighter when facing the light
"reflectiveness": 0.0,
# Larger shadow makes faces opposite the light darker
"shadow": 0.0,
# Makes parts bright where light gets reflected toward the camera
"gloss": 0.0,
# For shaders
"shader_folder": "",
"render_primitive": moderngl.TRIANGLE_STRIP,
@@ -82,6 +86,14 @@ class Mobject(object):
def __str__(self):
return self.__class__.__name__
def __add__(self, other: 'Mobject') -> 'Mobject':
assert(isinstance(other, Mobject))
return self.get_group_class()(self, other)
def __mul__(self, other: 'int') -> 'Mobject':
assert(isinstance(other, int))
return self.replicate(other)
def init_data(self):
self.data = {
"points": np.zeros((0, 3)),
@@ -94,6 +106,7 @@ class Mobject(object):
"is_fixed_in_frame": float(self.is_fixed_in_frame),
"gloss": self.gloss,
"shadow": self.shadow,
"reflectiveness": self.reflectiveness,
}
def init_colors(self):
@@ -145,6 +158,7 @@ class Mobject(object):
for mob in self.get_family():
for key in mob.data:
mob.data[key] = mob.data[key][::-1]
self.refresh_unit_normal()
return self
def apply_points_function(self, func, about_point=None, about_edge=ORIGIN, works_on_bounding_box=False):
@@ -300,6 +314,11 @@ class Mobject(object):
self.assemble_family()
return self
def insert_submobject(self, index, new_submob):
self.submobjects.insert(index, new_submob)
self.assemble_family()
return self
def set_submobjects(self, submobject_list):
self.remove(*self.submobjects)
self.add(*submobject_list)
@@ -386,6 +405,7 @@ class Mobject(object):
self.submobjects.sort(key=submob_func)
else:
self.submobjects.sort(key=lambda m: point_to_num_func(m.get_center()))
self.assemble_family()
return self
def shuffle(self, recurse=False):
@@ -393,6 +413,7 @@ class Mobject(object):
for submob in self.submobjects:
submob.shuffle(recurse=True)
random.shuffle(self.submobjects)
self.assemble_family()
return self
# Copying
@@ -576,7 +597,10 @@ class Mobject(object):
Otherwise, if about_point is given a value, scaling is done with
respect to that point.
"""
scale_factor = max(scale_factor, min_scale_factor)
if isinstance(scale_factor, Iterable):
scale_factor = np.array(scale_factor).clip(min=min_scale_factor)
else:
scale_factor = max(scale_factor, min_scale_factor)
self.apply_points_function(
lambda points: scale_factor * points,
about_point=about_point,
@@ -767,7 +791,7 @@ class Mobject(object):
return self.rescale_to_fit(height, 1, stretch=True, **kwargs)
def stretch_to_fit_depth(self, depth, **kwargs):
return self.rescale_to_fit(depth, 1, stretch=True, **kwargs)
return self.rescale_to_fit(depth, 2, stretch=True, **kwargs)
def set_width(self, width, stretch=False, **kwargs):
return self.rescale_to_fit(width, 0, stretch=stretch, **kwargs)
@@ -793,6 +817,21 @@ class Mobject(object):
self.set_depth(max_depth, **kwargs)
return self
def set_min_width(self, min_width, **kwargs):
if self.get_width() < min_width:
self.set_width(min_width, **kwargs)
return self
def set_min_height(self, min_height, **kwargs):
if self.get_height() < min_height:
self.set_height(min_height, **kwargs)
return self
def set_min_depth(self, min_depth, **kwargs):
if self.get_depth() < min_depth:
self.set_depth(min_depth, **kwargs)
return self
def set_coord(self, value, dim, direction=ORIGIN):
curr = self.get_coord(dim, direction)
shift_vect = np.zeros(self.dim)
@@ -966,12 +1005,12 @@ class Mobject(object):
def fade(self, darkness=0.5, recurse=True):
self.set_opacity(1.0 - darkness, recurse=recurse)
def get_gloss(self):
return self.uniforms["gloss"]
def get_reflectiveness(self):
return self.uniforms["reflectiveness"]
def set_gloss(self, gloss, recurse=True):
def set_reflectiveness(self, reflectiveness, recurse=True):
for mob in self.get_family(recurse):
mob.uniforms["gloss"] = gloss
mob.uniforms["reflectiveness"] = reflectiveness
return self
def get_shadow(self):
@@ -982,6 +1021,14 @@ class Mobject(object):
mob.uniforms["shadow"] = shadow
return self
def get_gloss(self):
return self.uniforms["gloss"]
def set_gloss(self, gloss, recurse=True):
for mob in self.get_family(recurse):
mob.uniforms["gloss"] = gloss
return self
# Background rectangle
def add_background_rectangle(self, color=None, opacity=0.75, **kwargs):
@@ -1149,21 +1196,21 @@ class Mobject(object):
def match_depth(self, mobject, **kwargs):
return self.match_dim_size(mobject, 2, **kwargs)
def match_coord(self, mobject, dim, direction=ORIGIN):
return self.set_coord(
mobject.get_coord(dim, direction),
dim=dim,
direction=direction,
)
def match_coord(self, mobject_or_point, dim, direction=ORIGIN):
if isinstance(mobject_or_point, Mobject):
coord = mobject_or_point.get_coord(dim, direction)
else:
coord = mobject_or_point[dim]
return self.set_coord(coord, dim=dim, direction=direction)
def match_x(self, mobject, direction=ORIGIN):
return self.match_coord(mobject, 0, direction)
def match_x(self, mobject_or_point, direction=ORIGIN):
return self.match_coord(mobject_or_point, 0, direction)
def match_y(self, mobject, direction=ORIGIN):
return self.match_coord(mobject, 1, direction)
def match_y(self, mobject_or_point, direction=ORIGIN):
return self.match_coord(mobject_or_point, 1, direction)
def match_z(self, mobject, direction=ORIGIN):
return self.match_coord(mobject, 2, direction)
def match_z(self, mobject_or_point, direction=ORIGIN):
return self.match_coord(mobject_or_point, 2, direction)
def align_to(self, mobject_or_point, direction=ORIGIN):
"""
@@ -1363,11 +1410,13 @@ class Mobject(object):
@affects_shader_info_id
def fix_in_frame(self):
self.uniforms["is_fixed_in_frame"] = 1.0
self.is_fixed_in_frame = True
return self
@affects_shader_info_id
def unfix_from_frame(self):
self.uniforms["is_fixed_in_frame"] = 0.0
self.is_fixed_in_frame = False
return self
@affects_shader_info_id
@@ -1610,6 +1659,10 @@ class Group(Mobject):
Mobject.__init__(self, **kwargs)
self.add(*mobjects)
def __add__(self, other: 'Mobject' or 'Group'):
assert(isinstance(other, Mobject))
return self.add(other)
class Point(Mobject):
CONFIG = {

View File

@@ -2,6 +2,7 @@ from manimlib.constants import *
from manimlib.mobject.svg.tex_mobject import SingleStringTex
from manimlib.mobject.svg.text_mobject import Text
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.iterables import hash_obj
string_to_mob_map = {}
@@ -20,23 +21,23 @@ class DecimalNumber(VMobject):
"include_background_rectangle": False,
"edge_to_fix": LEFT,
"font_size": 48,
"text_config": {} # Do not pass in font_size here
}
def __init__(self, number=0, **kwargs):
super().__init__(**kwargs)
self.set_submobjects_from_number(number)
self.init_colors()
def set_submobjects_from_number(self, number):
self.number = number
self.set_submobjects([])
string_to_mob_ = lambda s: self.string_to_mob(s, **self.text_config)
num_string = self.get_num_string(number)
self.add(*map(self.string_to_mob, num_string))
self.add(*map(string_to_mob_, num_string))
# Add non-numerical bits
if self.show_ellipsis:
dots = self.string_to_mob("...")
dots = string_to_mob_("...")
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
self.add(dots)
if self.unit is not None:
@@ -85,10 +86,10 @@ class DecimalNumber(VMobject):
def get_font_size(self):
return self.data["font_size"][0]
def string_to_mob(self, string, mob_class=Text):
if string not in string_to_mob_map:
string_to_mob_map[string] = mob_class(string, font_size=1)
mob = string_to_mob_map[string].copy()
def string_to_mob(self, string, mob_class=Text, **kwargs):
if (string, hash_obj(kwargs)) not in string_to_mob_map:
string_to_mob_map[(string, hash_obj(kwargs))] = mob_class(string, font_size=1, **kwargs)
mob = string_to_mob_map[(string, hash_obj(kwargs))].copy()
mob.scale(self.get_font_size())
return mob

View File

@@ -149,7 +149,9 @@ class BarChart(VGroup):
"height": 4,
"width": 6,
"n_ticks": 4,
"include_x_ticks": False,
"tick_width": 0.2,
"tick_height": 0.15,
"label_y_axis": True,
"y_axis_label_height": 0.25,
"max_value": 1,
@@ -165,6 +167,7 @@ class BarChart(VGroup):
if self.max_value is None:
self.max_value = max(values)
self.n_ticks_x = len(values)
self.add_axes()
self.add_bars(values)
self.center()
@@ -172,31 +175,42 @@ class BarChart(VGroup):
def add_axes(self):
x_axis = Line(self.tick_width * LEFT / 2, self.width * RIGHT)
y_axis = Line(MED_LARGE_BUFF * DOWN, self.height * UP)
ticks = VGroup()
y_ticks = VGroup()
heights = np.linspace(0, self.height, self.n_ticks + 1)
values = np.linspace(0, self.max_value, self.n_ticks + 1)
for y, value in zip(heights, values):
tick = Line(LEFT, RIGHT)
tick.set_width(self.tick_width)
tick.move_to(y * UP)
ticks.add(tick)
y_axis.add(ticks)
y_tick = Line(LEFT, RIGHT)
y_tick.set_width(self.tick_width)
y_tick.move_to(y * UP)
y_ticks.add(y_tick)
y_axis.add(y_ticks)
if self.include_x_ticks == True:
x_ticks = VGroup()
widths = np.linspace(0, self.width, self.n_ticks_x + 1)
label_values = np.linspace(0, len(self.bar_names), self.n_ticks_x + 1)
for x, value in zip(widths, label_values):
x_tick = Line(UP, DOWN)
x_tick.set_height(self.tick_height)
x_tick.move_to(x * RIGHT)
x_ticks.add(x_tick)
x_axis.add(x_ticks)
self.add(x_axis, y_axis)
self.x_axis, self.y_axis = x_axis, y_axis
if self.label_y_axis:
labels = VGroup()
for tick, value in zip(ticks, values):
for y_tick, value in zip(y_ticks, values):
label = Tex(str(np.round(value, 2)))
label.set_height(self.y_axis_label_height)
label.next_to(tick, LEFT, SMALL_BUFF)
label.next_to(y_tick, LEFT, SMALL_BUFF)
labels.add(label)
self.y_axis_labels = labels
self.add(labels)
def add_bars(self, values):
buff = float(self.width) / (2 * len(values) + 1)
buff = float(self.width) / (2 * len(values))
bars = VGroup()
for i, value in enumerate(values):
bar = Rectangle(
@@ -205,7 +219,7 @@ class BarChart(VGroup):
stroke_width=self.bar_stroke_width,
fill_opacity=self.bar_fill_opacity,
)
bar.move_to((2 * i + 1) * buff * RIGHT, DOWN + LEFT)
bar.move_to((2 * i + 0.5) * buff * RIGHT, DOWN + LEFT * 5)
bars.add(bar)
bars.set_color_by_gradient(*self.bar_colors)

View File

@@ -1,5 +1,6 @@
import numpy as np
import math
import copy
from manimlib.animation.composition import AnimationGroup
from manimlib.constants import *
@@ -88,6 +89,7 @@ class BraceLabel(VMobject):
CONFIG = {
"label_constructor": Tex,
"label_scale": 1,
"label_buff": DEFAULT_MOBJECT_TO_MOBJECT_BUFFER
}
def __init__(self, obj, text, brace_direction=DOWN, **kwargs):
@@ -104,7 +106,7 @@ class BraceLabel(VMobject):
if self.label_scale != 1:
self.label.scale(self.label_scale)
self.brace.put_at_tip(self.label)
self.brace.put_at_tip(self.label, buff=self.label_buff)
self.set_submobjects([self.brace, self.label])
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):

View File

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

View File

@@ -0,0 +1,603 @@
import itertools as it
import re
from types import MethodType
from manimlib.constants import WHITE
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.color import color_to_int_rgb
from manimlib.utils.config_ops import digest_config
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.iterables import remove_list_redundancies
from manimlib.utils.tex_file_writing import tex_to_svg_file
from manimlib.utils.tex_file_writing import get_tex_config
from manimlib.utils.tex_file_writing import display_during_execution
from manimlib.logger import log
SCALE_FACTOR_PER_FONT_POINT = 0.001
def _get_neighbouring_pairs(iterable):
return list(adjacent_pairs(iterable))[:-1]
class _TexParser(object):
def __init__(self, tex_string, additional_substrings):
self.tex_string = tex_string
self.whitespace_indices = self.get_whitespace_indices()
self.backslash_indices = self.get_backslash_indices()
self.script_indices = self.get_script_indices()
self.brace_indices_dict = self.get_brace_indices_dict()
self.tex_span_list = []
self.script_span_to_char_dict = {}
self.script_span_to_tex_span_dict = {}
self.neighbouring_script_span_pairs = []
self.specified_substrings = []
self.add_tex_span((0, len(tex_string)))
self.break_up_by_scripts()
self.break_up_by_double_braces()
self.break_up_by_additional_substrings(additional_substrings)
self.tex_span_list.sort(key=lambda t: (t[0], -t[1]))
self.specified_substrings = remove_list_redundancies(
self.specified_substrings
)
self.containing_labels_dict = self.get_containing_labels_dict()
def add_tex_span(self, tex_span):
if tex_span not in self.tex_span_list:
self.tex_span_list.append(tex_span)
def get_whitespace_indices(self):
return [
match_obj.start()
for match_obj in re.finditer(r"\s", self.tex_string)
]
def get_backslash_indices(self):
# Newlines (`\\`) don't count.
return [
match_obj.end() - 1
for match_obj in re.finditer(r"\\+", self.tex_string)
if len(match_obj.group()) % 2 == 1
]
def filter_out_escaped_characters(self, indices):
return list(filter(
lambda index: index - 1 not in self.backslash_indices,
indices
))
def get_script_indices(self):
return self.filter_out_escaped_characters([
match_obj.start()
for match_obj in re.finditer(r"[_^]", self.tex_string)
])
def get_brace_indices_dict(self):
tex_string = self.tex_string
indices = self.filter_out_escaped_characters([
match_obj.start()
for match_obj in re.finditer(r"[{}]", tex_string)
])
result = {}
left_brace_indices_stack = []
for index in indices:
if tex_string[index] == "{":
left_brace_indices_stack.append(index)
else:
left_brace_index = left_brace_indices_stack.pop()
result[left_brace_index] = index
return result
def break_up_by_scripts(self):
# Match subscripts & superscripts.
tex_string = self.tex_string
whitespace_indices = self.whitespace_indices
brace_indices_dict = self.brace_indices_dict
script_spans = []
for script_index in self.script_indices:
script_char = tex_string[script_index]
extended_begin = script_index
while extended_begin - 1 in whitespace_indices:
extended_begin -= 1
script_begin = script_index + 1
while script_begin in whitespace_indices:
script_begin += 1
if script_begin in brace_indices_dict.keys():
script_end = brace_indices_dict[script_begin] + 1
else:
pattern = re.compile(r"[a-zA-Z0-9]|\\[a-zA-Z]+")
match_obj = pattern.match(tex_string, pos=script_begin)
if not match_obj:
script_name = {
"_": "subscript",
"^": "superscript"
}[script_char]
log.warning(
f"Unclear {script_name} detected while parsing. "
"Please use braces to clarify"
)
continue
script_end = match_obj.end()
tex_span = (script_begin, script_end)
script_span = (extended_begin, script_end)
script_spans.append(script_span)
self.add_tex_span(tex_span)
self.script_span_to_char_dict[script_span] = script_char
self.script_span_to_tex_span_dict[script_span] = tex_span
if not script_spans:
return
_, sorted_script_spans = zip(*sorted([
(index, script_span)
for script_span in script_spans
for index in script_span
]))
for span_0, span_1 in _get_neighbouring_pairs(sorted_script_spans):
if span_0[1] == span_1[0]:
self.neighbouring_script_span_pairs.append((span_0, span_1))
def break_up_by_double_braces(self):
# Match paired double braces (`{{...}}`).
tex_string = self.tex_string
reversed_indices_dict = dict(
item[::-1] for item in self.brace_indices_dict.items()
)
skip = False
for prev_right_index, right_index in _get_neighbouring_pairs(
list(reversed_indices_dict.keys())
):
if skip:
skip = False
continue
if right_index != prev_right_index + 1:
continue
left_index = reversed_indices_dict[right_index]
prev_left_index = reversed_indices_dict[prev_right_index]
if left_index != prev_left_index - 1:
continue
tex_span = (left_index, right_index + 1)
self.add_tex_span(tex_span)
self.specified_substrings.append(tex_string[slice(*tex_span)])
skip = True
def break_up_by_additional_substrings(self, additional_substrings):
stripped_substrings = sorted(remove_list_redundancies([
string.strip()
for string in additional_substrings
]))
if "" in stripped_substrings:
stripped_substrings.remove("")
tex_string = self.tex_string
all_tex_spans = []
for string in stripped_substrings:
match_objs = list(re.finditer(re.escape(string), tex_string))
if not match_objs:
continue
self.specified_substrings.append(string)
for match_obj in match_objs:
all_tex_spans.append(match_obj.span())
former_script_spans_dict = dict([
script_span_pair[0][::-1]
for script_span_pair in self.neighbouring_script_span_pairs
])
for span_begin, span_end in all_tex_spans:
# Deconstruct spans containing one out of two scripts.
if span_end in former_script_spans_dict.keys():
span_end = former_script_spans_dict[span_end]
if span_begin >= span_end:
continue
self.add_tex_span((span_begin, span_end))
def get_containing_labels_dict(self):
tex_span_list = self.tex_span_list
result = {
tex_span: []
for tex_span in tex_span_list
}
overlapping_tex_span_pairs = []
for index_0, span_0 in enumerate(tex_span_list):
for index_1, span_1 in enumerate(tex_span_list[index_0:]):
if span_0[1] <= span_1[0]:
continue
if span_0[1] < span_1[1]:
overlapping_tex_span_pairs.append((span_0, span_1))
result[span_0].append(index_0 + index_1)
if overlapping_tex_span_pairs:
tex_string = self.tex_string
log.error("Partially overlapping substrings detected:")
for tex_span_pair in overlapping_tex_span_pairs:
log.error(", ".join(
f"\"{tex_string[slice(*tex_span)]}\""
for tex_span in tex_span_pair
))
raise ValueError
return result
def get_labelled_tex_string(self):
indices, _, flags, labels = zip(*sorted([
(*tex_span[::(1, -1)[flag]], flag, label)
for label, tex_span in enumerate(self.tex_span_list)
for flag in range(2)
], key=lambda t: (t[0], -t[2], -t[1])))
command_pieces = [
("{{" + self.get_color_command(label), "}}")[flag]
for flag, label in zip(flags, labels)
][1:-1]
command_pieces.insert(0, "")
string_pieces = [
self.tex_string[slice(*tex_span)]
for tex_span in _get_neighbouring_pairs(indices)
]
return "".join(it.chain(*zip(command_pieces, string_pieces)))
@staticmethod
def get_color_command(label):
rg, b = divmod(label, 256)
r, g = divmod(rg, 256)
return "".join([
"\\color[RGB]",
"{",
",".join(map(str, (r, g, b))),
"}"
])
def get_sorted_submob_indices(self, submob_labels):
def script_span_to_submob_range(script_span):
tex_span = self.script_span_to_tex_span_dict[script_span]
submob_indices = [
index for index, label in enumerate(submob_labels)
if label in self.containing_labels_dict[tex_span]
]
return range(submob_indices[0], submob_indices[-1] + 1)
filtered_script_span_pairs = filter(
lambda script_span_pair: all([
self.script_span_to_char_dict[script_span] == character
for script_span, character in zip(script_span_pair, "_^")
]),
self.neighbouring_script_span_pairs
)
switch_range_pairs = sorted([
tuple([
script_span_to_submob_range(script_span)
for script_span in script_span_pair
])
for script_span_pair in filtered_script_span_pairs
], key=lambda t: (t[0].stop, -t[0].start))
result = list(range(len(submob_labels)))
for range_0, range_1 in switch_range_pairs:
result = [
*result[:range_1.start],
*result[range_0.start:range_0.stop],
*result[range_1.stop:range_0.start],
*result[range_1.start:range_1.stop],
*result[range_0.stop:]
]
return result
def get_submob_tex_strings(self, submob_labels):
ordered_tex_spans = [
self.tex_span_list[label] for label in submob_labels
]
ordered_containing_labels = [
self.containing_labels_dict[tex_span]
for tex_span in ordered_tex_spans
]
ordered_span_begins, ordered_span_ends = zip(*ordered_tex_spans)
string_span_begins = [
prev_end if prev_label in containing_labels else curr_begin
for prev_end, prev_label, containing_labels, curr_begin in zip(
ordered_span_ends[:-1], submob_labels[:-1],
ordered_containing_labels[1:], ordered_span_begins[1:]
)
]
string_span_begins.insert(0, ordered_span_begins[0])
string_span_ends = [
next_begin if next_label in containing_labels else curr_end
for next_begin, next_label, containing_labels, curr_end in zip(
ordered_span_begins[1:], submob_labels[1:],
ordered_containing_labels[:-1], ordered_span_ends[:-1]
)
]
string_span_ends.append(ordered_span_ends[-1])
tex_string = self.tex_string
left_brace_indices = sorted(self.brace_indices_dict.keys())
right_brace_indices = sorted(self.brace_indices_dict.values())
ignored_indices = sorted(it.chain(
self.whitespace_indices,
left_brace_indices,
right_brace_indices,
self.script_indices
))
result = []
for span_begin, span_end in zip(string_span_begins, string_span_ends):
while span_begin in ignored_indices:
span_begin += 1
if span_begin >= span_end:
result.append("")
continue
while span_end - 1 in ignored_indices:
span_end -= 1
unclosed_left_brace = 0
unclosed_right_brace = 0
for index in range(span_begin, span_end):
if index in left_brace_indices:
unclosed_left_brace += 1
elif index in right_brace_indices:
if unclosed_left_brace == 0:
unclosed_right_brace += 1
else:
unclosed_left_brace -= 1
result.append("".join([
unclosed_right_brace * "{",
tex_string[span_begin:span_end],
unclosed_left_brace * "}"
]))
return result
def find_span_components_of_custom_span(self, custom_span):
skipped_indices = sorted(it.chain(
self.whitespace_indices,
self.script_indices
))
tex_span_choices = sorted(filter(
lambda tex_span: all([
tex_span[0] >= custom_span[0],
tex_span[1] <= custom_span[1]
]),
self.tex_span_list
))
# Choose spans that reach the farthest.
tex_span_choices_dict = dict(tex_span_choices)
span_begin, span_end = custom_span
result = []
while span_begin != span_end:
if span_begin not in tex_span_choices_dict.keys():
if span_begin in skipped_indices:
span_begin += 1
continue
return None
next_begin = tex_span_choices_dict[span_begin]
result.append((span_begin, next_begin))
span_begin = next_begin
return result
def get_containing_labels_by_tex_spans(self, tex_spans):
return remove_list_redundancies(list(it.chain(*[
self.containing_labels_dict[tex_span]
for tex_span in tex_spans
])))
def get_specified_substrings(self):
return self.specified_substrings
def get_isolated_substrings(self):
return remove_list_redundancies([
self.tex_string[slice(*tex_span)]
for tex_span in self.tex_span_list
])
class _TexSVG(SVGMobject):
CONFIG = {
"height": None,
"fill_opacity": 1.0,
"stroke_width": 0,
"path_string_config": {
"should_subdivide_sharp_curves": True,
"should_remove_null_curves": True,
},
}
class MTex(_TexSVG):
CONFIG = {
"color": WHITE,
"font_size": 48,
"alignment": "\\centering",
"tex_environment": "align*",
"isolate": [],
"tex_to_color_map": {},
"use_plain_tex": False,
}
def __init__(self, tex_string, **kwargs):
digest_config(self, kwargs)
tex_string = tex_string.strip()
# Prevent from passing an empty string.
if not tex_string:
tex_string = "\\quad"
self.tex_string = tex_string
self.parser = _TexParser(
self.tex_string,
[*self.tex_to_color_map.keys(), *self.isolate]
)
super().__init__(**kwargs)
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
@property
def hash_seed(self):
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.tex_string,
self.parser.specified_substrings,
self.alignment,
self.tex_environment,
self.use_plain_tex
)
def get_file_path(self):
return self._get_file_path(self.use_plain_tex)
def _get_file_path(self, use_plain_tex):
if use_plain_tex:
tex_string = self.tex_string
else:
tex_string = self.parser.get_labelled_tex_string()
full_tex = self.get_tex_file_body(tex_string)
with display_during_execution(f"Writing \"{self.tex_string}\""):
file_path = self.tex_to_svg_file_path(full_tex)
return file_path
def get_tex_file_body(self, tex_string):
if self.tex_environment:
tex_string = "\n".join([
f"\\begin{{{self.tex_environment}}}",
tex_string,
f"\\end{{{self.tex_environment}}}"
])
if self.alignment:
tex_string = "\n".join([self.alignment, tex_string])
tex_config = get_tex_config()
return tex_config["tex_body"].replace(
tex_config["text_to_replace"],
tex_string
)
@staticmethod
def tex_to_svg_file_path(tex_file_content):
return tex_to_svg_file(tex_file_content)
def generate_mobject(self):
super().generate_mobject()
if not self.use_plain_tex:
labelled_svg_glyphs = self
else:
file_path = self._get_file_path(use_plain_tex=False)
labelled_svg_glyphs = _TexSVG(file_path)
glyph_labels = [
self.color_to_label(labelled_glyph.get_fill_color())
for labelled_glyph in labelled_svg_glyphs
]
mob = self.build_mobject(self, glyph_labels)
self.set_submobjects(mob.submobjects)
@staticmethod
def color_to_label(color):
r, g, b = color_to_int_rgb(color)
rg = r * 256 + g
return rg * 256 + b
def build_mobject(self, svg_glyphs, glyph_labels):
if not svg_glyphs:
return VGroup()
# Simply pack together adjacent mobjects with the same label.
submobjects = []
submob_labels = []
new_glyphs = []
current_glyph_label = glyph_labels[0]
for glyph, label in zip(svg_glyphs, glyph_labels):
if label == current_glyph_label:
new_glyphs.append(glyph)
else:
submobject = VGroup(*new_glyphs)
submob_labels.append(current_glyph_label)
submobjects.append(submobject)
new_glyphs = [glyph]
current_glyph_label = label
submobject = VGroup(*new_glyphs)
submob_labels.append(current_glyph_label)
submobjects.append(submobject)
indices = self.parser.get_sorted_submob_indices(submob_labels)
rearranged_submobjects = [submobjects[index] for index in indices]
rearranged_labels = [submob_labels[index] for index in indices]
submob_tex_strings = self.parser.get_submob_tex_strings(
rearranged_labels
)
for submob, label, submob_tex in zip(
rearranged_submobjects, rearranged_labels, submob_tex_strings
):
submob.submob_label = label
submob.tex_string = submob_tex
# Support `get_tex()` method here.
submob.get_tex = MethodType(lambda inst: inst.tex_string, submob)
return VGroup(*rearranged_submobjects)
def get_part_by_tex_spans(self, tex_spans):
labels = self.parser.get_containing_labels_by_tex_spans(tex_spans)
return VGroup(*filter(
lambda submob: submob.submob_label in labels,
self.submobjects
))
def get_part_by_custom_span(self, custom_span):
tex_spans = self.parser.find_span_components_of_custom_span(
custom_span
)
if tex_spans is None:
tex = self.tex_string[slice(*custom_span)]
raise ValueError(f"Failed to match mobjects from tex: \"{tex}\"")
return self.get_part_by_tex_spans(tex_spans)
def get_parts_by_tex(self, tex):
return VGroup(*[
self.get_part_by_custom_span(match_obj.span())
for match_obj in re.finditer(
re.escape(tex.strip()), self.tex_string
)
])
def get_part_by_tex(self, tex, index=0):
all_parts = self.get_parts_by_tex(tex)
return all_parts[index]
def set_color_by_tex(self, tex, color):
self.get_parts_by_tex(tex).set_color(color)
return self
def set_color_by_tex_to_color_map(self, tex_to_color_map):
for tex, color in tex_to_color_map.items():
self.set_color_by_tex(tex, color)
return self
def indices_of_part(self, part):
indices = [
index for index, submob in enumerate(self.submobjects)
if submob in part
]
if not indices:
raise ValueError("Failed to find part in tex")
return indices
def indices_of_part_by_tex(self, tex, index=0):
part = self.get_part_by_tex(tex, index=index)
return self.indices_of_part(part)
def get_tex(self):
return self.tex_string
def get_submob_tex(self):
return [
submob.get_tex()
for submob in self.submobjects
]
def get_specified_substrings(self):
return self.parser.get_specified_substrings()
def get_isolated_substrings(self):
return self.parser.get_isolated_substrings()
class MTexText(MTex):
CONFIG = {
"tex_environment": None,
}

View File

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

View File

@@ -5,7 +5,6 @@ import re
from manimlib.constants import *
from manimlib.mobject.geometry import Line
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.config_ops import digest_config
from manimlib.utils.tex_file_writing import tex_to_svg_file
@@ -16,49 +15,51 @@ from manimlib.utils.tex_file_writing import display_during_execution
SCALE_FACTOR_PER_FONT_POINT = 0.001
tex_string_to_mob_map = {}
class SingleStringTex(VMobject):
class SingleStringTex(SVGMobject):
CONFIG = {
"height": None,
"fill_opacity": 1.0,
"stroke_width": 0,
"should_center": True,
"svg_default": {
"color": WHITE,
},
"path_string_config": {
"should_subdivide_sharp_curves": True,
"should_remove_null_curves": True,
},
"font_size": 48,
"height": None,
"organize_left_to_right": False,
"alignment": "\\centering",
"math_mode": True,
"organize_left_to_right": False,
}
def __init__(self, tex_string, **kwargs):
super().__init__(**kwargs)
assert(isinstance(tex_string, str))
assert isinstance(tex_string, str)
self.tex_string = tex_string
if tex_string not in tex_string_to_mob_map:
with display_during_execution(f" Writing \"{tex_string}\""):
full_tex = self.get_tex_file_body(tex_string)
filename = tex_to_svg_file(full_tex)
svg_mob = SVGMobject(
filename,
height=None,
path_string_config={
"should_subdivide_sharp_curves": True,
"should_remove_null_curves": True,
}
)
tex_string_to_mob_map[tex_string] = svg_mob
self.add(*(
sm.copy()
for sm in tex_string_to_mob_map[tex_string]
))
self.init_colors()
super().__init__(**kwargs)
if self.height is None:
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
@property
def hash_seed(self):
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.tex_string,
self.alignment,
self.math_mode
)
def get_file_path(self):
full_tex = self.get_tex_file_body(self.tex_string)
with display_during_execution(f"Writing \"{self.tex_string}\""):
file_path = tex_to_svg_file(full_tex)
return file_path
def get_tex_file_body(self, tex_string):
new_tex = self.get_modified_expression(tex_string)
if self.math_mode:
@@ -129,15 +130,22 @@ class SingleStringTex(VMobject):
def balance_braces(self, tex):
"""
Makes Tex resiliant to unmatched { at start
Makes Tex resiliant to unmatched braces
"""
num_lefts, num_rights = [tex.count(char) for char in "{}"]
while num_rights > num_lefts:
tex = "{" + tex
num_lefts += 1
while num_lefts > num_rights:
tex = tex + "}"
num_rights += 1
num_unclosed_brackets = 0
for i in range(len(tex)):
if i > 0 and tex[i - 1] == "\\":
# So as to not count '\{' type expressions
continue
char = tex[i]
if char == "{":
num_unclosed_brackets += 1
elif char == "}":
if num_unclosed_brackets == 0:
tex = "{" + tex
else:
num_unclosed_brackets -= 1
tex += num_unclosed_brackets * "}"
return tex
def get_tex(self):

View File

@@ -1,10 +1,8 @@
import copy
import hashlib
import os
import re
import io
import typing
import warnings
import xml.etree.ElementTree as ET
import functools
import pygments
@@ -15,6 +13,7 @@ from contextlib import contextmanager
from pathlib import Path
import manimpango
from manimlib.logger import log
from manimlib.constants import *
from manimlib.mobject.geometry import Dot
from manimlib.mobject.svg.svg_mobject import SVGMobject
@@ -55,10 +54,9 @@ class Text(SVGMobject):
self.full2short(kwargs)
digest_config(self, kwargs)
if self.size:
warnings.warn(
"self.size has been deprecated and will "
log.warning(
"`self.size` has been deprecated and will "
"be removed in future.",
DeprecationWarning
)
self.font_size = self.size
if self.lsh == -1:

View File

@@ -6,6 +6,8 @@ from manimlib.mobject.types.surface import SGroup
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.geometry import Square
from manimlib.mobject.geometry import Polygon
from manimlib.utils.bezier import interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import z_to_vector
@@ -14,9 +16,9 @@ from manimlib.utils.space_ops import compass_directions
class SurfaceMesh(VGroup):
CONFIG = {
"resolution": (21, 21),
"resolution": (21, 11),
"stroke_width": 1,
"normal_nudge": 1e-3,
"normal_nudge": 1e-2,
"depth_test": True,
"flat_stroke": False,
}
@@ -32,8 +34,11 @@ class SurfaceMesh(VGroup):
full_nu, full_nv = uv_surface.resolution
part_nu, part_nv = self.resolution
u_indices = np.linspace(0, full_nu, part_nu).astype(int)
v_indices = np.linspace(0, full_nv, part_nv).astype(int)
# 'indices' are treated as floats. Later, there will be
# an interpolation between the floor and ceiling of these
# indices
u_indices = np.linspace(0, full_nu - 1, part_nu)
v_indices = np.linspace(0, full_nv - 1, part_nv)
points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points()
normals = uv_surface.get_unit_normals()
@@ -42,12 +47,21 @@ class SurfaceMesh(VGroup):
for ui in u_indices:
path = VMobject()
full_ui = full_nv * ui
path.set_points_smoothly(nudged_points[full_ui:full_ui + full_nv])
low_ui = full_nv * int(math.floor(ui))
high_ui = full_nv * int(math.ceil(ui))
path.set_points_smoothly(interpolate(
nudged_points[low_ui:low_ui + full_nv],
nudged_points[high_ui:high_ui + full_nv],
ui % 1
))
self.add(path)
for vi in v_indices:
path = VMobject()
path.set_points_smoothly(nudged_points[vi::full_nv])
path.set_points_smoothly(interpolate(
nudged_points[int(math.floor(vi))::full_nv],
nudged_points[int(math.ceil(vi))::full_nv],
vi % 1
))
self.add(path)
@@ -208,6 +222,53 @@ class VCube(VGroup):
self.refresh_unit_normal()
class Dodecahedron(VGroup):
CONFIG = {
"fill_color": BLUE_E,
"fill_opacity": 1,
"stroke_width": 1,
"reflectiveness": 0.2,
"gloss": 0.3,
"shadow": 0.2,
"depth_test": True,
}
def init_points(self):
# Star by creating two of the pentagons, meeting
# back to back on the positive x-axis
phi = (1 + math.sqrt(5)) / 2
x, y, z = np.identity(3)
pentagon1 = Polygon(
[phi, 1 / phi, 0],
[1, 1, 1],
[1 / phi, 0, phi],
[1, -1, 1],
[phi, -1 / phi, 0],
)
pentagon2 = pentagon1.copy().stretch(-1, 2, about_point=ORIGIN)
pentagon2.reverse_points()
x_pair = VGroup(pentagon1, pentagon2)
z_pair = x_pair.copy().apply_matrix(np.array([z, -x, -y]).T)
y_pair = x_pair.copy().apply_matrix(np.array([y, z, x]).T)
self.add(*x_pair, *y_pair, *z_pair)
for pentagon in list(self):
pc = pentagon.copy()
pc.apply_function(lambda p: -p)
pc.reverse_points()
self.add(pc)
# # Rotate those two pentagons by all the axis permuations to fill
# # out the dodecahedron
# Id = np.identity(3)
# for i in range(3):
# perm = [j % 3 for j in range(i, i + 3)]
# for b in [1, -1]:
# matrix = b * np.array([Id[0][perm], Id[1][perm], Id[2][perm]])
# self.add(pentagon1.copy().apply_matrix(matrix, about_point=ORIGIN))
# self.add(pentagon2.copy().apply_matrix(matrix, about_point=ORIGIN))
class Prism(Cube):
CONFIG = {
"dimensions": [3, 2, 1]

View File

@@ -2,12 +2,14 @@ import numpy as np
import moderngl
from manimlib.constants import GREY_C
from manimlib.constants import YELLOW
from manimlib.constants import ORIGIN
from manimlib.mobject.types.point_cloud_mobject import PMobject
from manimlib.utils.iterables import resize_preserving_order
DEFAULT_DOT_RADIUS = 0.05
DEFAULT_GLOW_DOT_RADIUS = 0.2
DEFAULT_GRID_HEIGHT = 6
DEFAULT_BUFF_RATIO = 0.5
@@ -17,6 +19,7 @@ class DotCloud(PMobject):
"color": GREY_C,
"opacity": 1,
"radius": DEFAULT_DOT_RADIUS,
"glow_factor": 0,
"shader_folder": "true_dot",
"render_primitive": moderngl.POINTS,
"shader_dtype": [
@@ -36,6 +39,10 @@ class DotCloud(PMobject):
self.data["radii"] = np.zeros((1, 1))
self.set_radius(self.radius)
def init_uniforms(self):
super().init_uniforms()
self.uniforms["glow_factor"] = self.glow_factor
def to_grid(self, n_rows, n_cols, n_layers=1,
buff_ratio=None,
h_buff_ratio=1.0,
@@ -85,6 +92,12 @@ class DotCloud(PMobject):
def get_radius(self):
return self.get_radii().max()
def set_glow_factor(self, glow_factor):
self.uniforms["glow_factor"] = glow_factor
def get_glow_factor(self):
return self.uniforms["glow_factor"]
def compute_bounding_box(self):
bb = super().compute_bounding_box()
radius = self.get_radius()
@@ -98,8 +111,8 @@ class DotCloud(PMobject):
self.set_radii(scale_factor * self.get_radii())
return self
def make_3d(self, gloss=0.5, shadow=0.2):
self.set_gloss(gloss)
def make_3d(self, reflectiveness=0.5, shadow=0.2):
self.set_reflectiveness(reflectiveness)
self.set_shadow(shadow)
self.apply_depth_test()
return self
@@ -112,5 +125,13 @@ class DotCloud(PMobject):
class TrueDot(DotCloud):
def __init__(self, center=ORIGIN, radius=DEFAULT_DOT_RADIUS, **kwargs):
super().__init__(points=[center], radius=radius, **kwargs)
def __init__(self, center=ORIGIN, **kwargs):
super().__init__(points=[center], **kwargs)
class GlowDot(TrueDot):
CONFIG = {
"glow_factor": 2,
"radius": DEFAULT_GLOW_DOT_RADIUS,
"color": YELLOW,
}

View File

@@ -21,6 +21,8 @@ class PMobject(Mobject):
return self
def set_points(self, points):
if len(points) == 0:
points = np.zeros((0, 3))
super().set_points(points)
self.resize_points(len(points))
return self
@@ -34,14 +36,18 @@ class PMobject(Mobject):
if color is not None:
if opacity is None:
opacity = self.data["rgbas"][-1, 3]
new_rgbas = np.repeat(
rgbas = np.repeat(
[color_to_rgba(color, opacity)],
len(points),
axis=0
)
elif rgbas is not None:
new_rgbas = rgbas
self.data["rgbas"][-len(new_rgbas):] = new_rgbas
if rgbas is not None:
self.data["rgbas"][-len(rgbas):] = rgbas
return self
def add_point(self, point, rgba=None, color=None, opacity=None):
rgbas = None if rgba is None else [rgba]
self.add_points([point], rgbas, color, opacity)
return self
def set_color_by_gradient(self, *colors):

View File

@@ -20,7 +20,8 @@ class Surface(Mobject):
"resolution": (101, 101),
"color": GREY,
"opacity": 1.0,
"gloss": 0.3,
"reflectiveness": 0.3,
"gloss": 0.1,
"shadow": 0.4,
"prefered_creation_axis": 1,
# For du and dv steps. Much smaller and numerical error
@@ -161,6 +162,11 @@ class Surface(Mobject):
tri_is[k::3] = tri_is[k::3][indices]
return self
def always_sort_to_camera(self, camera):
self.add_updater(lambda m: m.sort_faces_back_to_front(
camera.get_location() - self.get_center()
))
# For shaders
def get_shader_data(self):
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()

View File

@@ -12,6 +12,7 @@ from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points
from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points
from manimlib.utils.bezier import get_quadratic_approximation_of_cubic
from manimlib.utils.bezier import interpolate
from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.bezier import partial_quadratic_bezier_points
from manimlib.utils.color import rgb_to_hex
@@ -74,7 +75,6 @@ class VMobject(Mobject):
self.needs_new_triangulation = True
self.triangulation = np.zeros(0, dtype='i4')
super().__init__(**kwargs)
self.refresh_unit_normal()
def get_group_class(self):
return VGroup
@@ -135,6 +135,10 @@ class VMobject(Mobject):
mob.draw_stroke_behind_fill = background
return self
def set_backstroke(self, color=BLACK, width=3, background=True):
self.set_stroke(color, width, background=background)
return self
def align_stroke_width_data_to_points(self, recurse=True):
for mob in self.get_family(recurse):
mob.data["stroke_width"] = resize_with_interpolation(
@@ -150,6 +154,7 @@ class VMobject(Mobject):
stroke_rgba=None,
stroke_width=None,
stroke_background=True,
reflectiveness=None,
gloss=None,
shadow=None,
recurse=True):
@@ -177,6 +182,8 @@ class VMobject(Mobject):
background=stroke_background,
)
if reflectiveness is not None:
self.set_reflectiveness(reflectiveness, recurse=recurse)
if gloss is not None:
self.set_gloss(gloss, recurse=recurse)
if shadow is not None:
@@ -185,10 +192,11 @@ class VMobject(Mobject):
def get_style(self):
return {
"fill_rgba": self.data['fill_rgba'],
"stroke_rgba": self.data['stroke_rgba'],
"stroke_width": self.data['stroke_width'],
"fill_rgba": self.data['fill_rgba'].copy(),
"stroke_rgba": self.data['stroke_rgba'].copy(),
"stroke_width": self.data['stroke_width'].copy(),
"stroke_background": self.draw_stroke_behind_fill,
"reflectiveness": self.get_reflectiveness(),
"gloss": self.get_gloss(),
"shadow": self.get_shadow(),
}
@@ -218,16 +226,17 @@ class VMobject(Mobject):
return self
def fade(self, darkness=0.5, recurse=True):
factor = 1.0 - darkness
self.set_fill(
opacity=factor * self.get_fill_opacity(),
recurse=False,
)
self.set_stroke(
opacity=factor * self.get_stroke_opacity(),
recurse=False,
)
super().fade(darkness, recurse)
mobs = self.get_family() if recurse else [self]
for mob in mobs:
factor = 1.0 - darkness
mob.set_fill(
opacity=factor * mob.get_fill_opacity(),
recurse=False,
)
mob.set_stroke(
opacity=factor * mob.get_stroke_opacity(),
recurse=False,
)
return self
def get_fill_colors(self):
@@ -277,9 +286,9 @@ class VMobject(Mobject):
return self.get_stroke_opacities()[0]
def get_color(self):
if self.has_stroke():
return self.get_stroke_color()
return self.get_fill_color()
if self.has_fill():
return self.get_fill_color()
return self.get_stroke_color()
def has_stroke(self):
return self.get_stroke_widths().any() and self.get_stroke_opacities().any()
@@ -373,7 +382,10 @@ class VMobject(Mobject):
def add_smooth_cubic_curve_to(self, handle, point):
self.throw_error_if_no_points()
new_handle = self.get_reflection_of_last_handle()
if self.get_num_points() == 1:
new_handle = self.get_points()[-1]
else:
new_handle = self.get_reflection_of_last_handle()
self.add_cubic_bezier_curve_to(new_handle, handle, point)
def has_new_path_started(self):
@@ -504,10 +516,10 @@ class VMobject(Mobject):
nppc = self.n_points_per_curve
remainder = len(points) % nppc
points = points[:len(points) - remainder]
return [
return (
points[i:i + nppc]
for i in range(0, len(points), nppc)
]
)
def get_bezier_tuples(self):
return self.get_bezier_tuples_from_points(self.get_points())
@@ -543,12 +555,35 @@ class VMobject(Mobject):
def get_num_curves(self):
return self.get_num_points() // self.n_points_per_curve
def point_from_proportion(self, alpha):
def quick_point_from_proportion(self, alpha):
# Assumes all curves have the same length, so is inaccurate
num_curves = self.get_num_curves()
n, residue = integer_interpolate(0, num_curves, alpha)
curve_func = self.get_nth_curve_function(n)
return curve_func(residue)
def point_from_proportion(self, alpha):
if alpha <= 0:
return self.get_start()
elif alpha >= 1:
return self.get_end()
partials = [0]
for tup in self.get_bezier_tuples():
# Approximate length with straight line from start to end
arclen = get_norm(tup[0] - tup[-1])
partials.append(partials[-1] + arclen)
full = partials[-1]
if full == 0:
return self.get_start()
# First index where the partial lenth is more alpha times the full length
i = next(
(i for i, x in enumerate(partials) if x >= full * alpha),
len(partials) # Default
)
residue = inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha)
return self.get_nth_curve_function(i - 1)(residue)
def get_anchors_and_handles(self):
"""
returns anchors1, handles, anchors2,
@@ -629,17 +664,19 @@ class VMobject(Mobject):
area_vect = self.get_area_vector()
area = get_norm(area_vect)
if area > 0:
return area_vect / area
normal = area_vect / area
else:
points = self.get_points()
return get_unit_normal(
normal = get_unit_normal(
points[1] - points[0],
points[2] - points[1],
)
self.data["unit_normal"][:] = normal
return normal
def refresh_unit_normal(self):
for mob in self.get_family():
mob.data["unit_normal"][:] = mob.get_unit_normal(recompute=True)
mob.get_unit_normal(recompute=True)
return self
# Alignment
@@ -701,7 +738,7 @@ class VMobject(Mobject):
if len(points) == 1:
return np.repeat(points, nppc * n, 0)
bezier_groups = self.get_bezier_tuples_from_points(points)
bezier_groups = list(self.get_bezier_tuples_from_points(points))
norms = np.array([
get_norm(bg[nppc - 1] - bg[0])
for bg in bezier_groups
@@ -797,7 +834,7 @@ class VMobject(Mobject):
# how to send the points as to the vertex shader.
# First triangles come directly from the points
if normal_vector is None:
normal_vector = self.get_unit_normal()
normal_vector = self.get_unit_normal(recompute=True)
if not self.needs_new_triangulation:
return self.triangulation
@@ -853,7 +890,7 @@ class VMobject(Mobject):
def triggers_refreshed_triangulation(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
old_points = self.get_points()
old_points = self.get_points().copy()
func(self, *args, **kwargs)
if not np.all(self.get_points() == old_points):
self.refresh_unit_normal()
@@ -999,6 +1036,10 @@ class VGroup(VMobject):
super().__init__(**kwargs)
self.add(*vmobjects)
def __add__(self: 'VGroup', other: 'VMobject' or 'VGroup'):
assert(isinstance(other, VMobject))
return self.add(other)
class VectorizedPoint(Point, VMobject):
CONFIG = {
@@ -1010,7 +1051,8 @@ class VectorizedPoint(Point, VMobject):
}
def __init__(self, location=ORIGIN, **kwargs):
super().__init__(**kwargs)
Point.__init__(self, **kwargs)
VMobject.__init__(self, **kwargs)
self.set_points(np.array([location]))

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

@@ -36,6 +36,7 @@ class Scene(object):
"end_at_animation_number": None,
"leave_progress_bars": False,
"preview": True,
"presenter_mode": False,
"linger_after_completion": True,
}
@@ -56,10 +57,13 @@ class Scene(object):
self.time = 0
self.skip_time = 0
self.original_skipping_status = self.skip_animations
if self.start_at_animation_number is not None:
self.skip_animations = True
# Items associated with interaction
self.mouse_point = Point()
self.mouse_drag_point = Point()
self.hold_on_wait = self.presenter_mode
# Much nicer to work with deterministic scenes
if self.random_seed is not None:
@@ -112,7 +116,7 @@ class Scene(object):
if self.quit_interaction:
self.unlock_mobject_data()
def embed(self):
def embed(self, close_scene_on_exit=True):
if not self.preview:
# If the scene is just being
# written, ignore embed calls
@@ -135,10 +139,11 @@ class Scene(object):
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
local_ns[term] = getattr(self, term)
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
shell(local_ns=local_ns, stack_depth=2)
# End scene when exiting an embed.
raise EndSceneEarlyException()
# End scene when exiting an embed
if close_scene_on_exit:
raise EndSceneEarlyException()
def __str__(self):
return self.__class__.__name__
@@ -184,6 +189,13 @@ class Scene(object):
for mob in self.mobjects
])
def has_time_based_updaters(self):
return any([
sm.has_time_based_updater()
for mob in self.mobjects()
for sm in mob.get_family()
])
# Related to time
def get_time(self):
return self.time
@@ -271,59 +283,54 @@ class Scene(object):
def update_skipping_status(self):
if self.start_at_animation_number is not None:
if self.num_plays == self.start_at_animation_number:
self.stop_skipping()
self.skip_time = self.time
if not self.original_skipping_status:
self.stop_skipping()
if self.end_at_animation_number is not None:
if self.num_plays >= self.end_at_animation_number:
raise EndSceneEarlyException()
def stop_skipping(self):
if self.skip_animations:
self.skip_animations = False
self.skip_time += self.time
self.virtual_animation_start_time = self.time
self.skip_animations = False
# Methods associated with running animations
def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False):
def get_time_progression(self, run_time, n_iterations=None, desc="", override_skip_animations=False):
if self.skip_animations and not override_skip_animations:
times = [run_time]
return [run_time]
else:
step = 1 / self.camera.frame_rate
times = np.arange(0, run_time, step)
time_progression = ProgressDisplay(
if self.file_writer.has_progress_display:
self.file_writer.set_progress_display_subdescription(desc)
return times
return ProgressDisplay(
times,
total=n_iterations,
leave=self.leave_progress_bars,
ascii=True if platform.system() == 'Windows' else None
ascii=True if platform.system() == 'Windows' else None,
desc=desc,
)
return time_progression
def get_run_time(self, animations):
return np.max([animation.run_time for animation in animations])
def get_animation_time_progression(self, animations):
run_time = self.get_run_time(animations)
time_progression = self.get_time_progression(run_time)
time_progression.set_description("".join([
f"Animation {self.num_plays}: {animations[0]}",
", etc." if len(animations) > 1 else "",
]))
description = f"{self.num_plays} {animations[0]}"
if len(animations) > 1:
description += ", etc."
time_progression = self.get_time_progression(run_time, desc=description)
return time_progression
def get_wait_time_progression(self, duration, stop_condition):
def get_wait_time_progression(self, duration, stop_condition=None):
kw = {"desc": f"{self.num_plays} Waiting"}
if stop_condition is not None:
time_progression = self.get_time_progression(
duration,
n_iterations=-1, # So it doesn't show % progress
override_skip_animations=True
)
time_progression.set_description(
"Waiting for {}".format(stop_condition.__name__)
)
else:
time_progression = self.get_time_progression(duration)
time_progression.set_description(
"Waiting {}".format(self.num_plays)
)
return time_progression
kw["n_iterations"] = -1 # So it doesn't show % progress
kw["override_skip_animations"] = True
return self.get_time_progression(duration, **kw)
def anims_from_play_args(self, *args, **kwargs):
"""
@@ -428,6 +435,11 @@ class Scene(object):
def unlock_mobject_data(self):
self.camera.release_static_mobjects()
def refresh_locked_data(self):
self.unlock_mobject_data()
self.lock_static_mobject_data()
return self
def begin_animations(self, animations):
for animation in animations:
animation.begin()
@@ -473,10 +485,20 @@ class Scene(object):
self.unlock_mobject_data()
@handle_play_like_call
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
def wait(self,
duration=DEFAULT_WAIT_TIME,
stop_condition=None,
note=None,
ignore_presenter_mode=False):
if note:
log.info(note)
self.update_mobjects(dt=0) # Any problems with this?
if self.should_update_mobjects():
self.lock_static_mobject_data()
self.lock_static_mobject_data()
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
while self.hold_on_wait:
self.update_frame(dt=1 / self.camera.frame_rate)
self.hold_on_wait = True
else:
time_progression = self.get_wait_time_progression(duration, stop_condition)
last_t = 0
for t in time_progression:
@@ -487,15 +509,7 @@ class Scene(object):
if stop_condition is not None and stop_condition():
time_progression.close()
break
self.unlock_mobject_data()
elif self.skip_animations:
# Do nothing
return self
else:
self.update_frame(duration)
n_frames = int(duration * self.camera.frame_rate)
for n in range(n_frames):
self.emit_frame()
self.unlock_mobject_data()
return self
def wait_until(self, stop_condition, max_time=60):
@@ -615,6 +629,10 @@ class Scene(object):
self.camera.frame.to_default_state()
elif char == "q":
self.quit_interaction = True
elif char == " " or symbol == 65363: # Space or right arrow
self.hold_on_wait = False
elif char == "e" and modifiers == 3: # ctrl + shift + e
self.embed(close_scene_on_exit=False)
def on_resize(self, width: int, height: int):
self.camera.reset_pixel_shape(width, height)

View File

@@ -5,6 +5,7 @@ import subprocess as sp
import os
import sys
import platform
from tqdm import tqdm as ProgressDisplay
from manimlib.constants import FFMPEG_BIN
from manimlib.utils.config_ops import digest_config
@@ -35,12 +36,15 @@ class SceneFileWriter(object):
"open_file_upon_completion": False,
"show_file_location_upon_completion": False,
"quiet": False,
"total_frames": 0,
"progress_description_len": 60,
}
def __init__(self, scene, **kwargs):
digest_config(self, kwargs)
self.scene = scene
self.writing_process = None
self.has_progress_display = False
self.init_output_directories()
self.init_audio()
@@ -72,10 +76,14 @@ class SceneFileWriter(object):
return path
def get_default_scene_name(self):
if self.file_name is None:
return self.scene.__class__.__name__
else:
return self.file_name
name = str(self.scene)
saan = self.scene.start_at_animation_number
eaan = self.scene.end_at_animation_number
if saan is not None:
name += f"_{saan}"
if eaan is not None:
name += f"_{eaan}"
return name
def get_resolution_directory(self):
pixel_height = self.scene.camera.pixel_height
@@ -205,15 +213,39 @@ class SceneFileWriter(object):
command += [self.temp_file_path]
self.writing_process = sp.Popen(command, stdin=sp.PIPE)
if self.total_frames > 0:
self.progress_display = ProgressDisplay(
range(self.total_frames),
# bar_format="{l_bar}{bar}|{n_fmt}/{total_fmt}",
leave=False,
ascii=True if platform.system() == 'Windows' else None,
dynamic_ncols=True,
)
self.has_progress_display = True
def set_progress_display_subdescription(self, sub_desc):
desc_len = self.progress_description_len
file = os.path.split(self.get_movie_file_path())[1]
full_desc = f"Rendering {file} ({sub_desc})"
if len(full_desc) > desc_len:
full_desc = full_desc[:desc_len - 4] + "...)"
else:
full_desc += " " * (desc_len - len(full_desc))
self.progress_display.set_description(full_desc)
def write_frame(self, camera):
if self.write_to_movie:
raw_bytes = camera.get_raw_fbo_data()
self.writing_process.stdin.write(raw_bytes)
if self.has_progress_display:
self.progress_display.update()
def close_movie_pipe(self):
self.writing_process.stdin.close()
self.writing_process.wait()
self.writing_process.terminate()
if self.has_progress_display:
self.progress_display.close()
shutil.move(self.temp_file_path, self.final_file_path)
def combine_movie_files(self):
@@ -276,7 +308,7 @@ class SceneFileWriter(object):
)
temp_file_path = stem + "_temp" + ext
commands = [
"ffmpeg",
FFMPEG_BIN,
"-i", movie_file_path,
"-i", sound_file_path,
'-y', # overwrite output file if it exists
@@ -301,7 +333,8 @@ class SceneFileWriter(object):
self.print_file_ready_message(file_path)
def print_file_ready_message(self, file_path):
log.info(f"File ready at {file_path}")
if not self.quiet:
log.info(f"File ready at {file_path}")
def should_open_file(self):
return any([

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

@@ -14,10 +14,10 @@ def bezier(points):
n = len(points) - 1
def result(t):
return sum([
return sum(
((1 - t)**(n - k)) * (t**k) * choose(n, k) * point
for k, point in enumerate(points)
])
)
return result

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

View File

@@ -139,3 +139,14 @@ def remove_nones(sequence):
def concatenate_lists(*list_of_lists):
return [item for l in list_of_lists for item in l]
def hash_obj(obj):
if isinstance(obj, dict):
new_obj = {k: hash_obj(v) for k, v in obj.items()}
return hash(tuple(frozenset(sorted(new_obj.items()))))
if isinstance(obj, (set, tuple, list)):
return hash(tuple(hash_obj(e) for e in obj))
return hash(obj)

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

@@ -10,6 +10,15 @@ from manimlib.constants import OUT
from manimlib.constants import PI
from manimlib.constants import TAU
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.simple_functions import clip
def cross(v1, v2):
return [
v1[1] * v2[2] - v1[2] * v2[1],
v1[2] * v2[0] - v1[0] * v2[2],
v1[0] * v2[1] - v1[1] * v2[0]
]
def get_norm(vect):
@@ -147,6 +156,15 @@ def z_to_vector(vector):
return rotation_matrix(angle, axis=axis)
def rotation_between_vectors(v1, v2):
if np.all(np.isclose(v1, v2)):
return np.identity(3)
return rotation_matrix(
angle=angle_between_vectors(v1, v2),
axis=normalize(np.cross(v1, v2))
)
def angle_of_vector(vector):
"""
Returns polar coordinate theta when vector is project on xy plane
@@ -159,8 +177,7 @@ def angle_between_vectors(v1, v2):
Returns the angle between two 3D vectors.
This angle will always be btw 0 and pi
"""
diff = (angle_of_vector(v2) - angle_of_vector(v1)) % TAU
return min(diff, TAU - diff)
return math.acos(clip(np.dot(normalize(v1), normalize(v2)), -1, 1))
def project_along_vector(point, vector):
@@ -186,14 +203,6 @@ def normalize_along_axis(array, axis, fall_back=None):
return array
def cross(v1, v2):
return np.array([
v1[1] * v2[2] - v1[2] * v2[1],
v1[2] * v2[0] - v1[0] * v2[2],
v1[0] * v2[1] - v1[1] * v2[0]
])
def get_unit_normal(v1, v2, tol=1e-6):
v1 = normalize(v1)
v2 = normalize(v2)

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

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

View File

@@ -1,6 +1,6 @@
[metadata]
name = manimgl
version = 1.2.0
version = 1.5.0
author = Grant Sanderson
author_email= grant@3blue1brown.com
description = Animation engine for explanatory math videos
@@ -12,12 +12,24 @@ project_urls =
Documentation = https://3b1b.github.io/manim/
Source Code = https://github.com/3b1b/manim
license = MIT
classifiers =
Development Status :: 4 - Beta
License :: OSI Approved :: MIT License
Topic :: Scientific/Engineering
Topic :: Multimedia :: Video
Topic :: Multimedia :: Graphics
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3 :: Only
Natural Language :: English
[options]
packages = find:
include_package_data=True
install_requires =
argparse
include_package_data = True
install_requires =
colour
numpy
Pillow
@@ -28,16 +40,18 @@ install_requires =
matplotlib
moderngl
moderngl_window
skia-pathops
pydub
pygments
pyyaml
rich
screeninfo
pyreadline; sys_platform == 'win32'
validators
ipython
PyOpenGL
manimpango>=0.2.0,<0.4.0
isosurfaces
svgelements
[options.entry_points]
console_scripts =