231 Commits

Author SHA1 Message Date
Grant Sanderson
9da66250ee Allow option for BulletedList to be numbered 2025-10-20 10:03:31 -07:00
Grant Sanderson
bca82da8a8 Merge branch 'master' of github.com:3b1b/manim into video-work 2025-10-14 09:17:27 -05:00
Grant Sanderson
e5298385ed Video work (#2402)
* Bug fix for TransformMatchingStrings with incompatible lengths

* Change faded line in NumberPlane initialization to be more explicit, and lower opacity

* Add option hide_zero_components_on_complex to DecimalNumber

* Validate syntax before reloading

* Add remembered stroke_config to TracedPath

* Add CLAUDE.md to gitignore

* Move pre-calculated traced points to TracingTail

* Fix interplay between time_span and alpha in Animation

* Clearer init for points in TracingTail

* Fix CoordinateSystem.get_area_under_graph

* Allow ComplexPlane.n2p to take in array of complex numbers

* Add put_start_on and put_end_on

* Add Slider

* Add \minus option for Tex to give shorter negative sign

* Put interp_by_hsl option in various color interpretation functions

* Swap priority of matched_keys vs key_map is TransformMatchingStrings

* Have z-index apply recursively

* Set self.svg_string property for SVGMobject

* Fix num_decimal_places config in Tex.make_number_changeable

* Add Surface. color_by_uv_function

* Add VMobject. set_color_by_proportion

* Add \mathcal to tex_to_symbol_count
2025-10-14 07:15:39 -07:00
Grant Sanderson
30303e0ab1 Add \mathcal to tex_to_symbol_count 2025-10-14 09:11:46 -05:00
Grant Sanderson
b39b3e6256 Add VMobject. set_color_by_proportion 2025-10-14 09:11:36 -05:00
Grant Sanderson
b54f0659b7 Add Surface. color_by_uv_function 2025-10-14 09:11:26 -05:00
Grant Sanderson
15f9bfff6f Fix num_decimal_places config in Tex.make_number_changeable 2025-10-14 09:11:01 -05:00
Grant Sanderson
cf168b415c Set self.svg_string property for SVGMobject 2025-10-14 09:10:20 -05:00
Grant Sanderson
13054d6c3c Have z-index apply recursively 2025-10-14 09:09:58 -05:00
Grant Sanderson
69f09615b1 Swap priority of matched_keys vs key_map is TransformMatchingStrings 2025-10-14 09:09:49 -05:00
Grant Sanderson
f4c04244c7 Put interp_by_hsl option in various color interpretation functions 2025-09-22 13:06:42 -05:00
Grant Sanderson
043a0273ef Add \minus option for Tex to give shorter negative sign 2025-09-22 13:05:32 -05:00
Grant Sanderson
e2cc6cd56e Add Slider 2025-09-22 13:04:50 -05:00
Grant Sanderson
d301c5037f Add put_start_on and put_end_on 2025-09-22 13:04:40 -05:00
Grant Sanderson
0c245d5e09 Allow ComplexPlane.n2p to take in array of complex numbers 2025-09-22 13:04:30 -05:00
Grant Sanderson
828cbec384 Fix CoordinateSystem.get_area_under_graph 2025-09-22 13:04:14 -05:00
Grant Sanderson
91bbd5566d Clearer init for points in TracingTail 2025-09-22 13:03:44 -05:00
Grant Sanderson
75a789f37e Fix interplay between time_span and alpha in Animation 2025-09-22 13:02:23 -05:00
Grant Sanderson
db3bde18ba Move pre-calculated traced points to TracingTail 2025-07-01 15:01:25 -05:00
Grant Sanderson
30763084f1 Add CLAUDE.md to gitignore 2025-06-30 14:29:25 -05:00
Grant Sanderson
7839e1d6b3 Add remembered stroke_config to TracedPath 2025-06-30 13:42:19 -05:00
Grant Sanderson
22fa38fab9 Validate syntax before reloading 2025-06-30 13:06:05 -05:00
Grant Sanderson
057900ef8b Add option hide_zero_components_on_complex to DecimalNumber 2025-06-30 10:00:08 -05:00
Grant Sanderson
60772ccfdc Change faded line in NumberPlane initialization to be more explicit, and lower opacity 2025-06-30 09:59:45 -05:00
Grant Sanderson
b13498ab31 Bug fix for TransformMatchingStrings with incompatible lengths 2025-06-30 09:59:06 -05:00
Grant Sanderson
41613db7ec Remove stray prints 2025-06-14 10:47:52 -05:00
Grant Sanderson
c48c4b6a9f Merge branch 'master' of github.com:3b1b/manim 2025-06-10 10:59:56 -05:00
Grant Sanderson
cc5fbe17b9 Fix bug with mirroring file prefixes 2025-06-10 10:59:47 -05:00
Eivar Morales
98faf7ed55 added information for M1 Users (#2044)
* added information for M1 Users

* Update README.md

Co-authored-by: Paweł Cisło <pyxelr@gmail.com>

---------

Co-authored-by: Paweł Cisło <pyxelr@gmail.com>
2025-06-10 08:42:40 -07:00
Grant Sanderson
d330e1db7f Added documentation from @AkashKarnatak to geometry.py
Based on https://github.com/3b1b/manim/pull/1024
2025-06-10 10:41:09 -05:00
ScarWann
e81dfab0e6 Fixed minor typos in example_scenes.py (#2351) 2025-06-10 08:16:31 -07:00
Abdallah Soliman
fd2a6a69e5 Created a method `remove_all_except()` in scene.py and interactive_scene.py, and made default colors easily configurable. (#2346)
* created a method remove_all_except() in scene.py and interactive_scene.py

* Made it such that default mobject colors can be set through the yaml config file.

* * Default color initialisation wasn't working.
Changed conditional expression to `or` instead.

* Added default values to yaml file.

* added set_background_color() function to Scene class

* Changed default font back to Consolas
2025-06-10 08:15:55 -07:00
Irvanal Haq
6fb1845f4a Enhance Autocompletion for mobject.animate. to Display Mobject Methods (#2342)
* Improve autocompletion for mobject.animate to show Mobject methods

- Added type hint `-> _AnimationBuilder | Self` to `Mobject.animate`, enabling autocompletion for `Mobject` methods after `mobject.animate`.

- Prioritized `typing_extensions.Self` over `typing.Self` in imports, so autocompletion of `Mobject` methods also works in Python < 3.11.

* Support `mobject.animate.` autocompletion in IPython

* Add docstring to `__dir__` and add return type hint

* improve docsting `__dir__` _AnimationBuilder
2025-06-10 08:13:29 -07:00
feng lui
7787730743 Fix No matching distribution found for audioop-lts (python < 3.13) (#2283) (#2341) 2025-06-10 08:12:31 -07:00
Irvanal Haq
a5a73cb2da Fix typos in GLSL comments and add constants PURE_RED, PURE_GREEN, PURE_BLUE (#2340)
* Fix typos in GLSL comments

* Fix typos in GLSL comments

* add constants PURE_RED, PURE_GREEN, PURE_BLUE
2025-06-10 08:12:11 -07:00
Refael Ackermann
bd8d2fbc99 Update URL to pango in README.md (#2334)
https://pango.gnome.org gets `ERR_NAME_NOT_RESOLVED`
2025-06-10 08:11:36 -07:00
Irvanal Haq
53bc83d94a Refactor: move type validation to top of Animation.__init__ and extract into method (#2332) 2025-06-10 08:11:16 -07:00
Varniex
8b9ae95703 Resolving minor bug in StreamLines (#2330)
* resolving minor bug in StreamLines

* minute changes
2025-06-10 08:05:07 -07:00
Irvanal Haq
c667136060 Fix error when using VFadeIn (and its subclasses) (#2328)
* Fix error when using VFadeIn and its subclasses

* add np.floating in type checking of Mobject.set_rgba_array_by_color and VMobject.set_stroke and removing the change in VFadeIn
2025-06-10 08:04:13 -07:00
Calvin Witt
66e8b04507 update readme instructions to add manimgl to path first (#2327)
* update readme instructions to add manimgl to path first

* change
2025-06-10 08:02:58 -07:00
Grant Sanderson
c7ef8404b7 Video work (#2356)
* Only use -no-pdf for xelatex rendering

* Instead of tracking du and dv points on surface, track points off the surface in the normal direction

This means that surface shading will not necessarily work well for arbitrary transformations of the surface. But the existing solution was flimsy anyway, and caused annoying issues with singularity points.

* Have density of anchor points on arcs depend on arc length

* Allow for specifying true normals and orientation of Sphere

* Change miter threshold on stroke shader

* Add get_start_and_end to DashedLine

* Add min_total_width option to DecimalNumber

* Have BackgroundRectangle.set_style absorb (and ignore) added configuration

Note, this feels suboptimal

* Add LineBrace

* Update font_size adjustment in Tex

* Add scale_factor parameter to BulletedList.fade_all_but

* Minor import tweaks

* Add play_sound

* Small if -> elif update

* Always use Group for FadeTransform

* Use time_spanned_alpha in ChangingDecimal

* Change priority of number_config vs. self.decimal_number_config in NumberLine init

* Fix clock animation

* Allow sample_coords to be passed into VectorField
2025-06-10 08:02:32 -07:00
Grant Sanderson
f4737828f6 Video work (#2326)
* Only use -no-pdf for xelatex rendering

* Instead of tracking du and dv points on surface, track points off the surface in the normal direction

This means that surface shading will not necessarily work well for arbitrary transformations of the surface. But the existing solution was flimsy anyway, and caused annoying issues with singularity points.

* Have density of anchor points on arcs depend on arc length

* Allow for specifying true normals and orientation of Sphere

* Change miter threshold on stroke shader

* Add get_start_and_end to DashedLine

* Add min_total_width option to DecimalNumber

* Have BackgroundRectangle.set_style absorb (and ignore) added configuration

Note, this feels suboptimal

* Add LineBrace

* Update font_size adjustment in Tex

* Add scale_factor parameter to BulletedList.fade_all_but

* Minor import tweaks

* Add play_sound
2025-03-20 12:00:35 -07:00
jkjkil4
be7d93cf40 Fix path arc handling for SVGMobject when a matrix transform is present in the SVG (#2322) 2025-03-20 11:59:06 -07:00
Varniex
dbfe7ac75d Performance improved in set_color_by_rgba_func (#2316)
* removing 1 in neg axis if unit_tex is specified

* performance improved in `set_color_by_rgba_func`

* resolving imag axis number mob in ComplexPlane
2025-03-20 11:56:29 -07:00
AStarySky
7a61a13691 Fix issues in number_line.py (#2310)
Fix the issue that changes in decimal_number_config["font_size"] get rewritten by number_config
2025-03-20 11:54:59 -07:00
Йордан Миладинов
3e307926fd Add missing dependencies to setup.cfg and requirements.txt (#2304) 2025-03-20 11:52:48 -07:00
Shinapri De Lucania
2ddec95ce5 Fix --fps type conversion to int (#2299) 2025-03-20 11:52:17 -07:00
Grant Sanderson
db421e3981 Video work (#2318)
* Only use -no-pdf for xelatex rendering

* Instead of tracking du and dv points on surface, track points off the surface in the normal direction

This means that surface shading will not necessarily work well for arbitrary transformations of the surface. But the existing solution was flimsy anyway, and caused annoying issues with singularity points.

* Have density of anchor points on arcs depend on arc length

* Allow for specifying true normals and orientation of Sphere

* Change miter threshold on stroke shader

* Add get_start_and_end to DashedLine

* Add min_total_width option to DecimalNumber

* Have BackgroundRectangle.set_style absorb (and ignore) added configuration

Note, this feels suboptimal

* Add LineBrace

* Update font_size adjustment in Tex
2025-02-26 07:52:59 -08:00
Grant Sanderson
7a7bf83f11 Only use -no-pdf for xelatex rendering (#2298) 2025-01-08 08:22:03 -08:00
Varniex
24eefef5bf Automatically identify the class name based on the specified line number. (#2280)
* identify the scene name based on the line number

* resolving a minor bug in string_mobject

* removing bug of string validation

* Update manimlib/default_config.yml

Co-authored-by: Splines <37160523+Splines@users.noreply.github.com>

* Update manimlib/extract_scene.py

Co-authored-by: Splines <37160523+Splines@users.noreply.github.com>

* update search scene names

---------

Co-authored-by: Splines <37160523+Splines@users.noreply.github.com>
2024-12-28 07:18:32 -08:00
Grant Sanderson
96d44bd560 Video work (#2284)
* Comment tweak

* Directly print traceback

Since the shell.showtraceback is giving some issues

* Make InteracrtiveSceneEmbed into a class

This way it can keep track of it's internal shell; use of get_ipython has a finicky relationship with reloading.

* Move remaining checkpoint_paste logic into scene_embed.py

This involved making a few context managers for Scene: temp_record, temp_skip, temp_progress_bar, which seem useful in and of themselves.

* Change null key to be the empty string

* Ensure temporary svg paths for Text are deleted

* Remove unused dict_ops.py functions

* Remove break_into_partial_movies from file_writer configuration

* Rewrite guarantee_existence using Path

* Clean up SceneFileWriter

It had a number of vestigial functions no longer used, and some setup that could be made more organized.

* Remove --save_pngs CLI arg (which did nothing)

* Add --subdivide CLI arg

* Remove add_extension_if_not_present

* Remove get_sorted_integer_files

* Have find_file return Path

* Minor clean up

* Clean up num_tex_symbols

* Fix find_file

* Minor cleanup for extract_scene.py

* Add preview_frame_while_skipping option to scene config

* Use shell.showtraceback function

* Move keybindings to config, instead of in-place constants

* Replace DEGREES -> DEG

* Add arg to clear the cache

* Separate out full_tex_to_svg from tex_to_svg

And only cache to disk the results of full_tex_to_svg.  Otherwise, making edits to the tex_templates would not show up without clearing the cache.

* Bug fix in handling BlankScene

* Make checkpoint_states an instance variable of CheckpointManager

As per https://github.com/3b1b/manim/issues/2272

* Move resizing out of Window.focus, and into Window.init_for_scene

* Make default output directory "." instead of ""

To address https://github.com/3b1b/manim/issues/2261

* Remove input_file_path arg from SceneFileWriter

* Use Dict syntax in place of dict for config more consistently across config.py

* Simplify get_output_directory

* Swap order of preamble and additional preamble

* Minor stylistic tweak

* Have UnitInterval pass on kwargs to NumberLine

* Add simple get_dist function

* Have TracedPath always update to the stroke configuration passed in

* Have Mobject.match_points apply to all parts of data in pointlike_data_key

* Always call Mobject.update upon adding an updater

* Add Surface.uv_to_point

* Make sure Surface.set_opacity takes in a recurse option

* Update num_tex_symbols to account for \{ and \}
2024-12-26 09:35:34 -08:00
Grant Sanderson
39fbb677dc Have autoreload update shell namespace with reloaded module variables (#2278)
* Have autoreload update shell namespace with reloaded module variables

* Update comments
2024-12-13 13:23:50 -08:00
syhner
c13d2a946b fix typos (#2270) 2024-12-13 11:05:48 -08:00
Grant Sanderson
0c69ab6a32 Update version number 2024-12-12 20:54:37 -06:00
Grant Sanderson
f427fc67df A few bug fixes (#2277)
* Comment tweak

* Directly print traceback

Since the shell.showtraceback is giving some issues

* Make InteracrtiveSceneEmbed into a class

This way it can keep track of it's internal shell; use of get_ipython has a finicky relationship with reloading.

* Move remaining checkpoint_paste logic into scene_embed.py

This involved making a few context managers for Scene: temp_record, temp_skip, temp_progress_bar, which seem useful in and of themselves.

* Change null key to be the empty string

* Ensure temporary svg paths for Text are deleted

* Remove unused dict_ops.py functions

* Remove break_into_partial_movies from file_writer configuration

* Rewrite guarantee_existence using Path

* Clean up SceneFileWriter

It had a number of vestigial functions no longer used, and some setup that could be made more organized.

* Remove --save_pngs CLI arg (which did nothing)

* Add --subdivide CLI arg

* Remove add_extension_if_not_present

* Remove get_sorted_integer_files

* Have find_file return Path

* Minor clean up

* Clean up num_tex_symbols

* Fix find_file

* Minor cleanup for extract_scene.py

* Add preview_frame_while_skipping option to scene config

* Use shell.showtraceback function

* Move keybindings to config, instead of in-place constants

* Replace DEGREES -> DEG

* Add arg to clear the cache

* Separate out full_tex_to_svg from tex_to_svg

And only cache to disk the results of full_tex_to_svg.  Otherwise, making edits to the tex_templates would not show up without clearing the cache.

* Bug fix in handling BlankScene

* Make checkpoint_states an instance variable of CheckpointManager

As per https://github.com/3b1b/manim/issues/2272

* Move resizing out of Window.focus, and into Window.init_for_scene

* Make default output directory "." instead of ""

To address https://github.com/3b1b/manim/issues/2261

* Remove input_file_path arg from SceneFileWriter

* Use Dict syntax in place of dict for config more consistently across config.py

* Simplify get_output_directory

* Swap order of preamble and additional preamble
2024-12-12 18:45:34 -08:00
Grant Sanderson
3d9a0cd25e Move resizing out of Window.focus, and into Window.init_for_scene (#2274) 2024-12-12 14:16:45 -08:00
Grant Sanderson
33dbf04985 Make checkpoint_states an instance variable of CheckpointManager (#2273)
As per https://github.com/3b1b/manim/issues/2272
2024-12-12 14:07:55 -08:00
Grant Sanderson
744e695340 Misc. clean up (#2269)
* Comment tweak

* Directly print traceback

Since the shell.showtraceback is giving some issues

* Make InteracrtiveSceneEmbed into a class

This way it can keep track of it's internal shell; use of get_ipython has a finicky relationship with reloading.

* Move remaining checkpoint_paste logic into scene_embed.py

This involved making a few context managers for Scene: temp_record, temp_skip, temp_progress_bar, which seem useful in and of themselves.

* Change null key to be the empty string

* Ensure temporary svg paths for Text are deleted

* Remove unused dict_ops.py functions

* Remove break_into_partial_movies from file_writer configuration

* Rewrite guarantee_existence using Path

* Clean up SceneFileWriter

It had a number of vestigial functions no longer used, and some setup that could be made more organized.

* Remove --save_pngs CLI arg (which did nothing)

* Add --subdivide CLI arg

* Remove add_extension_if_not_present

* Remove get_sorted_integer_files

* Have find_file return Path

* Minor clean up

* Clean up num_tex_symbols

* Fix find_file

* Minor cleanup for extract_scene.py

* Add preview_frame_while_skipping option to scene config

* Use shell.showtraceback function

* Move keybindings to config, instead of in-place constants

* Replace DEGREES -> DEG
2024-12-12 08:39:54 -08:00
Grant Sanderson
00b34f2020 Autoreload v2 (#2268)
* Add autoreload

* Typo correction

* Add --autoreload to configuration docts

Co-Authored-By: Splines <37160523+Splines@users.noreply.github.com>

---------

Co-authored-by: Splines <37160523+Splines@users.noreply.github.com>
2024-12-12 06:52:03 -08:00
Grant Sanderson
bafea89ac9 Update InteractiveSceneEmbed (#2267)
* Comment tweak

* Directly print traceback

Since the shell.showtraceback is giving some issues

* Make InteracrtiveSceneEmbed into a class

This way it can keep track of it's internal shell; use of get_ipython has a finicky relationship with reloading.

* Move remaining checkpoint_paste logic into scene_embed.py

This involved making a few context managers for Scene: temp_record, temp_skip, temp_progress_bar, which seem useful in and of themselves.

* Change null key to be the empty string
2024-12-11 11:33:48 -08:00
Grant Sanderson
eeb4fdf270 Merge pull request #2266 from 3b1b/video-work
Refactor config
2024-12-11 10:53:26 -06:00
Grant Sanderson
e2e785d6c9 Remove init_config.py
It may become a bit unwieldy to make sure this matches the structure of default_config, given the amount of code repetition involved. It seems easier for a user to just create their own custom_config.yml file directly.
2024-12-11 10:50:53 -06:00
Grant Sanderson
c6c1a49ede Update setup.cfg 2024-12-11 10:38:30 -06:00
Grant Sanderson
6d753a297a Remove stray imports 2024-12-11 10:38:23 -06:00
Grant Sanderson
f9fc543b07 Merge branch 'master' of github.com:3b1b/manim into video-work 2024-12-11 10:36:52 -06:00
Grant Sanderson
bac0c0c9b9 Merge pull request #2265 from Varniex/master
Minor Import Bug Fixed + Adding Required Packages
2024-12-11 10:35:44 -06:00
Grant Sanderson
9ae5b4dee3 Use addict.Dict for scene config 2024-12-11 10:33:50 -06:00
Grant Sanderson
0b350e248b Change global_attrs back to global_config in Text 2024-12-11 10:18:05 -06:00
Grant Sanderson
7148d6bced Add addict to requirements 2024-12-11 10:03:49 -06:00
Grant Sanderson
b470a47da7 Remove unnecessary import 2024-12-11 10:03:39 -06:00
Grant Sanderson
13fdc9629d No need for the shortcuts into the manim_config 2024-12-11 09:58:51 -06:00
Grant Sanderson
fce92347fa Replace get_global_config() with manim_config, and make it an addict Dict 2024-12-11 09:50:17 -06:00
Grant Sanderson
185f642826 Focus and sync window when initialized for a scene 2024-12-11 09:29:04 -06:00
Grant Sanderson
4a6a125739 Change "style" in default config to "text" 2024-12-11 08:30:31 -06:00
Grant Sanderson
8246d0da5d Fix bug with xelatex rendering 2024-12-11 08:23:17 -06:00
Grant Sanderson
1794e4d0ba Better align docs description of configuration with the updated format 2024-12-11 07:37:52 -06:00
Grant Sanderson
4d7f6093b4 Update how tex configuration default is passed in 2024-12-11 07:18:30 -06:00
Grant Sanderson
37a05094ea Small comment changes 2024-12-11 07:17:20 -06:00
Varniex
76afc42e9a adding required packages to setup.cfg file 2024-12-11 16:46:09 +05:30
Varniex
5fcb668f07 fixing get_ipython import error 2024-12-11 16:40:56 +05:30
Grant Sanderson
2d7b9d579a Move comment 2024-12-10 20:19:30 -06:00
Grant Sanderson
9ac16ab722 Remove DEFAULT_FPS constant
It's a bit silly to have it's valued defined by camera_config, when it's only function is to be a default value for Camera's configuration
2024-12-10 20:19:25 -06:00
Grant Sanderson
8744c878f4 Make log_level configurable in default_config 2024-12-10 20:12:38 -06:00
Grant Sanderson
9fcdd0de5f Use pyglet.window.key for key constant values 2024-12-10 20:00:03 -06:00
Grant Sanderson
9f785a5fba Move key to int constants to interactive_scene.py 2024-12-10 19:42:53 -06:00
Grant Sanderson
a03accff9c Rename local colors variable in constants.py 2024-12-10 19:36:19 -06:00
Grant Sanderson
7d3758c44c Move joint_type_map out of constants to VMobject 2024-12-10 19:33:06 -06:00
Grant Sanderson
f9a44c9975 Make ffmpeg_bin specification a piece of file_writer_config 2024-12-10 19:29:55 -06:00
Grant Sanderson
d5c36de3c5 DEFAULT_MOBJECT_TO_MOBJECT_BUFFER -> DEFAULT_MOBJECT_TO_MOBJECT_BUFF
And likewise DEFAULT_MOBJECT_TO_MOBJECT_BUFFER -> DEFAULT_MOBJECT_TO_MOBJECT_BUFF
2024-12-10 19:23:15 -06:00
Grant Sanderson
c9b6ee57a8 Make default_wait_time a piece of scene configuration 2024-12-10 19:21:16 -06:00
Grant Sanderson
2c43d293a5 Move arbitrary constant definitions into default_config
This should make things like the color palette and frame size more easily customizable.
2024-12-10 19:17:55 -06:00
Grant Sanderson
3d3f8258f4 Merge branch 'master' of github.com:3b1b/manim into video-work 2024-12-10 18:49:44 -06:00
Grant Sanderson
17f37ff02a Merge pull request #2264 from Varniex/master
Fixing a Cairo Bug on Windows OS
2024-12-10 18:49:26 -06:00
Grant Sanderson
2359ed9aa4 Remove tempfile from requirements.txt 2024-12-10 17:00:33 -06:00
Grant Sanderson
32d36a09f6 Update commend on reload_scene 2024-12-10 15:46:34 -06:00
Grant Sanderson
8cf95ec9a4 Move ReloadManager logic into __main__.py
Since the reload logic no longer relies on any state, the relevant loop is simple enough that it feels clearest to include it in the main entry point file.
2024-12-10 15:46:17 -06:00
Grant Sanderson
24697377db Make the fact that the global configuration is a mutable global dictionary a bit more explicit
Instead of implicit through the use of lru_cache
2024-12-10 15:31:43 -06:00
Grant Sanderson
d21fbd02bc Minor tweak to reload_scene 2024-12-10 14:46:03 -06:00
Grant Sanderson
284c1d8f2c Move message for no scenes found to extract_scene 2024-12-10 14:43:10 -06:00
Grant Sanderson
ae93d8fcc6 Move update to is_reload status of run_config out of ReloadManager 2024-12-10 14:42:53 -06:00
Grant Sanderson
1d67768a13 Move reload out of Scene, instead have it directly update the global run configuration 2024-12-10 14:34:46 -06:00
Grant Sanderson
07bb34793e Add simple function descriptions 2024-12-10 14:25:26 -06:00
Grant Sanderson
cd744024ea Minor reorganization of ReloadManager.retrieve_scenes_and_run 2024-12-10 14:20:43 -06:00
Grant Sanderson
667cfaf160 Remove args from ReloadManager 2024-12-10 14:16:29 -06:00
Grant Sanderson
c61e0bcee5 Move window_config out of run_config 2024-12-10 14:16:07 -06:00
Grant Sanderson
d1080aa6fd Add run configuration to global config 2024-12-10 14:08:12 -06:00
Grant Sanderson
f9fa8ac846 Make scene configuration part of the global configuration 2024-12-10 13:58:03 -06:00
Grant Sanderson
bcc4235e2f Move embed configuration out of Scene, and get rid of error sound option 2024-12-10 12:43:29 -06:00
Varniex
c51a84a6ee Fixing a Cairo Bug (Windows OS) 2024-12-11 00:10:06 +05:30
Grant Sanderson
6b38011078 Refactor config.py 2024-12-10 12:34:18 -06:00
Grant Sanderson
858d8c122b Rename "file_writer_config" in default_config to simply "file_writer" 2024-12-10 11:43:48 -06:00
Grant Sanderson
4b483b75ce Minor tweak 2024-12-10 11:39:23 -06:00
Grant Sanderson
4cc2e5ed17 Consolidate camera configuration 2024-12-10 11:39:13 -06:00
Grant Sanderson
d4c5c4736a Move logic for window size and position into Window class 2024-12-10 11:07:54 -06:00
Grant Sanderson
178cca0ca5 Factor out get_window_position 2024-12-10 10:35:31 -06:00
Grant Sanderson
c02259a39e Remove import 2024-12-10 10:35:21 -06:00
Grant Sanderson
1276724891 Pull out the initial Window.to_default_position from init_for_scene 2024-12-10 10:14:59 -06:00
Grant Sanderson
9e77b0dcdd Consolidate window configuration 2024-12-10 10:10:58 -06:00
Grant Sanderson
1a14a6bd0d Merge pull request #2262 from 3b1b/video-work
Refactor scene creation
2024-12-10 09:53:18 -06:00
Grant Sanderson
950ac31b9b Replace IGNORE_MANIMLIB_MODULES constant with a piece of global configuration 2024-12-09 16:57:55 -06:00
Grant Sanderson
8706ba1589 No real need to track ReloadManager.scenes
This was to be able to loop through an tear them down, but tear down is primarily about ending any file writing, and potentially cleaning up a window, which for the sake of reusing a window we don't want to do anyway.
2024-12-09 16:46:13 -06:00
Grant Sanderson
dd508b8cfc No need to track ReloadManager.start_at_line 2024-12-09 16:43:08 -06:00
Grant Sanderson
88bae476ce Don't print filename that is being reloaded 2024-12-09 16:25:18 -06:00
Grant Sanderson
7a69807ce6 Remove mobject.save_to_file
This simply didn't work, and had no resilience to changes to the library. For cases where this might be useful, it's likely much better deliberately save specific data which is time-consuming to generate on the fly.
2024-12-09 16:24:50 -06:00
Grant Sanderson
6d0b23f914 Slightly simplify ReloadManager 2024-12-09 16:14:27 -06:00
Grant Sanderson
bf81d94362 Don't make reload_manager a global variable 2024-12-09 15:54:16 -06:00
Grant Sanderson
5b315d5c70 Get rid of the (hacky) solution to redefining Scene methods, since reload handles it better 2024-12-09 14:02:22 -06:00
Grant Sanderson
cb3e115a6c Minor cleaning 2024-12-09 14:01:34 -06:00
Grant Sanderson
40b5c7c1c1 Slightly clean up interactive_scene_embed 2024-12-09 13:56:33 -06:00
Grant Sanderson
636fb3a45b Factor interactive embed logic out of Scene class 2024-12-09 13:53:03 -06:00
Grant Sanderson
ea3f77e3f1 Add blank line 2024-12-09 11:59:22 -06:00
Grant Sanderson
0692afdfec Bug fix 2024-12-09 11:59:16 -06:00
Grant Sanderson
14c6fdc1d9 Slight refactor of get_indent 2024-12-09 09:49:48 -06:00
Grant Sanderson
89bf0b1297 Track all mobjects as a set in Scene. begin_animations 2024-12-07 08:21:33 -07:00
Grant Sanderson
2e8a282cc7 Merge branch 'master' of github.com:3b1b/manim into video-work 2024-12-07 08:15:11 -07:00
Grant Sanderson
5fa99b7723 Set default log level to "WARNING" 2024-12-07 08:14:56 -07:00
Benjamín Ubilla
df1e067480 Fix 3D overlap when animating by checking Mobject family members recursively instead of self.mobjects (#2254)
* Add Animation.setup_scene method to make Animation more customizable

* Remove Animation.setup_scene method and let scene check all mobject family members
2024-12-07 07:13:35 -08:00
Grant Sanderson
0ef12ad7e4 Move FRAME_HEIGHT back to constants
Where it belongs
2024-12-06 12:35:39 -07:00
Grant Sanderson
09c27a654f Minor cleaning of imports 2024-12-06 12:26:54 -07:00
Grant Sanderson
90dfb02cc6 Move get_scene_module logic to extract_scene.py 2024-12-06 12:24:16 -07:00
Grant Sanderson
e270f5c3d3 Change from get_module_with_inserted_embed_line to insert_embed_line_to_module
Rather than taking in a file_name and reading it in, directly take the module and edit its code.
2024-12-06 11:59:18 -07:00
Grant Sanderson
fadd045fc1 Don't write new file when inserting embed line
Instead, load the relevant module of the true file, and execute the modified code within that.

This also cleans up some of the previous now-unnecessary code around get_module_with_inserted_embed_line
2024-12-06 11:05:57 -07:00
Grant Sanderson
dd0aa14442 Clean up get_module_with_inserted_embed_line, only accept line number as embed arg 2024-12-06 10:39:02 -06:00
Grant Sanderson
d357e21c1d Change how ModuleLoader receives is_reload information
Use on the fly import of reload_manager rather than altering the args
2024-12-06 10:07:07 -06:00
Grant Sanderson
dd251ab8c2 Remove "preview" as a scene parameter, just look for whether window is None 2024-12-06 09:54:14 -06:00
Grant Sanderson
2e49c60148 Use config.get_resolution for constants 2024-12-06 09:49:21 -06:00
Grant Sanderson
33c7f6d063 Factor out resolution from get_camera_config 2024-12-06 09:46:26 -06:00
Grant Sanderson
53b6c34ebe Create Window outside of Scene, and pass it in as an argument 2024-12-06 09:39:12 -06:00
Grant Sanderson
49c2b5cfe0 Check if animation.mobject is in the full family of scene mobjects before adding 2024-12-06 08:51:08 -06:00
Grant Sanderson
09fb8d324e Merge branch 'master' of github.com:3b1b/manim into video-work 2024-12-05 18:18:28 -06:00
Splines
6196daa5ec Reload user-defined modules during reload() (#2257)
* Experiment a lot with module loading

* Extract methods out of experimental mess

* Fix get module return type

* Only reload() modules during reload() command

* Remove unnecessary default parameter

* Add docstrings and logging statements

* Delete unwanted printout

* Improve logging messages

* Extract methods to a new class ModuleLoader

* Remove unused builtins import

* exec_module in any case at the end

* Clarify docstrings & move get_module method up in file

* Add more additionally excluded modules as array

* Distinguish between user-defined modules and external libraries like numpy

* Improved tracked_import docstring

* Remove _insert_embed suffix before logging

* Fix args.is_reload not defined error

* Refine logic to determine whether module is user-defined or not

* Fix list vs. set type annotations

* Improve docstrings & change order of early return

* Fix spelling mistake of "Reloading"

* Try out custom deep reload

* Make deep reload more robust

* Also reload modules imported as classes

* Move early return up to greatly improve performance

* Clean up comments

* Make methods of Module Loader "private"

* Add backticks around function in docstring

---------

Co-authored-by: Grant Sanderson <grant@3blue1brown.com>
2024-12-05 16:18:10 -08:00
Grant Sanderson
e05cae6775 Merge branch 'master' of github.com:3b1b/manim into video-work 2024-12-05 16:51:35 -06:00
Grant Sanderson
94f6f0aa96 Cleaner local caching of Tex/Text data, and partially cleaned up configuration (#2259)
* Remove print("Reloading...")

* Change where exception mode is set, to be quieter

* Add default fallback monitor for when no monitors are detected

* Have StringMobject work with svg strings rather than necessarily writing to file

Change SVGMobject to allow taking in a string of svg code as an input

* Add caching functionality, and have Tex and Text both use it for saved svg strings

* Clean up tex_file_writing

* Get rid of get_tex_dir and get_text_dir

* Allow for a configurable cache location

* Make caching on disk a decorator, and update implementations for Tex and Text mobjects

* Remove stray prints

* Clean up how configuration is handled

In principle, all we need here is that manim looks to the default_config.yaml file, and updates it based on any local configuration files, whether in the current working directory or as specified by a CLI argument.

* Make the default size for hash_string an option

* Remove utils/customization.py

* Remove stray prints

* Consolidate camera configuration

This is still not optimal, but at least makes clearer the way that importing from constants.py kicks off some of the configuration code.

* Factor out configuration to be passed into a scene vs. that used to run a scene

* Use newer extract_scene.main interface

* Add clarifying message to note what exactly is being reloaded

* Minor clean up

* Minor clean up

* If it's worth caching to disk, then might as well do so in memory too during development

* No longer any need for custom hash_seeds in Tex and Text

* Remove display_during_execution

* Get rid of (no longer used) mobject_data directory reference

* Remove get_downloads_dir reference from register_font

* Update where downloads go

* Easier use of subdirectories in configuration

* Add new pip requirements
2024-12-05 14:51:14 -08:00
Grant Sanderson
0e83c9c0d9 Merge branch 'master' into video-work 2024-12-05 16:50:13 -06:00
Iñaki Rabanillo
5a70d67b98 Update coordinate_systems.py (#2258) 2024-12-05 14:49:16 -08:00
henri-gasc
66862db9b2 Drop pyrr (#2256) 2024-12-05 14:43:14 -08:00
Varniex
5d3f730824 Cleaning up some imports + Minor Bug fixed in VectorField (#2253)
* cleaning up imports

* sample_points -> sample_coords
2024-12-05 14:42:46 -08:00
Grant Sanderson
3cd3e8cedc Add new pip requirements 2024-12-05 15:56:29 -06:00
Grant Sanderson
08acfa6f1f Easier use of subdirectories in configuration 2024-12-05 15:52:39 -06:00
Grant Sanderson
75527563de Update where downloads go 2024-12-05 15:27:57 -06:00
Grant Sanderson
c96734ace0 Remove get_downloads_dir reference from register_font 2024-12-05 15:14:37 -06:00
Grant Sanderson
71e440be93 Get rid of (no longer used) mobject_data directory reference 2024-12-05 15:08:25 -06:00
Grant Sanderson
8098149006 Remove display_during_execution 2024-12-05 15:05:37 -06:00
Grant Sanderson
4251ff436a No longer any need for custom hash_seeds in Tex and Text 2024-12-05 15:05:26 -06:00
Grant Sanderson
85f8456228 If it's worth caching to disk, then might as well do so in memory too during development 2024-12-05 14:56:35 -06:00
Grant Sanderson
e0031c63bc Minor clean up 2024-12-05 14:55:28 -06:00
Grant Sanderson
361d9d0652 Minor clean up 2024-12-05 14:42:22 -06:00
Grant Sanderson
1d14bae092 Add clarifying message to note what exactly is being reloaded 2024-12-05 14:37:14 -06:00
Grant Sanderson
8dfd4c1c4e Use newer extract_scene.main interface 2024-12-05 14:36:43 -06:00
Grant Sanderson
96a4a4b76f Factor out configuration to be passed into a scene vs. that used to run a scene 2024-12-05 14:36:21 -06:00
Grant Sanderson
0496402c55 Consolidate camera configuration
This is still not optimal, but at least makes clearer the way that importing from constants.py kicks off some of the configuration code.
2024-12-05 14:17:53 -06:00
Grant Sanderson
fc32f162a0 Remove stray prints 2024-12-05 13:46:47 -06:00
Grant Sanderson
3b9ef57b22 Remove utils/customization.py 2024-12-05 11:59:01 -06:00
Grant Sanderson
b593cde317 Make the default size for hash_string an option 2024-12-05 11:53:55 -06:00
Grant Sanderson
34ad61d013 Clean up how configuration is handled
In principle, all we need here is that manim looks to the default_config.yaml file, and updates it based on any local configuration files, whether in the current working directory or as specified by a CLI argument.
2024-12-05 11:53:18 -06:00
Grant Sanderson
cfb7d2fa47 Remove stray prints 2024-12-05 10:09:48 -06:00
Grant Sanderson
43821ab2ba Make caching on disk a decorator, and update implementations for Tex and Text mobjects 2024-12-05 10:09:15 -06:00
Grant Sanderson
89ddfadf6b Allow for a configurable cache location 2024-12-04 20:50:42 -06:00
Grant Sanderson
0c385e820f Get rid of get_tex_dir and get_text_dir 2024-12-04 20:33:43 -06:00
Grant Sanderson
ac01b144e8 Clean up tex_file_writing 2024-12-04 20:30:53 -06:00
Grant Sanderson
129e512b0c Add caching functionality, and have Tex and Text both use it for saved svg strings 2024-12-04 19:51:01 -06:00
Grant Sanderson
88370d4d5d Have StringMobject work with svg strings rather than necessarily writing to file
Change SVGMobject to allow taking in a string of svg code as an input
2024-12-04 19:11:21 -06:00
Grant Sanderson
671a31b298 Add default fallback monitor for when no monitors are detected 2024-12-03 15:14:48 -06:00
Grant Sanderson
f8280a12be Change where exception mode is set, to be quieter 2024-11-30 10:08:54 -06:00
Grant Sanderson
d78fe93743 Remove print("Reloading...") 2024-11-30 10:08:41 -06:00
Anon
8239f1bf35 Update README.md for better readability (#2246) 2024-11-26 10:11:04 -08:00
Splines
1fa17030a2 Add reload() command for interactive scene reloading (#2240)
* Init reload command (lots of things not working yet)

* Add back in class line (accidentally deleted)

* Add back in key modifiers (accidentally deleted)

* Unpack tuple from changed `get_module`

* Init MainRunManager & respawn IPython shell

* Init cleanup of scenes from manager

* Restore string quotes

* Still take `self.preview` into account

* Remove left-over code from module experimentation

* Remove double window activation

* Reset scenes array in RunManager

* Move self.args None check up

* Use first available window

* Don't use constructor for RunManager

* Use self. syntax

* Init moderngl context manually

* Add some comments for failed attempts to reset scene

* Reuse existing shell (this fixed the bug 🎉)

* Remove unused code

* Remove unnecessary intermediate ReloadSceneException

* Allow users to finally exit

* Rename main_run_manager to reload_manager

* Add docstrings to `ReloadManager`

* Improve reset management in window

* Clarify why we use magic exit_raise command

* Add comment about window reuse

* Improve docstrings in ReloadManager & handle case of 0 scenes

* Set scene and title earlier

* Run linter suggestions
2024-11-26 10:09:43 -08:00
Grant Sanderson
530cb4f104 Merge pull request #2250 from 3b1b/video-work
Video work
2024-11-25 13:44:08 -06:00
Grant Sanderson
85638d88dc Update parameter range for sphere 2024-11-25 12:39:41 -07:00
Grant Sanderson
fbce0b132c Temporary band-aide for degenerate normal vector calculations
This solution is a bit too specific to the case of spheres.
2024-11-25 12:39:32 -07:00
Grant Sanderson
dd51b696e5 Only apply non-flat-stroke correction in non-zero joint angle vertices 2024-11-25 12:35:32 -07:00
Grant Sanderson
9cd6a87ff8 Make sure VMobject uniform flat_stroke matches the use inside the quadratic_bezier/stroke/geom.glsl code 2024-11-25 12:28:31 -07:00
Grant Sanderson
54c8a9014b Add scale_stroke_with_zoom option to VMobject 2024-11-25 11:27:11 -07:00
Grant Sanderson
e19ceaaff0 Have TexMobject keep track of font_size 2024-11-25 11:02:54 -07:00
Grant Sanderson
5b88d2347c Allow for LaTeX in DecimalNumber, e.g. for units 2024-11-25 11:01:38 -07:00
Grant Sanderson
c6b9826f84 Update TimeVaryingVectorField to match new VectorField configuration 2024-11-25 10:50:12 -07:00
Grant Sanderson
90ab2f64bb Clean up style arguments on VectorField 2024-11-25 10:49:29 -07:00
Grant Sanderson
ed2f9f3305 Fix import of pyplot 2024-11-25 10:49:05 -07:00
Grant Sanderson
1d0deb8a33 Remove OldVectorfield 2024-11-25 10:14:23 -07:00
Grant Sanderson
753a042dbe Remove unused method 2024-11-25 10:13:44 -07:00
Grant Sanderson
55b12c902c Use density as a parameter instead of step_multiple 2024-11-25 10:13:37 -07:00
Grant Sanderson
e80b9d0e47 Less collision-prone file names for downloads 2024-11-25 09:31:15 -07:00
Grant Sanderson
1248abd922 Merge pull request #2233 from mitkonikov/modifier-keys-fix
Properly check modifier keys.
2024-11-25 10:09:10 -06:00
Grant Sanderson
314ca89a45 Merge pull request #2241 from Splines/feature/focus
Add `focus()` command
2024-11-25 10:06:48 -06:00
Grant Sanderson
0ad5a0e76e Further development on VectorField 2024-11-15 09:07:46 -08:00
Grant Sanderson
64ae1364ca Update the Vector Field interface 2024-11-12 11:21:19 -08:00
Splines
af923a2327 Add docstring to user-facing focus() method 2024-11-10 19:10:53 +01:00
Splines
97b6e39abb Init new focus() command 2024-11-10 18:48:33 +01:00
Grant Sanderson
b84376d6fd Add Cone 2024-11-08 14:28:17 -06:00
Grant Sanderson
9475fcd19e Have clip plane recurse through family 2024-11-08 14:27:20 -06:00
Grant Sanderson
003c4d8626 Merge pull request #2231 from MathItYT/master
Fix bad 3D overlapping
2024-10-27 13:07:35 -05:00
MathItYT
693a859caf revert changes in mobject.py and camera.py 2024-10-27 14:10:12 -03:00
MathItYT
52948f846e Merge branch 'master' of https://github.com/MathItYT/manimgl 2024-10-27 14:07:03 -03:00
MathItYT
1738876f43 fix bad 3D overlapping using z_index 2024-10-27 14:06:35 -03:00
Mitko Nikov
dc731f8bf2 Properly check modifier keys. 2024-10-25 00:01:30 +02:00
MathItYT
e5cf0558d8 fix 3D bad overlapping 2024-10-23 20:31:52 -03:00
Grant Sanderson
1139b545f9 Merge pull request #2214 from 3b1b/update-pango-requirement
Update ManimPango requirement
2024-10-23 17:59:08 -05:00
Grant Sanderson
0b65e4c7b6 Merge pull request #2220 from Varniex/master
Minor Bug fixed: window's bg color now changing.
2024-10-23 17:58:52 -05:00
Grant Sanderson
371fca147b Update version in setup.cfg 2024-10-23 17:54:18 -05:00
Grant Sanderson
e1816c2ac5 Merge pull request #2230 from 3b1b/video-work
Misc. bug fixes
2024-10-23 17:44:08 -05:00
Grant Sanderson
199395b6e3 Fix negative winding issue
https://github.com/3b1b/manim/issues/2146
2024-10-23 17:40:31 -05:00
Grant Sanderson
837bb14c03 Merge branch 'master' of github.com:3b1b/manim into video-work 2024-10-23 11:41:29 -05:00
Grant Sanderson
eca370f5ce Merge pull request #2229 from 3b1b:add-dependency
Add mapbox-earcut dependency
2024-10-23 11:41:06 -05:00
Grant Sanderson
5505fc1d54 Add mapbox-earcut dependency 2024-10-23 11:40:14 -05:00
Varniex
04295ec177 Minor Bug fixed: window's bg color now changing. 2024-10-20 16:53:47 +05:30
Grant Sanderson
0c7c9dee93 Merge branch 'master' of github.com:3b1b/manim into video-work 2024-10-17 12:43:57 -05:00
Grant Sanderson
1a65498f97 Merge pull request #2218 from 3b1b/3b1b-patch-1
Update setup.cfg
2024-10-17 12:43:28 -05:00
Grant Sanderson
a34c4482f6 Update setup.cfg 2024-10-17 10:43:18 -07:00
Grant Sanderson
e3e87f6110 Update Pango requirement 2024-10-17 12:32:11 -05:00
Grant Sanderson
aaa28a2712 Discard transparent parts of textured surfaces 2024-10-17 12:31:53 -05:00
Grant Sanderson
aa18373eb7 Update ManimPango requirement 2024-10-15 09:51:55 -07:00
85 changed files with 3025 additions and 2199 deletions

2
.gitignore vendored
View File

@@ -151,3 +151,5 @@ dmypy.json
# For manim
/videos
/custom_config.yml
test.py
CLAUDE.md

View File

@@ -15,14 +15,16 @@ Manim is an engine for precise programmatic animations, designed for creating ex
Note, there are two versions of manim. This repository began as a personal project by the author of [3Blue1Brown](https://www.3blue1brown.com/) for the purpose of animating those videos, with video-specific code available [here](https://github.com/3b1b/videos). In 2020 a group of developers forked it into what is now the [community edition](https://github.com/ManimCommunity/manim/), with a goal of being more stable, better tested, quicker to respond to community contributions, and all around friendlier to get started with. See [this page](https://docs.manim.community/en/stable/faq/installation.html#different-versions) for more details.
## Installation
> **WARNING:** These instructions are for ManimGL _only_. Trying to use these instructions to install [ManimCommunity/manim](https://github.com/ManimCommunity/manim) or instructions there to install this version will cause problems. You should first decide which version you wish to install, then only follow the instructions for your desired version.
>
> [!Warning]
> **WARNING:** These instructions are for ManimGL _only_. Trying to use these instructions to install [Manim Community/manim](https://github.com/ManimCommunity/manim) or instructions there to install this version will cause problems. You should first decide which version you wish to install, then only follow the instructions for your desired version.
> [!Note]
> **Note**: To install manim directly through pip, please pay attention to the name of the installed package. This repository is ManimGL of 3b1b. The package name is `manimgl` instead of `manim` or `manimlib`. Please use `pip install manimgl` to install the version in this repository.
Manim runs on Python 3.7 or higher.
System requirements are [FFmpeg](https://ffmpeg.org/), [OpenGL](https://www.opengl.org/) and [LaTeX](https://www.latex-project.org) (optional, if you want to use LaTeX).
For Linux, [Pango](https://pango.gnome.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
For Linux, [Pango](https://pango.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
### Directly
@@ -67,13 +69,18 @@ manim-render example_scenes.py OpeningManimExample
```sh
brew install ffmpeg mactex
```
2. If you are using an ARM-based processor, install Cairo.
```sh
arch -arm64 brew install pkg-config cairo
```
2. Install latest version of manim using these command.
3. Install latest version of manim using these command.
```sh
git clone https://github.com/3b1b/manim.git
cd manim
pip install -e .
manimgl example_scenes.py OpeningManimExample
manimgl example_scenes.py OpeningManimExample (make sure to add manimgl to path first.)
```
## Anaconda Install

View File

@@ -8,38 +8,35 @@ they are only used inside manim.
Frame and pixel shape
---------------------
These values will be determined based on the ``camera`` configuration in default_config.yml or custom_config.yml
.. code-block:: python
ASPECT_RATIO = 16.0 / 9.0
FRAME_HEIGHT = 8.0
FRAME_WIDTH = FRAME_HEIGHT * ASPECT_RATIO
FRAME_Y_RADIUS = FRAME_HEIGHT / 2
FRAME_X_RADIUS = FRAME_WIDTH / 2
ASPECT_RATIO
FRAME_HEIGHT
FRAME_WIDTH
FRAME_Y_RADIUS
FRAME_X_RADIUS
DEFAULT_PIXEL_HEIGHT = 1080
DEFAULT_PIXEL_WIDTH = 1920
DEFAULT_FPS = 30
DEFAULT_PIXEL_HEIGHT
DEFAULT_PIXEL_WIDTH
DEFAULT_FPS
Buffs
-----
.. code-block:: python
These values will be determined based on the ``size`` configuration in default_config.yml or custom_config.yml
SMALL_BUFF = 0.1
MED_SMALL_BUFF = 0.25
MED_LARGE_BUFF = 0.5
LARGE_BUFF = 1
DEFAULT_MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF # Distance between object and edge
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF # Distance between objects
Run times
---------
.. code-block:: python
DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0
DEFAULT_WAIT_TIME = 1.0
SMALL_BUFF
MED_SMALL_BUFF
MED_LARGE_BUFF
LARGE_BUFF
DEFAULT_MOBJECT_TO_EDGE_BUFF
DEFAULT_MOBJECT_TO_MOBJECT_BUFF
Coordinates
-----------
@@ -77,7 +74,7 @@ Mathematical constant
PI = np.pi
TAU = 2 * PI
DEGREES = TAU / 360
DEG = TAU / 360
Text
----
@@ -89,16 +86,11 @@ Text
OBLIQUE = "OBLIQUE"
BOLD = "BOLD"
Stroke width
------------
.. code-block:: python
DEFAULT_STROKE_WIDTH = 4
Colours
-------
Color constants are determined based on the ``color`` configuration in default_config.yml or custom_config.yml
Here are the preview of default colours. (Modified from
`elteoremadebeethoven <https://elteoremadebeethoven.github.io/manim_3feb_docs.github.io/html/_static/colors/colors.html>`_)

View File

@@ -9,6 +9,10 @@ custom_config
running file under the ``output`` path, and save the output (``images/``
or ``videos/``) in it.
- ``base``
The root directory that will hold files, such as video files manim renders,
or image resources that it pulls from
- ``output``
Output file path, the videos will be saved in the ``videos/`` folder under it,
and the pictures will be saved in the ``images/`` folder under it.
@@ -66,34 +70,62 @@ custom_config
The directory for storing sound files to be used in ``Scene.add_sound()`` (
including ``.wav`` and ``.mp3``).
- ``temporary_storage``
- ``cache``
The directory for storing temporarily generated cache files, including
``Tex`` cache, ``Text`` cache and storage of object points.
``tex``
``window``
----------
- ``position_string``
The relative position of the playback window on the display (two characters,
the first character means upper(U) / middle(O) / lower(D), the second character
means left(L) / middle(O) / right(R)).
- ``monitor_index``
If using multiple monitors, which one should the window show up in?
- ``full_screen``
Should the preview window be full screen. If not, it defaults to half the screen
- ``position``
This is an option to more manually set the default window position, in pixel
coordinates, e.g. (500, 300)
- ``size``
Option to more manually set the default window size, in pixel coordinates,
e.g. (1920, 1080)
``camera``
----------
- ``resolution``
Resolution to render at, e.g. (1920, 1080)
- ``background_color``
Default background color of scenes
- ``fps``
Framerate
- ``background_opacity``
Opacity of the background
``file_writer``
---------------
Configuration specifying how files are written, e.g. what ffmpeg parameters to use
``scene``
-------
Some default configuration for the Scene class
- ``executable``
The executable program used to compile LaTeX (``latex`` or ``xelatex -no-pdf``
is recommended)
- ``template_file``
LaTeX template used, in ``manimlib/tex_templates``
- ``intermediate_filetype``
The type of intermediate vector file generated after compilation (``dvi`` if
``latex`` is used, ``xdv`` if ``xelatex`` is used)
- ``text_to_replace``
The text to be replaced in the template (needn't to change)
``universal_import_line``
-------------------------
Import line that need to execute when entering interactive mode directly.
``style``
---------
``text``
-------
- ``font``
Default font of Text
@@ -101,57 +133,44 @@ Import line that need to execute when entering interactive mode directly.
- ``text_alignment``
Default text alignment for LaTeX
- ``background_color``
Default background color
``window_position``
-------------------
The relative position of the playback window on the display (two characters,
the first character means upper(U) / middle(O) / lower(D), the second character
means left(L) / middle(O) / right(R)).
``window_monitor``
------------------
The number of the monitor you want the preview window to pop up on. (default is 0)
``full_screen``
---------------
Whether open the window in full screen. (default is false)
``break_into_partial_movies``
-----------------------------
If this 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
to form the full scene.
Sometimes video-editing is made easier when working with the broken up scene, which
effectively has cuts at all the places you might want.
``camera_resolutions``
----------------------
Export resolutions
- ``low``
Low resolutions (default is 480p)
- ``medium``
Medium resolutions (default is 720p)
- ``high``
High resolutions (default is 1080p)
- ``ultra_high``
Ultra high resolutions (default is 4K)
- ``default_resolutions``
Default resolutions (one of the above four, default is high)
``fps``
``tex``
-------
Export frame rate. (default is 30)
- ``template``
Which configuration from the manimlib/tex_template.yml file should be used
to determine the latex compiler to use, and what preamble to include for
rendering tex.
``sizes``
---------
Valuess for various constants used in manimm to specify distances, like the height
of the frame, the value of SMALL_BUFF, LARGE_BUFF, etc.
``colors``
----------
Color pallete to use, determining values of color constants like RED, BLUE_E, TEAL, etc.
``loglevel``
------------
Can be DEBUG / INFO / WARNING / ERROR / CRITICAL
``universal_import_line``
-------------------------
Import line that need to execute when entering interactive mode directly.
``ignore_manimlib_modules_on_reload``
-------------------------------------
When calling ``reload`` during the interactive mode, imported modules are
by default reloaded, in case the user writing a scene which pulls from various
other files they have written. By default, modules withinn the manim library will
be ignored, but one developing manim may want to set this to be False so that
edits to the library are reloaded as well.

View File

@@ -63,6 +63,7 @@ flag abbr function
``--video_dir VIDEO_DIR`` Directory to write video
``--config_file CONFIG_FILE`` Path to the custom configuration file
``--log-level LOG_LEVEL`` Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL
``--autoreload`` Automatically reload Python modules to pick up code changes across during an interactive embedding
========================================================== ====== =====================================================================================================================================================================================================
custom_config

View File

@@ -34,7 +34,7 @@ InteractiveDevlopment
self.play(ReplacementTransform(square, circle))
self.wait()
self.play(circle.animate.stretch(4, 0))
self.play(Rotate(circle, 90 * DEGREES))
self.play(Rotate(circle, 90 * DEG))
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
text = Text("""
@@ -221,7 +221,7 @@ TexTransformExample
self.play(
TransformMatchingTex(
lines[0].copy(), lines[1],
path_arc=90 * DEGREES,
path_arc=90 * DEG,
),
**play_kw
)
@@ -599,8 +599,8 @@ SurfaceExample
# Set perspective
frame = self.camera.frame
frame.set_euler_angles(
theta=-30 * DEGREES,
phi=70 * DEGREES,
theta=-30 * DEG,
phi=70 * DEG,
)
surface = surfaces[0]
@@ -624,8 +624,8 @@ SurfaceExample
self.play(
Transform(surface, surfaces[2]),
# Move camera frame during the transition
frame.animate.increment_phi(-10 * DEGREES),
frame.animate.increment_theta(-20 * DEGREES),
frame.animate.increment_phi(-10 * DEG),
frame.animate.increment_theta(-20 * DEG),
run_time=3
)
# Add ambient rotation

View File

@@ -103,7 +103,6 @@ Below is the directory structure of manim:
├── family_ops.py # Process family members
├── file_ops.py # Process files and directories
├── images.py # Read image
├── init_config.py # Configuration guide
├── iterables.py # Functions related to list/dictionary processing
├── paths.py # Curve path
├── rate_functions.py # Some defined rate_functions

View File

@@ -190,7 +190,7 @@ class TexTransformExample(Scene):
# to go to a non-equal substring from the target,
# use the key map.
key_map={"+": "-"},
path_arc=90 * DEGREES,
path_arc=90 * DEG,
),
)
self.wait()
@@ -203,7 +203,7 @@ class TexTransformExample(Scene):
TransformMatchingStrings(
lines[2].copy(), lines[3],
key_map={"2": R"\sqrt"},
path_arc=-30 * DEGREES,
path_arc=-30 * DEG,
),
)
self.wait(2)
@@ -326,7 +326,7 @@ class UpdatersExample(Scene):
)
self.wait()
# In general, you can alway call Mobject.add_updater, and pass in
# In general, you can always call Mobject.add_updater, and pass in
# a function that you want to be called on every frame. The function
# should take in either one argument, the mobject, or two arguments,
# the mobject and the amount of time since the last frame.
@@ -534,7 +534,7 @@ class TexAndNumbersExample(Scene):
rate_func=there_and_back,
)
# By default, tex.make_number_changeable replaces the first occurance
# By default, tex.make_number_changeable replaces the first occurrence
# of the number,but by passing replace_all=True it replaces all and
# returns a group of the results
exponents = tex.make_number_changeable("2", replace_all=True)
@@ -616,8 +616,8 @@ class SurfaceExample(ThreeDScene):
self.play(
Transform(surface, surfaces[2]),
# Move camera frame during the transition
self.frame.animate.increment_phi(-10 * DEGREES),
self.frame.animate.increment_theta(-20 * DEGREES),
self.frame.animate.increment_phi(-10 * DEG),
self.frame.animate.increment_theta(-20 * DEG),
run_time=3
)
# Add ambient rotation
@@ -666,7 +666,7 @@ class InteractiveDevelopment(Scene):
self.play(ReplacementTransform(square, circle))
self.wait()
self.play(circle.animate.stretch(4, 0))
self.play(Rotate(circle, 90 * DEGREES))
self.play(Rotate(circle, 90 * DEG))
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
text = Text("""

View File

@@ -61,9 +61,9 @@ from manimlib.scene.interactive_scene import *
from manimlib.scene.scene import *
from manimlib.utils.bezier import *
from manimlib.utils.cache import *
from manimlib.utils.color import *
from manimlib.utils.dict_ops import *
from manimlib.utils.customization import *
from manimlib.utils.debug import *
from manimlib.utils.directories import *
from manimlib.utils.file_ops import *

View File

@@ -1,28 +1,64 @@
#!/usr/bin/env python
from addict import Dict
from manimlib import __version__
import manimlib.config
from manimlib.config import manim_config
from manimlib.config import parse_cli
import manimlib.extract_scene
import manimlib.logger
import manimlib.utils.init_config
from manimlib.utils.cache import clear_cache
from manimlib.window import Window
from IPython.terminal.embed import KillEmbedded
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from argparse import Namespace
def run_scenes():
"""
Runs the scenes in a loop and detects when a scene reload is requested.
"""
# Create a new dict to be able to upate without
# altering global configuration
scene_config = Dict(manim_config.scene)
run_config = manim_config.run
if run_config.show_in_window:
# Create a reusable window
window = Window(**manim_config.window)
scene_config.update(window=window)
while True:
try:
# Blocking call since a scene may init an IPython shell()
scenes = manimlib.extract_scene.main(scene_config, run_config)
for scene in scenes:
scene.run()
return
except KillEmbedded:
# Requested via the `exit_raise` IPython runline magic
# by means of the reload_scene() command
pass
except KeyboardInterrupt:
break
def main():
"""
Main entry point for ManimGL.
"""
print(f"ManimGL \033[32mv{__version__}\033[0m")
args = manimlib.config.parse_cli()
args = parse_cli()
if args.version and args.file is None:
return
if args.log_level:
manimlib.logger.log.setLevel(args.log_level)
if args.clear_cache:
clear_cache()
if args.config:
manimlib.utils.init_config.init_customization()
else:
config = manimlib.config.get_configuration(args)
scenes = manimlib.extract_scene.main(config)
for scene in scenes:
scene.run()
run_scenes()
if __name__ == "__main__":

View File

@@ -6,7 +6,6 @@ from manimlib.mobject.mobject import _AnimationBuilder
from manimlib.mobject.mobject import Mobject
from manimlib.utils.iterables import remove_list_redundancies
from manimlib.utils.rate_functions import smooth
from manimlib.utils.rate_functions import squish_rate_func
from manimlib.utils.simple_functions import clip
from typing import TYPE_CHECKING
@@ -34,7 +33,7 @@ class Animation(object):
lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO,
rate_func: Callable[[float], float] = smooth,
name: str = "",
# Does this animation add or remove a mobject form the screen
# Does this animation add or remove a mobject from the screen
remover: bool = False,
# What to enter into the update function upon completion
final_alpha_value: float = 1.0,
@@ -43,6 +42,7 @@ class Animation(object):
# updating, call mobject.suspend_updating() before the animation
suspend_mobject_updating: bool = False,
):
self._validate_input_type(mobject)
self.mobject = mobject
self.run_time = run_time
self.time_span = time_span
@@ -53,7 +53,9 @@ class Animation(object):
self.lag_ratio = lag_ratio
self.suspend_mobject_updating = suspend_mobject_updating
assert isinstance(mobject, Mobject)
def _validate_input_type(self, mobject: Mobject) -> None:
if not isinstance(mobject, Mobject):
raise TypeError("Animation only works for Mobjects.")
def __str__(self) -> str:
return self.name

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.animation.animation import prepare_animation
from manimlib.mobject.mobject import _AnimationBuilder
@@ -45,7 +43,7 @@ class AnimationGroup(Animation):
mobs = remove_list_redundancies([a.mobject for a in self.animations])
if group is not None:
self.group = group
if group_type is not None:
elif group_type is not None:
self.group = group_type(*mobs)
elif all(isinstance(anim.mobject, VMobject) for anim in animations):
self.group = VGroup(*mobs)

View File

@@ -5,7 +5,6 @@ from abc import ABC, abstractmethod
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.constants import WHITE
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import integer_interpolate

View File

@@ -5,9 +5,8 @@ import numpy as np
from manimlib.animation.animation import Animation
from manimlib.animation.transform import Transform
from manimlib.constants import ORIGIN
from manimlib.mobject.mobject import Group
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.mobject import Group
from manimlib.utils.bezier import interpolate
from manimlib.utils.rate_functions import there_and_back
@@ -103,7 +102,7 @@ class FadeTransform(Transform):
self.dim_to_match = dim_to_match
mobject.save_state()
super().__init__(mobject.get_group_class()(mobject, target_mobject.copy()), **kwargs)
super().__init__(Group(mobject, target_mobject.copy()), **kwargs)
def begin(self) -> None:
self.ending_mobject = self.mobject.copy()

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
from manimlib.animation.transform import Transform
from manimlib.constants import PI
from typing import TYPE_CHECKING

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
import math
from os import remove
import numpy as np
from manimlib.animation.animation import Animation
@@ -16,7 +14,7 @@ from manimlib.animation.transform import Transform
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import ORIGIN, RIGHT, UP
from manimlib.constants import SMALL_BUFF
from manimlib.constants import DEGREES
from manimlib.constants import DEG
from manimlib.constants import TAU
from manimlib.constants import GREY, YELLOW
from manimlib.mobject.geometry import Circle
@@ -397,7 +395,7 @@ class WiggleOutThenIn(Animation):
class TurnInsideOut(Transform):
def __init__(self, mobject: Mobject, path_arc: float = 90 * DEGREES, **kwargs):
def __init__(self, mobject: Mobject, path_arc: float = 90 * DEG, **kwargs):
super().__init__(mobject, path_arc=path_arc, **kwargs)
def create_target(self) -> Mobject:

View File

@@ -29,9 +29,9 @@ class ChangingDecimal(Animation):
self.mobject = decimal_mob
def interpolate_mobject(self, alpha: float) -> None:
self.mobject.set_value(
self.number_update_func(alpha)
)
true_alpha = self.time_spanned_alpha(alpha)
new_value = self.number_update_func(true_alpha)
self.mobject.set_value(new_value)
class ChangeDecimalToValue(ChangingDecimal):

View File

@@ -5,7 +5,7 @@ import inspect
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.constants import DEGREES
from manimlib.constants import DEG
from manimlib.constants import OUT
from manimlib.mobject.mobject import Group
from manimlib.mobject.mobject import Mobject
@@ -314,7 +314,7 @@ class ApplyComplexFunction(ApplyMethod):
class CyclicReplace(Transform):
def __init__(self, *mobjects: Mobject, path_arc=90 * DEGREES, **kwargs):
def __init__(self, *mobjects: Mobject, path_arc=90 * DEG, **kwargs):
super().__init__(Group(*mobjects), path_arc=path_arc, **kwargs)
def create_target(self) -> Mobject:

View File

@@ -88,7 +88,7 @@ class TransformMatchingParts(AnimationGroup):
if not source_is_new or not target_is_new:
return
transform_type = self.mismatch_animation
transform_type = self.mismatch_animation
if source.has_same_shape_as(target):
transform_type = self.match_animation
@@ -154,16 +154,16 @@ class TransformMatchingStrings(TransformMatchingParts):
counts2 = list(map(target.substr_to_path_count, syms2))
# Start with user specified matches
blocks = [(source[key], target[key]) for key in matched_keys]
blocks += [(source[key1], target[key2]) for key1, key2 in key_map.items()]
blocks = [(source[key1], target[key2]) for key1, key2 in key_map.items()]
blocks += [(source[key], target[key]) for key in matched_keys]
# Nullify any intersections with those matches in the two symbol lists
for sub_source, sub_target in blocks:
for i in range(len(syms1)):
if source[i] in sub_source.family_members_with_points():
if i < len(source) and source[i] in sub_source.family_members_with_points():
syms1[i] = "Null1"
for j in range(len(syms2)):
if target[j] in sub_target.family_members_with_points():
if j < len(target) and target[j] in sub_target.family_members_with_points():
syms2[j] = "Null2"
# Group together longest matching substrings

View File

@@ -46,7 +46,8 @@ class UpdateFromAlphaFunc(Animation):
super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs)
def interpolate_mobject(self, alpha: float) -> None:
self.update_function(self.mobject, alpha)
true_alpha = self.rate_func(self.time_spanned_alpha(alpha))
self.update_function(self.mobject, true_alpha)
class MaintainPositionRelativeTo(Animation):

View File

@@ -6,10 +6,8 @@ import OpenGL.GL as gl
from PIL import Image
from manimlib.camera.camera_frame import CameraFrame
from manimlib.constants import ASPECT_RATIO
from manimlib.constants import BLACK
from manimlib.constants import DEFAULT_FPS
from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH
from manimlib.constants import DEFAULT_RESOLUTION
from manimlib.constants import FRAME_HEIGHT
from manimlib.constants import FRAME_WIDTH
from manimlib.mobject.mobject import Mobject
@@ -30,10 +28,9 @@ class Camera(object):
window: Optional[Window] = None,
background_image: Optional[str] = None,
frame_config: dict = dict(),
pixel_width: int = DEFAULT_PIXEL_WIDTH,
pixel_height: int = DEFAULT_PIXEL_HEIGHT,
fps: int = DEFAULT_FPS,
# Note: frame height and width will be resized to match the pixel aspect ratio
# Note: frame height and width will be resized to match this resolution aspect ratio
resolution=DEFAULT_RESOLUTION,
fps: int = 30,
background_color: ManimColor = BLACK,
background_opacity: float = 1.0,
# Points in vectorized mobjects with norm greater
@@ -48,9 +45,9 @@ class Camera(object):
# to set samples to be greater than 0.
samples: int = 0,
):
self.background_image = background_image
self.window = window
self.default_pixel_shape = (pixel_width, pixel_height)
self.background_image = background_image
self.default_pixel_shape = resolution # Rename?
self.fps = fps
self.max_allowable_norm = max_allowable_norm
self.image_mode = image_mode
@@ -127,7 +124,7 @@ class Camera(object):
def clear(self) -> None:
self.fbo.clear(*self.background_rgba)
if self.window:
self.window.clear()
self.window.clear(*self.background_rgba)
def blit(self, src_fbo, dst_fbo):
"""
@@ -221,8 +218,8 @@ class Camera(object):
frame_height = frame_width / aspect_ratio
else:
frame_width = aspect_ratio * frame_height
self.frame.set_height(frame_height, stretch=true)
self.frame.set_width(frame_width, stretch=true)
self.frame.set_height(frame_height, stretch=True)
self.frame.set_width(frame_width, stretch=True)
# Rendering
def capture(self, *mobjects: Mobject) -> None:

View File

@@ -5,9 +5,8 @@ import warnings
import numpy as np
from scipy.spatial.transform import Rotation
from pyrr import Matrix44
from manimlib.constants import DEGREES, RADIANS
from manimlib.constants import DEG, RADIANS
from manimlib.constants import FRAME_SHAPE
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.constants import PI
@@ -27,7 +26,7 @@ class CameraFrame(Mobject):
frame_shape: tuple[float, float] = FRAME_SHAPE,
center_point: Vect3 = ORIGIN,
# Field of view in the y direction
fovy: float = 45 * DEGREES,
fovy: float = 45 * DEG,
euler_axes: str = "zxz",
# This keeps it ordered first in a scene
z_index=-1,
@@ -182,7 +181,7 @@ class CameraFrame(Mobject):
Shortcut for set_euler_angles, defaulting to taking
in angles in degrees
"""
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES)
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEG)
if center is not None:
self.move_to(np.array(center))
if height is not None:
@@ -210,7 +209,7 @@ class CameraFrame(Mobject):
self.increment_euler_angles(dgamma=dgamma, units=units)
return self
def add_ambient_rotation(self, angular_speed=1 * DEGREES):
def add_ambient_rotation(self, angular_speed=1 * DEG):
self.add_updater(lambda m, dt: m.increment_theta(angular_speed * dt))
return self

View File

@@ -1,25 +1,54 @@
from __future__ import annotations
import argparse
from argparse import Namespace
import colour
import importlib
import inspect
import os
import screeninfo
import sys
import yaml
from pathlib import Path
from ast import literal_eval
from addict import Dict
from manimlib.logger import log
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.init_config import init_customization
from typing import TYPE_CHECKING
if TYPE_CHECKING:
Module = importlib.util.types.ModuleType
from argparse import Namespace
from typing import Optional
__config_file__ = "custom_config.yml"
def initialize_manim_config() -> Dict:
"""
Return default configuration for various classes in manim, such as
Scene, Window, Camera, and SceneFileWriter, as well as configuration
determining how the scene is run (e.g. written to file or previewed in window).
The result is initially on the contents of default_config.yml in the manimlib directory,
which can be further updated by a custom configuration file custom_config.yml.
It is further updated based on command line argument.
"""
args = parse_cli()
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
config = Dict(merge_dicts_recursively(
load_yaml(global_defaults_file),
load_yaml("custom_config.yml"), # From current working directory
load_yaml(args.config_file) if args.config_file else dict(),
))
log.setLevel(args.log_level or config["log_level"])
update_directory_config(config)
update_window_config(config, args)
update_camera_config(config, args)
update_file_writer_config(config, args)
update_scene_config(config, args)
update_run_config(config, args)
update_embed_config(config, args)
return config
def parse_cli():
@@ -49,12 +78,12 @@ def parse_cli():
parser.add_argument(
"-l", "--low_quality",
action="store_true",
help="Render at a low quality (for faster rendering)",
help="Render at 480p",
)
parser.add_argument(
"-m", "--medium_quality",
action="store_true",
help="Render at a medium quality",
help="Render at 720p",
)
parser.add_argument(
"--hd",
@@ -77,11 +106,6 @@ def parse_cli():
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",
help="Save each frame as a png",
)
parser.add_argument(
"-i", "--gif",
action="store_true",
@@ -121,9 +145,10 @@ def parse_cli():
help="Show the output file in finder",
)
parser.add_argument(
"--config",
"--subdivide",
action="store_true",
help="Guide for automatic configuration",
help="Divide the output animation into individual movie files " +
"for each animation",
)
parser.add_argument(
"--file_name",
@@ -138,12 +163,9 @@ def parse_cli():
)
parser.add_argument(
"-e", "--embed",
nargs="?",
const="",
help="Creates a new file where the line `self.embed` is inserted " + \
"into the Scenes construct method. " + \
"If a string is passed in, the line will be inserted below the " + \
"last line of code including that string."
metavar="LINE_NUMBER",
help="Adds a breakpoint at the inputted file dropping into an " + \
"interactive iPython session at that point of the code."
)
parser.add_argument(
"-r", "--resolution",
@@ -152,6 +174,7 @@ def parse_cli():
parser.add_argument(
"--fps",
help="Frame rate, as an integer",
type=int,
)
parser.add_argument(
"-c", "--color",
@@ -190,6 +213,17 @@ def parse_cli():
"--log-level",
help="Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL"
)
parser.add_argument(
"--clear-cache",
action="store_true",
help="Erase the cache used for Tex and Text Mobjects"
)
parser.add_argument(
"--autoreload",
action="store_true",
help="Automatically reload Python modules to pick up code changes " +
"across different files",
)
args = parser.parse_args()
args.write_file = any([args.write_file, args.open, args.finder])
return args
@@ -198,171 +232,133 @@ def parse_cli():
sys.exit(2)
def update_directory_config(config: Dict):
dir_config = config.directories
base = dir_config.base
for key, subdir in dir_config.subdirs.items():
dir_config[key] = os.path.join(base, subdir)
def update_window_config(config: Dict, args: Namespace):
window_config = config.window
for key in "position", "size":
if window_config.get(key):
window_config[key] = literal_eval(window_config[key])
if args.full_screen:
window_config.full_screen = True
def update_camera_config(config: Dict, args: Namespace):
camera_config = config.camera
arg_resolution = get_resolution_from_args(args, config.resolution_options)
camera_config.resolution = arg_resolution or literal_eval(camera_config.resolution)
if args.fps:
camera_config.fps = args.fps
if args.color:
try:
camera_config.background_color = colour.Color(args.color)
except Exception as err:
log.error("Please use a valid color")
log.error(err)
sys.exit(2)
if args.transparent:
camera_config.background_opacity = 0.0
def update_file_writer_config(config: Dict, args: Namespace):
file_writer_config = config.file_writer
file_writer_config.update(
write_to_movie=(not args.skip_animations and args.write_file),
subdivide_output=args.subdivide,
save_last_frame=(args.skip_animations and args.write_file),
png_mode=("RGBA" if args.transparent else "RGB"),
movie_file_extension=(get_file_ext(args)),
output_directory=get_output_directory(args, config),
file_name=args.file_name,
open_file_upon_completion=args.open,
show_file_location_upon_completion=args.finder,
quiet=args.quiet,
)
if args.vcodec:
file_writer_config.video_codec = args.vcodec
elif args.transparent:
file_writer_config.video_codec = 'prores_ks'
file_writer_config.pixel_format = ''
elif args.gif:
file_writer_config.video_codec = ''
if args.pix_fmt:
file_writer_config.pixel_format = args.pix_fmt
def update_scene_config(config: Dict, args: Namespace):
scene_config = config.scene
start, end = get_animations_numbers(args)
scene_config.update(
# Note, Scene.__init__ makes use of both manimlib.camera and
# manimlib.file_writer below, so the arguments here are just for
# any future specifications beyond what the global configuration holds
camera_config=dict(),
file_writer_config=dict(),
skip_animations=args.skip_animations,
start_at_animation_number=start,
end_at_animation_number=end,
presenter_mode=args.presenter_mode,
)
if args.leave_progress_bars:
scene_config.leave_progress_bars = True
if args.show_animation_progress:
scene_config.show_animation_progress = True
def update_run_config(config: Dict, args: Namespace):
config.run = Dict(
file_name=args.file,
embed_line=(int(args.embed) if args.embed is not None else None),
is_reload=False,
prerun=args.prerun,
scene_names=args.scene_names,
quiet=args.quiet or args.write_all,
write_all=args.write_all,
show_in_window=not args.write_file
)
def update_embed_config(config: Dict, args: Namespace):
if args.autoreload:
config.embed.autoreload = True
# Helpers for the functions above
def load_yaml(file_path: str):
try:
with open(file_path, "r") as file:
return yaml.safe_load(file) or {}
except FileNotFoundError:
return {}
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 get_module(file_name: str | None) -> Module:
if file_name is None:
return None
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
def get_indent(line: str):
return len(line) - len(line.lstrip())
def get_module_with_inserted_embed_line(
file_name: str, scene_name: str, line_marker: str
):
"""
This is hacky, but convenient. When user includes the argument "-e", it will try
to recreate a file that inserts the line `self.embed()` into the end of the scene's
construct method. If there is an argument passed in, it will insert the line after
the last line in the sourcefile which includes that string.
"""
with open(file_name, 'r') as fp:
lines = fp.readlines()
try:
scene_line_number = next(
i for i, line in enumerate(lines)
if line.startswith(f"class {scene_name}")
)
except StopIteration:
log.error(f"No scene {scene_name}")
return
prev_line_num = -1
n_spaces = None
if len(line_marker) == 0:
# Find the end of the construct method
in_construct = False
for index in range(scene_line_number, len(lines) - 1):
line = lines[index]
if line.lstrip().startswith("def construct"):
in_construct = True
n_spaces = get_indent(line) + 4
elif in_construct:
if len(line.strip()) > 0 and get_indent(line) < (n_spaces or 0):
prev_line_num = index - 1
break
if prev_line_num < 0:
prev_line_num = len(lines) - 1
elif line_marker.isdigit():
# Treat the argument as a line number
prev_line_num = int(line_marker) - 1
elif len(line_marker) > 0:
# Treat the argument as a string
try:
prev_line_num = next(
i
for i in range(scene_line_number, len(lines) - 1)
if line_marker in lines[i]
)
except StopIteration:
log.error(f"No lines matching {line_marker}")
sys.exit(2)
# Insert the embed line, rewrite file, then write it back when done
if n_spaces is None:
n_spaces = get_indent(lines[prev_line_num])
inserted_line = " " * n_spaces + "self.embed()\n"
new_lines = list(lines)
new_lines.insert(prev_line_num + 1, inserted_line)
new_file = file_name.replace(".py", "_insert_embed.py")
with open(new_file, 'w') as fp:
fp.writelines(new_lines)
module = get_module(new_file)
# This is to pretend the module imported from the edited lines
# of code actually comes from the original file.
module.__file__ = file_name
os.remove(new_file)
return module
def get_scene_module(args: Namespace) -> Module:
if args.embed is None:
return get_module(args.file)
else:
return get_module_with_inserted_embed_line(
args.file, args.scene_names[0], args.embed
)
def get_custom_config():
global __config_file__
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
if os.path.exists(global_defaults_file):
with open(global_defaults_file, "r") as file:
custom_config = yaml.safe_load(file)
if os.path.exists(__config_file__):
with open(__config_file__, "r") as file:
local_defaults = yaml.safe_load(file)
if local_defaults:
custom_config = merge_dicts_recursively(
custom_config,
local_defaults,
)
else:
with open(__config_file__, "r") as file:
custom_config = yaml.safe_load(file)
# Check temporary storage(custom_config)
if custom_config["directories"]["temporary_storage"] == "" and sys.platform == "win32":
log.warning(
"You may be using Windows platform and have not specified the path of" + \
" `temporary_storage`, which may cause OSError. So it is recommended" + \
" to specify the `temporary_storage` in the config file (.yml)"
)
return custom_config
def init_global_config(config_file):
global __config_file__
# ensure __config_file__ always exists
if config_file is not None:
if not os.path.exists(config_file):
log.error(f"Can't find {config_file}.")
if sys.platform == 'win32':
log.info(f"Copying default configuration file to {config_file}...")
os.system(f"copy default_config.yml {config_file}")
elif sys.platform in ["linux2", "darwin"]:
log.info(f"Copying default configuration file to {config_file}...")
os.system(f"cp default_config.yml {config_file}")
else:
log.info("Please create the configuration file manually.")
log.info("Read configuration from default_config.yml.")
else:
__config_file__ = config_file
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
if not (os.path.exists(global_defaults_file) or os.path.exists(__config_file__)):
log.info("There is no configuration file detected. Switch to the config file initializer:")
init_customization()
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`"
)
def get_resolution_from_args(args: Optional[Namespace], resolution_options: dict) -> Optional[tuple[int, int]]:
if args.resolution:
return tuple(map(int, args.resolution.split("x")))
if args.low_quality:
return literal_eval(resolution_options["low"])
if args.medium_quality:
return literal_eval(resolution_options["med"])
if args.hd:
return literal_eval(resolution_options["high"])
if args.uhd:
return literal_eval(resolution_options["4k"])
return None
def get_file_ext(args: Namespace) -> str:
@@ -385,159 +381,19 @@ def get_animations_numbers(args: Namespace) -> tuple[int | None, int | None]:
return int(stan), None
def get_output_directory(args: Namespace, custom_config: dict) -> str:
dir_config = custom_config["directories"]
output_directory = args.video_dir or dir_config["output"]
if dir_config["mirror_module_path"] and args.file:
to_cut = dir_config["removed_mirror_prefix"]
ext = os.path.abspath(args.file)
ext = ext.replace(to_cut, "").replace(".py", "")
if ext.startswith("_"):
ext = ext[1:]
output_directory = os.path.join(output_directory, ext)
return output_directory
def get_output_directory(args: Namespace, config: Dict) -> str:
dir_config = config.directories
out_dir = args.video_dir or dir_config.output
if dir_config.mirror_module_path and args.file:
file_path = Path(args.file).absolute()
if str(file_path).startswith(dir_config.removed_mirror_prefix):
rel_path = file_path.relative_to(dir_config.removed_mirror_prefix)
rel_path = Path(str(rel_path).lstrip("_"))
else:
rel_path = file_path.stem
out_dir = Path(out_dir, rel_path).with_suffix("")
return out_dir
def get_file_writer_config(args: Namespace, custom_config: dict) -> dict:
result = {
"write_to_movie": not args.skip_animations and args.write_file,
"save_last_frame": args.skip_animations and args.write_file,
"save_pngs": args.save_pngs,
# If -t is passed in (for transparent), this will be RGBA
"png_mode": "RGBA" if args.transparent else "RGB",
"movie_file_extension": get_file_ext(args),
"output_directory": get_output_directory(args, custom_config),
"file_name": args.file_name,
"input_file_path": args.file or "",
"open_file_upon_completion": args.open,
"show_file_location_upon_completion": args.finder,
"quiet": args.quiet,
**custom_config["file_writer_config"],
}
if args.vcodec:
result["video_codec"] = args.vcodec
elif args.transparent:
result["video_codec"] = 'prores_ks'
result["pixel_format"] = ''
elif args.gif:
result["video_codec"] = ''
if args.pix_fmt:
result["pixel_format"] = args.pix_fmt
return result
def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) -> dict:
# Default to making window half the screen size
# but make it full screen if -f is passed in
try:
monitors = screeninfo.get_monitors()
except screeninfo.ScreenInfoError:
pass
mon_index = custom_config["window_monitor"]
monitor = monitors[min(mon_index, len(monitors) - 1)]
aspect_ratio = camera_config["pixel_width"] / camera_config["pixel_height"]
window_width = monitor.width
if not (args.full_screen or custom_config["full_screen"]):
window_width //= 2
window_height = int(window_width / aspect_ratio)
return dict(size=(window_width, window_height))
def get_camera_config(args: Namespace, custom_config: dict) -> dict:
camera_config = {}
camera_resolutions = custom_config["camera_resolutions"]
if args.resolution:
resolution = args.resolution
elif args.low_quality:
resolution = camera_resolutions["low"]
elif args.medium_quality:
resolution = camera_resolutions["med"]
elif args.hd:
resolution = camera_resolutions["high"]
elif args.uhd:
resolution = camera_resolutions["4k"]
else:
resolution = camera_resolutions[camera_resolutions["default_resolution"]]
if args.fps:
fps = int(args.fps)
else:
fps = custom_config["fps"]
width_str, height_str = resolution.split("x")
width = int(width_str)
height = int(height_str)
camera_config.update({
"pixel_width": width,
"pixel_height": height,
"frame_config": {
"frame_shape": ((width / height) * get_frame_height(), get_frame_height()),
},
"fps": fps,
})
try:
bg_color = args.color or custom_config["style"]["background_color"]
camera_config["background_color"] = colour.Color(bg_color)
except ValueError as err:
log.error("Please use a valid color")
log.error(err)
sys.exit(2)
# If rendering a transparent image/move, make sure the
# scene has a background opacity of 0
if args.transparent:
camera_config["background_opacity"] = 0
return camera_config
def get_configuration(args: Namespace) -> dict:
init_global_config(args.config_file)
custom_config = get_custom_config()
camera_config = get_camera_config(args, custom_config)
window_config = get_window_config(args, custom_config, camera_config)
start, end = get_animations_numbers(args)
return {
"module": get_scene_module(args),
"scene_names": args.scene_names,
"file_writer_config": get_file_writer_config(args, custom_config),
"camera_config": camera_config,
"window_config": window_config,
"quiet": args.quiet or args.write_all,
"write_all": args.write_all,
"skip_animations": args.skip_animations,
"start_at_animation_number": start,
"end_at_animation_number": end,
"preview": not args.write_file,
"presenter_mode": args.presenter_mode,
"leave_progress_bars": args.leave_progress_bars,
"show_animation_progress": args.show_animation_progress,
"prerun": args.prerun,
"embed_exception_mode": custom_config["embed_exception_mode"],
"embed_error_sound": custom_config["embed_error_sound"],
}
def get_frame_height():
return 8.0
def get_aspect_ratio():
cam_config = get_camera_config(parse_cli(), get_custom_config())
return cam_config['pixel_width'] / cam_config['pixel_height']
def get_default_pixel_width():
cam_config = get_camera_config(parse_cli(), get_custom_config())
return cam_config['pixel_width']
def get_default_pixel_height():
cam_config = get_camera_config(parse_cli(), get_custom_config())
return cam_config['pixel_height']
# Create global configuration
manim_config: Dict = initialize_manim_config()

View File

@@ -1,42 +1,39 @@
from __future__ import annotations
import numpy as np
from manimlib.config import get_aspect_ratio
from manimlib.config import get_default_pixel_width
from manimlib.config import get_default_pixel_height
from manimlib.config import get_frame_height
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List
from manimlib.typing import ManimColor, Vect3
# See manimlib/default_config.yml
from manimlib.config import manim_config
DEFAULT_RESOLUTION: tuple[int, int] = manim_config.camera.resolution
DEFAULT_PIXEL_WIDTH: int = DEFAULT_RESOLUTION[0]
DEFAULT_PIXEL_HEIGHT: int = DEFAULT_RESOLUTION[1]
# Sizes relevant to default camera frame
ASPECT_RATIO: float = get_aspect_ratio()
FRAME_HEIGHT: float = get_frame_height()
ASPECT_RATIO: float = DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT
FRAME_HEIGHT: float = manim_config.sizes.frame_height
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
FRAME_X_RADIUS: float = FRAME_WIDTH / 2
DEFAULT_PIXEL_HEIGHT: int = get_default_pixel_height()
DEFAULT_PIXEL_WIDTH: int = get_default_pixel_width()
DEFAULT_FPS: int = 30
SMALL_BUFF: float = 0.1
MED_SMALL_BUFF: float = 0.25
MED_LARGE_BUFF: float = 0.5
LARGE_BUFF: float = 1
# Helpful values for positioning mobjects
SMALL_BUFF: float = manim_config.sizes.small_buff
MED_SMALL_BUFF: float = manim_config.sizes.med_small_buff
MED_LARGE_BUFF: float = manim_config.sizes.med_large_buff
LARGE_BUFF: float = manim_config.sizes.large_buff
DEFAULT_MOBJECT_TO_EDGE_BUFFER: float = MED_LARGE_BUFF
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER: float = MED_SMALL_BUFF
# In seconds
DEFAULT_WAIT_TIME: float = 1.0
DEFAULT_MOBJECT_TO_EDGE_BUFF: float = manim_config.sizes.default_mobject_to_edge_buff
DEFAULT_MOBJECT_TO_MOBJECT_BUFF: float = manim_config.sizes.default_mobject_to_mobject_buff
# Standard vectors
ORIGIN: Vect3 = np.array([0., 0., 0.])
UP: Vect3 = np.array([0., 1., 0.])
DOWN: Vect3 = np.array([0., -1., 0.])
@@ -61,111 +58,83 @@ BOTTOM: Vect3 = FRAME_Y_RADIUS * DOWN
LEFT_SIDE: Vect3 = FRAME_X_RADIUS * LEFT
RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT
# Angles
PI: float = np.pi
TAU: float = 2 * PI
DEGREES: float = TAU / 360
DEG: float = TAU / 360
DEGREES = DEG # Many older animations use the full name
# Nice to have a constant for readability
# when juxtaposed with expressions like 30 * DEGREES
# when juxtaposed with expressions like 30 * DEG
RADIANS: float = 1
FFMPEG_BIN: str = "ffmpeg"
JOINT_TYPE_MAP: dict = {
"no_joint": 0,
"auto": 1,
"bevel": 2,
"miter": 3,
}
# Related to Text
NORMAL: str = "NORMAL"
ITALIC: str = "ITALIC"
OBLIQUE: str = "OBLIQUE"
BOLD: str = "BOLD"
DEFAULT_STROKE_WIDTH: float = 4
# For keyboard interactions
CTRL_SYMBOL: int = 65508
SHIFT_SYMBOL: int = 65505
COMMAND_SYMBOL: int = 65517
DELETE_SYMBOL: int = 65288
ARROW_SYMBOLS: list[int] = list(range(65361, 65365))
SHIFT_MODIFIER: int = 1
CTRL_MODIFIER: int = 2
COMMAND_MODIFIER: int = 64
DEFAULT_STROKE_WIDTH: float = manim_config.vmobject.default_stroke_width
# Colors
BLUE_E: ManimColor = manim_config.colors.blue_e
BLUE_D: ManimColor = manim_config.colors.blue_d
BLUE_C: ManimColor = manim_config.colors.blue_c
BLUE_B: ManimColor = manim_config.colors.blue_b
BLUE_A: ManimColor = manim_config.colors.blue_a
TEAL_E: ManimColor = manim_config.colors.teal_e
TEAL_D: ManimColor = manim_config.colors.teal_d
TEAL_C: ManimColor = manim_config.colors.teal_c
TEAL_B: ManimColor = manim_config.colors.teal_b
TEAL_A: ManimColor = manim_config.colors.teal_a
GREEN_E: ManimColor = manim_config.colors.green_e
GREEN_D: ManimColor = manim_config.colors.green_d
GREEN_C: ManimColor = manim_config.colors.green_c
GREEN_B: ManimColor = manim_config.colors.green_b
GREEN_A: ManimColor = manim_config.colors.green_a
YELLOW_E: ManimColor = manim_config.colors.yellow_e
YELLOW_D: ManimColor = manim_config.colors.yellow_d
YELLOW_C: ManimColor = manim_config.colors.yellow_c
YELLOW_B: ManimColor = manim_config.colors.yellow_b
YELLOW_A: ManimColor = manim_config.colors.yellow_a
GOLD_E: ManimColor = manim_config.colors.gold_e
GOLD_D: ManimColor = manim_config.colors.gold_d
GOLD_C: ManimColor = manim_config.colors.gold_c
GOLD_B: ManimColor = manim_config.colors.gold_b
GOLD_A: ManimColor = manim_config.colors.gold_a
RED_E: ManimColor = manim_config.colors.red_e
RED_D: ManimColor = manim_config.colors.red_d
RED_C: ManimColor = manim_config.colors.red_c
RED_B: ManimColor = manim_config.colors.red_b
RED_A: ManimColor = manim_config.colors.red_a
MAROON_E: ManimColor = manim_config.colors.maroon_e
MAROON_D: ManimColor = manim_config.colors.maroon_d
MAROON_C: ManimColor = manim_config.colors.maroon_c
MAROON_B: ManimColor = manim_config.colors.maroon_b
MAROON_A: ManimColor = manim_config.colors.maroon_a
PURPLE_E: ManimColor = manim_config.colors.purple_e
PURPLE_D: ManimColor = manim_config.colors.purple_d
PURPLE_C: ManimColor = manim_config.colors.purple_c
PURPLE_B: ManimColor = manim_config.colors.purple_b
PURPLE_A: ManimColor = manim_config.colors.purple_a
GREY_E: ManimColor = manim_config.colors.grey_e
GREY_D: ManimColor = manim_config.colors.grey_d
GREY_C: ManimColor = manim_config.colors.grey_c
GREY_B: ManimColor = manim_config.colors.grey_b
GREY_A: ManimColor = manim_config.colors.grey_a
WHITE: ManimColor = manim_config.colors.white
BLACK: ManimColor = manim_config.colors.black
GREY_BROWN: ManimColor = manim_config.colors.grey_brown
DARK_BROWN: ManimColor = manim_config.colors.dark_brown
LIGHT_BROWN: ManimColor = manim_config.colors.light_brown
PINK: ManimColor = manim_config.colors.pink
LIGHT_PINK: ManimColor = manim_config.colors.light_pink
GREEN_SCREEN: ManimColor = manim_config.colors.green_screen
ORANGE: ManimColor = manim_config.colors.orange
PURE_RED: ManimColor = manim_config.colors.pure_red
PURE_GREEN: ManimColor = manim_config.colors.pure_green
PURE_BLUE: ManimColor = manim_config.colors.pure_blue
BLUE_E: ManimColor = "#1C758A"
BLUE_D: ManimColor = "#29ABCA"
BLUE_C: ManimColor = "#58C4DD"
BLUE_B: ManimColor = "#9CDCEB"
BLUE_A: ManimColor = "#C7E9F1"
TEAL_E: ManimColor = "#49A88F"
TEAL_D: ManimColor = "#55C1A7"
TEAL_C: ManimColor = "#5CD0B3"
TEAL_B: ManimColor = "#76DDC0"
TEAL_A: ManimColor = "#ACEAD7"
GREEN_E: ManimColor = "#699C52"
GREEN_D: ManimColor = "#77B05D"
GREEN_C: ManimColor = "#83C167"
GREEN_B: ManimColor = "#A6CF8C"
GREEN_A: ManimColor = "#C9E2AE"
YELLOW_E: ManimColor = "#E8C11C"
YELLOW_D: ManimColor = "#F4D345"
YELLOW_C: ManimColor = "#FFFF00"
YELLOW_B: ManimColor = "#FFEA94"
YELLOW_A: ManimColor = "#FFF1B6"
GOLD_E: ManimColor = "#C78D46"
GOLD_D: ManimColor = "#E1A158"
GOLD_C: ManimColor = "#F0AC5F"
GOLD_B: ManimColor = "#F9B775"
GOLD_A: ManimColor = "#F7C797"
RED_E: ManimColor = "#CF5044"
RED_D: ManimColor = "#E65A4C"
RED_C: ManimColor = "#FC6255"
RED_B: ManimColor = "#FF8080"
RED_A: ManimColor = "#F7A1A3"
MAROON_E: ManimColor = "#94424F"
MAROON_D: ManimColor = "#A24D61"
MAROON_C: ManimColor = "#C55F73"
MAROON_B: ManimColor = "#EC92AB"
MAROON_A: ManimColor = "#ECABC1"
PURPLE_E: ManimColor = "#644172"
PURPLE_D: ManimColor = "#715582"
PURPLE_C: ManimColor = "#9A72AC"
PURPLE_B: ManimColor = "#B189C6"
PURPLE_A: ManimColor = "#CAA3E8"
GREY_E: ManimColor = "#222222"
GREY_D: ManimColor = "#444444"
GREY_C: ManimColor = "#888888"
GREY_B: ManimColor = "#BBBBBB"
GREY_A: ManimColor = "#DDDDDD"
WHITE: ManimColor = "#FFFFFF"
BLACK: ManimColor = "#000000"
GREY_BROWN: ManimColor = "#736357"
DARK_BROWN: ManimColor = "#8B4513"
LIGHT_BROWN: ManimColor = "#CD853F"
PINK: ManimColor = "#D147BD"
LIGHT_PINK: ManimColor = "#DC75CD"
GREEN_SCREEN: ManimColor = "#00FF00"
ORANGE: ManimColor = "#FF862F"
MANIM_COLORS: List[ManimColor] = [
BLACK, GREY_E, GREY_D, GREY_C, GREY_B, GREY_A, WHITE,
BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A,
TEAL_E, TEAL_D, TEAL_C, TEAL_B, TEAL_A,
GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A,
YELLOW_E, YELLOW_D, YELLOW_C, YELLOW_B, YELLOW_A,
GOLD_E, GOLD_D, GOLD_C, GOLD_B, GOLD_A,
RED_E, RED_D, RED_C, RED_B, RED_A,
MAROON_E, MAROON_D, MAROON_C, MAROON_B, MAROON_A,
PURPLE_E, PURPLE_D, PURPLE_C, PURPLE_B, PURPLE_A,
GREY_BROWN, DARK_BROWN, LIGHT_BROWN,
PINK, LIGHT_PINK,
]
MANIM_COLORS: List[ManimColor] = list(manim_config.colors.values())
# Abbreviated names for the "median" colors
BLUE: ManimColor = BLUE_C
@@ -179,3 +148,12 @@ PURPLE: ManimColor = PURPLE_C
GREY: ManimColor = GREY_C
COLORMAP_3B1B: List[ManimColor] = [BLUE_E, GREEN, YELLOW, RED]
# Default mobject colors should be configurable just like background color
# DEFAULT_MOBJECT_COLOR is mainly for text, tex, line, etc... mobjects. Default is WHITE
# DEFAULT_LIGHT_COLOR is mainly for things like axes, arrows, annulus and other lightly colored mobjects. Default is GREY_B
DEFAULT_MOBJECT_COLOR: ManimColor = manim_config.mobject.default_mobject_color or WHITE
DEFAULT_LIGHT_COLOR: ManimColor = manim_config.mobject.default_light_color or GREY_B
DEFAULT_VMOBJECT_STROKE_COLOR : ManimColor = manim_config.vmobject.default_stroke_color or GREY_A
DEFAULT_VMOBJECT_FILL_COLOR : ManimColor = manim_config.vmobject.default_fill_color or GREY_C

View File

@@ -1,51 +1,184 @@
# This file determines the default configuration for how manim is
# run, including names for directories it will write to, default
# parameters for various classes, style choices, etc. To customize
# your own, create a custom_config.yml file in whatever directory
# you are running manim. For 3blue1brown, for instance, mind is
# here: https://github.com/3b1b/videos/blob/master/custom_config.yml
# Alternatively, you can create it wherever you like, and on running
# manim, pass in `--config_file /path/to/custom/config/file.yml`
directories:
# Set this to true if you want the path to video files
# to match the directory structure of the path to the
# sourcecode generating that video
# source code generating that video
mirror_module_path: False
# Where should manim output video and image files?
output: ""
# If you want to use images, manim will look to these folders to find them
raster_images: ""
vector_images: ""
# If you want to use sounds, manim will look here to find it.
sounds: ""
# Manim often generates tex_files or other kinds of serialized data
# to keep from having to generate the same thing too many times. By
# default, these will be stored at tempfile.gettempdir(), e.g. this might
# return whatever is at to the TMPDIR environment variable. If you want to
# specify them elsewhere,
temporary_storage: ""
universal_import_line: "from manimlib import *"
style:
tex_template: "default"
font: "Consolas"
text_alignment: "LEFT"
# Manim may write to and read from the file system, e.g.
# to render videos and to look for svg/png assets. This
# will specify where those assets live, with a base directory,
# and various subdirectory names within it
base: ""
subdirs:
# Where should manim output video and image files?
output: "videos"
# If you want to use images, manim will look to these folders to find them
raster_images: "raster_images"
vector_images: "vector_images"
# If you want to use sounds, manim will look here to find it.
sounds: "sounds"
# Place for other forms of data relevant to any projects, like csv's
data: "data"
# When downloading, say an image, where will it go?
downloads: "downloads"
# For certain object types, especially Tex and Text, manim will save information
# to file to prevent the need to re-compute, e.g. recompiling the latex. By default,
# it stores this saved data to whatever directory appdirs.user_cache_dir("manim") returns,
# but here a user can specify a different cache location
cache: ""
window:
# The position of window on screen. UR -> Upper Right, and likewise DL -> Down and Left,
# UO would be upper middle, etc.
position_string: UR
# If using multiple monitors, which one should show the window
monitor_index: 0
# If not full screen, the default to give it half the screen width
full_screen: False
# Other optional specifications that override the above include:
# position: (500, 500) # Specific position, in pixel coordinates, for upper right corner
# size: (1920, 1080) # Specific size, in pixels
camera:
resolution: (1920, 1080)
background_color: "#333333"
# Set the position of preview window, you can use directions, e.g. UL/DR/OL/OO/...
# also, you can also specify the position(pixel) of the upper left corner of
# the window on the monitor, e.g. "960,540"
window_position: UR
window_monitor: 0
full_screen: False
file_writer_config:
# 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
# to form the full scene. Sometimes video-editing is made
# easier when working with the broken up scene, which
# effectively has cuts at all the places you might want.
break_into_partial_movies: False
fps: 30
background_opacity: 1.0
file_writer:
# What command to use for ffmpeg
ffmpeg_bin: "ffmpeg"
# Parameters to pass into ffmpeg
video_codec: "libx264"
pixel_format: "yuv420p"
saturation: 1.0
gamma: 1.0
camera_resolutions:
low: "854x480"
med: "1280x720"
high: "1920x1080"
4k: "3840x2160"
default_resolution: "high"
fps: 30
embed_exception_mode: "Verbose"
embed_error_sound: False
# Most of the scene configuration will come from CLI arguments,
# but defaults can be set here
scene:
show_animation_progress: False
leave_progress_bars: False
# When skipping animations, should a single frame be rendered
# at the end of each play call?
preview_while_skipping: True
# How long does a scene pause on Scene.wait calls
default_wait_time: 1.0
vmobject:
default_stroke_width: 4.0
default_stroke_color: "#DDDDDD" # Default is GREY_A
default_fill_color: "#888888" # Default is GREY_C
mobject:
default_mobject_color: "#FFFFFF" # Default is WHITE
default_light_color: "#BBBBBB" # Default is GREY_B
tex:
# See tex_templates.yml
template: "default"
text:
# font: "Cambria Math"
font: "Consolas"
alignment: "LEFT"
embed:
exception_mode: "Verbose"
autoreload: False
resolution_options:
# When the user passes in -l, -m, --hd or --uhd, these are the corresponding
# resolutions
low: (854, 480)
med: (1280, 720)
high: (1920, 1080)
4k: (3840, 2160)
sizes:
# This determines the scale of the manim coordinate system with respect to
# the viewing frame
frame_height: 8.0
# These determine the constants SMALL_BUFF, MED_SMALL_BUFF, etc., useful
# for nudging things around and having default spacing values
small_buff: 0.1
med_small_buff: 0.25
med_large_buff: 0.5
large_buff: 1.0
# Default buffers used in Mobject.next_to or Mobject.to_edge
default_mobject_to_edge_buff: 0.5
default_mobject_to_mobject_buff: 0.25
key_bindings:
pan_3d: "d"
pan: "f"
reset: "r"
quit: "q" # Together with command
select: "s"
unselect: "u"
grab: "g"
x_grab: "h"
y_grab: "v"
resize: "t"
color: "c"
information: "i"
cursor: "k"
colors:
blue_e: "#1C758A"
blue_d: "#29ABCA"
blue_c: "#58C4DD"
blue_b: "#9CDCEB"
blue_a: "#C7E9F1"
teal_e: "#49A88F"
teal_d: "#55C1A7"
teal_c: "#5CD0B3"
teal_b: "#76DDC0"
teal_a: "#ACEAD7"
green_e: "#699C52"
green_d: "#77B05D"
green_c: "#83C167"
green_b: "#A6CF8C"
green_a: "#C9E2AE"
yellow_e: "#E8C11C"
yellow_d: "#F4D345"
yellow_c: "#FFFF00"
yellow_b: "#FFEA94"
yellow_a: "#FFF1B6"
gold_e: "#C78D46"
gold_d: "#E1A158"
gold_c: "#F0AC5F"
gold_b: "#F9B775"
gold_a: "#F7C797"
red_e: "#CF5044"
red_d: "#E65A4C"
red_c: "#FC6255"
red_b: "#FF8080"
red_a: "#F7A1A3"
maroon_e: "#94424F"
maroon_d: "#A24D61"
maroon_c: "#C55F73"
maroon_b: "#EC92AB"
maroon_a: "#ECABC1"
purple_e: "#644172"
purple_d: "#715582"
purple_c: "#9A72AC"
purple_b: "#B189C6"
purple_a: "#CAA3E8"
grey_e: "#222222"
grey_d: "#444444"
grey_c: "#888888"
grey_b: "#BBBBBB"
grey_a: "#DDDDDD"
white: "#FFFFFF"
black: "#000000"
grey_brown: "#736357"
dark_brown: "#8B4513"
light_brown: "#CD853F"
pink: "#D147BD"
light_pink: "#DC75CD"
green_screen: "#00FF00"
orange: "#FF862F"
pure_red: "#FF0000"
pure_green: "#00FF00"
pure_blue: "#0000FF"
# Can be DEBUG / INFO / WARNING / ERROR / CRITICAL
log_level: "INFO"
universal_import_line: "from manimlib import *"
ignore_manimlib_modules_on_reload: True

View File

@@ -1,16 +1,27 @@
from __future__ import annotations
import copy
import inspect
import sys
from manimlib.config import get_custom_config
from manimlib.module_loader import ModuleLoader
from manimlib.config import manim_config
from manimlib.logger import log
from manimlib.scene.interactive_scene import InteractiveScene
from manimlib.scene.scene import Scene
from typing import TYPE_CHECKING
if TYPE_CHECKING:
Module = importlib.util.types.ModuleType
from typing import Optional
from addict import Dict
class BlankScene(InteractiveScene):
def construct(self):
exec(get_custom_config()["universal_import_line"])
exec(manim_config.universal_import_line)
self.embed()
@@ -34,11 +45,7 @@ def prompt_user_for_choice(scene_classes):
print(f"{str(idx).zfill(max_digits)}: {name}")
name_to_class[name] = scene_class
try:
user_input = input(
"\nThat module has multiple scenes, " + \
"which ones would you like to render?" + \
"\nScene Name or Number: "
)
user_input = input("\nSelect which scene to render (by name or number): ")
return [
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(",")
@@ -53,14 +60,6 @@ def prompt_user_for_choice(scene_classes):
sys.exit(1)
def get_scene_config(config):
scene_parameters = inspect.signature(Scene).parameters.keys()
return {
key: config[key]
for key in set(scene_parameters).intersection(config.keys())
}
def compute_total_frames(scene_class, scene_config):
"""
When a scene is being written to file, a copy of the scene is run with
@@ -76,41 +75,44 @@ def compute_total_frames(scene_class, scene_config):
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"]["fps"])
return int(total_time * manim_config.camera.fps)
def scene_from_class(scene_class, scene_config, config):
fw_config = scene_config["file_writer_config"]
if fw_config["write_to_movie"] and config["prerun"]:
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
def scene_from_class(scene_class, scene_config: Dict, run_config: Dict):
fw_config = manim_config.file_writer
if fw_config.write_to_movie and run_config.prerun:
scene_config.file_writer_config.total_frames = compute_total_frames(scene_class, scene_config)
return scene_class(**scene_config)
def get_scenes_to_render(all_scene_classes, scene_config, config):
if config["write_all"]:
return [sc(**scene_config) for sc in all_scene_classes]
def note_missing_scenes(arg_names, module_names):
for name in arg_names:
if name not in module_names:
log.error(f"No scene named {name} found")
names_to_classes = {sc.__name__ : sc for sc in all_scene_classes}
scene_names = config["scene_names"]
for name in set.difference(set(scene_names), names_to_classes):
log.error(f"No scene named {name} found")
scene_names.remove(name)
if scene_names:
classes_to_run = [names_to_classes[name] for name in scene_names]
elif len(all_scene_classes) == 1:
classes_to_run = [all_scene_classes[0]]
def get_scenes_to_render(all_scene_classes: list, scene_config: Dict, run_config: Dict):
if run_config["write_all"] or len(all_scene_classes) == 1:
classes_to_run = all_scene_classes
else:
name_to_class = {sc.__name__: sc for sc in all_scene_classes}
classes_to_run = [name_to_class.get(name) for name in run_config.scene_names]
classes_to_run = list(filter(lambda x: x, classes_to_run)) # Remove Nones
note_missing_scenes(run_config.scene_names, name_to_class.keys())
if len(classes_to_run) == 0:
classes_to_run = prompt_user_for_choice(all_scene_classes)
return [
scene_from_class(scene_class, scene_config, config)
scene_from_class(scene_class, scene_config, run_config)
for scene_class in classes_to_run
]
def get_scene_classes_from_module(module):
def get_scene_classes(module: Optional[Module]):
if module is None:
# If no module was passed in, just play the blank scene
return [BlankScene]
if hasattr(module, "SCENES_IN_ORDER"):
return module.SCENES_IN_ORDER
else:
@@ -123,13 +125,70 @@ def get_scene_classes_from_module(module):
]
def main(config):
module = config["module"]
scene_config = get_scene_config(config)
if module is None:
# If no module was passed in, just play the blank scene
return [BlankScene(**scene_config)]
def get_indent(code_lines: list[str], line_number: int) -> str:
"""
Find the indent associated with a given line of python code,
as a string of spaces
"""
# Find most recent non-empty line
try:
line = next(filter(lambda line: line.strip(), code_lines[line_number - 1::-1]))
except StopIteration:
return ""
all_scene_classes = get_scene_classes_from_module(module)
scenes = get_scenes_to_render(all_scene_classes, scene_config, config)
# Either return its leading spaces, or add for if it ends with colon
n_spaces = len(line) - len(line.lstrip())
if line.endswith(":"):
n_spaces += 4
return n_spaces * " "
def insert_embed_line_to_module(module: Module, run_config: Dict) -> None:
"""
This is hacky, but convenient. When user includes the argument "-e", it will try
to recreate a file that inserts the line `self.embed()` into the end of the scene's
construct method. If there is an argument passed in, it will insert the line after
the last line in the sourcefile which includes that string.
"""
lines = inspect.getsource(module).splitlines()
line_number = run_config.embed_line
# Add the relevant embed line to the code
indent = get_indent(lines, line_number)
lines.insert(line_number, indent + "self.embed()")
new_code = "\n".join(lines)
# When the user executes the `-e <line_number>` command
# without specifying scene_names, the nearest class name above
# `<line_number>` will be automatically used as 'scene_names'.
if not run_config.scene_names:
classes = list(filter(lambda line: line.startswith("class"), lines[:line_number]))
if classes:
from re import search
scene_name = search(r"(\w+)\(", classes[-1])
run_config.update(scene_names=[scene_name.group(1)])
else:
log.error(f"No 'class' found above {line_number}!")
# Execute the code, which presumably redefines the user's
# scene to include this embed line, within the relevant module.
code_object = compile(new_code, module.__name__, 'exec')
exec(code_object, module.__dict__)
def get_module(run_config: Dict) -> Module:
module = ModuleLoader.get_module(run_config.file_name, run_config.is_reload)
if run_config.embed_line:
insert_embed_line_to_module(module, run_config)
return module
def main(scene_config: Dict, run_config: Dict):
module = get_module(run_config)
all_scene_classes = get_scene_classes(module)
scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config)
if len(scenes) == 0:
print("No scenes found to run")
return scenes

View File

@@ -11,4 +11,3 @@ logging.basicConfig(
)
log = logging.getLogger("manimgl")
log.setLevel("DEBUG")

View File

@@ -7,7 +7,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
# Boolean operations between 2D mobjects
# Borrowed from from https://github.com/ManimCommunity/manim/
# Borrowed from https://github.com/ManimCommunity/manim/
def _convert_vmobject_to_skia_path(vmobject: VMobject) -> pathops.Path:
path = pathops.Path()

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import numpy as np
from manimlib.constants import BLUE_B, BLUE_D, BLUE_E, GREY_BROWN, WHITE
from manimlib.constants import BLUE_B, BLUE_D, BLUE_E, GREY_BROWN, DEFAULT_MOBJECT_COLOR
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
@@ -101,10 +101,17 @@ class TracedPath(VMobject):
traced_point_func: Callable[[], Vect3],
time_traced: float = np.inf,
time_per_anchor: float = 1.0 / 15,
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
stroke_width: float | Iterable[float] = 2.0,
stroke_color: ManimColor = WHITE,
stroke_opacity: float = 1.0,
**kwargs
):
self.stroke_config = dict(
color=stroke_color,
width=stroke_width,
opacity=stroke_opacity,
)
super().__init__(**kwargs)
self.traced_point_func = traced_point_func
self.time_traced = time_traced
@@ -112,7 +119,6 @@ class TracedPath(VMobject):
self.time: float = 0
self.traced_points: list[np.ndarray] = []
self.add_updater(lambda m, dt: m.update_path(dt))
self.set_stroke(stroke_color, stroke_width)
def update_path(self, dt: float) -> Self:
if dt == 0:
@@ -136,6 +142,8 @@ class TracedPath(VMobject):
if points:
self.set_points_smoothly(points)
self.set_stroke(**self.stroke_config)
self.time += dt
return self
@@ -145,21 +153,24 @@ class TracingTail(TracedPath):
self,
mobject_or_func: Mobject | Callable[[], np.ndarray],
time_traced: float = 1.0,
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
stroke_width: float | Iterable[float] = (0, 3),
stroke_opacity: float | Iterable[float] = (0, 1),
stroke_color: ManimColor = WHITE,
**kwargs
):
if isinstance(mobject_or_func, Mobject):
func = mobject_or_func.get_center
else:
func = mobject_or_func
super().__init__(
func,
time_traced=time_traced,
stroke_color=stroke_color,
stroke_width=stroke_width,
stroke_opacity=stroke_opacity,
stroke_color=stroke_color,
**kwargs
)
self.add_updater(lambda m: m.set_stroke(width=stroke_width, opacity=stroke_opacity))
curr_point = self.traced_point_func()
n_points = int(self.time_traced / self.time_per_anchor)
self.traced_points: list[np.ndarray] = n_points * [curr_point]

View File

@@ -6,8 +6,8 @@ import numbers
import numpy as np
import itertools as it
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
from manimlib.constants import DEGREES, PI
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, RED, DEFAULT_MOBJECT_COLOR
from manimlib.constants import DEG, PI
from manimlib.constants import DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
@@ -45,6 +45,12 @@ DEFAULT_X_RANGE = (-8.0, 8.0, 1.0)
DEFAULT_Y_RANGE = (-4.0, 4.0, 1.0)
def full_range_specifier(range_args):
if len(range_args) == 2:
return (*range_args, 1)
return range_args
class CoordinateSystem(ABC):
"""
Abstract class for Axes and NumberPlane
@@ -57,8 +63,8 @@ class CoordinateSystem(ABC):
y_range: RangeSpecifier = DEFAULT_Y_RANGE,
num_sampled_graph_points_per_tick: int = 5,
):
self.x_range = x_range
self.y_range = y_range
self.x_range = full_range_specifier(x_range)
self.y_range = full_range_specifier(y_range)
self.num_sampled_graph_points_per_tick = num_sampled_graph_points_per_tick
@abstractmethod
@@ -301,7 +307,7 @@ class CoordinateSystem(ABC):
point = self.input_to_graph_point(x, graph)
angle = self.angle_of_tangent(x, graph)
normal = rotate_vector(RIGHT, angle + 90 * DEGREES)
normal = rotate_vector(RIGHT, angle + 90 * DEG)
if normal[1] < 0:
normal *= -1
label.next_to(point, normal, buff=buff)
@@ -399,22 +405,26 @@ class CoordinateSystem(ABC):
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_opacity=fill_opacity,
stroke_background=stroke_background
stroke_behind=stroke_background
)
for rect in result:
if not rect.positive:
rect.set_fill(negative_color)
return result
def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=0.5):
if not hasattr(graph, "x_range"):
raise Exception("Argument `graph` must have attribute `x_range`")
def get_area_under_graph(self, graph, x_range=None, fill_color=BLUE, fill_opacity=0.5):
if x_range is None:
x_range = [
self.x_axis.p2n(graph.get_start()),
self.x_axis.p2n(graph.get_end()),
]
alpha_bounds = [
inverse_interpolate(*graph.x_range, x)
inverse_interpolate(*graph.x_range[:2], x)
for x in x_range
]
sub_graph = graph.copy()
sub_graph.clear_updaters()
sub_graph.pointwise_become_partial(graph, *alpha_bounds)
sub_graph.add_line_to(self.c2p(x_range[1], 0))
sub_graph.add_line_to(self.c2p(x_range[0], 0))
@@ -468,7 +478,7 @@ class Axes(VGroup, CoordinateSystem):
),
length=height,
)
self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN)
self.y_axis.rotate(90 * DEG, about_point=ORIGIN)
# Add as a separate group in case various other
# mobjects are added to self, as for example in
# NumberPlane below
@@ -536,7 +546,7 @@ class ThreeDAxes(Axes):
):
Axes.__init__(self, x_range, y_range, **kwargs)
self.z_range = z_range
self.z_range = full_range_specifier(z_range)
self.z_axis = self.create_axis(
self.z_range,
axis_config=merge_dicts_recursively(
@@ -611,7 +621,7 @@ class ThreeDAxes(Axes):
class NumberPlane(Axes):
default_axis_config: dict = dict(
stroke_color=WHITE,
stroke_color=DEFAULT_MOBJECT_COLOR,
stroke_width=2,
include_ticks=False,
include_tip=False,
@@ -632,7 +642,10 @@ class NumberPlane(Axes):
stroke_opacity=1,
),
# Defaults to a faded version of line_config
faded_line_style: dict = dict(),
faded_line_style: dict = dict(
stroke_width=1,
stroke_opacity=0.25,
),
faded_line_ratio: int = 4,
make_smooth_after_applying_functions: bool = True,
**kwargs
@@ -645,14 +658,8 @@ class NumberPlane(Axes):
self.init_background_lines()
def init_background_lines(self) -> None:
if not self.faded_line_style:
style = dict(self.background_line_style)
# For anything numerical, like stroke_width
# and stroke_opacity, chop it in half
for key in style:
if isinstance(style[key], numbers.Number):
style[key] *= 0.5
self.faded_line_style = style
if "stroke_color" not in self.faded_line_style:
self.faded_line_style["stroke_color"] = self.background_line_style["stroke_color"]
self.background_lines, self.faded_lines = self.get_lines()
self.background_lines.set_style(**self.background_line_style)
@@ -720,11 +727,10 @@ class NumberPlane(Axes):
class ComplexPlane(NumberPlane):
def number_to_point(self, number: complex | float) -> Vect3:
number = complex(number)
return self.coords_to_point(number.real, number.imag)
def number_to_point(self, number: complex | float | np.array) -> Vect3:
return self.coords_to_point(np.real(number), np.imag(number))
def n2p(self, number: complex | float) -> Vect3:
def n2p(self, number: complex | float | np.array) -> Vect3:
return self.number_to_point(number)
def point_to_number(self, point: Vect3) -> complex:
@@ -764,13 +770,6 @@ class ComplexPlane(NumberPlane):
axis = self.get_x_axis()
value = z.real
number_mob = axis.get_number_mobject(value, font_size=font_size, **kwargs)
# For -i, remove the "1"
if z.imag == -1:
number_mob.remove(number_mob[1])
number_mob[0].next_to(
number_mob[1], LEFT,
buff=number_mob[0].get_width() / 4
)
self.coordinate_labels.add(number_mob)
self.add(self.coordinate_labels)
return self

View File

@@ -37,6 +37,7 @@ class FullScreenRectangle(ScreenRectangle):
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
**kwargs
)

View File

@@ -1,21 +1,18 @@
from __future__ import annotations
import math
import numbers
import numpy as np
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UL, UP, UR
from manimlib.constants import GREY_A, RED, WHITE, BLACK
from manimlib.constants import RED, BLACK, DEFAULT_MOBJECT_COLOR, DEFAULT_LIGHT_COLOR
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
from manimlib.constants import DEGREES, PI, TAU
from manimlib.constants import DEG, PI, TAU
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.vectorized_mobject import DashedVMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import bezier
from manimlib.utils.bezier import quadratic_bezier_points_for_arc
from manimlib.utils.bezier import partial_quadratic_bezier_points
from manimlib.utils.iterables import adjacent_n_tuples
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.simple_functions import clip
@@ -30,7 +27,6 @@ from manimlib.utils.space_ops import normalize
from manimlib.utils.space_ops import rotate_vector
from manimlib.utils.space_ops import rotation_matrix_transpose
from manimlib.utils.space_ops import rotation_between_vectors
from manimlib.utils.space_ops import rotation_about_z
from typing import TYPE_CHECKING
@@ -207,17 +203,42 @@ class TipableVMobject(VMobject):
class Arc(TipableVMobject):
'''
Creates an arc.
Parameters
-----
start_angle : float
Starting angle of the arc in radians. (Angles are measured counter-clockwise)
angle : float
Angle subtended by the arc at its center in radians. (Angles are measured counter-clockwise)
radius : float
Radius of the arc
arc_center : array_like
Center of the arc
Examples :
arc = Arc(start_angle=TAU/4, angle=TAU/2, radius=3, arc_center=ORIGIN)
arc = Arc(angle=TAU/4, radius=4.5, arc_center=(1,2,0), color=BLUE)
Returns
-----
out : Arc object
An Arc object satisfying the specified parameters
'''
def __init__(
self,
start_angle: float = 0,
angle: float = TAU / 4,
radius: float = 1.0,
n_components: int = 8,
n_components: Optional[int] = None,
arc_center: Vect3 = ORIGIN,
**kwargs
):
super().__init__(**kwargs)
if n_components is None:
# 16 components for a full circle
n_components = int(15 * (abs(angle) / TAU)) + 1
self.set_points(quadratic_bezier_points_for_arc(angle, n_components))
self.rotate(start_angle, about_point=ORIGIN)
self.scale(radius, about_point=ORIGIN)
@@ -252,6 +273,26 @@ class Arc(TipableVMobject):
class ArcBetweenPoints(Arc):
'''
Creates an arc passing through the specified points with "angle" as the
angle subtended at its center.
Parameters
-----
start : array_like
Starting point of the arc
end : array_like
Ending point of the arc
angle : float
Angle subtended by the arc at its center in radians. (Angles are measured counter-clockwise)
Examples :
arc = ArcBetweenPoints(start=(0, 0, 0), end=(1, 2, 0), angle=TAU / 2)
arc = ArcBetweenPoints(start=(-2, 3, 0), end=(1, 2, 0), angle=-TAU / 12, color=BLUE)
Returns
-----
out : ArcBetweenPoints object
An ArcBetweenPoints object satisfying the specified parameters
'''
def __init__(
self,
start: Vect3,
@@ -266,6 +307,26 @@ class ArcBetweenPoints(Arc):
class CurvedArrow(ArcBetweenPoints):
'''
Creates a curved arrow passing through the specified points with "angle" as the
angle subtended at its center.
Parameters
-----
start_point : array_like
Starting point of the curved arrow
end_point : array_like
Ending point of the curved arrow
angle : float
Angle subtended by the curved arrow at its center in radians. (Angles are measured counter-clockwise)
Examples :
curvedArrow = CurvedArrow(start_point=(0, 0, 0), end_point=(1, 2, 0), angle=TAU/2)
curvedArrow = CurvedArrow(start_point=(-2, 3, 0), end_point=(1, 2, 0), angle=-TAU/12, color=BLUE)
Returns
-----
out : CurvedArrow object
A CurvedArrow object satisfying the specified parameters
'''
def __init__(
self,
start_point: Vect3,
@@ -277,6 +338,26 @@ class CurvedArrow(ArcBetweenPoints):
class CurvedDoubleArrow(CurvedArrow):
'''
Creates a curved double arrow passing through the specified points with "angle" as the
angle subtended at its center.
Parameters
-----
start_point : array_like
Starting point of the curved double arrow
end_point : array_like
Ending point of the curved double arrow
angle : float
Angle subtended by the curved double arrow at its center in radians. (Angles are measured counter-clockwise)
Examples :
curvedDoubleArrow = CurvedDoubleArrow(start_point = (0, 0, 0), end_point = (1, 2, 0), angle = TAU/2)
curvedDoubleArrow = CurvedDoubleArrow(start_point = (-2, 3, 0), end_point = (1, 2, 0), angle = -TAU/12, color = BLUE)
Returns
-----
out : CurvedDoubleArrow object
A CurvedDoubleArrow object satisfying the specified parameters
'''
def __init__(
self,
start_point: Vect3,
@@ -288,6 +369,23 @@ class CurvedDoubleArrow(CurvedArrow):
class Circle(Arc):
'''
Creates a circle.
Parameters
-----
radius : float
Radius of the circle
arc_center : array_like
Center of the circle
Examples :
circle = Circle(radius=2, arc_center=(1,2,0))
circle = Circle(radius=3.14, arc_center=2 * LEFT + UP, color=DARK_BLUE)
Returns
-----
out : Circle object
A Circle object satisfying the specified parameters
'''
def __init__(
self,
start_angle: float = 0,
@@ -323,6 +421,21 @@ class Circle(Arc):
class Dot(Circle):
'''
Creates a dot. Dot is a filled white circle with no bounary and DEFAULT_DOT_RADIUS.
Parameters
-----
point : array_like
Coordinates of center of the dot.
Examples :
dot = Dot(point=(1, 2, 0))
Returns
-----
out : Dot object
A Dot object satisfying the specified parameters
'''
def __init__(
self,
point: Vect3 = ORIGIN,
@@ -330,7 +443,7 @@ class Dot(Circle):
stroke_color: ManimColor = BLACK,
stroke_width: float = 0.0,
fill_opacity: float = 1.0,
fill_color: ManimColor = WHITE,
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
**kwargs
):
super().__init__(
@@ -345,6 +458,21 @@ class Dot(Circle):
class SmallDot(Dot):
'''
Creates a small dot. Small dot is a filled white circle with no bounary and DEFAULT_SMALL_DOT_RADIUS.
Parameters
-----
point : array_like
Coordinates of center of the small dot.
Examples :
smallDot = SmallDot(point=(1, 2, 0))
Returns
-----
out : SmallDot object
A SmallDot object satisfying the specified parameters
'''
def __init__(
self,
point: Vect3 = ORIGIN,
@@ -355,6 +483,25 @@ class SmallDot(Dot):
class Ellipse(Circle):
'''
Creates an ellipse.
Parameters
-----
width : float
Width of the ellipse
height : float
Height of the ellipse
arc_center : array_like
Coordinates of center of the ellipse
Examples :
ellipse = Ellipse(width=4, height=1, arc_center=(3, 3, 0))
ellipse = Ellipse(width=2, height=5, arc_center=ORIGIN, color=BLUE)
Returns
-----
out : Ellipse object
An Ellipse object satisfying the specified parameters
'''
def __init__(
self,
width: float = 2.0,
@@ -367,6 +514,28 @@ class Ellipse(Circle):
class AnnularSector(VMobject):
'''
Creates an annular sector.
Parameters
-----
inner_radius : float
Inner radius of the annular sector
outer_radius : float
Outer radius of the annular sector
start_angle : float
Starting angle of the annular sector (Angles are measured counter-clockwise)
angle : float
Angle subtended at the center of the annular sector (Angles are measured counter-clockwise)
arc_center : array_like
Coordinates of center of the annular sector
Examples :
annularSector = AnnularSector(inner_radius=1, outer_radius=2, angle=TAU/2, start_angle=TAU*3/4, arc_center=(1,-2,0))
Returns
-----
out : AnnularSector object
An AnnularSector object satisfying the specified parameters
'''
def __init__(
self,
angle: float = TAU / 4,
@@ -374,7 +543,7 @@ class AnnularSector(VMobject):
inner_radius: float = 1.0,
outer_radius: float = 2.0,
arc_center: Vect3 = ORIGIN,
fill_color: ManimColor = GREY_A,
fill_color: ManimColor = DEFAULT_LIGHT_COLOR,
fill_opacity: float = 1.0,
stroke_width: float = 0.0,
**kwargs,
@@ -403,6 +572,27 @@ class AnnularSector(VMobject):
class Sector(AnnularSector):
'''
Creates a sector.
Parameters
-----
outer_radius : float
Radius of the sector
start_angle : float
Starting angle of the sector in radians. (Angles are measured counter-clockwise)
angle : float
Angle subtended by the sector at its center in radians. (Angles are measured counter-clockwise)
arc_center : array_like
Coordinates of center of the sector
Examples :
sector = Sector(outer_radius=1, start_angle=TAU/3, angle=TAU/2, arc_center=[0,3,0])
sector = Sector(outer_radius=3, start_angle=TAU/4, angle=TAU/4, arc_center=ORIGIN, color=PINK)
Returns
-----
out : Sector object
An Sector object satisfying the specified parameters
'''
def __init__(
self,
angle: float = TAU / 4,
@@ -418,13 +608,32 @@ class Sector(AnnularSector):
class Annulus(VMobject):
'''
Creates an annulus.
Parameters
-----
inner_radius : float
Inner radius of the annulus
outer_radius : float
Outer radius of the annulus
arc_center : array_like
Coordinates of center of the annulus
Examples :
annulus = Annulus(inner_radius=2, outer_radius=3, arc_center=(1, -1, 0))
annulus = Annulus(inner_radius=2, outer_radius=3, stroke_width=20, stroke_color=RED, fill_color=BLUE, arc_center=ORIGIN)
Returns
-----
out : Annulus object
An Annulus object satisfying the specified parameters
'''
def __init__(
self,
inner_radius: float = 1.0,
outer_radius: float = 2.0,
fill_opacity: float = 1.0,
stroke_width: float = 0.0,
fill_color: ManimColor = GREY_A,
fill_color: ManimColor = DEFAULT_LIGHT_COLOR,
center: Vect3 = ORIGIN,
**kwargs,
):
@@ -444,6 +653,23 @@ class Annulus(VMobject):
class Line(TipableVMobject):
'''
Creates a line joining the points "start" and "end".
Parameters
-----
start : array_like
Starting point of the line
end : array_like
Ending point of the line
Examples :
line = Line((0, 0, 0), (3, 0, 0))
line = Line((1, 2, 0), (-2, -3, 0), color=BLUE)
Returns
-----
out : Line object
A Line object satisfying the specified parameters
'''
def __init__(
self,
start: Vect3 | Mobject = LEFT,
@@ -563,6 +789,25 @@ class Line(TipableVMobject):
class DashedLine(Line):
'''
Creates a dashed line joining the points "start" and "end".
Parameters
-----
start : array_like
Starting point of the dashed line
end : array_like
Ending point of the dashed line
dash_length : float
length of each dash
Examples :
line = DashedLine((0, 0, 0), (3, 0, 0))
line = DashedLine((1, 2, 3), (4, 5, 6), dash_length=0.01)
Returns
-----
out : DashedLine object
A DashedLine object satisfying the specified parameters
'''
def __init__(
self,
start: Vect3 = LEFT,
@@ -601,6 +846,9 @@ class DashedLine(Line):
else:
return Line.get_end(self)
def get_start_and_end(self) -> Tuple[Vect3, Vect3]:
return self.get_start(), self.get_end()
def get_first_handle(self) -> Vect3:
return self.submobjects[0].get_points()[1]
@@ -609,6 +857,26 @@ class DashedLine(Line):
class TangentLine(Line):
'''
Creates a tangent line to the specified vectorized math object.
Parameters
-----
vmob : VMobject object
Vectorized math object which the line will be tangent to
alpha : float
Point on the perimeter of the vectorized math object. It takes value between 0 and 1
both inclusive.
length : float
Length of the tangent line
Examples :
circle = Circle(arc_center=ORIGIN, radius=3, color=GREEN)
tangentLine = TangentLine(vmob=circle, alpha=1/3, length=6, color=BLUE)
Returns
-----
out : TangentLine object
A TangentLine object satisfying the specified parameters
'''
def __init__(
self,
vmob: VMobject,
@@ -624,6 +892,22 @@ class TangentLine(Line):
class Elbow(VMobject):
'''
Creates an elbow. Elbow is an L-shaped shaped object.
Parameters
-----
width : float
Width of the elbow
angle : float
Angle of the elbow in radians with the horizontal. (Angles are measured counter-clockwise)
Examples :
line = Elbow(width=2, angle=TAU/16)
Returns
-----
out : Elbow object
A Elbow object satisfying the specified parameters
'''
def __init__(
self,
width: float = 0.2,
@@ -641,7 +925,7 @@ class StrokeArrow(Line):
self,
start: Vect3 | Mobject,
end: Vect3 | Mobject,
stroke_color: ManimColor = GREY_A,
stroke_color: ManimColor = DEFAULT_LIGHT_COLOR,
stroke_width: float = 5,
buff: float = 0.25,
tip_width_ratio: float = 5,
@@ -733,6 +1017,47 @@ class StrokeArrow(Line):
class Arrow(Line):
'''
Creates an arrow.
Parameters
----------
start : array_like
Starting point of the arrow
end : array_like
Ending point of the arrow
buff : float, optional
Buffer distance from the start and end points. Default is MED_SMALL_BUFF.
path_arc : float, optional
If set to a non-zero value, the arrow will be curved to subtend a circle by this angle.
Default is 0 (straight arrow).
thickness : float, optional
How wide should the base of the arrow be. This affects the shaft width. Default is 3.0.
tip_width_ratio : float, optional
Ratio of the tip width to the shaft width. Default is 5.
tip_angle : float, optional
Angle of the arrow tip in radians. Default is PI/3 (60 degrees).
max_tip_length_to_length_ratio : float, optional
Maximum ratio of tip length to total arrow length. Prevents tips from being too large
relative to the arrow. Default is 0.5.
max_width_to_length_ratio : float, optional
Maximum ratio of arrow width to total arrow length. Prevents arrows from being too wide
relative to their length. Default is 0.1.
**kwargs
Additional keyword arguments passed to the parent Line class.
Examples
--------
>>> arrow = Arrow((0, 0, 0), (3, 0, 0))
>>> curved_arrow = Arrow(LEFT, RIGHT, path_arc=PI/4)
>>> thick_arrow = Arrow(UP, DOWN, thickness=5.0, tip_width_ratio=3)
Returns
-------
Arrow
An Arrow object satisfying the specified parameters.
'''
tickness_multiplier = 0.015
def __init__(
@@ -741,7 +1066,7 @@ class Arrow(Line):
end: Vect3 | Mobject = LEFT,
buff: float = MED_SMALL_BUFF,
path_arc: float = 0,
fill_color: ManimColor = GREY_A,
fill_color: ManimColor = DEFAULT_LIGHT_COLOR,
fill_opacity: float = 1.0,
stroke_width: float = 0.0,
thickness: float = 3.0,
@@ -895,6 +1220,20 @@ class Arrow(Line):
class Vector(Arrow):
'''
Creates a vector. Vector is an arrow with start point as ORIGIN
Parameters
-----
direction : array_like
Coordinates of direction of the arrow
Examples :
arrow = Vector(direction=LEFT)
Returns
-----
out : Vector object
A Vector object satisfying the specified parameters
'''
def __init__(
self,
direction: Vect3 = RIGHT,
@@ -907,6 +1246,33 @@ class Vector(Arrow):
class CubicBezier(VMobject):
'''
Creates a cubic Bézier curve.
A cubic Bézier curve is defined by four control points: two anchor points (start and end)
and two handle points that control the curvature. The curve starts at the first anchor
point, is "pulled" toward the handle points, and ends at the second anchor point.
Parameters
----------
a0 : array_like
First anchor point (starting point of the curve).
h0 : array_like
First handle point (controls the initial direction and curvature from a0).
h1 : array_like
Second handle point (controls the final direction and curvature toward a1).
a1 : array_like
Second anchor point (ending point of the curve).
**kwargs
Additional keyword arguments passed to the parent VMobject class, such as
stroke_color, stroke_width, fill_color, fill_opacity, etc.
Returns
-------
CubicBezier
A CubicBezier object representing the specified cubic Bézier curve.
'''
def __init__(
self,
a0: Vect3,
@@ -920,6 +1286,20 @@ class CubicBezier(VMobject):
class Polygon(VMobject):
'''
Creates a polygon by joining the specified vertices.
Parameters
-----
*vertices : array_like
Vertex of the polygon
Examples :
triangle = Polygon((-3,0,0), (3,0,0), (0,3,0))
Returns
-----
out : Polygon object
A Polygon object satisfying the specified parameters
'''
def __init__(
self,
*vertices: Vect3,
@@ -978,6 +1358,22 @@ class Polyline(VMobject):
class RegularPolygon(Polygon):
'''
Creates a regular polygon of edge length 1 at the center of the screen.
Parameters
-----
n : int
Number of vertices of the regular polygon
start_angle : float
Starting angle of the regular polygon in radians. (Angles are measured counter-clockwise)
Examples :
pentagon = RegularPolygon(n=5, start_angle=30 * DEGREES)
Returns
-----
out : RegularPolygon object
A RegularPolygon object satisfying the specified parameters
'''
def __init__(
self,
n: int = 6,
@@ -987,13 +1383,27 @@ class RegularPolygon(Polygon):
):
# Defaults to 0 for odd, 90 for even
if start_angle is None:
start_angle = (n % 2) * 90 * DEGREES
start_angle = (n % 2) * 90 * DEG
start_vect = rotate_vector(radius * RIGHT, start_angle)
vertices = compass_directions(n, start_vect)
super().__init__(*vertices, **kwargs)
class Triangle(RegularPolygon):
'''
Creates a triangle of edge length 1 at the center of the screen.
Parameters
-----
start_angle : float
Starting angle of the triangle in radians. (Angles are measured counter-clockwise)
Examples :
triangle = Triangle(start_angle=45 * DEGREES)
Returns
-----
out : Triangle object
A Triangle object satisfying the specified parameters
'''
def __init__(self, **kwargs):
super().__init__(n=3, **kwargs)
@@ -1005,7 +1415,7 @@ class ArrowTip(Triangle):
width: float = DEFAULT_ARROW_TIP_WIDTH,
length: float = DEFAULT_ARROW_TIP_LENGTH,
fill_opacity: float = 1.0,
fill_color: ManimColor = WHITE,
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
stroke_width: float = 0.0,
tip_style: int = 0, # triangle=0, inner_smooth=1, dot=2
**kwargs
@@ -1044,6 +1454,22 @@ class ArrowTip(Triangle):
class Rectangle(Polygon):
'''
Creates a rectangle at the center of the screen.
Parameters
-----
width : float
Width of the rectangle
height : float
Height of the rectangle
Examples :
rectangle = Rectangle(width=3, height=4, color=BLUE)
Returns
-----
out : Rectangle object
A Rectangle object satisfying the specified parameters
'''
def __init__(
self,
width: float = 4.0,
@@ -1062,11 +1488,43 @@ class Rectangle(Polygon):
class Square(Rectangle):
'''
Creates a square at the center of the screen.
Parameters
-----
side_length : float
Edge length of the square
Examples :
square = Square(side_length=5, color=PINK)
Returns
-----
out : Square object
A Square object satisfying the specified parameters
'''
def __init__(self, side_length: float = 2.0, **kwargs):
super().__init__(side_length, side_length, **kwargs)
class RoundedRectangle(Rectangle):
'''
Creates a rectangle with round edges at the center of the screen.
Parameters
-----
width : float
Width of the rounded rectangle
height : float
Height of the rounded rectangle
corner_radius : float
Corner radius of the rectangle
Examples :
rRectangle = RoundedRectangle(width=3, height=4, corner_radius=1, color=BLUE)
Returns
-----
out : RoundedRectangle object
A RoundedRectangle object satisfying the specified parameters
'''
def __init__(
self,
width: float = 4.0,

View File

@@ -6,7 +6,7 @@ from pyglet.window import key as PygletWindowKeys
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, UP
from manimlib.constants import MED_LARGE_BUFF, MED_SMALL_BUFF, SMALL_BUFF
from manimlib.constants import BLACK, BLUE, GREEN, GREY_A, GREY_C, RED, WHITE
from manimlib.constants import BLACK, BLUE, GREEN, GREY_A, GREY_C, RED, WHITE, DEFAULT_MOBJECT_COLOR
from manimlib.mobject.mobject import Group
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.geometry import Circle
@@ -387,7 +387,7 @@ class Textbox(ControlMobject):
box_kwargs: dict = {
"width": 2.0,
"height": 1.0,
"fill_color": WHITE,
"fill_color": DEFAULT_MOBJECT_COLOR,
"fill_opacity": 1.0,
},
text_kwargs: dict = {

View File

@@ -1,11 +1,9 @@
from __future__ import annotations
import itertools as it
import numpy as np
from manimlib.constants import DOWN, LEFT, RIGHT, ORIGIN
from manimlib.constants import DEGREES
from manimlib.constants import DEG
from manimlib.mobject.numbers import DecimalNumber
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.types.vectorized_mobject import VGroup
@@ -14,7 +12,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Sequence, Union, Tuple, Optional
from typing import Sequence, Union, Optional
from manimlib.typing import ManimColor, Vect3, VectNArray, Self
StringMatrixType = Union[Sequence[Sequence[str]], np.ndarray[int, np.dtype[np.str_]]]
@@ -198,7 +196,7 @@ class Matrix(VMobject):
dots.set_width(hdots_width)
self.swap_entry_for_dots(row[col_index], dots)
if use_vdots and use_hdots:
rows[row_index][col_index].rotate(-45 * DEGREES)
rows[row_index][col_index].rotate(-45 * DEG)
return self
def get_mob_matrix(self) -> VMobjectMatrixType:

View File

@@ -12,13 +12,13 @@ import moderngl
import numbers
import numpy as np
from manimlib.constants import DEFAULT_MOBJECT_TO_EDGE_BUFFER
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER
from manimlib.constants import DEFAULT_MOBJECT_TO_EDGE_BUFF
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF
from manimlib.constants import DOWN, IN, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import MED_SMALL_BUFF
from manimlib.constants import TAU
from manimlib.constants import WHITE
from manimlib.constants import DEFAULT_MOBJECT_COLOR
from manimlib.event_handler import EVENT_DISPATCHER
from manimlib.event_handler.event_listner import EventListener
from manimlib.event_handler.event_type import EventType
@@ -52,7 +52,7 @@ SubmobjectType = TypeVar('SubmobjectType', bound='Mobject')
if TYPE_CHECKING:
from typing import Callable, Iterator, Union, Tuple, Optional, Any
import numpy.typing as npt
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, UniformDict, Self
from manimlib.typing import ManimColor, Vect3, Vect4Array, Vect3Array, UniformDict, Self
from moderngl.context import Context
T = TypeVar('T')
@@ -78,7 +78,7 @@ class Mobject(object):
def __init__(
self,
color: ManimColor = WHITE,
color: ManimColor = DEFAULT_MOBJECT_COLOR,
opacity: float = 1.0,
shading: Tuple[float, float, float] = (0.0, 0.0, 0.0),
# For shaders
@@ -160,10 +160,10 @@ class Mobject(object):
return self
@property
def animate(self) -> _AnimationBuilder:
def animate(self) -> _AnimationBuilder | Self:
"""
Methods called with Mobject.animate.method() can be passed
into a Scene.play call, as if you were calling
into a Scene.play call, as if you were calling
ApplyMethod(mobject.method)
Borrowed from https://github.com/ManimCommunity/manim/
@@ -287,10 +287,7 @@ class Mobject(object):
about_point = self.get_bounding_box_point(about_edge)
for mob in self.get_family():
arrs = []
if mob.has_points():
for key in mob.pointlike_data_keys:
arrs.append(mob.data[key])
arrs = [mob.data[key] for key in mob.pointlike_data_keys if mob.has_points()]
if works_on_bounding_box:
arrs.append(mob.get_bounding_box())
@@ -307,12 +304,15 @@ class Mobject(object):
parent.refresh_bounding_box()
return self
# Others related to points
@affects_data
def match_points(self, mobject: Mobject) -> Self:
self.set_points(mobject.get_points())
self.resize_points(len(mobject.data), resize_func=resize_preserving_order)
for key in self.pointlike_data_keys:
self.data[key][:] = mobject.data[key]
return self
# Others related to points
def get_points(self) -> Vect3Array:
return self.data["point"]
@@ -715,21 +715,6 @@ class Mobject(object):
self.become(self.saved_state)
return self
def save_to_file(self, file_path: str) -> Self:
with open(file_path, "wb") as fp:
fp.write(self.serialize())
log.info(f"Saved mobject to {file_path}")
return self
@staticmethod
def load(file_path) -> Mobject:
if not os.path.exists(file_path):
log.error(f"No file found at {file_path}")
sys.exit(2)
with open(file_path, "rb") as fp:
mobject = pickle.load(fp)
return mobject
def become(self, mobject: Mobject, match_updaters=False) -> Self:
"""
Edit all data and submobjects to be idential
@@ -857,6 +842,7 @@ class Mobject(object):
if call:
self.update(dt=0)
self.refresh_has_updater_status()
self.update()
return self
def insert_updater(self, update_func: Updater, index=0):
@@ -1070,7 +1056,7 @@ class Mobject(object):
def align_on_border(
self,
direction: Vect3,
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF
) -> Self:
"""
Direction just needs to be a vector pointing towards side or
@@ -1086,14 +1072,14 @@ class Mobject(object):
def to_corner(
self,
corner: Vect3 = LEFT + DOWN,
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF
) -> Self:
return self.align_on_border(corner, buff)
def to_edge(
self,
edge: Vect3 = LEFT,
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF
) -> Self:
return self.align_on_border(edge, buff)
@@ -1101,7 +1087,7 @@ class Mobject(object):
self,
mobject_or_point: Mobject | Vect3,
direction: Vect3 = RIGHT,
buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF,
aligned_edge: Vect3 = ORIGIN,
submobject_to_align: Mobject | None = None,
index_of_submobject_to_align: int | slice | None = None,
@@ -1132,7 +1118,7 @@ class Mobject(object):
space_lengths = [FRAME_X_RADIUS, FRAME_Y_RADIUS]
for vect in UP, DOWN, LEFT, RIGHT:
dim = np.argmax(np.abs(vect))
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFFER)
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFF)
max_val = space_lengths[dim] - buff
edge_center = self.get_edge_center(vect)
if np.dot(edge_center, vect) > max_val:
@@ -1246,8 +1232,9 @@ class Mobject(object):
def set_z(self, z: float, direction: Vect3 = ORIGIN) -> Self:
return self.set_coord(z, 2, direction)
def set_z_index(self, z_index: int) -> Self:
self.z_index = z_index
def set_z_index(self, z_index: int, recurse=True) -> Self:
for mob in self.get_family(recurse):
mob.z_index = z_index
return self
def space_out_submobjects(self, factor: float = 1.5, **kwargs) -> Self:
@@ -1298,6 +1285,14 @@ class Mobject(object):
self.scale((length + buff) / length)
return self
def put_start_on(self, point: Vect3) -> Self:
self.shift(point - self.get_start())
return self
def put_end_on(self, point: Vect3) -> Self:
self.shift(point - self.get_end())
return self
def put_start_and_end_on(self, start: Vect3, end: Vect3) -> Self:
curr_start, curr_end = self.get_start_and_end()
curr_vect = curr_end - curr_start
@@ -1334,20 +1329,19 @@ class Mobject(object):
def set_color_by_rgba_func(
self,
func: Callable[[Vect3], Vect4],
func: Callable[[Vect3Array], Vect4Array],
recurse: bool = True
) -> Self:
"""
Func should take in a point in R3 and output an rgba value
"""
for mob in self.get_family(recurse):
rgba_array = [func(point) for point in mob.get_points()]
mob.set_rgba_array(rgba_array)
mob.set_rgba_array(func(mob.get_points()))
return self
def set_color_by_rgb_func(
self,
func: Callable[[Vect3], Vect3],
func: Callable[[Vect3Array], Vect3Array],
opacity: float = 1,
recurse: bool = True
) -> Self:
@@ -1355,8 +1349,9 @@ class Mobject(object):
Func should take in a point in R3 and output an rgb value
"""
for mob in self.get_family(recurse):
rgba_array = [[*func(point), opacity] for point in mob.get_points()]
mob.set_rgba_array(rgba_array)
points = mob.get_points()
opacity = np.ones((points.shape[0], 1)) * opacity
mob.set_rgba_array(np.hstack((func(points), opacity)))
return self
@affects_family_data
@@ -1375,7 +1370,7 @@ class Mobject(object):
rgbs = resize_with_interpolation(rgbs, len(data))
data[name][:, :3] = rgbs
if opacity is not None:
if not isinstance(opacity, (float, int)):
if not isinstance(opacity, (float, int, np.floating)):
opacity = resize_with_interpolation(np.array(opacity), len(data))
data[name][:, 3] = opacity
return self
@@ -1950,12 +1945,14 @@ class Mobject(object):
def set_clip_plane(
self,
vect: Vect3 | None = None,
threshold: float | None = None
threshold: float | None = None,
recurse=True
) -> Self:
if vect is not None:
self.uniforms["clip_plane"][:3] = vect
if threshold is not None:
self.uniforms["clip_plane"][3] = threshold
for submob in self.get_family(recurse):
if vect is not None:
submob.uniforms["clip_plane"][:3] = vect
if threshold is not None:
submob.uniforms["clip_plane"][3] = threshold
return self
def deactivate_clip_plane(self) -> Self:
@@ -2268,6 +2265,18 @@ class _AnimationBuilder:
def __call__(self, **kwargs):
return self.set_anim_args(**kwargs)
def __dir__(self) -> list[str]:
"""
Extend attribute list of _AnimationBuilder object to include mobject attributes
for better autocompletion in the IPython terminal when using interactive mode.
"""
methods = super().__dir__()
mobject_methods = [
attr for attr in dir(self.mobject)
if not attr.startswith('_')
]
return sorted(set(methods+mobject_methods))
def set_anim_args(self, **kwargs):
'''
You can change the args of :class:`~manimlib.animation.transform.Transform`, such as

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import inspect
from manimlib.constants import DEGREES
from manimlib.constants import DEG
from manimlib.constants import RIGHT
from manimlib.mobject.mobject import Mobject
from manimlib.utils.simple_functions import clip
@@ -71,7 +71,7 @@ def always_shift(
def always_rotate(
mobject: Mobject,
rate: float = 20 * DEGREES,
rate: float = 20 * DEG,
**kwargs
) -> Mobject:
mobject.add_updater(

View File

@@ -3,20 +3,24 @@ from __future__ import annotations
import numpy as np
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import GREY_B
from manimlib.constants import MED_SMALL_BUFF
from manimlib.mobject.geometry import Line
from manimlib.constants import DEFAULT_LIGHT_COLOR
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
from manimlib.constants import YELLOW, DEG
from manimlib.mobject.geometry import Line, ArrowTip
from manimlib.mobject.numbers import DecimalNumber
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.value_tracker import ValueTracker
from manimlib.utils.bezier import interpolate
from manimlib.utils.bezier import outer_interpolate
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.simple_functions import fdiv
from manimlib.utils.space_ops import rotate_vector, angle_of_vector
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterable, Optional
from typing import Iterable, Optional, Tuple, Dict, Any
from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier
@@ -24,7 +28,7 @@ class NumberLine(Line):
def __init__(
self,
x_range: RangeSpecifier = (-8, 8, 1),
color: ManimColor = GREY_B,
color: ManimColor = DEFAULT_LIGHT_COLOR,
stroke_width: float = 2.0,
# How big is one one unit of this number line in terms of absolute spacial distance
unit_size: float = 1.0,
@@ -182,9 +186,13 @@ class NumberLine(Line):
if x < 0 and direction[0] == 0:
# Align without the minus sign
num_mob.shift(num_mob[0].get_width() * LEFT / 2)
if x == unit and unit_tex:
if abs(x) == unit and unit_tex:
center = num_mob.get_center()
num_mob.remove(num_mob[0])
if x > 0:
num_mob.remove(num_mob[0])
else:
num_mob.remove(num_mob[1])
num_mob[0].next_to(num_mob[1], LEFT, buff=num_mob[0].get_width() / 4)
num_mob.move_to(center)
return num_mob
@@ -221,11 +229,77 @@ class UnitInterval(NumberLine):
big_tick_numbers: list[float] = [0, 1],
decimal_number_config: dict = dict(
num_decimal_places=1,
)
),
**kwargs
):
super().__init__(
x_range=x_range,
unit_size=unit_size,
big_tick_numbers=big_tick_numbers,
decimal_number_config=decimal_number_config,
**kwargs
)
class Slider(VGroup):
def __init__(
self,
value_tracker: ValueTracker,
x_range: Tuple[float, float] = (-5, 5),
var_name: Optional[str] = None,
width: float = 3,
unit_size: float = 1,
arrow_width: float = 0.15,
arrow_length: float = 0.15,
arrow_color: ManimColor = YELLOW,
font_size: int = 24,
label_buff: float = SMALL_BUFF,
num_decimal_places: int = 2,
tick_size: float = 0.05,
number_line_config: Dict[str, Any] = dict(),
arrow_tip_config: Dict[str, Any] = dict(),
decimal_config: Dict[str, Any] = dict(),
angle: float = 0,
label_direction: Optional[np.ndarray] = None,
add_tick_labels: bool = True,
tick_label_font_size: int = 16,
):
get_value = value_tracker.get_value
if label_direction is None:
label_direction = np.round(rotate_vector(UP, angle), 2)
# Initialize number line
number_line_kw = dict(x_range=x_range, width=width, tick_size=tick_size)
number_line_kw.update(number_line_config)
number_line = NumberLine(**number_line_kw)
number_line.rotate(angle)
if add_tick_labels:
number_line.add_numbers(
font_size=tick_label_font_size,
buff=2 * tick_size,
direction=-label_direction
)
# Initialize arrow tip
arrow_tip_kw = dict(
width=arrow_width,
length=arrow_length,
fill_color=arrow_color,
angle=-180 * DEG + angle_of_vector(label_direction),
)
arrow_tip_kw.update(arrow_tip_config)
tip = ArrowTip(**arrow_tip_kw)
tip.add_updater(lambda m: m.move_to(number_line.n2p(get_value()), -label_direction))
# Initialize label
dec_string = f"{{:.{num_decimal_places}f}}".format(0)
lhs = f"{var_name} = " if var_name is not None else ""
label = Tex(lhs + dec_string, font_size=font_size)
label[var_name].set_fill(arrow_color)
decimal = label.make_number_changeable(dec_string)
decimal.add_updater(lambda m: m.set_value(get_value()))
label.add_updater(lambda m: m.next_to(tip, label_direction, label_buff))
# Assemble group
super().__init__(number_line, tip, label)
self.set_stroke(behind=True)

View File

@@ -4,7 +4,7 @@ from functools import lru_cache
import numpy as np
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import WHITE
from manimlib.constants import DEFAULT_MOBJECT_COLOR
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.text_mobject import Text
from manimlib.mobject.types.vectorized_mobject import VMobject
@@ -14,7 +14,8 @@ from manimlib.utils.bezier import interpolate
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import TypeVar
from typing import TypeVar, Callable
from manimlib.mobject.mobject import Mobject
from manimlib.typing import ManimColor, Vect3, Self
T = TypeVar("T", bound=VMobject)
@@ -22,24 +23,31 @@ if TYPE_CHECKING:
@lru_cache()
def char_to_cahced_mob(char: str, **text_config):
return Text(char, **text_config)
if "\\" in char:
# This is for when the "character" is a LaTeX command
# like ^\circ or \dots
return Tex(char, **text_config)
else:
return Text(char, **text_config)
class DecimalNumber(VMobject):
def __init__(
self,
number: float | complex = 0,
color: ManimColor = WHITE,
color: ManimColor = DEFAULT_MOBJECT_COLOR,
stroke_width: float = 0,
fill_opacity: float = 1.0,
fill_border_width: float = 0.5,
num_decimal_places: int = 2,
min_total_width: Optional[int] = 0,
include_sign: bool = False,
group_with_commas: bool = True,
digit_buff_per_font_unit: float = 0.001,
show_ellipsis: bool = False,
unit: str | None = None, # Aligned to bottom unless it starts with "^"
include_background_rectangle: bool = False,
hide_zero_components_on_complex: bool = True,
edge_to_fix: Vect3 = LEFT,
font_size: float = 48,
text_config: dict = dict(), # Do not pass in font_size here
@@ -48,10 +56,12 @@ class DecimalNumber(VMobject):
self.num_decimal_places = num_decimal_places
self.include_sign = include_sign
self.group_with_commas = group_with_commas
self.min_total_width = min_total_width
self.digit_buff_per_font_unit = digit_buff_per_font_unit
self.show_ellipsis = show_ellipsis
self.unit = unit
self.include_background_rectangle = include_background_rectangle
self.hide_zero_components_on_complex = hide_zero_components_on_complex
self.edge_to_fix = edge_to_fix
self.font_size = font_size
self.text_config = dict(text_config)
@@ -112,7 +122,14 @@ class DecimalNumber(VMobject):
def get_num_string(self, number: float | complex) -> str:
if isinstance(number, complex):
formatter = self.get_complex_formatter()
if self.hide_zero_components_on_complex and number.imag == 0:
number = number.real
formatter = self.get_formatter()
elif self.hide_zero_components_on_complex and number.real == 0:
number = number.imag
formatter = self.get_formatter() + "i"
else:
formatter = self.get_complex_formatter()
else:
formatter = self.get_formatter()
if self.num_decimal_places == 0 and isinstance(number, float):
@@ -161,6 +178,7 @@ class DecimalNumber(VMobject):
"include_sign",
"group_with_commas",
"num_decimal_places",
"min_total_width",
]
])
config.update(kwargs)
@@ -170,6 +188,7 @@ class DecimalNumber(VMobject):
config.get("field_name", ""),
":",
"+" if config["include_sign"] else "",
"0" + str(config.get("min_total_width", "")) if config.get("min_total_width") else "",
"," if config["group_with_commas"] else "",
f".{ndp}f" if ndp > 0 else "d",
"}",

View File

@@ -43,6 +43,7 @@ class SampleSpace(Rectangle):
fill_opacity=fill_opacity,
stroke_width=stroke_width,
stroke_color=stroke_color,
**kwargs
)
self.default_label_scale_val = default_label_scale_val

View File

@@ -2,14 +2,14 @@ from __future__ import annotations
from colour import Color
from manimlib.constants import BLACK, RED, YELLOW, WHITE
from manimlib.config import manim_config
from manimlib.constants import BLACK, RED, YELLOW, DEFAULT_MOBJECT_COLOR
from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR
from manimlib.constants import SMALL_BUFF
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.customization import get_customization
from typing import TYPE_CHECKING
@@ -57,7 +57,7 @@ class BackgroundRectangle(SurroundingRectangle):
**kwargs
):
if color is None:
color = get_customization()['style']['background_color']
color = manim_config.camera.background_color
super().__init__(
mobject,
color=color,
@@ -79,7 +79,8 @@ class BackgroundRectangle(SurroundingRectangle):
stroke_width: float | None = None,
fill_color: ManimColor | None = None,
fill_opacity: float | None = None,
family: bool = True
family: bool = True,
**kwargs
) -> Self:
# Unchangeable style, except for fill_opacity
VMobject.set_style(
@@ -117,7 +118,7 @@ class Underline(Line):
self,
mobject: Mobject,
buff: float = SMALL_BUFF,
stroke_color=WHITE,
stroke_color=DEFAULT_MOBJECT_COLOR,
stroke_width: float | Sequence[float] = [0, 3, 3, 0],
stretch_factor=1.2,
**kwargs

View File

@@ -5,8 +5,8 @@ import copy
import numpy as np
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, SMALL_BUFF
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, UP, DL, DR, UL
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF, SMALL_BUFF
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL, UP
from manimlib.constants import PI
from manimlib.animation.composition import AnimationGroup
from manimlib.animation.fading import FadeIn
@@ -79,7 +79,7 @@ class Brace(Tex):
)
else:
mob.move_to(self.get_tip())
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFFER)
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFF)
shift_distance = mob.get_width() / 2.0 + buff
mob.shift(self.get_direction() * shift_distance)
return self
@@ -116,7 +116,7 @@ class BraceLabel(VMobject):
text: str | Iterable[str],
brace_direction: np.ndarray = DOWN,
label_scale: float = 1.0,
label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF,
**kwargs
) -> None:
super().__init__(**kwargs)
@@ -174,3 +174,12 @@ class BraceLabel(VMobject):
class BraceText(BraceLabel):
label_constructor: type = TexText
class LineBrace(Brace):
def __init__(self, line: Line, direction=UP, **kwargs):
angle = line.get_angle()
line.rotate(-angle)
super().__init__(line, direction, **kwargs)
line.rotate(angle)
self.rotate(angle, about_point=line.get_center())

View File

@@ -25,7 +25,6 @@ from manimlib.constants import LEFT
from manimlib.constants import LEFT
from manimlib.constants import MED_LARGE_BUFF
from manimlib.constants import MED_SMALL_BUFF
from manimlib.constants import LARGE_BUFF
from manimlib.constants import ORIGIN
from manimlib.constants import OUT
from manimlib.constants import PI
@@ -52,12 +51,9 @@ from manimlib.mobject.geometry import Polygon
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import Square
from manimlib.mobject.geometry import AnnularSector
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.numbers import Integer
from manimlib.mobject.shape_matchers import SurroundingRectangle
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import TexText
from manimlib.mobject.svg.special_tex import TexTextFromPresetString
from manimlib.mobject.three_dimensions import Prismify
from manimlib.mobject.three_dimensions import VCube
@@ -348,6 +344,8 @@ class ClockPassesTime(AnimationGroup):
angle=12 * hour_radians,
**rot_kwargs
),
group=clock,
run_time=run_time,
**kwargs
)
@@ -421,7 +419,7 @@ class Bubble(VGroup):
super().flip(axis=axis, **kwargs)
if only_body:
# Flip in place, don't use kwargs
self.content.flip(axis=axis)
self.content.flip(axis=axis)
if abs(axis[1]) > 0:
self.direction = -np.array(self.direction)
return self

View File

@@ -4,10 +4,10 @@ from functools import reduce
import operator as op
import re
from manimlib.constants import BLACK, WHITE
from manimlib.constants import BLACK, DEFAULT_MOBJECT_COLOR
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from manimlib.utils.tex_file_writing import latex_to_svg
from typing import TYPE_CHECKING
@@ -26,10 +26,10 @@ class SingleStringTex(SVGMobject):
self,
tex_string: str,
height: float | None = None,
fill_color: ManimColor = WHITE,
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
fill_opacity: float = 1.0,
stroke_width: float = 0,
svg_default: dict = dict(fill_color=WHITE),
svg_default: dict = dict(fill_color=DEFAULT_MOBJECT_COLOR),
path_string_config: dict = dict(),
font_size: int = 48,
alignment: str = R"\centering",
@@ -76,12 +76,8 @@ class SingleStringTex(SVGMobject):
self.additional_preamble
)
def get_file_path(self) -> str:
content = self.get_tex_file_body(self.tex_string)
file_path = tex_content_to_svg_file(
content, self.template, self.additional_preamble, self.tex_string
)
return file_path
def get_svg_string_by_content(self, content: str) -> str:
return latex_to_svg(content, self.template, self.additional_preamble)
def get_tex_file_body(self, tex_string: str) -> str:
new_tex = self.get_modified_expression(tex_string)

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from manimlib.constants import MED_SMALL_BUFF, WHITE, GREY_C
from manimlib.constants import MED_SMALL_BUFF, DEFAULT_MOBJECT_COLOR, GREY_C
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import FRAME_WIDTH
from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF
@@ -21,13 +21,15 @@ class BulletedList(VGroup):
*items: str,
buff: float = MED_LARGE_BUFF,
aligned_edge: Vect3 = LEFT,
numbered: bool = False,
**kwargs
):
):
labelled_content = [R"\item " + item for item in items]
enum_str = "enumerate" if numbered else "itemize"
tex_string = "\n".join([
R"\begin{itemize}",
fR"\begin{{{enum_str}}}",
*labelled_content,
R"\end{itemize}"
fR"\end{{{enum_str}}}"
])
tex_text = TexText(tex_string, isolate=labelled_content, **kwargs)
lines = (tex_text.select_part(part) for part in labelled_content)
@@ -36,14 +38,17 @@ class BulletedList(VGroup):
self.arrange(DOWN, buff=buff, aligned_edge=aligned_edge)
def fade_all_but(self, index: int, opacity: float = 0.25) -> None:
def fade_all_but(self, index: int, opacity: float = 0.25, scale_factor=0.7) -> None:
max_dot_height = max([item[0].get_height() for item in self.submobjects])
for i, part in enumerate(self.submobjects):
trg_dot_height = (1.0 if i == index else scale_factor) * max_dot_height
part.set_fill(opacity=(1.0 if i == index else opacity))
part.scale(trg_dot_height / part[0].get_height(), about_edge=LEFT)
class TexTextFromPresetString(TexText):
tex: str = ""
default_color: ManimColor = WHITE
default_color: ManimColor = DEFAULT_MOBJECT_COLOR
def __init__(self, **kwargs):
super().__init__(

View File

@@ -6,7 +6,7 @@ import re
from scipy.optimize import linear_sum_assignment
from scipy.spatial.distance import cdist
from manimlib.constants import WHITE
from manimlib.constants import DEFAULT_MOBJECT_COLOR
from manimlib.logger import log
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
@@ -46,11 +46,11 @@ class StringMobject(SVGMobject, ABC):
def __init__(
self,
string: str,
fill_color: ManimColor = WHITE,
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
fill_border_width: float = 0.5,
stroke_color: ManimColor = WHITE,
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
stroke_width: float = 0,
base_color: ManimColor = WHITE,
base_color: ManimColor = DEFAULT_MOBJECT_COLOR,
isolate: Selector = (),
protect: Selector = (),
# When set to true, only the labelled svg is
@@ -60,23 +60,24 @@ class StringMobject(SVGMobject, ABC):
**kwargs
):
self.string = string
self.base_color = base_color or WHITE
self.base_color = base_color or DEFAULT_MOBJECT_COLOR
self.isolate = isolate
self.protect = protect
self.use_labelled_svg = use_labelled_svg
self.parse()
super().__init__(**kwargs)
svg_string = self.get_svg_string()
super().__init__(svg_string=svg_string, **kwargs)
self.set_stroke(stroke_color, stroke_width)
self.set_fill(fill_color, border_width=fill_border_width)
self.labels = [submob.label for submob in self.submobjects]
def get_file_path(self, is_labelled: bool = False) -> str:
is_labelled = is_labelled or self.use_labelled_svg
return self.get_file_path_by_content(self.get_content(is_labelled))
def get_svg_string(self, is_labelled: bool = False) -> str:
content = self.get_content(is_labelled or self.use_labelled_svg)
return self.get_svg_string_by_content(content)
@abstractmethod
def get_file_path_by_content(self, content: str) -> str:
def get_svg_string_by_content(self, content: str) -> str:
return ""
def assign_labels_by_color(self, mobjects: list[VMobject]) -> None:
@@ -109,8 +110,8 @@ class StringMobject(SVGMobject, ABC):
)
)
def mobjects_from_file(self, file_path: str) -> list[VMobject]:
submobs = super().mobjects_from_file(file_path)
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
submobs = super().mobjects_from_svg_string(svg_string)
if self.use_labelled_svg:
# This means submobjects are colored according to spans
@@ -121,8 +122,8 @@ class StringMobject(SVGMobject, ABC):
# of submobject which are and use those for labels
unlabelled_submobs = submobs
labelled_content = self.get_content(is_labelled=True)
labelled_file = self.get_file_path_by_content(labelled_content)
labelled_submobs = super().mobjects_from_file(labelled_file)
labelled_file = self.get_svg_string_by_content(labelled_content)
labelled_submobs = super().mobjects_from_svg_string(labelled_file)
self.labelled_submobs = labelled_submobs
self.unlabelled_submobs = unlabelled_submobs

View File

@@ -1,13 +1,14 @@
from __future__ import annotations
import os
from xml.etree import ElementTree as ET
import numpy as np
import svgelements as se
import io
from pathlib import Path
from manimlib.constants import RIGHT
from manimlib.constants import TAU
from manimlib.logger import log
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Line
@@ -16,14 +17,13 @@ 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 VMobject
from manimlib.utils.directories import get_mobject_data_dir
from manimlib.utils.bezier import quadratic_bezier_points_for_arc
from manimlib.utils.images import get_full_vector_image_path
from manimlib.utils.iterables import hash_obj
from manimlib.utils.simple_functions import hash_string
from manimlib.utils.space_ops import rotation_about_z
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Tuple
from manimlib.typing import ManimColor, Vect3Array
@@ -43,6 +43,7 @@ class SVGMobject(VMobject):
def __init__(
self,
file_name: str = "",
svg_string: str = "",
should_center: bool = True,
height: float | None = None,
width: float | None = None,
@@ -67,11 +68,19 @@ class SVGMobject(VMobject):
path_string_config: dict = dict(),
**kwargs
):
self.file_name = file_name or self.file_name
if svg_string != "":
self.svg_string = svg_string
elif file_name != "":
self.svg_string = self.file_name_to_svg_string(file_name)
elif self.file_name != "":
self.svg_string = self.file_name_to_svg_string(self.file_name)
else:
raise Exception("Must specify either a file_name or svg_string SVGMobject")
self.svg_default = dict(svg_default)
self.path_string_config = dict(path_string_config)
super().__init__(**kwargs )
super().__init__(**kwargs)
self.init_svg_mobject()
self.ensure_positive_orientation()
@@ -101,7 +110,7 @@ class SVGMobject(VMobject):
if hash_val in SVG_HASH_TO_MOB_MAP:
submobs = [sm.copy() for sm in SVG_HASH_TO_MOB_MAP[hash_val]]
else:
submobs = self.mobjects_from_file(self.get_file_path())
submobs = self.mobjects_from_svg_string(self.svg_string)
SVG_HASH_TO_MOB_MAP[hash_val] = [sm.copy() for sm in submobs]
self.add(*submobs)
@@ -115,11 +124,11 @@ class SVGMobject(VMobject):
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.file_name
self.svg_string
)
def mobjects_from_file(self, file_path: str) -> list[VMobject]:
element_tree = ET.parse(file_path)
def mobjects_from_svg_string(self, svg_string: str) -> list[VMobject]:
element_tree = ET.ElementTree(ET.fromstring(svg_string))
new_tree = self.modify_xml_tree(element_tree)
# New svg based on tree contents
@@ -131,10 +140,8 @@ class SVGMobject(VMobject):
return self.mobjects_from_svg(svg)
def get_file_path(self) -> str:
if self.file_name is None:
raise Exception("Must specify file for SVGMobject")
return get_full_vector_image_path(self.file_name)
def file_name_to_svg_string(self, file_name: str) -> str:
return Path(get_full_vector_image_path(file_name)).read_text()
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
config_style_attrs = self.generate_config_style_dict()
@@ -296,8 +303,9 @@ class VMobjectFromSVGPath(VMobject):
path_obj: se.Path,
**kwargs
):
# Get rid of arcs
path_obj.approximate_arcs_with_quads()
# caches (transform.inverse(), rot, shift)
self.transform_cache: tuple[se.Matrix, np.ndarray, np.ndarray] | None = None
self.path_obj = path_obj
super().__init__(**kwargs)
@@ -324,13 +332,55 @@ class VMobjectFromSVGPath(VMobject):
}
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)
if segment_class is se.Arc:
self.handle_arc(segment)
else:
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)
# Get rid of the side effect of trailing "Z M" commands.
if self.has_new_path_started():
self.resize_points(self.get_num_points() - 2)
def handle_arc(self, arc: se.Arc) -> None:
if self.transform_cache is not None:
transform, rot, shift = self.transform_cache
else:
# The transform obtained in this way considers the combined effect
# of all parent group transforms in the SVG.
# Therefore, the arc can be transformed inversely using this transform
# to correctly compute the arc path before transforming it back.
transform = se.Matrix(self.path_obj.values.get('transform', ''))
rot = np.array([
[transform.a, transform.c],
[transform.b, transform.d]
])
shift = np.array([transform.e, transform.f, 0])
transform.inverse()
self.transform_cache = (transform, rot, shift)
# Apply inverse transformation to the arc so that its path can be correctly computed
arc *= transform
# The value of n_components is chosen based on the implementation of VMobject.arc_to
n_components = int(np.ceil(8 * abs(arc.sweep) / TAU))
# Obtain the required angular segments on the unit circle
arc_points = quadratic_bezier_points_for_arc(arc.sweep, n_components)
arc_points @= np.array(rotation_about_z(arc.get_start_t())).T
# Transform to an ellipse, considering rotation and translating the ellipse center
arc_points[:, 0] *= arc.rx
arc_points[:, 1] *= arc.ry
arc_points @= np.array(rotation_about_z(arc.get_rotation().as_radians)).T
arc_points += [*arc.center, 0]
# Transform back
arc_points[:, :2] @= rot.T
arc_points += shift
self.append_points(arc_points[1:])

View File

@@ -1,23 +1,24 @@
from __future__ import annotations
import re
from pathlib import Path
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.color import color_to_hex
from manimlib.utils.color import hex_to_int
from manimlib.utils.tex_file_writing import tex_content_to_svg_file
from manimlib.utils.tex_file_writing import latex_to_svg
from manimlib.utils.tex import num_tex_symbols
from manimlib.logger import log
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import ManimColor, Span, Selector
from manimlib.typing import ManimColor, Span, Selector, Self
SCALE_FACTOR_PER_FONT_POINT = 0.001
TEX_MOB_SCALE_FACTOR = 0.001
class Tex(StringMobject):
@@ -62,29 +63,17 @@ class Tex(StringMobject):
)
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
self.scale(TEX_MOB_SCALE_FACTOR * font_size)
@property
def hash_seed(self) -> tuple:
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.base_color,
self.isolate,
self.protect,
self.tex_string,
self.alignment,
self.tex_environment,
self.tex_to_color_map,
self.template,
self.additional_preamble
)
self.font_size = font_size # Important for this to go after the scale call
def get_file_path_by_content(self, content: str) -> str:
return tex_content_to_svg_file(
content, self.template, self.additional_preamble, self.tex_string
)
def get_svg_string_by_content(self, content: str) -> str:
return latex_to_svg(content, self.template, self.additional_preamble, short_tex=self.tex_string)
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
if hasattr(self, "font_size"):
self.font_size *= scale_factor
return self
# Parsing
@@ -253,15 +242,10 @@ class Tex(StringMobject):
decimal_mobs = []
for part in parts:
if "." in substr:
num_decimal_places = len(substr.split(".")[1])
else:
num_decimal_places = 0
decimal_mob = DecimalNumber(
float(value),
num_decimal_places=num_decimal_places,
**config,
)
if "num_decimal_places" not in config:
ndp = len(substr.split(".")[1]) if "." in substr else 0
config["num_decimal_places"] = ndp
decimal_mob = DecimalNumber(float(value), **config)
decimal_mob.replace(part)
decimal_mob.match_style(part)
if len(part) > 1:

View File

@@ -4,21 +4,22 @@ from contextlib import contextmanager
import os
from pathlib import Path
import re
import tempfile
from functools import lru_cache
import manimpango
import pygments
import pygments.formatters
import pygments.lexers
from manimlib.config import manim_config
from manimlib.constants import DEFAULT_PIXEL_WIDTH, FRAME_WIDTH
from manimlib.constants import NORMAL
from manimlib.logger import log
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.utils.customization import get_customization
from manimlib.utils.cache import cache_on_disk
from manimlib.utils.color import color_to_hex
from manimlib.utils.color import int_to_hex
from manimlib.utils.directories import get_downloads_dir
from manimlib.utils.directories import get_text_dir
from manimlib.utils.simple_functions import hash_string
from typing import TYPE_CHECKING
@@ -49,6 +50,56 @@ class _Alignment:
self.value = _Alignment.VAL_DICT[s.upper()]
@lru_cache(maxsize=128)
@cache_on_disk
def markup_to_svg(
markup_str: str,
justify: bool = False,
indent: float = 0,
alignment: str = "CENTER",
line_width: float | None = None,
) -> str:
validate_error = manimpango.MarkupUtils.validate(markup_str)
if validate_error:
raise ValueError(
f"Invalid markup string \"{markup_str}\"\n" + \
f"{validate_error}"
)
# `manimpango` is under construction,
# so the following code is intended to suit its interface
alignment = _Alignment(alignment)
if line_width is None:
pango_width = -1
else:
pango_width = line_width / FRAME_WIDTH * DEFAULT_PIXEL_WIDTH
# Write the result to a temporary svg file, and return it's contents.
temp_file = Path(tempfile.gettempdir(), hash_string(markup_str)).with_suffix(".svg")
manimpango.MarkupUtils.text2svg(
text=markup_str,
font="", # Already handled
slant="NORMAL", # Already handled
weight="NORMAL", # Already handled
size=1, # Already handled
_=0, # Empty parameter
disable_liga=False,
file_name=str(temp_file),
START_X=0,
START_Y=0,
width=DEFAULT_CANVAS_WIDTH,
height=DEFAULT_CANVAS_HEIGHT,
justify=justify,
indent=indent,
line_spacing=None, # Already handled
alignment=alignment,
pango_width=pango_width
)
result = temp_file.read_text()
os.remove(temp_file)
return result
class MarkupText(StringMobject):
# See https://docs.gtk.org/Pango/pango_markup.html
MARKUP_TAGS = {
@@ -102,13 +153,14 @@ class MarkupText(StringMobject):
isolate: Selector = re.compile(r"\w+", re.U),
**kwargs
):
text_config = manim_config.text
self.text = text
self.font_size = font_size
self.justify = justify
self.indent = indent
self.alignment = alignment or get_customization()["style"]["text_alignment"]
self.alignment = alignment or text_config.alignment
self.line_width = line_width
self.font = font or get_customization()["style"]["font"]
self.font = font or text_config.font
self.slant = slant
self.weight = weight
@@ -124,9 +176,6 @@ class MarkupText(StringMobject):
self.disable_ligatures = disable_ligatures
self.isolate = isolate
if not isinstance(self, Text):
self.validate_markup_string(text)
super().__init__(text, height=height, **kwargs)
if self.t2g:
@@ -141,88 +190,14 @@ class MarkupText(StringMobject):
if height is None:
self.scale(TEXT_MOB_SCALE_FACTOR)
@property
def hash_seed(self) -> tuple:
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.base_color,
self.isolate,
self.protect,
self.text,
self.font_size,
self.lsh,
self.justify,
self.indent,
self.alignment,
self.line_width,
self.font,
self.slant,
self.weight,
self.t2c,
self.t2f,
self.t2s,
self.t2w,
self.global_config,
self.local_configs,
self.disable_ligatures
)
def get_file_path_by_content(self, content: str) -> str:
hash_content = str((
def get_svg_string_by_content(self, content: str) -> str:
self.content = content
return markup_to_svg(
content,
self.justify,
self.indent,
self.alignment,
self.line_width
))
svg_file = os.path.join(
get_text_dir(), hash_string(hash_content) + ".svg"
)
if not os.path.exists(svg_file):
self.markup_to_svg(content, svg_file)
return svg_file
def markup_to_svg(self, markup_str: str, file_name: str) -> str:
self.validate_markup_string(markup_str)
# `manimpango` is under construction,
# so the following code is intended to suit its interface
alignment = _Alignment(self.alignment)
if self.line_width is None:
pango_width = -1
else:
pango_width = self.line_width / FRAME_WIDTH * DEFAULT_PIXEL_WIDTH
return manimpango.MarkupUtils.text2svg(
text=markup_str,
font="", # Already handled
slant="NORMAL", # Already handled
weight="NORMAL", # Already handled
size=1, # Already handled
_=0, # Empty parameter
disable_liga=False,
file_name=file_name,
START_X=0,
START_Y=0,
width=DEFAULT_CANVAS_WIDTH,
height=DEFAULT_CANVAS_HEIGHT,
justify=self.justify,
indent=self.indent,
line_spacing=None, # Already handled
alignment=alignment,
pango_width=pango_width
)
@staticmethod
def validate_markup_string(markup_str: str) -> None:
validate_error = manimpango.MarkupUtils.validate(markup_str)
if not validate_error:
return
raise ValueError(
f"Invalid markup string \"{markup_str}\"\n" + \
f"{validate_error}"
alignment=self.alignment,
line_width=self.line_width
)
# Toolkits
@@ -511,20 +486,10 @@ def register_font(font_file: str | Path):
method with previous releases will raise an :class:`AttributeError` on macOS.
"""
input_folder = Path(get_downloads_dir()).parent.resolve()
possible_paths = [
Path(font_file),
input_folder / font_file,
]
for path in possible_paths:
path = path.resolve()
if path.exists():
file_path = path
break
else:
error = f"Can't find {font_file}." f"Tried these : {possible_paths}"
file_path = Path(font_file).resolve()
if not file_path.exists():
error = f"Can't find {font_file}."
raise FileNotFoundError(error)
try:
assert manimpango.register_font(str(file_path))
yield

View File

@@ -94,23 +94,30 @@ class Sphere(Surface):
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
v_range: Tuple[float, float] = (1e-5, PI - 1e-5),
v_range: Tuple[float, float] = (0, PI),
resolution: Tuple[int, int] = (101, 51),
radius: float = 1.0,
true_normals: bool = True,
clockwise=False,
**kwargs,
):
self.radius = radius
self.clockwise = clockwise
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs
)
# Add bespoke normal specification to avoid issue at poles
if true_normals:
self.data['d_normal_point'] = self.data['point'] * ((radius + self.normal_nudge) / radius)
def uv_func(self, u: float, v: float) -> np.ndarray:
sign = -1 if self.clockwise else +1
return self.radius * np.array([
math.cos(u) * math.sin(v),
math.sin(u) * math.sin(v),
math.cos(sign * u) * math.sin(v),
math.sin(sign * u) * math.sin(v),
-math.cos(v)
])
@@ -158,7 +165,6 @@ class Cylinder(Surface):
**kwargs
)
def init_points(self):
super().init_points()
self.scale(self.radius)
@@ -169,6 +175,20 @@ class Cylinder(Surface):
return np.array([np.cos(u), np.sin(u), v])
class Cone(Cylinder):
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
v_range: Tuple[float, float] = (0, 1),
*args,
**kwargs,
):
super().__init__(u_range=u_range, v_range=v_range, *args, **kwargs)
def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([(1 - v) * np.cos(u), (1 - v) * np.sin(u), v])
class Line3D(Cylinder):
def __init__(
self,

View File

@@ -8,9 +8,11 @@ from manimlib.constants import OUT
from manimlib.mobject.mobject import Mobject
from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.bezier import interpolate
from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.images import get_full_raster_image_path
from manimlib.utils.iterables import listify
from manimlib.utils.iterables import resize_with_interpolation
from manimlib.utils.simple_functions import clip
from manimlib.utils.space_ops import normalize_along_axis
from manimlib.utils.space_ops import cross
@@ -28,11 +30,10 @@ class Surface(Mobject):
shader_folder: str = "surface"
data_dtype: np.dtype = np.dtype([
('point', np.float32, (3,)),
('du_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('d_normal_point', np.float32, (3,)),
('rgba', np.float32, (4,)),
])
pointlike_data_keys = ['point', 'du_point', 'dv_point']
pointlike_data_keys = ['point', 'd_normal_point']
def __init__(
self,
@@ -46,9 +47,11 @@ class Surface(Mobject):
# rows/columns of approximating squares
resolution: Tuple[int, int] = (101, 101),
prefered_creation_axis: int = 1,
# For du and dv steps. Much smaller and numerical error
# can crop up in the shaders.
epsilon: float = 1e-4,
# For du and dv steps.
epsilon: float = 1e-3,
# Step off the surface to a new point which will
# be used to determine the normal direction
normal_nudge: float = 1e-3,
**kwargs
):
self.u_range = u_range
@@ -56,6 +59,7 @@ class Surface(Mobject):
self.resolution = resolution
self.prefered_creation_axis = prefered_creation_axis
self.epsilon = epsilon
self.normal_nudge = normal_nudge
super().__init__(
**kwargs,
@@ -71,16 +75,12 @@ class Surface(Mobject):
@Mobject.affects_data
def init_points(self):
dim = self.dim
nu, nv = self.resolution
u_range = np.linspace(*self.u_range, nu)
v_range = np.linspace(*self.v_range, nv)
# Get three lists:
# - Points generated by pure uv values
# - Those generated by values nudged by du
# - Those generated by values nudged by dv
uv_grid = np.array([[[u, v] for v in v_range] for u in u_range])
nu, nv = self.resolution
uv_grid = self.get_uv_grid()
uv_plus_du = uv_grid.copy()
uv_plus_du[:, :, 0] += self.epsilon
uv_plus_dv = uv_grid.copy()
@@ -89,12 +89,51 @@ class Surface(Mobject):
points, du_points, dv_points = [
np.apply_along_axis(
lambda p: self.uv_func(*p), 2, grid
).reshape((nu * nv, dim))
).reshape((nu * nv, self.dim))
for grid in (uv_grid, uv_plus_du, uv_plus_dv)
]
crosses = cross(du_points - points, dv_points - points)
normals = normalize_along_axis(crosses, 1)
self.set_points(points)
self.data['du_point'][:] = du_points
self.data['dv_point'][:] = dv_points
self.data['d_normal_point'] = points + self.normal_nudge * normals
def get_uv_grid(self) -> np.array:
"""
Returns an (nu, nv, 2) array of all pairs of u, v values, where
(nu, nv) is the resolution
"""
nu, nv = self.resolution
u_range = np.linspace(*self.u_range, nu)
v_range = np.linspace(*self.v_range, nv)
U, V = np.meshgrid(u_range, v_range, indexing='ij')
return np.stack([U, V], axis=-1)
def uv_to_point(self, u, v):
nu, nv = self.resolution
verts_by_uv = np.reshape(self.get_points(), (nu, nv, self.dim))
alpha1 = clip(inverse_interpolate(*self.u_range[:2], u), 0, 1)
alpha2 = clip(inverse_interpolate(*self.v_range[:2], v), 0, 1)
scaled_u = alpha1 * (nu - 1)
scaled_v = alpha2 * (nv - 1)
u_int = int(scaled_u)
v_int = int(scaled_v)
u_int_plus = min(u_int + 1, nu - 1)
v_int_plus = min(v_int + 1, nv - 1)
a = verts_by_uv[u_int, v_int, :]
b = verts_by_uv[u_int, v_int_plus, :]
c = verts_by_uv[u_int_plus, v_int, :]
d = verts_by_uv[u_int_plus, v_int_plus, :]
u_res = scaled_u % 1
v_res = scaled_v % 1
return interpolate(
interpolate(a, b, v_res),
interpolate(c, d, v_res),
u_res
)
def apply_points_function(self, *args, **kwargs) -> Self:
super().apply_points_function(*args, **kwargs)
@@ -124,12 +163,8 @@ class Surface(Mobject):
return self.triangle_indices
def get_unit_normals(self) -> Vect3Array:
points = self.get_points()
crosses = cross(
self.data['du_point'] - points,
self.data['dv_point'] - points,
)
return normalize_along_axis(crosses, 1)
# TOOD, I could try a more resiliant way to compute this using the neighboring grid values
return normalize_along_axis(self.data['d_normal_point'] - self.data['point'], 1)
@Mobject.affects_data
def pointwise_become_partial(
@@ -212,6 +247,14 @@ class Surface(Mobject):
self.add_updater(updater)
return self
def color_by_uv_function(self, uv_to_color: Callable[[Vect2], Color]):
uv_grid = self.get_uv_grid()
self.set_rgba_array_by_color([
uv_to_color(u, v)
for u, v in uv_grid.reshape(-1, 2)
])
return self
def get_shader_vert_indices(self) -> np.ndarray:
return self.get_triangle_indices()
@@ -248,8 +291,7 @@ class TexturedSurface(Surface):
shader_folder: str = "textured_surface"
data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
('point', np.float32, (3,)),
('du_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('d_normal_point', np.float32, (3,)),
('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)),
]
@@ -293,8 +335,7 @@ class TexturedSurface(Surface):
self.resize_points(surf.get_num_points())
self.resolution = surf.resolution
self.data['point'][:] = surf.data['point']
self.data['du_point'][:] = surf.data['du_point']
self.data['dv_point'][:] = surf.data['dv_point']
self.data['d_normal_point'][:] = surf.data['d_normal_point']
self.data['opacity'][:, 0] = surf.data["rgba"][:, 3]
self.data["im_coords"] = np.array([
[u, v]
@@ -307,7 +348,7 @@ class TexturedSurface(Surface):
self.uniforms["num_textures"] = self.num_textures
@Mobject.affects_data
def set_opacity(self, opacity: float | Iterable[float]) -> Self:
def set_opacity(self, opacity: float | Iterable[float], recurse=True) -> Self:
op_arr = np.array(listify(opacity))
self.data["opacity"][:, 0] = resize_with_interpolation(op_arr, len(self.data))
return self

View File

@@ -2,16 +2,13 @@ from __future__ import annotations
from functools import wraps
import moderngl
import numpy as np
import operator as op
import itertools as it
from manimlib.constants import GREY_A, GREY_C, GREY_E
from manimlib.constants import DEFAULT_VMOBJECT_FILL_COLOR, DEFAULT_VMOBJECT_STROKE_COLOR
from manimlib.constants import BLACK
from manimlib.constants import DEFAULT_STROKE_WIDTH
from manimlib.constants import DEGREES
from manimlib.constants import JOINT_TYPE_MAP
from manimlib.constants import DEG
from manimlib.constants import ORIGIN, OUT
from manimlib.constants import PI
from manimlib.constants import TAU
@@ -35,10 +32,7 @@ from manimlib.utils.iterables import make_even
from manimlib.utils.iterables import resize_array
from manimlib.utils.iterables import resize_with_interpolation
from manimlib.utils.iterables import resize_preserving_order
from manimlib.utils.iterables import arrays_match
from manimlib.utils.simple_functions import fdiv
from manimlib.utils.space_ops import angle_between_vectors
from manimlib.utils.space_ops import cross
from manimlib.utils.space_ops import cross2d
from manimlib.utils.space_ops import earclip_triangulation
from manimlib.utils.space_ops import get_norm
@@ -49,7 +43,6 @@ from manimlib.utils.space_ops import rotation_between_vectors
from manimlib.utils.space_ops import rotation_matrix_transpose
from manimlib.utils.space_ops import poly_line_length
from manimlib.utils.space_ops import z_to_vector
from manimlib.shader_wrapper import ShaderWrapper
from manimlib.shader_wrapper import VShaderWrapper
from typing import TYPE_CHECKING
@@ -57,13 +50,10 @@ from typing import Generic, TypeVar, Iterable
SubVmobjectType = TypeVar('SubVmobjectType', bound='VMobject')
if TYPE_CHECKING:
from typing import Callable, Tuple, Any
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array, Self
from typing import Callable, Tuple, Any, Optional
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Self
from moderngl.context import Context
DEFAULT_STROKE_COLOR = GREY_A
DEFAULT_FILL_COLOR = GREY_C
class VMobject(Mobject):
data_dtype: np.dtype = np.dtype([
@@ -79,6 +69,12 @@ class VMobject(Mobject):
make_smooth_after_applying_functions: bool = False
# TODO, do we care about accounting for varying zoom levels?
tolerance_for_point_equality: float = 1e-8
joint_type_map: dict = {
"no_joint": 0,
"auto": 1,
"bevel": 2,
"miter": 3,
}
def __init__(
self,
@@ -94,15 +90,16 @@ class VMobject(Mobject):
# Could also be "no_joint", "bevel", "miter"
joint_type: str = "auto",
flat_stroke: bool = False,
scale_stroke_with_zoom: bool = False,
use_simple_quadratic_approx: bool = False,
# Measured in pixel widths
anti_alias_width: float = 1.5,
fill_border_width: float = 0.0,
**kwargs
):
self.fill_color = fill_color or color or DEFAULT_FILL_COLOR
self.fill_color = fill_color or color or DEFAULT_VMOBJECT_FILL_COLOR
self.fill_opacity = fill_opacity
self.stroke_color = stroke_color or color or DEFAULT_STROKE_COLOR
self.stroke_color = stroke_color or color or DEFAULT_VMOBJECT_STROKE_COLOR
self.stroke_opacity = stroke_opacity
self.stroke_width = stroke_width
self.stroke_behind = stroke_behind
@@ -110,6 +107,7 @@ class VMobject(Mobject):
self.long_lines = long_lines
self.joint_type = joint_type
self.flat_stroke = flat_stroke
self.scale_stroke_with_zoom = scale_stroke_with_zoom
self.use_simple_quadratic_approx = use_simple_quadratic_approx
self.anti_alias_width = anti_alias_width
self.fill_border_width = fill_border_width
@@ -126,9 +124,12 @@ class VMobject(Mobject):
def init_uniforms(self):
super().init_uniforms()
self.uniforms["anti_alias_width"] = self.anti_alias_width
self.uniforms["joint_type"] = JOINT_TYPE_MAP[self.joint_type]
self.uniforms["flat_stroke"] = float(self.flat_stroke)
self.uniforms.update(
anti_alias_width=self.anti_alias_width,
joint_type=self.joint_type_map[self.joint_type],
flat_stroke=float(self.flat_stroke),
scale_stroke_with_zoom=float(self.scale_stroke_with_zoom)
)
def add(self, *vmobjects: VMobject) -> Self:
if not all((isinstance(m, VMobject) for m in vmobjects)):
@@ -182,7 +183,7 @@ class VMobject(Mobject):
if width is not None:
for mob in self.get_family(recurse):
data = mob.data if mob.get_num_points() > 0 else mob._data_defaults
if isinstance(width, (float, int)):
if isinstance(width, (float, int, np.floating)):
data['stroke_width'][:, 0] = width
else:
data['stroke_width'][:, 0] = resize_with_interpolation(
@@ -302,6 +303,11 @@ class VMobject(Mobject):
self.set_stroke(opacity=opacity, recurse=recurse)
return self
def set_color_by_proportion(self, prop_to_color: Callable[[float], Color]) -> Self:
colors = list(map(prop_to_color, np.linspace(0, 1, self.get_num_points())))
self.set_stroke(color=colors)
return self
def set_anti_alias_width(self, anti_alias_width: float, recurse: bool = True) -> Self:
self.set_uniform(recurse, anti_alias_width=anti_alias_width)
return self
@@ -399,9 +405,16 @@ class VMobject(Mobject):
def get_flat_stroke(self) -> bool:
return self.uniforms["flat_stroke"] == 1.0
def set_scale_stroke_with_zoom(self, scale_stroke_with_zoom: bool = True, recurse: bool = True) -> Self:
self.set_uniform(recurse, scale_stroke_with_zoom=float(scale_stroke_with_zoom))
pass
def get_scale_stroke_with_zoom(self) -> bool:
return self.uniforms["flat_stroke"] == 1.0
def set_joint_type(self, joint_type: str, recurse: bool = True) -> Self:
for mob in self.get_family(recurse):
mob.uniforms["joint_type"] = JOINT_TYPE_MAP[joint_type]
mob.uniforms["joint_type"] = self.joint_type_map[joint_type]
return self
def get_joint_type(self) -> float:
@@ -480,7 +493,7 @@ class VMobject(Mobject):
v1 = handle1 - last
v2 = anchor - handle2
angle = angle_between_vectors(v1, v2)
if self.use_simple_quadratic_approx and angle < 45 * DEGREES:
if self.use_simple_quadratic_approx and angle < 45 * DEG:
quad_approx = [last, find_intersection(last, v1, anchor, -v2), anchor]
else:
quad_approx = get_quadratic_approximation_of_cubic(
@@ -606,7 +619,7 @@ class VMobject(Mobject):
def subdivide_sharp_curves(
self,
angle_threshold: float = 30 * DEGREES,
angle_threshold: float = 30 * DEG,
recurse: bool = True
) -> Self:
def tuple_to_subdivisions(b0, b1, b2):
@@ -646,7 +659,7 @@ class VMobject(Mobject):
self.make_smooth(approx=approx)
return self
def is_smooth(self, angle_tol=1 * DEGREES) -> bool:
def is_smooth(self, angle_tol=1 * DEG) -> bool:
angles = np.abs(self.get_joint_angles()[0::2])
return (angles < angle_tol).all()

View File

@@ -3,29 +3,26 @@ from __future__ import annotations
import itertools as it
import numpy as np
from scipy.integrate import solve_ivp
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import BLUE, WHITE
from manimlib.constants import ORIGIN
from manimlib.constants import DEFAULT_MOBJECT_COLOR
from manimlib.animation.indication import VShowPassingFlash
from manimlib.mobject.geometry import Arrow
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import interpolate
from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.color import get_colormap_list
from manimlib.utils.color import rgb_to_color
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.color import get_color_map
from manimlib.utils.iterables import cartesian_product
from manimlib.utils.rate_functions import linear
from manimlib.utils.simple_functions import sigmoid
from manimlib.utils.space_ops import get_norm
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, Sequence, TypeVar, Tuple
from manimlib.typing import ManimColor, Vect3, VectN, Vect3Array
from typing import Callable, Iterable, Sequence, TypeVar, Tuple, Optional
from manimlib.typing import ManimColor, Vect3, VectN, VectArray, Vect3Array, Vect4Array
from manimlib.mobject.coordinate_systems import CoordinateSystem
from manimlib.mobject.mobject import Mobject
@@ -33,6 +30,7 @@ if TYPE_CHECKING:
T = TypeVar("T")
#### Delete these two ###
def get_vectorized_rgb_gradient_function(
min_value: T,
max_value: T,
@@ -52,6 +50,7 @@ def get_vectorized_rgb_gradient_function(
inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3))
result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas)
return result
return func
@@ -62,6 +61,17 @@ def get_rgb_gradient_function(
) -> Callable[[float], Vect3]:
vectorized_func = get_vectorized_rgb_gradient_function(min_value, max_value, color_map)
return lambda value: vectorized_func(np.array([value]))[0]
####
def ode_solution_points(function, state0, time, dt=0.01):
solution = solve_ivp(
lambda t, state: function(state),
t_span=(0, time),
y0=state0,
t_eval=np.arange(0, time, dt)
)
return solution.y.T
def move_along_vector_field(
@@ -98,24 +108,31 @@ def move_points_along_vector_field(
cs = coordinate_system
origin = cs.get_origin()
def apply_nudge(self, dt):
mobject.apply_function(
def apply_nudge(mob, dt):
mob.apply_function(
lambda p: p + (cs.c2p(*func(*cs.p2c(p))) - origin) * dt
)
mobject.add_updater(apply_nudge)
return mobject
def get_sample_points_from_coordinate_system(
def get_sample_coords(
coordinate_system: CoordinateSystem,
step_multiple: float
density: float = 1.0
) -> it.product[tuple[Vect3, ...]]:
ranges = []
for range_args in coordinate_system.get_all_ranges():
_min, _max, step = range_args
step *= step_multiple
step /= density
ranges.append(np.arange(_min, _max + step, step))
return it.product(*ranges)
return np.array(list(it.product(*ranges)))
def vectorize(pointwise_function: Callable[[Tuple], Tuple]):
def v_func(coords_array: VectArray) -> VectArray:
return np.array([pointwise_function(*coords) for coords in coords_array])
return v_func
# Mobjects
@@ -124,65 +141,73 @@ def get_sample_points_from_coordinate_system(
class VectorField(VMobject):
def __init__(
self,
func,
stroke_color: ManimColor = BLUE,
# Vectorized function: Takes in an array of coordinates, returns an array of outputs.
func: Callable[[VectArray], VectArray],
# Typically a set of Axes or NumberPlane
coordinate_system: CoordinateSystem,
sample_coords: Optional[VectArray] = None,
density: float = 2.0,
magnitude_range: Optional[Tuple[float, float]] = None,
color: Optional[ManimColor] = None,
color_map_name: Optional[str] = "3b1b_colormap",
color_map: Optional[Callable[[Sequence[float]], Vect4Array]] = None,
stroke_opacity: float = 1.0,
center: Vect3 = ORIGIN,
sample_points: Optional[Vect3Array] = None,
x_density: float = 2.0,
y_density: float = 2.0,
z_density: float = 2.0,
width: float = 14.0,
height: float = 8.0,
depth: float = 0.0,
stroke_width: float = 2,
stroke_width: float = 3,
tip_width_ratio: float = 4,
tip_len_to_width: float = 0.01,
max_vect_len: float | None = None,
min_drawn_norm: float = 1e-2,
max_vect_len_to_step_size: float = 0.8,
flat_stroke: bool = False,
norm_to_opacity_func=None,
norm_to_rgb_func=None,
norm_to_opacity_func=None, # TODO, check on this
**kwargs
):
self.func = func
self.coordinate_system = coordinate_system
self.stroke_width = stroke_width
self.tip_width_ratio = tip_width_ratio
self.tip_len_to_width = tip_len_to_width
self.min_drawn_norm = min_drawn_norm
self.norm_to_opacity_func = norm_to_opacity_func
self.norm_to_rgb_func = norm_to_rgb_func
if max_vect_len is not None:
self.max_vect_len = max_vect_len
# Search for sample_points
if sample_coords is not None:
self.sample_coords = sample_coords
else:
densities = np.array([x_density, y_density, z_density])
dims = np.array([width, height, depth])
self.max_vect_len = 1.0 / densities[dims > 0].mean()
self.sample_coords = get_sample_coords(coordinate_system, density)
self.update_sample_points()
if sample_points is None:
self.sample_points = self.get_sample_points(
center, width, height, depth,
x_density, y_density, z_density
)
if max_vect_len is None:
step_size = get_norm(self.sample_points[1] - self.sample_points[0])
self.max_displayed_vect_len = max_vect_len_to_step_size * step_size
else:
self.sample_points = sample_points
self.max_displayed_vect_len = max_vect_len * coordinate_system.x_axis.get_unit_size()
self.init_base_stroke_width_array(len(self.sample_points))
# Prepare the color map
if magnitude_range is None:
max_value = max(map(get_norm, func(self.sample_coords)))
magnitude_range = (0, max_value)
self.magnitude_range = magnitude_range
if color is not None:
self.color_map = None
else:
self.color_map = color_map or get_color_map(color_map_name)
self.init_base_stroke_width_array(len(self.sample_coords))
super().__init__(
stroke_color=stroke_color,
stroke_opacity=stroke_opacity,
flat_stroke=flat_stroke,
**kwargs
)
n_samples = len(self.sample_points)
self.set_points(np.zeros((8 * n_samples - 1, 3)))
self.set_stroke(width=stroke_width)
self.set_joint_type('no_joint')
self.set_stroke(color, stroke_width)
self.update_vectors()
def init_points(self):
n_samples = len(self.sample_coords)
self.set_points(np.zeros((8 * n_samples - 1, 3)))
self.set_joint_type('no_joint')
def get_sample_points(
self,
center: np.ndarray,
@@ -211,8 +236,8 @@ class VectorField(VMobject):
arr[7::8] = 0
self.base_stroke_width_array = arr
def set_sample_points(self, sample_points: Vect3Array):
self.sample_points = sample_points
def set_sample_coords(self, sample_coords: VectArray):
self.sample_coords = sample_coords
return self
def set_stroke(self, color=None, width=None, opacity=None, behind=None, flat=None, recurse=True):
@@ -227,35 +252,40 @@ class VectorField(VMobject):
self.stroke_width = width
return self
def update_sample_points(self):
self.sample_points = self.coordinate_system.c2p(*self.sample_coords.T)
def update_vectors(self):
tip_width = self.tip_width_ratio * self.stroke_width
tip_len = self.tip_len_to_width * tip_width
samples = self.sample_points
# Get raw outputs and lengths
outputs = self.func(samples)
norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis]
# Outputs in the coordinate system
outputs = self.func(self.sample_coords)
output_norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis]
# How long should the arrows be drawn?
max_len = self.max_vect_len
# Corresponding vector values in global coordinates
out_vects = self.coordinate_system.c2p(*outputs.T) - self.coordinate_system.get_origin()
out_vect_norms = np.linalg.norm(out_vects, axis=1)[:, np.newaxis]
unit_outputs = np.zeros_like(out_vects)
np.true_divide(out_vects, out_vect_norms, out=unit_outputs, where=(out_vect_norms > 0))
# How long should the arrows be drawn, in global coordinates
max_len = self.max_displayed_vect_len
if max_len < np.inf:
drawn_norms = max_len * np.tanh(norms / max_len)
drawn_norms = max_len * np.tanh(out_vect_norms / max_len)
else:
drawn_norms = norms
drawn_norms = out_vect_norms
# What's the distance from the base of an arrow to
# the base of its head?
dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf)
dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf) # Mixing units!
# Set all points
unit_outputs = np.zeros_like(outputs)
np.true_divide(outputs, norms, out=unit_outputs, where=(norms > self.min_drawn_norm))
points = self.get_points()
points[0::8] = samples
points[2::8] = samples + dist_to_head_base * unit_outputs
points[0::8] = self.sample_points
points[2::8] = self.sample_points + dist_to_head_base * unit_outputs
points[4::8] = points[2::8]
points[6::8] = samples + drawn_norms * unit_outputs
points[6::8] = self.sample_points + drawn_norms * unit_outputs
for i in (1, 3, 5):
points[i::8] = 0.5 * (points[i - 1::8] + points[i + 1::8])
points[7::8] = points[6:-1:8]
@@ -267,14 +297,16 @@ class VectorField(VMobject):
self.get_stroke_widths()[:] = width_scalars * width_arr
# Potentially adjust opacity and color
if self.color_map is not None:
self.get_stroke_colors() # Ensures the array is updated to appropriate length
low, high = self.magnitude_range
self.data['stroke_rgba'][:, :3] = self.color_map(
inverse_interpolate(low, high, np.repeat(output_norms, 8)[:-1])
)[:, :3]
if self.norm_to_opacity_func is not None:
self.get_stroke_opacities()[:] = self.norm_to_opacity_func(
np.repeat(norms, 8)[:-1]
)
if self.norm_to_rgb_func is not None:
self.get_stroke_colors()
self.data['stroke_rgba'][:, :3] = self.norm_to_rgb_func(
np.repeat(norms, 8)[:-1]
np.repeat(output_norms, 8)[:-1]
)
self.note_changed_data()
@@ -285,90 +317,33 @@ class TimeVaryingVectorField(VectorField):
def __init__(
self,
# Takes in an array of points and a float for time
time_func,
time_func: Callable[[VectArray, float], VectArray],
coordinate_system: CoordinateSystem,
**kwargs
):
self.time = 0
super().__init__(func=lambda p: time_func(p, self.time), **kwargs)
def func(coords):
return time_func(coords, self.time)
super().__init__(func, coordinate_system, **kwargs)
self.add_updater(lambda m, dt: m.increment_time(dt))
always(self.update_vectors)
self.always.update_vectors()
def increment_time(self, dt):
self.time += dt
class OldVectorField(VGroup):
def __init__(
self,
func: Callable[[float, float], Sequence[float]],
coordinate_system: CoordinateSystem,
step_multiple: float = 0.5,
magnitude_range: Tuple[float, float] = (0, 2),
color_map: str = "3b1b_colormap",
# Takes in actual norm, spits out displayed norm
length_func: Callable[[float], float] = lambda norm: 0.45 * sigmoid(norm),
opacity: float = 1.0,
vector_config: dict = dict(),
**kwargs
):
super().__init__(**kwargs)
self.func = func
self.coordinate_system = coordinate_system
self.step_multiple = step_multiple
self.magnitude_range = magnitude_range
self.color_map = color_map
self.length_func = length_func
self.opacity = opacity
self.vector_config = dict(vector_config)
self.value_to_rgb = get_rgb_gradient_function(
*self.magnitude_range, self.color_map,
)
samples = get_sample_points_from_coordinate_system(
coordinate_system, self.step_multiple
)
self.add(*(
self.get_vector(coords)
for coords in samples
))
def get_vector(self, coords: Iterable[float], **kwargs) -> Arrow:
vector_config = merge_dicts_recursively(
self.vector_config,
kwargs
)
output = np.array(self.func(*coords))
norm = get_norm(output)
if norm > 0:
output *= self.length_func(norm) / norm
origin = self.coordinate_system.get_origin()
_input = self.coordinate_system.c2p(*coords)
_output = self.coordinate_system.c2p(*output)
vect = Arrow(
origin, _output, buff=0,
**vector_config
)
vect.shift(_input - origin)
vect.set_color(
rgb_to_color(self.value_to_rgb(norm)),
opacity=self.opacity,
)
return vect
class StreamLines(VGroup):
def __init__(
self,
func: Callable[[float, float], Sequence[float]],
func: Callable[[VectArray], VectArray],
coordinate_system: CoordinateSystem,
step_multiple: float = 0.5,
density: float = 1.0,
n_repeats: int = 1,
noise_factor: float | None = None,
# Config for drawing lines
solution_time: float = 3,
dt: float = 0.05,
arc_len: float = 3,
max_time_steps: int = 200,
@@ -376,7 +351,7 @@ class StreamLines(VGroup):
cutoff_norm: float = 15,
# Style info
stroke_width: float = 1.0,
stroke_color: ManimColor = WHITE,
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
stroke_opacity: float = 1,
color_by_magnitude: bool = True,
magnitude_range: Tuple[float, float] = (0, 2.0),
@@ -387,9 +362,10 @@ class StreamLines(VGroup):
super().__init__(**kwargs)
self.func = func
self.coordinate_system = coordinate_system
self.step_multiple = step_multiple
self.density = density
self.n_repeats = n_repeats
self.noise_factor = noise_factor
self.solution_time = solution_time
self.dt = dt
self.arc_len = arc_len
self.max_time_steps = max_time_steps
@@ -406,48 +382,37 @@ class StreamLines(VGroup):
self.draw_lines()
self.init_style()
def point_func(self, point: Vect3) -> Vect3:
in_coords = self.coordinate_system.p2c(point)
out_coords = self.func(*in_coords)
return self.coordinate_system.c2p(*out_coords)
def point_func(self, points: Vect3Array) -> Vect3:
in_coords = np.array(self.coordinate_system.p2c(points)).T
out_coords = self.func(in_coords)
origin = self.coordinate_system.get_origin()
return self.coordinate_system.c2p(*out_coords.T) - origin
def draw_lines(self) -> None:
lines = []
origin = self.coordinate_system.get_origin()
for point in self.get_start_points():
points = [point]
total_arc_len = 0
time = 0
for x in range(self.max_time_steps):
time += self.dt
last_point = points[-1]
new_point = last_point + self.dt * (self.point_func(last_point) - origin)
points.append(new_point)
total_arc_len += get_norm(new_point - last_point)
if get_norm(last_point) > self.cutoff_norm:
break
if total_arc_len > self.arc_len:
break
# Todo, it feels like coordinate system should just have
# the ODE solver built into it, no?
lines = []
for coords in self.get_sample_coords():
solution_coords = ode_solution_points(self.func, coords, self.solution_time, self.dt)
line = VMobject()
line.virtual_time = time
step = max(1, int(len(points) / self.n_samples_per_line))
line.set_points_as_corners(points[::step])
line.make_smooth(approx=True)
line.set_points_smoothly(self.coordinate_system.c2p(*solution_coords.T))
# TODO, account for arc length somehow?
line.virtual_time = self.solution_time
lines.append(line)
self.set_submobjects(lines)
def get_start_points(self) -> Vect3Array:
def get_sample_coords(self):
cs = self.coordinate_system
sample_coords = get_sample_points_from_coordinate_system(
cs, self.step_multiple,
)
sample_coords = get_sample_coords(cs, self.density)
noise_factor = self.noise_factor
if noise_factor is None:
noise_factor = cs.x_range[2] * self.step_multiple * 0.5
noise_factor = (cs.x_axis.get_unit_size() / self.density) * 0.5
return np.array([
cs.c2p(*coords) + noise_factor * np.random.random(3)
coords + noise_factor * np.random.random(coords.shape)
for n in range(self.n_repeats)
for coords in sample_coords
])
@@ -460,7 +425,7 @@ class StreamLines(VGroup):
cs = self.coordinate_system
for line in self.submobjects:
norms = [
get_norm(self.func(*cs.p2c(point)))
get_norm(self.func(cs.p2c(point)))
for point in line.get_points()
]
rgbs = values_to_rgbs(norms)
@@ -483,6 +448,7 @@ class AnimatedStreamLines(VGroup):
self,
stream_lines: StreamLines,
lag_range: float = 4,
rate_multiple: float = 1.0,
line_anim_config: dict = dict(
rate_func=linear,
time_width=1.0,
@@ -495,7 +461,7 @@ class AnimatedStreamLines(VGroup):
for line in stream_lines:
line.anim = VShowPassingFlash(
line,
run_time=line.virtual_time,
run_time=line.virtual_time / rate_multiple,
**line_anim_config,
)
line.anim.begin()
@@ -504,7 +470,7 @@ class AnimatedStreamLines(VGroup):
self.add_updater(lambda m, dt: m.update(dt))
def update(self, dt: float) -> None:
def update(self, dt: float = 0) -> None:
stream_lines = self.stream_lines
for line in stream_lines:
line.time += dt

176
manimlib/module_loader.py Normal file
View File

@@ -0,0 +1,176 @@
from __future__ import annotations
import builtins
import importlib
import os
import sys
import sysconfig
from manimlib.config import manim_config
from manimlib.logger import log
Module = importlib.util.types.ModuleType
class ModuleLoader:
"""
Utility class to load a module from a file and handle its imports.
Most parts of this class are only needed for the reload functionality,
while the `get_module` method is the main entry point to import a module.
"""
@staticmethod
def get_module(file_name: str | None, is_during_reload=False) -> Module | None:
"""
Imports a module from a file and returns it.
During reload (when the user calls `reload()` in the IPython shell), we
also track the imported modules and reload them as well (they would be
cached otherwise). See the reload_manager where the reload parameter is set.
Note that `exec_module()` is called twice when reloading a module:
1. In exec_module_and_track_imports to track the imports
2. Here to actually execute the module again with the respective
imported modules reloaded.
"""
if file_name is None:
return None
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)
if is_during_reload:
imported_modules = ModuleLoader._exec_module_and_track_imports(spec, module)
reloaded_modules_tracker = set()
ModuleLoader._reload_modules(imported_modules, reloaded_modules_tracker)
spec.loader.exec_module(module)
return module
@staticmethod
def _exec_module_and_track_imports(spec, module: Module) -> set[str]:
"""
Executes the given module (imports it) and returns all the modules that
are imported during its execution.
This is achieved by replacing the __import__ function with a custom one
that tracks the imported modules. At the end, the original __import__
built-in function is restored.
"""
imported_modules: set[str] = set()
original_import = builtins.__import__
def tracked_import(name, globals=None, locals=None, fromlist=(), level=0):
"""
Custom __import__ function that does exactly the same as the original
one, but also tracks the imported modules by means of adding their
names to a set.
"""
result = original_import(name, globals, locals, fromlist, level)
imported_modules.add(name)
return result
builtins.__import__ = tracked_import
try:
module_name = module.__name__
log.debug('Reloading module "%s"', module_name)
spec.loader.exec_module(module)
finally:
builtins.__import__ = original_import
return imported_modules
@staticmethod
def _reload_modules(modules: set[str], reloaded_modules_tracker: set[str]):
"""
Out of the given modules, reloads the ones that were not already imported.
We skip modules that are not user-defined (see `is_user_defined_module()`).
"""
for mod in modules:
if mod in reloaded_modules_tracker:
continue
if not ModuleLoader._is_user_defined_module(mod):
continue
module = sys.modules[mod]
ModuleLoader._deep_reload(module, reloaded_modules_tracker)
reloaded_modules_tracker.add(mod)
@staticmethod
def _is_user_defined_module(mod: str) -> bool:
"""
Returns whether the given module is user-defined or not.
A module is considered user-defined if
- it is not part of the standard library
- AND it is not an external library (site-packages or dist-packages)
"""
if mod not in sys.modules:
return False
if mod in sys.builtin_module_names:
return False
module = sys.modules[mod]
module_path = getattr(module, "__file__", None)
if module_path is None:
return False
module_path = os.path.abspath(module_path)
# External libraries (site-packages or dist-packages), e.g. numpy
if "site-packages" in module_path or "dist-packages" in module_path:
return False
# Standard lib
standard_lib_path = sysconfig.get_path("stdlib")
if module_path.startswith(standard_lib_path):
return False
return True
@staticmethod
def _deep_reload(module: Module, reloaded_modules_tracker: set[str]):
"""
Recursively reloads modules imported by the given module.
Only user-defined modules are reloaded, see `is_user_defined_module()`.
"""
ignore_manimlib_modules = manim_config.ignore_manimlib_modules_on_reload
if ignore_manimlib_modules and module.__name__.startswith("manimlib"):
return
if module.__name__.startswith("manimlib.config"):
# We don't want to reload global manim_config
return
if not hasattr(module, "__dict__"):
return
# Prevent reloading the same module multiple times
if module.__name__ in reloaded_modules_tracker:
return
reloaded_modules_tracker.add(module.__name__)
# Recurse for all imported modules
for _attr_name, attr_value in module.__dict__.items():
if isinstance(attr_value, Module):
if ModuleLoader._is_user_defined_module(attr_value.__name__):
ModuleLoader._deep_reload(attr_value, reloaded_modules_tracker)
# Also reload modules that are part of a class or function
# e.g. when importing `from custom_module import CustomClass`
elif hasattr(attr_value, "__module__"):
attr_module_name = attr_value.__module__
if ModuleLoader._is_user_defined_module(attr_module_name):
attr_module = sys.modules[attr_module_name]
ModuleLoader._deep_reload(attr_module, reloaded_modules_tracker)
# Reload
log.debug('Reloading module "%s"', module.__name__)
importlib.reload(module)

View File

@@ -4,14 +4,14 @@ import itertools as it
import numpy as np
import pyperclip
from IPython.core.getipython import get_ipython
from pyglet.window import key as PygletWindowKeys
from manimlib.animation.fading import FadeIn
from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL
from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER
from manimlib.config import manim_config
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF
from manimlib.constants import PI
from manimlib.constants import DEGREES
from manimlib.constants import DEG
from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
@@ -27,7 +27,6 @@ from manimlib.mobject.types.vectorized_mobject import VHighlight
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.scene.scene import Scene
from manimlib.scene.scene import SceneState
from manimlib.scene.scene import PAN_3D_KEY
from manimlib.utils.family_ops import extract_mobject_family_members
from manimlib.utils.space_ops import get_norm
from manimlib.utils.tex_file_writing import LatexError
@@ -38,18 +37,27 @@ if TYPE_CHECKING:
from manimlib.typing import Vect3
SELECT_KEY = 's'
UNSELECT_KEY = 'u'
GRAB_KEY = 'g'
X_GRAB_KEY = 'h'
Y_GRAB_KEY = 'v'
SELECT_KEY = manim_config.key_bindings.select
UNSELECT_KEY = manim_config.key_bindings.unselect
GRAB_KEY = manim_config.key_bindings.grab
X_GRAB_KEY = manim_config.key_bindings.x_grab
Y_GRAB_KEY = manim_config.key_bindings.y_grab
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
RESIZE_KEY = 't'
COLOR_KEY = 'c'
INFORMATION_KEY = 'i'
CURSOR_KEY = 'k'
COPY_FRAME_POSITION_KEY = 'p'
RESIZE_KEY = manim_config.key_bindings.resize # TODO
COLOR_KEY = manim_config.key_bindings.color
INFORMATION_KEY = manim_config.key_bindings.information
CURSOR_KEY = manim_config.key_bindings.cursor
# For keyboard interactions
ARROW_SYMBOLS: list[int] = [
PygletWindowKeys.LEFT,
PygletWindowKeys.UP,
PygletWindowKeys.RIGHT,
PygletWindowKeys.DOWN,
]
ALL_MODIFIERS = PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_SHIFT
# Note, a lot of the functionality here is still buggy and very much a work in progress.
@@ -237,6 +245,10 @@ class InteractiveScene(Scene):
super().remove(*mobjects)
self.regenerate_selection_search_set()
def remove_all_except(self, *mobjects_to_keep : Mobject):
super().remove_all_except(*mobjects_to_keep)
self.regenerate_selection_search_set()
# Related to selection
def toggle_selection_mode(self):
@@ -460,59 +472,51 @@ class InteractiveScene(Scene):
nudge *= 10
self.selection.shift(nudge * vect)
def save_selection_to_file(self):
if len(self.selection) == 1:
self.save_mobject_to_file(self.selection[0])
else:
self.save_mobject_to_file(self.selection)
# Key actions
def on_key_press(self, symbol: int, modifiers: int) -> None:
super().on_key_press(symbol, modifiers)
char = chr(symbol)
if char == SELECT_KEY and modifiers == 0:
if char == SELECT_KEY and (modifiers & ALL_MODIFIERS) == 0:
self.enable_selection()
if char == UNSELECT_KEY:
self.clear_selection()
elif char in GRAB_KEYS and modifiers == 0:
elif char in GRAB_KEYS and (modifiers & ALL_MODIFIERS) == 0:
self.prepare_grab()
elif char == RESIZE_KEY and modifiers in [0, SHIFT_MODIFIER]:
self.prepare_resizing(about_corner=(modifiers == SHIFT_MODIFIER))
elif symbol == SHIFT_SYMBOL:
elif char == RESIZE_KEY and (modifiers & PygletWindowKeys.MOD_SHIFT):
self.prepare_resizing(about_corner=((modifiers & PygletWindowKeys.MOD_SHIFT) > 0))
elif symbol == PygletWindowKeys.LSHIFT:
if self.window.is_key_pressed(ord("t")):
self.prepare_resizing(about_corner=True)
elif char == COLOR_KEY and modifiers == 0:
elif char == COLOR_KEY and (modifiers & ALL_MODIFIERS) == 0:
self.toggle_color_palette()
elif char == INFORMATION_KEY and modifiers == 0:
elif char == INFORMATION_KEY and (modifiers & ALL_MODIFIERS) == 0:
self.display_information()
elif char == "c" and modifiers == COMMAND_MODIFIER:
elif char == "c" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.copy_selection()
elif char == "v" and modifiers == COMMAND_MODIFIER:
elif char == "v" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.paste_selection()
elif char == "x" and modifiers == COMMAND_MODIFIER:
elif char == "x" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.copy_selection()
self.delete_selection()
elif symbol == DELETE_SYMBOL:
elif symbol == PygletWindowKeys.BACKSPACE:
self.delete_selection()
elif char == "a" and modifiers == COMMAND_MODIFIER:
elif char == "a" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.clear_selection()
self.add_to_selection(*self.mobjects)
elif char == "g" and modifiers == COMMAND_MODIFIER:
elif char == "g" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.group_selection()
elif char == "g" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
elif char == "g" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_SHIFT)):
self.ungroup_selection()
elif char == "t" and modifiers == COMMAND_MODIFIER:
elif char == "t" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.toggle_selection_mode()
elif char == "s" and modifiers == COMMAND_MODIFIER:
self.save_selection_to_file()
elif char == "d" and modifiers == SHIFT_MODIFIER:
elif char == "d" and (modifiers & PygletWindowKeys.MOD_SHIFT):
self.copy_frame_positioning()
elif char == "c" and modifiers == SHIFT_MODIFIER:
elif char == "c" and (modifiers & PygletWindowKeys.MOD_SHIFT):
self.copy_cursor_position()
elif symbol in ARROW_SYMBOLS:
self.nudge_selection(
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
large=(modifiers & SHIFT_MODIFIER),
large=(modifiers & PygletWindowKeys.MOD_SHIFT),
)
# Adding crosshair
if char == CURSOR_KEY:
@@ -535,7 +539,7 @@ class InteractiveScene(Scene):
self.is_grabbing = False
elif chr(symbol) == INFORMATION_KEY:
self.display_information(False)
elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)):
elif symbol == PygletWindowKeys.LSHIFT and self.window.is_key_pressed(ord(RESIZE_KEY)):
self.prepare_resizing(about_corner=False)
# Mouse actions
@@ -552,7 +556,7 @@ class InteractiveScene(Scene):
if not hasattr(self, "scale_about_point"):
return
vect = point - self.scale_about_point
if self.window.is_key_pressed(CTRL_SYMBOL):
if self.window.is_key_pressed(PygletWindowKeys.LCTRL):
for i in (0, 1):
scalar = vect[i] / self.scale_ref_vect[i]
self.selection.rescale_to_fit(
@@ -597,7 +601,7 @@ class InteractiveScene(Scene):
self.handle_grabbing(point)
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
self.handle_resizing(point)
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL):
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(PygletWindowKeys.LSHIFT):
self.handle_sweeping_selection(point)
def on_mouse_drag(
@@ -625,7 +629,7 @@ class InteractiveScene(Scene):
angles = frame.get_euler_angles()
call = f"reorient("
theta, phi, gamma = (angles / DEGREES).astype(int)
theta, phi, gamma = (angles / DEG).astype(int)
call += f"{theta}, {phi}, {gamma}"
if any(center != 0):
call += f", {tuple(np.round(center, 2))}"

View File

@@ -1,51 +1,45 @@
from __future__ import annotations
from collections import OrderedDict
import inspect
import os
import platform
import pyperclip
import random
import time
import re
from functools import wraps
from IPython.terminal import pt_inputhooks
from IPython.terminal.embed import InteractiveShellEmbed
from IPython.core.getipython import get_ipython
from contextlib import contextmanager
from contextlib import ExitStack
import numpy as np
from tqdm.auto import tqdm as ProgressDisplay
from pyglet.window import key as PygletWindowKeys
from manimlib.animation.animation import prepare_animation
from manimlib.animation.fading import VFadeInThenOut
from manimlib.camera.camera import Camera
from manimlib.camera.camera_frame import CameraFrame
from manimlib.config import get_module
from manimlib.constants import ARROW_SYMBOLS
from manimlib.constants import DEFAULT_WAIT_TIME
from manimlib.constants import COMMAND_MODIFIER
from manimlib.constants import SHIFT_MODIFIER
from manimlib.constants import RED
from manimlib.config import manim_config
from manimlib.event_handler import EVENT_DISPATCHER
from manimlib.event_handler.event_type import EventType
from manimlib.logger import log
from manimlib.mobject.frame import FullScreenRectangle
from manimlib.mobject.mobject import _AnimationBuilder
from manimlib.mobject.mobject import Group
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Point
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.scene.scene_embed import InteractiveSceneEmbed
from manimlib.scene.scene_embed import CheckpointManager
from manimlib.scene.scene_file_writer import SceneFileWriter
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.family_ops import extract_mobject_family_members
from manimlib.utils.family_ops import recursive_mobject_remove
from manimlib.utils.iterables import batch_by_property
from manimlib.utils.sounds import play_sound
from manimlib.utils.color import color_to_rgba
from manimlib.window import Window
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, TypeVar
from typing import Callable, Iterable, TypeVar, Optional
from manimlib.typing import Vect3
T = TypeVar('T')
@@ -55,12 +49,6 @@ if TYPE_CHECKING:
from manimlib.animation.animation import Animation
PAN_3D_KEY = 'd'
FRAME_SHIFT_KEY = 'f'
RESET_FRAME_KEY = 'r'
QUIT_KEY = 'q'
class Scene(object):
random_seed: int = 0
pan_sensitivity: float = 0.5
@@ -68,7 +56,6 @@ class Scene(object):
drag_to_pan: bool = True
max_num_saved_states: int = 50
default_camera_config: dict = dict()
default_window_config: dict = dict()
default_file_writer_config: dict = dict()
samples = 0
# Euler angles, in degrees
@@ -76,48 +63,52 @@ class Scene(object):
def __init__(
self,
window_config: dict = dict(),
window: Optional[Window] = None,
camera_config: dict = dict(),
file_writer_config: dict = dict(),
skip_animations: bool = False,
always_update_mobjects: bool = False,
start_at_animation_number: int | None = None,
end_at_animation_number: int | None = None,
leave_progress_bars: bool = False,
preview: bool = True,
presenter_mode: bool = False,
show_animation_progress: bool = False,
embed_exception_mode: str = "",
embed_error_sound: bool = False,
leave_progress_bars: bool = False,
preview_while_skipping: bool = True,
presenter_mode: bool = False,
default_wait_time: float = 1.0,
):
self.skip_animations = skip_animations
self.always_update_mobjects = always_update_mobjects
self.start_at_animation_number = start_at_animation_number
self.end_at_animation_number = end_at_animation_number
self.leave_progress_bars = leave_progress_bars
self.preview = preview
self.presenter_mode = presenter_mode
self.show_animation_progress = show_animation_progress
self.embed_exception_mode = embed_exception_mode
self.embed_error_sound = embed_error_sound
self.leave_progress_bars = leave_progress_bars
self.preview_while_skipping = preview_while_skipping
self.presenter_mode = presenter_mode
self.default_wait_time = default_wait_time
self.camera_config = {**self.default_camera_config, **camera_config}
self.window_config = {**self.default_window_config, **window_config}
for config in self.camera_config, self.window_config:
config["samples"] = self.samples
self.file_writer_config = {**self.default_file_writer_config, **file_writer_config}
self.camera_config = merge_dicts_recursively(
manim_config.camera, # Global default
self.default_camera_config, # Updated configuration that subclasses may specify
camera_config, # Updated configuration from instantiation
)
self.file_writer_config = merge_dicts_recursively(
manim_config.file_writer,
self.default_file_writer_config,
file_writer_config,
)
# Initialize window, if applicable
if self.preview:
from manimlib.window import Window
self.window = Window(scene=self, **self.window_config)
self.camera_config["window"] = self.window
self.camera_config["fps"] = 30 # Where's that 30 from?
else:
self.window = None
self.window = window
if self.window:
self.window.init_for_scene(self)
# Make sure camera and Pyglet window sync
self.camera_config["fps"] = 30
# Core state of the scene
self.camera: Camera = Camera(**self.camera_config)
self.camera: Camera = Camera(
window=self.window,
samples=self.samples,
**self.camera_config
)
self.frame: CameraFrame = self.camera.frame
self.frame.reorient(*self.default_frame_orientation)
self.frame.make_orientation_default()
@@ -130,7 +121,6 @@ class Scene(object):
self.time: float = 0
self.skip_time: float = 0
self.original_skipping_status: bool = self.skip_animations
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
self.undo_stack = []
self.redo_stack = []
@@ -153,6 +143,9 @@ class Scene(object):
def __str__(self) -> str:
return self.__class__.__name__
def get_window(self) -> Window | None:
return self.window
def run(self) -> None:
self.virtual_animation_start_time: float = 0
self.real_animation_start_time: float = time.time()
@@ -212,76 +205,14 @@ class Scene(object):
close_scene_on_exit: bool = True,
show_animation_progress: bool = False,
) -> None:
if not self.preview:
# Embed is only relevant with a preview
if not self.window:
# Embed is only relevant for interactive development with a Window
return
self.show_animation_progress = show_animation_progress
self.stop_skipping()
self.update_frame(force_draw=True)
self.save_state()
self.show_animation_progress = show_animation_progress
# Create embedded IPython terminal configured to have access to
# the local namespace of the caller
caller_frame = inspect.currentframe().f_back
module = get_module(caller_frame.f_globals["__file__"])
shell = InteractiveShellEmbed(user_module=module)
# Add a few custom shortcuts to that local namespace
local_ns = dict(caller_frame.f_locals)
local_ns.update(
play=self.play,
wait=self.wait,
add=self.add,
remove=self.remove,
clear=self.clear,
save_state=self.save_state,
undo=self.undo,
redo=self.redo,
i2g=self.i2g,
i2m=self.i2m,
checkpoint_paste=self.checkpoint_paste,
touch=lambda: shell.enable_gui("manim"),
notouch=lambda: shell.enable_gui(None),
)
# Update the shell module with the caller's locals + shortcuts
module.__dict__.update(local_ns)
# Enables gui interactions during the embed
def inputhook(context):
while not context.input_is_ready():
if not self.is_window_closing():
self.update_frame(dt=0)
if self.is_window_closing():
shell.ask_exit()
pt_inputhooks.register("manim", inputhook)
shell.enable_gui("manim")
# Operation to run after each ipython command
def post_cell_func(*args, **kwargs):
if not self.is_window_closing():
self.update_frame(dt=0, force_draw=True)
shell.events.register("post_run_cell", post_cell_func)
# Flash border, and potentially play sound, on exceptions
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
# Show the error don't just swallow it
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
if self.embed_error_sound:
os.system("printf '\a'")
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
rect.fix_in_frame()
self.play(VFadeInThenOut(rect, run_time=0.5))
shell.set_custom_exc((Exception,), custom_exc)
# Set desired exception mode
shell.magic(f"xmode {self.embed_exception_mode}")
# Launch shell
shell()
InteractiveSceneEmbed(self).launch()
# End scene when exiting an embed
if close_scene_on_exit:
@@ -374,7 +305,7 @@ class Scene(object):
"""
batches = batch_by_property(
self.mobjects,
lambda m: str(type(m)) + str(m.get_shader_wrapper(self.camera.ctx).get_id())
lambda m: str(type(m)) + str(m.get_shader_wrapper(self.camera.ctx).get_id()) + str(m.z_index)
)
for group in self.render_groups:
@@ -450,6 +381,11 @@ class Scene(object):
new_mobjects, _ = recursive_mobject_remove(self.mobjects, to_remove)
self.mobjects = new_mobjects
@affects_mobject_list
def remove_all_except(self, *mobjects_to_keep : Mobject):
self.clear()
self.add(*mobjects_to_keep)
def bring_to_front(self, *mobjects: Mobject):
self.add(*mobjects)
return self
@@ -597,13 +533,14 @@ class Scene(object):
if not self.skip_animations:
self.file_writer.end_animation()
if self.skip_animations and self.window is not None:
if self.preview_while_skipping and self.skip_animations and self.window is not None:
# Show some quick frames along the way
self.update_frame(dt=0, force_draw=True)
self.num_plays += 1
def begin_animations(self, animations: Iterable[Animation]) -> None:
all_mobjects = set(self.get_mobject_family_members())
for animation in animations:
animation.begin()
# Anything animated that's not already in the
@@ -611,8 +548,9 @@ class Scene(object):
# animated mobjects that are in the family of
# those on screen, this can result in a restructuring
# of the scene.mobjects list, which is usually desired.
if animation.mobject not in self.mobjects:
if animation.mobject not in all_mobjects:
self.add(animation.mobject)
all_mobjects = all_mobjects.union(animation.mobject.get_family())
def progress_through_animations(self, animations: Iterable[Animation]) -> None:
last_t = 0
@@ -657,11 +595,13 @@ class Scene(object):
def wait(
self,
duration: float = DEFAULT_WAIT_TIME,
duration: Optional[float] = None,
stop_condition: Callable[[], bool] = None,
note: str = None,
ignore_presenter_mode: bool = False
):
if duration is None:
duration = self.default_wait_time
self.pre_play()
self.update_mobjects(dt=0) # Any problems with this?
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
@@ -724,8 +664,6 @@ class Scene(object):
scene_state.restore_scene(self)
def save_state(self) -> None:
if not self.preview:
return
state = self.get_state()
if self.undo_stack and state.mobjects_match(self.undo_stack[-1]):
return
@@ -744,102 +682,44 @@ class Scene(object):
self.undo_stack.append(self.get_state())
self.restore_state(self.redo_stack.pop())
def checkpoint_paste(
self,
skip: bool = False,
record: bool = False,
progress_bar: bool = True
):
"""
Used during interactive development to run (or re-run)
a block of scene code.
If the copied selection starts with a comment, this will
revert to the state of the scene the first time this function
was called on a block of code starting with that comment.
"""
shell = get_ipython()
if shell is None or self.window is None:
raise Exception(
"Scene.checkpoint_paste cannot be called outside of " +
"an ipython shell"
)
pasted = pyperclip.paste()
lines = pasted.split("\n")
# Commented lines trigger saved checkpoints
if lines[0].lstrip().startswith("#"):
if lines[0] not in self.checkpoint_states:
self.checkpoint(lines[0])
else:
self.revert_to_checkpoint(lines[0])
# Copied methods of a scene are handled specially
# A bit hacky, yes, but convenient
method_pattern = r"^def\s+([a-zA-Z_]\w*)\s*\(self.*\):"
method_names = re.findall(method_pattern ,lines[0].strip())
if method_names:
method_name = method_names[0]
indent = " " * lines[0].index(lines[0].strip())
pasted = "\n".join([
# Remove self from function signature
re.sub(r"self(,\s*)?", "", lines[0]),
*lines[1:],
# Attach to scene via self.func_name = func_name
f"{indent}self.{method_name} = {method_name}"
])
# Keep track of skipping and progress bar status
self.skip_animations = skip
@contextmanager
def temp_skip(self):
prev_status = self.skip_animations
self.skip_animations = True
try:
yield
finally:
if not prev_status:
self.stop_skipping()
@contextmanager
def temp_progress_bar(self):
prev_progress = self.show_animation_progress
self.show_animation_progress = progress_bar
self.show_animation_progress = True
try:
yield
finally:
self.show_animation_progress = prev_progress
if record:
self.camera.use_window_fbo(False)
self.file_writer.begin_insert()
shell.run_cell(pasted)
if record:
@contextmanager
def temp_record(self):
self.camera.use_window_fbo(False)
self.file_writer.begin_insert()
try:
yield
finally:
self.file_writer.end_insert()
self.camera.use_window_fbo(True)
self.stop_skipping()
self.show_animation_progress = prev_progress
def checkpoint(self, key: str):
self.checkpoint_states[key] = self.get_state()
def revert_to_checkpoint(self, key: str):
if key not in self.checkpoint_states:
log.error(f"No checkpoint at {key}")
return
all_keys = list(self.checkpoint_states.keys())
index = all_keys.index(key)
for later_key in all_keys[index + 1:]:
self.checkpoint_states.pop(later_key)
self.restore_state(self.checkpoint_states[key])
def clear_checkpoints(self):
self.checkpoint_states = dict()
def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None:
if file_path is None:
file_path = self.file_writer.get_saved_mobject_path(mobject)
if file_path is None:
return
mobject.save_to_file(file_path)
def load_mobject(self, file_name):
if os.path.exists(file_name):
path = file_name
else:
directory = self.file_writer.get_saved_mobject_directory()
path = os.path.join(directory, file_name)
return Mobject.load(path)
def temp_config_change(self, skip=False, record=False, progress_bar=False):
stack = ExitStack()
if skip:
stack.enter_context(self.temp_skip())
if record:
stack.enter_context(self.temp_record())
if progress_bar:
stack.enter_context(self.temp_progress_bar())
return stack
def is_window_closing(self):
return self.window and (self.window.is_closing or self.quit_interaction)
@@ -868,13 +748,13 @@ class Scene(object):
frame = self.camera.frame
# Handle perspective changes
if self.window.is_key_pressed(ord(PAN_3D_KEY)):
if self.window.is_key_pressed(ord(manim_config.key_bindings.pan_3d)):
ff_d_point = frame.to_fixed_frame_point(d_point, relative=True)
ff_d_point *= self.pan_sensitivity
frame.increment_theta(-ff_d_point[0])
frame.increment_phi(ff_d_point[1])
# Handle frame movements
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
elif self.window.is_key_pressed(ord(manim_config.key_bindings.pan)):
frame.shift(-d_point)
def on_mouse_drag(
@@ -960,17 +840,17 @@ class Scene(object):
if propagate_event is not None and propagate_event is False:
return
if char == RESET_FRAME_KEY:
if char == manim_config.key_bindings.reset:
self.play(self.camera.frame.animate.to_default_state())
elif char == "z" and modifiers == COMMAND_MODIFIER:
elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.undo()
elif char == "z" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_SHIFT)):
self.redo()
# command + q
elif char == QUIT_KEY and modifiers == COMMAND_MODIFIER:
elif char == manim_config.key_bindings.quit and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.quit_interaction = True
# Space or right arrow
elif char == " " or symbol == ARROW_SYMBOLS[2]:
elif char == " " or symbol == PygletWindowKeys.RIGHT:
self.hold_on_wait = False
def on_resize(self, width: int, height: int) -> None:
@@ -985,6 +865,19 @@ class Scene(object):
def on_close(self) -> None:
pass
def focus(self) -> None:
"""
Puts focus on the ManimGL window.
"""
if not self.window:
return
self.window.focus()
def set_background_color(self, background_color, background_opacity=1) -> None:
self.camera.background_rgba = list(color_to_rgba(
background_color, background_opacity
))
class SceneState():
def __init__(self, scene: Scene, ignore: list[Mobject] | None = None):

View File

@@ -0,0 +1,236 @@
from __future__ import annotations
import inspect
import pyperclip
import traceback
from IPython.terminal import pt_inputhooks
from IPython.terminal.embed import InteractiveShellEmbed
from manimlib.animation.fading import VFadeInThenOut
from manimlib.config import manim_config
from manimlib.constants import RED
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.frame import FullScreenRectangle
from manimlib.module_loader import ModuleLoader
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.scene.scene import Scene
class InteractiveSceneEmbed:
def __init__(self, scene: Scene):
self.scene = scene
self.checkpoint_manager = CheckpointManager()
self.shell = self.get_ipython_shell_for_embedded_scene()
self.enable_gui()
self.ensure_frame_update_post_cell()
self.ensure_flash_on_error()
if manim_config.embed.autoreload:
self.auto_reload()
def launch(self):
self.shell()
def get_ipython_shell_for_embedded_scene(self) -> InteractiveShellEmbed:
"""
Create embedded IPython terminal configured to have access to
the local namespace of the caller
"""
# Triple back should take us to the context in a user's scene definition
# which is calling "self.embed"
caller_frame = inspect.currentframe().f_back.f_back.f_back
# Update the module's namespace to include local variables
module = ModuleLoader.get_module(caller_frame.f_globals["__file__"])
module.__dict__.update(caller_frame.f_locals)
module.__dict__.update(self.get_shortcuts())
exception_mode = manim_config.embed.exception_mode
return InteractiveShellEmbed(
user_module=module,
display_banner=False,
xmode=exception_mode
)
def get_shortcuts(self):
"""
A few custom shortcuts useful to have in the interactive shell namespace
"""
scene = self.scene
return dict(
play=scene.play,
wait=scene.wait,
add=scene.add,
remove=scene.remove,
clear=scene.clear,
focus=scene.focus,
save_state=scene.save_state,
undo=scene.undo,
redo=scene.redo,
i2g=scene.i2g,
i2m=scene.i2m,
checkpoint_paste=self.checkpoint_paste,
clear_checkpoints=self.checkpoint_manager.clear_checkpoints,
reload=self.reload_scene # Defined below
)
def enable_gui(self):
"""Enables gui interactions during the embed"""
def inputhook(context):
while not context.input_is_ready():
if not self.scene.is_window_closing():
self.scene.update_frame(dt=0)
if self.scene.is_window_closing():
self.shell.ask_exit()
pt_inputhooks.register("manim", inputhook)
self.shell.enable_gui("manim")
def ensure_frame_update_post_cell(self):
"""Ensure the scene updates its frame after each ipython cell"""
def post_cell_func(*args, **kwargs):
if not self.scene.is_window_closing():
self.scene.update_frame(dt=0, force_draw=True)
self.shell.events.register("post_run_cell", post_cell_func)
def ensure_flash_on_error(self):
"""Flash border, and potentially play sound, on exceptions"""
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
# Show the error don't just swallow it
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
rect.fix_in_frame()
self.scene.play(VFadeInThenOut(rect, run_time=0.5))
self.shell.set_custom_exc((Exception,), custom_exc)
def validate_syntax(self, file_path: str) -> bool:
"""
Validates the syntax of a Python file without executing it.
Returns True if syntax is valid, False otherwise.
Prints syntax errors to the console if found.
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
source_code = f.read()
# Use compile() to check for syntax errors without executing
compile(source_code, file_path, 'exec')
return True
except SyntaxError as e:
print(f"\nSyntax Error in {file_path}:")
print(f" Line {e.lineno}: {e.text.strip() if e.text else ''}")
print(f" {' ' * (e.offset - 1 if e.offset else 0)}^")
print(f" {e.msg}")
return False
except Exception as e:
print(f"\nError reading {file_path}: {e}")
return False
def reload_scene(self, embed_line: int | None = None) -> None:
"""
Reloads the scene just like the `manimgl` command would do with the
same arguments that were provided for the initial startup. This allows
for quick iteration during scene development since we don't have to exit
the IPython kernel and re-run the `manimgl` command again. The GUI stays
open during the reload.
If `embed_line` is provided, the scene will be reloaded at that line
number. This corresponds to the `linemarker` param of the
`extract_scene.insert_embed_line_to_module()` method.
Before reload, the scene is cleared and the entire state is reset, such
that we can start from a clean slate. This is taken care of by the
run_scenes function in __main__.py, which will catch the error raised by the
`exit_raise` magic command that we invoke here.
Note that we cannot define a custom exception class for this error,
since the IPython kernel will swallow any exception. While we can catch
such an exception in our custom exception handler registered with the
`set_custom_exc` method, we cannot break out of the IPython shell by
this means.
"""
# Get the current file path for syntax validation
current_file = self.shell.user_module.__file__
# Validate syntax before attempting reload
if not self.validate_syntax(current_file):
print("[ERROR] Reload cancelled due to syntax errors. Fix the errors and try again.")
return
# Update the global run configuration.
run_config = manim_config.run
run_config.is_reload = True
if embed_line:
run_config.embed_line = embed_line
print("Reloading...")
self.shell.run_line_magic("exit_raise", "")
def auto_reload(self):
"""Enables reload the shell's module before all calls"""
def pre_cell_func(*args, **kwargs):
new_mod = ModuleLoader.get_module(self.shell.user_module.__file__, is_during_reload=True)
self.shell.user_ns.update(vars(new_mod))
self.shell.events.register("pre_run_cell", pre_cell_func)
def checkpoint_paste(
self,
skip: bool = False,
record: bool = False,
progress_bar: bool = True
):
with self.scene.temp_config_change(skip, record, progress_bar):
self.checkpoint_manager.checkpoint_paste(self.shell, self.scene)
class CheckpointManager:
def __init__(self):
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
def checkpoint_paste(self, shell, scene):
"""
Used during interactive development to run (or re-run)
a block of scene code.
If the copied selection starts with a comment, this will
revert to the state of the scene the first time this function
was called on a block of code starting with that comment.
"""
code_string = pyperclip.paste()
checkpoint_key = self.get_leading_comment(code_string)
self.handle_checkpoint_key(scene, checkpoint_key)
shell.run_cell(code_string)
@staticmethod
def get_leading_comment(code_string: str) -> str:
leading_line = code_string.partition("\n")[0].lstrip()
if leading_line.startswith("#"):
return leading_line
return ""
def handle_checkpoint_key(self, scene, key: str):
if not key:
return
elif key in self.checkpoint_states:
# Revert to checkpoint
scene.restore_state(self.checkpoint_states[key])
# Clear out any saved states that show up later
all_keys = list(self.checkpoint_states.keys())
index = all_keys.index(key)
for later_key in all_keys[index + 1:]:
self.checkpoint_states.pop(later_key)
else:
self.checkpoint_states[key] = scene.get_state()
def clear_checkpoints(self):
self.checkpoint_states = dict()

View File

@@ -11,11 +11,8 @@ from pydub import AudioSegment
from tqdm.auto import tqdm as ProgressDisplay
from pathlib import Path
from manimlib.constants import FFMPEG_BIN
from manimlib.logger import log
from manimlib.mobject.mobject import Mobject
from manimlib.utils.file_ops import add_extension_if_not_present
from manimlib.utils.file_ops import get_sorted_integer_files
from manimlib.utils.file_ops import guarantee_existence
from manimlib.utils.sounds import get_full_sound_file_path
@@ -33,22 +30,20 @@ class SceneFileWriter(object):
self,
scene: Scene,
write_to_movie: bool = False,
break_into_partial_movies: bool = False,
save_pngs: bool = False, # TODO, this currently does nothing
subdivide_output: bool = False,
png_mode: str = "RGBA",
save_last_frame: bool = False,
movie_file_extension: str = ".mp4",
# What python file is generating this scene
input_file_path: str = "",
# Where should this be written
output_directory: str | None = None,
output_directory: str = ".",
file_name: str | None = None,
subdirectory_for_videos: bool = False,
open_file_upon_completion: bool = False,
show_file_location_upon_completion: bool = False,
quiet: bool = False,
total_frames: int = 0,
progress_description_len: int = 40,
# Name of the binary used for ffmpeg
ffmpeg_bin: str = "ffmpeg",
video_codec: str = "libx264",
pixel_format: str = "yuv420p",
saturation: float = 1.0,
@@ -56,20 +51,18 @@ class SceneFileWriter(object):
):
self.scene: Scene = scene
self.write_to_movie = write_to_movie
self.break_into_partial_movies = break_into_partial_movies
self.save_pngs = save_pngs
self.subdivide_output = subdivide_output
self.png_mode = png_mode
self.save_last_frame = save_last_frame
self.movie_file_extension = movie_file_extension
self.input_file_path = input_file_path
self.output_directory = output_directory
self.file_name = file_name
self.open_file_upon_completion = open_file_upon_completion
self.subdirectory_for_videos = subdirectory_for_videos
self.show_file_location_upon_completion = show_file_location_upon_completion
self.quiet = quiet
self.total_frames = total_frames
self.progress_description_len = progress_description_len
self.ffmpeg_bin = ffmpeg_bin
self.video_codec = video_codec
self.pixel_format = pixel_format
self.saturation = saturation
@@ -79,40 +72,39 @@ class SceneFileWriter(object):
self.writing_process: sp.Popen | None = None
self.progress_display: ProgressDisplay | None = None
self.ended_with_interrupt: bool = False
self.init_output_directories()
self.init_audio()
# Output directories and files
def init_output_directories(self) -> None:
out_dir = self.output_directory or ""
scene_name = self.file_name or self.get_default_scene_name()
if self.save_last_frame:
image_dir = guarantee_existence(os.path.join(out_dir, "images"))
image_file = add_extension_if_not_present(scene_name, ".png")
self.image_file_path = os.path.join(image_dir, image_file)
self.image_file_path = self.init_image_file_path()
if self.write_to_movie:
if self.subdirectory_for_videos:
movie_dir = guarantee_existence(os.path.join(out_dir, "videos"))
else:
movie_dir = guarantee_existence(out_dir)
movie_file = add_extension_if_not_present(scene_name, self.movie_file_extension)
self.movie_file_path = os.path.join(movie_dir, movie_file)
if self.break_into_partial_movies:
self.partial_movie_directory = guarantee_existence(os.path.join(
movie_dir, "partial_movie_files", scene_name,
))
# A place to save mobjects
self.saved_mobject_directory = os.path.join(
out_dir, "mobjects", str(self.scene)
self.movie_file_path = self.init_movie_file_path()
if self.subdivide_output:
self.partial_movie_directory = self.init_partial_movie_directory()
def init_image_file_path(self) -> Path:
return self.get_output_file_rootname().with_suffix(".png")
def init_movie_file_path(self) -> Path:
return self.get_output_file_rootname().with_suffix(self.movie_file_extension)
def init_partial_movie_directory(self):
return guarantee_existence(self.get_output_file_rootname())
def get_output_file_rootname(self) -> Path:
return Path(
guarantee_existence(self.output_directory),
self.get_output_file_name()
)
def get_default_module_directory(self) -> str:
path, _ = os.path.splitext(self.input_file_path)
if path.startswith("_"):
path = path[1:]
return path
def get_default_scene_name(self) -> str:
def get_output_file_name(self) -> str:
if self.file_name:
return self.file_name
# Otherwise, use the name of the scene, potentially
# appending animation numbers
name = str(self.scene)
saan = self.scene.start_at_animation_number
eaan = self.scene.end_at_animation_number
@@ -122,63 +114,17 @@ class SceneFileWriter(object):
name += f"_{eaan}"
return name
def get_resolution_directory(self) -> str:
pixel_height = self.scene.camera.pixel_height
fps = self.scene.camera.fps
return "{}p{}".format(
pixel_height, fps
)
# Directory getters
def get_image_file_path(self) -> str:
return self.image_file_path
def get_next_partial_movie_path(self) -> str:
result = os.path.join(
self.partial_movie_directory,
"{:05}{}".format(
self.scene.num_plays,
self.movie_file_extension,
)
)
return result
result = Path(self.partial_movie_directory, f"{self.scene.num_plays:05}")
return result.with_suffix(self.movie_file_extension)
def get_movie_file_path(self) -> str:
return self.movie_file_path
def get_saved_mobject_directory(self) -> str:
return guarantee_existence(self.saved_mobject_directory)
def get_saved_mobject_path(self, mobject: Mobject) -> str | None:
directory = self.get_saved_mobject_directory()
files = os.listdir(directory)
default_name = str(mobject) + "_0.mob"
index = 0
while default_name in files:
default_name = default_name.replace(str(index), str(index + 1))
index += 1
if platform.system() == 'Darwin':
cmds = [
"osascript", "-e",
f"""
set chosenfile to (choose file name default name "{default_name}" default location "{directory}")
POSIX path of chosenfile
""",
]
process = sp.Popen(cmds, stdout=sp.PIPE)
file_path = process.stdout.read().decode("utf-8").split("\n")[0]
if not file_path:
return
else:
user_name = input(f"Enter mobject file name (default is {default_name}): ")
file_path = os.path.join(directory, user_name or default_name)
if os.path.exists(file_path) or os.path.exists(file_path + ".mob"):
if input(f"{file_path} already exists. Overwrite (y/n)? ") != "y":
return
if not file_path.endswith(".mob"):
file_path = file_path + ".mob"
return file_path
# Sound
def init_audio(self) -> None:
self.includes_sound: bool = False
@@ -230,23 +176,20 @@ class SceneFileWriter(object):
# Writers
def begin(self) -> None:
if not self.break_into_partial_movies and self.write_to_movie:
if not self.subdivide_output and self.write_to_movie:
self.open_movie_pipe(self.get_movie_file_path())
def begin_animation(self) -> None:
if self.break_into_partial_movies and self.write_to_movie:
if self.subdivide_output and self.write_to_movie:
self.open_movie_pipe(self.get_next_partial_movie_path())
def end_animation(self) -> None:
if self.break_into_partial_movies and self.write_to_movie:
if self.subdivide_output and self.write_to_movie:
self.close_movie_pipe()
def finish(self) -> None:
if self.write_to_movie:
if self.break_into_partial_movies:
self.combine_movie_files()
else:
self.close_movie_pipe()
if not self.subdivide_output and self.write_to_movie:
self.close_movie_pipe()
if self.includes_sound:
self.add_sound_to_video()
self.print_file_ready_message(self.get_movie_file_path())
@@ -265,11 +208,10 @@ class SceneFileWriter(object):
width, height = self.scene.camera.get_pixel_shape()
vf_arg = 'vflip'
# if self.pixel_format.startswith("yuv"):
vf_arg += f',eq=saturation={self.saturation}:gamma={self.gamma}'
command = [
FFMPEG_BIN,
self.ffmpeg_bin,
'-y', # overwrite output file if it exists
'-f', 'rawvideo',
'-s', f'{width}x{height}', # size of one frame
@@ -277,7 +219,7 @@ class SceneFileWriter(object):
'-r', str(fps), # frames per second
'-i', '-', # The input comes from a pipe
'-vf', vf_arg,
'-an', # Tells FFMPEG not to expect any audio
'-an', # Tells ffmpeg not to expect any audio
'-loglevel', 'error',
]
if self.video_codec:
@@ -304,8 +246,8 @@ class SceneFileWriter(object):
movie_path = Path(self.get_movie_file_path())
scene_name = movie_path.stem
insert_dir = Path(movie_path.parent, "inserts")
guarantee_existence(str(insert_dir))
return Path(insert_dir, f"{scene_name}_{index}{movie_path.suffix}")
guarantee_existence(insert_dir)
return Path(insert_dir, f"{scene_name}_{index}").with_suffix(self.movie_file_extension)
def begin_insert(self):
# Begin writing process
@@ -314,7 +256,7 @@ class SceneFileWriter(object):
index = 0
while (insert_path := self.get_insert_file_path(index)).exists():
index += 1
self.inserted_file_path = str(insert_path)
self.inserted_file_path = insert_path
self.open_movie_pipe(self.inserted_file_path)
def end_insert(self):
@@ -358,54 +300,6 @@ class SceneFileWriter(object):
else:
self.movie_file_path = self.temp_file_path
def combine_movie_files(self) -> None:
kwargs = {
"remove_non_integer_files": True,
"extension": self.movie_file_extension,
}
if self.scene.start_at_animation_number is not None:
kwargs["min_index"] = self.scene.start_at_animation_number
if self.scene.end_at_animation_number is not None:
kwargs["max_index"] = self.scene.end_at_animation_number
else:
kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1
partial_movie_files = get_sorted_integer_files(
self.partial_movie_directory,
**kwargs
)
if len(partial_movie_files) == 0:
log.warning("No animations in this scene")
return
# Write a file partial_file_list.txt containing all
# partial movie files
file_list = os.path.join(
self.partial_movie_directory,
"partial_movie_file_list.txt"
)
with open(file_list, 'w') as fp:
for pf_path in partial_movie_files:
if os.name == 'nt':
pf_path = pf_path.replace('\\', '/')
fp.write(f"file \'{pf_path}\'\n")
movie_file_path = self.get_movie_file_path()
commands = [
FFMPEG_BIN,
'-y', # overwrite output file if it exists
'-f', 'concat',
'-safe', '0',
'-i', file_list,
'-loglevel', 'error',
'-c', 'copy',
movie_file_path
]
if not self.includes_sound:
commands.insert(-1, '-an')
combine_process = sp.Popen(commands)
combine_process.wait()
def add_sound_to_video(self) -> None:
movie_file_path = self.get_movie_file_path()
stem, ext = os.path.splitext(movie_file_path)
@@ -418,7 +312,7 @@ class SceneFileWriter(object):
)
temp_file_path = stem + "_temp" + ext
commands = [
FFMPEG_BIN,
self.ffmpeg_bin,
"-i", movie_file_path,
"-i", sound_file_path,
'-y', # overwrite output file if it exists

View File

@@ -10,8 +10,7 @@ import numpy as np
from functools import lru_cache
from manimlib.config import parse_cli
from manimlib.config import get_configuration
from manimlib.utils.iterables import resize_array
from manimlib.config import manim_config
from manimlib.utils.shaders import get_shader_code_from_file
from manimlib.utils.shaders import get_shader_program
from manimlib.utils.shaders import image_path_to_texture
@@ -20,8 +19,10 @@ from manimlib.utils.shaders import set_program_uniform
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List, Optional, Dict
from typing import Optional, Tuple, Iterable
from manimlib.typing import UniformDict
from moderngl.vertex_array import VertexArray
from moderngl.framebuffer import Framebuffer
# Mobjects that should be rendered with
# the same shader will be organized and
@@ -409,8 +410,7 @@ class VShaderWrapper(ShaderWrapper):
which can display that texture as a simple quad onto a screen,
along with the rgb value which is meant to be discarded.
"""
cam_config = get_configuration(parse_cli())['camera_config']
size = (cam_config['pixel_width'], cam_config['pixel_height'])
size = manim_config.camera.resolution
double_size = (2 * size[0], 2 * size[1])
# Important to make sure dtype is floating point (not fixed point)
@@ -446,6 +446,11 @@ class VShaderWrapper(ShaderWrapper):
color = texture(Texture, uv);
if(color.a == 0) discard;
if(color.a < 0){
color.a = -color.a / (1.0 - color.a);
color.rgb *= (color.a - 1);
}
// Counteract scaling in fill frag
color *= 1.06;

View File

@@ -6,7 +6,7 @@ uniform vec4 clip_plane;
void emit_gl_Position(vec3 point){
vec4 result = vec4(point, 1.0);
// This allow for smooth transitions between objects fixed and unfixed from frame
// This allows for smooth transitions between objects fixed and unfixed from frame
result = mix(view * result, result, is_fixed_in_frame);
// Essentially a projection matrix
result.xyz *= frame_rescale_factors;

View File

@@ -27,7 +27,7 @@ vec4 add_light(vec4 color, vec3 point, vec3 unit_normal){
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
// For glossy surface, add extra shine if light beam goes towards camera
vec3 light_reflection = reflect(-to_light, unit_normal);
float light_to_cam = dot(light_reflection, to_camera);
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));

View File

@@ -4,7 +4,7 @@ layout (triangles) in;
layout (triangle_strip, max_vertices = 64) out; // Related to MAX_STEPS below
uniform float anti_alias_width;
uniform float flat_stroke_float;
uniform float flat_stroke;
uniform float pixel_size;
uniform float joint_type;
uniform float frame_scale;
@@ -33,7 +33,7 @@ const float COS_THRESHOLD = 0.999;
// Used to determine how many lines to break the curve into
const float POLYLINE_FACTOR = 100;
const int MAX_STEPS = 32;
const float MITER_COS_ANGLE_THRESHOLD = -0.9;
const float MITER_COS_ANGLE_THRESHOLD = -0.8;
#INSERT emit_gl_Position.glsl
#INSERT finalize_color.glsl
@@ -62,13 +62,13 @@ vec3 rotate_vector(vec3 vect, vec3 unit_normal, float angle){
}
vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angle, bool inside_curve, bool flat_stroke){
vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angle, bool inside_curve, bool draw_flat){
/*
Step the the left of a curve.
First a perpendicular direction is calculated, then it is adjusted
so as to make a joint.
*/
vec3 unit_tan = normalize(flat_stroke ? tangent : project(tangent, unit_normal));
vec3 unit_tan = normalize(draw_flat ? tangent : project(tangent, unit_normal));
// Step to stroke width bound should be perpendicular
// both to the tangent and the normal direction
@@ -78,11 +78,13 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angl
// lines up very closely with the direction to the camera, treated here
// as the unit normal. To avoid those, this smoothly transitions to a step
// direction perpendicular to the true curve normal.
float alignment = abs(dot(normalize(tangent), unit_normal));
float alignment_threshold = 0.97; // This could maybe be chosen in a more principled way based on stroke width
if (alignment > alignment_threshold) {
vec3 perp = normalize(cross(v_unit_normal[1], tangent));
step = mix(step, project(step, perp), smoothstep(alignment_threshold, 1.0, alignment));
if(joint_angle != 0){
float alignment = abs(dot(normalize(tangent), unit_normal));
float alignment_threshold = 0.97; // This could maybe be chosen in a more principled way based on stroke width
if (alignment > alignment_threshold) {
vec3 perp = normalize(cross(v_unit_normal[1], tangent));
step = mix(step, project(step, perp), smoothstep(alignment_threshold, 1.0, alignment));
}
}
if (inside_curve || int(joint_type) == NO_JOINT) return step;
@@ -93,7 +95,7 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, float joint_angl
if (abs(cos_angle) > COS_THRESHOLD) return step;
// Below here, figure out the adjustment to bevel or miter a joint
if (!flat_stroke){
if (!draw_flat){
// Figure out what joint product would be for everything projected onto
// the plane perpendicular to the normal direction (which here would be to_camera)
step = normalize(cross(unit_normal, unit_tan)); // Back to original step
@@ -128,17 +130,17 @@ void emit_point_with_width(
float width,
vec4 joint_color,
bool inside_curve,
bool flat_stroke
bool draw_flat
){
// Find unit normal
vec3 unit_normal = flat_stroke ? v_unit_normal[1] : normalize(camera_position - point);
vec3 unit_normal = draw_flat ? v_unit_normal[1] : normalize(camera_position - point);
// Set styling
color = finalize_color(joint_color, point, unit_normal);
// Figure out the step from the point to the corners of the
// triangle strip around the polyline
vec3 step = step_to_corner(point, tangent, unit_normal, joint_angle, inside_curve, flat_stroke);
vec3 step = step_to_corner(point, tangent, unit_normal, joint_angle, inside_curve, draw_flat);
float aaw = max(anti_alias_width * pixel_size, 1e-8);
// Emit two corners
@@ -163,7 +165,7 @@ void main() {
if (vec3(v_stroke_width[0], v_stroke_width[1], v_stroke_width[2]) == vec3(0.0, 0.0, 0.0)) return;
if (vec3(v_color[0].a, v_color[1].a, v_color[2].a) == vec3(0.0, 0.0, 0.0)) return;
bool flat_stroke = bool(flat_stroke_float) || bool(is_fixed_in_frame);
bool draw_flat = bool(flat_stroke) || bool(is_fixed_in_frame);
// Coefficients such that the quadratic bezier is c0 + c1 * t + c2 * t^2
vec3 c0 = verts[0];
@@ -207,7 +209,7 @@ void main() {
emit_point_with_width(
point, tangent, joint_angle,
stroke_width, color,
inside_curve, flat_stroke
inside_curve, draw_flat
);
}
EndPrimitive();

View File

@@ -2,6 +2,7 @@
uniform float frame_scale;
uniform float is_fixed_in_frame;
uniform float scale_stroke_with_zoom;
in vec3 point;
in vec4 stroke_rgba;
@@ -22,7 +23,7 @@ const float STROKE_WIDTH_CONVERSION = 0.01;
void main(){
verts = point;
v_color = stroke_rgba;
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, is_fixed_in_frame);
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, scale_stroke_with_zoom);
v_joint_angle = joint_angle;
v_unit_normal = unit_normal;
}

View File

@@ -1,8 +1,7 @@
#version 330
in vec3 point;
in vec3 du_point;
in vec3 dv_point;
in vec3 d_normal_point;
in vec4 rgba;
out vec4 v_color;
@@ -11,8 +10,10 @@ out vec4 v_color;
#INSERT get_unit_normal.glsl
#INSERT finalize_color.glsl
const float EPSILON = 1e-10;
void main(){
emit_gl_Position(point);
vec3 normal = cross(normalize(du_point - point), normalize(dv_point - point));
v_color = finalize_color(rgba, point, normalize(normal));
vec3 unit_normal = normalize(d_normal_point - point);
v_color = finalize_color(rgba, point, unit_normal);
}

View File

@@ -26,6 +26,7 @@ void main() {
float alpha = smoothstep(-dark_shift, dark_shift, dp);
color = mix(dark_color, color, alpha);
}
if (color.a == 0) discard;
frag_color = finalize_color(
color,

View File

@@ -1,8 +1,7 @@
#version 330
in vec3 point;
in vec3 du_point;
in vec3 dv_point;
in vec3 d_normal_point;
in vec2 im_coords;
in float opacity;
@@ -11,15 +10,17 @@ out vec3 v_unit_normal;
out vec2 v_im_coords;
out float v_opacity;
uniform float is_sphere;
uniform vec3 center;
#INSERT emit_gl_Position.glsl
#INSERT get_unit_normal.glsl
const float EPSILON = 1e-10;
void main(){
v_point = point;
v_unit_normal = normalize(cross(
normalize(du_point - point),
normalize(dv_point - point)
));
v_unit_normal = normalize(d_normal_point - point);;
v_im_coords = im_coords;
v_opacity = opacity;
emit_gl_Position(point);

View File

@@ -13,7 +13,7 @@ in vec2 uv_coords;
out vec4 frag_color;
// This include a delaration of uniform vec3 shading
// This includes a declaration of uniform vec3 shading
#INSERT finalize_color.glsl
void main() {

View File

@@ -24,7 +24,8 @@ default:
\usepackage{pifont}
\DisableLigatures{encoding = *, family = * }
\linespread{1}
%% Borrowed from https://tex.stackexchange.com/questions/6058/making-a-shorter-minus
\DeclareMathSymbol{\minus}{\mathbin}{AMSa}{"39}
ctex:
description: ""
compiler: xelatex

View File

@@ -1,15 +1,15 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Union, Tuple, Annotated, Literal, Iterable
from typing import Union, Tuple, Annotated, Literal, Iterable, Dict
from colour import Color
import numpy as np
import re
try:
from typing import Self
except ImportError:
from typing_extensions import Self
except ImportError:
from typing import Self
# Abbreviations for a common types
ManimColor = Union[str, Color, None]
@@ -28,7 +28,7 @@ if TYPE_CHECKING:
# These are various alternate names for np.ndarray meant to specify
# certain shapes.
#
#
# In theory, these annotations could be used to check arrays sizes
# at runtime, but at the moment nothing actually uses them, and
# the names are here primarily to enhance readibility and allow
@@ -40,6 +40,7 @@ if TYPE_CHECKING:
Vect4 = Annotated[FloatArray, Literal[4]]
VectN = Annotated[FloatArray, Literal["N"]]
Matrix3x3 = Annotated[FloatArray, Literal[3, 3]]
VectArray = Annotated[FloatArray, Literal["N", 1]]
Vect2Array = Annotated[FloatArray, Literal["N", 2]]
Vect3Array = Annotated[FloatArray, Literal["N", 3]]
Vect4Array = Annotated[FloatArray, Literal["N", 4]]

34
manimlib/utils/cache.py Normal file
View File

@@ -0,0 +1,34 @@
from __future__ import annotations
import os
from diskcache import Cache
from contextlib import contextmanager
from functools import wraps
from manimlib.utils.directories import get_cache_dir
from manimlib.utils.simple_functions import hash_string
from typing import TYPE_CHECKING
if TYPE_CHECKING:
T = TypeVar('T')
CACHE_SIZE = 1e9 # 1 Gig
_cache = Cache(get_cache_dir(), size_limit=CACHE_SIZE)
def cache_on_disk(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args, **kwargs):
key = hash_string(f"{func.__name__}{args}{kwargs}")
value = _cache.get(key)
if value is None:
value = func(*args, **kwargs)
_cache.set(key, value)
return value
return wrapper
def clear_cache():
_cache.clear()

View File

@@ -5,6 +5,7 @@ from colour import hex2rgb
from colour import rgb2hex
import numpy as np
import random
from matplotlib import pyplot
from manimlib.constants import COLORMAP_3B1B
from manimlib.constants import WHITE
@@ -14,8 +15,8 @@ from manimlib.utils.iterables import resize_with_interpolation
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterable, Sequence
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array
from typing import Iterable, Sequence, Callable
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array, NDArray
def color_to_rgb(color: ManimColor) -> Vect3:
@@ -77,19 +78,25 @@ def int_to_hex(rgb_int: int) -> str:
def color_gradient(
reference_colors: Iterable[ManimColor],
length_of_output: int
length_of_output: int,
interp_by_hsl: bool = False,
) -> list[Color]:
if length_of_output == 0:
return []
rgbs = list(map(color_to_rgb, reference_colors))
alphas = np.linspace(0, (len(rgbs) - 1), length_of_output)
n_ref_colors = len(reference_colors)
alphas = np.linspace(0, (n_ref_colors - 1), length_of_output)
floors = alphas.astype('int')
alphas_mod1 = alphas % 1
# End edge case
alphas_mod1[-1] = 1
floors[-1] = len(rgbs) - 2
floors[-1] = n_ref_colors - 2
return [
rgb_to_color(np.sqrt(interpolate(rgbs[i]**2, rgbs[i + 1]**2, alpha)))
interpolate_color(
reference_colors[i],
reference_colors[i + 1],
alpha,
interp_by_hsl=interp_by_hsl,
)
for i, alpha in zip(floors, alphas_mod1)
]
@@ -97,10 +104,16 @@ def color_gradient(
def interpolate_color(
color1: ManimColor,
color2: ManimColor,
alpha: float
alpha: float,
interp_by_hsl: bool = False,
) -> Color:
rgb = np.sqrt(interpolate(color_to_rgb(color1)**2, color_to_rgb(color2)**2, alpha))
return rgb_to_color(rgb)
if interp_by_hsl:
hsl1 = np.array(Color(color1).get_hsl())
hsl2 = np.array(Color(color2).get_hsl())
return Color(hsl=interpolate(hsl1, hsl2, alpha))
else:
rgb = np.sqrt(interpolate(color_to_rgb(color1)**2, color_to_rgb(color2)**2, alpha))
return rgb_to_color(rgb)
def interpolate_color_by_hsl(
@@ -108,9 +121,7 @@ def interpolate_color_by_hsl(
color2: ManimColor,
alpha: float
) -> Color:
hsl1 = np.array(Color(color1).get_hsl())
hsl2 = np.array(Color(color2).get_hsl())
return Color(hsl=interpolate(hsl1, hsl2, alpha))
return interpolate_color(color1, color2, alpha, interp_by_hsl=True)
def average_color(*colors: ManimColor) -> Color:
@@ -134,6 +145,33 @@ def random_bright_color(
))
def get_colormap_from_colors(colors: Iterable[ManimColor]) -> Callable[[Sequence[float]], Vect4Array]:
"""
Returns a funciton which takes in values between 0 and 1, and returns
a corresponding list of rgba values
"""
rgbas = np.array([color_to_rgba(color) for color in colors])
def func(values):
alphas = np.clip(values, 0, 1)
scaled_alphas = alphas * (len(rgbas) - 1)
indices = scaled_alphas.astype(int)
next_indices = np.clip(indices + 1, 0, len(rgbas) - 1)
inter_alphas = scaled_alphas % 1
inter_alphas = inter_alphas.repeat(4).reshape((len(indices), 4))
result = interpolate(rgbas[indices], rgbas[next_indices], inter_alphas)
return result
return func
def get_color_map(map_name: str) -> Callable[[Sequence[float]], Vect4Array]:
if map_name == "3b1b_colormap":
return get_colormap_from_colors(COLORMAP_3B1B)
return pyplot.get_cmap(map_name)
# Delete this?
def get_colormap_list(
map_name: str = "viridis",
n_colors: int = 9

View File

@@ -1,24 +0,0 @@
import os
import tempfile
from manimlib.config import get_custom_config
from manimlib.config import get_manim_dir
CUSTOMIZATION = {}
def get_customization():
if not CUSTOMIZATION:
CUSTOMIZATION.update(get_custom_config())
directories = CUSTOMIZATION["directories"]
# Unless user has specified otherwise, use the system default temp
# directory for storing tex files, mobject_data, etc.
if not directories["temporary_storage"]:
directories["temporary_storage"] = tempfile.gettempdir()
# Assumes all shaders are written into manimlib/shaders
directories["shaders"] = os.path.join(
get_manim_dir(), "manimlib", "shaders"
)
return CUSTOMIZATION

View File

@@ -20,29 +20,3 @@ def merge_dicts_recursively(*dicts):
else:
result[key] = value
return result
def soft_dict_update(d1, d2):
"""
Adds key values pairs of d2 to d1 only when d1 doesn't
already have that key
"""
for key, value in list(d2.items()):
if key not in d1:
d1[key] = value
def dict_eq(d1, d2):
if len(d1) != len(d2):
return False
for key in d1:
value1 = d1[key]
value2 = d2[key]
if type(value1) != type(value2):
return False
if type(d1[key]) == np.ndarray:
if any(d1[key] != d2[key]):
return False
elif d1[key] != d2[key]:
return False
return True

View File

@@ -1,33 +1,29 @@
from __future__ import annotations
import os
import tempfile
import appdirs
from manimlib.utils.customization import get_customization
from manimlib.config import manim_config
from manimlib.config import get_manim_dir
from manimlib.utils.file_ops import guarantee_existence
def get_directories() -> dict[str, str]:
return get_customization()["directories"]
return manim_config.directories
def get_cache_dir() -> str:
return get_directories()["cache"] or appdirs.user_cache_dir("manim")
def get_temp_dir() -> str:
return get_directories()["temporary_storage"]
def get_tex_dir() -> str:
return guarantee_existence(os.path.join(get_temp_dir(), "Tex"))
def get_text_dir() -> str:
return guarantee_existence(os.path.join(get_temp_dir(), "Text"))
def get_mobject_data_dir() -> str:
return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data"))
return get_directories()["temporary_storage"] or tempfile.gettempdir()
def get_downloads_dir() -> str:
return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads"))
return get_directories()["downloads"] or appdirs.user_cache_dir("manim_downloads")
def get_output_dir() -> str:
@@ -47,4 +43,4 @@ def get_sound_dir() -> str:
def get_shader_dir() -> str:
return get_directories()["shaders"]
return os.path.join(get_manim_dir(), "manimlib", "shaders")

View File

@@ -1,9 +1,15 @@
from __future__ import annotations
import os
from pathlib import Path
import hashlib
import numpy as np
import validators
import urllib.request
import manimlib.utils.directories
from manimlib.utils.simple_functions import hash_string
from typing import TYPE_CHECKING
@@ -11,81 +17,41 @@ if TYPE_CHECKING:
from typing import Iterable
def add_extension_if_not_present(file_name: str, extension: str) -> str:
# This could conceivably be smarter about handling existing differing extensions
if(file_name[-len(extension):] != extension):
return file_name + extension
else:
return file_name
def guarantee_existence(path: str) -> str:
if not os.path.exists(path):
os.makedirs(path)
return os.path.abspath(path)
def guarantee_existence(path: str | Path) -> Path:
path = Path(path)
path.mkdir(parents=True, exist_ok=True)
return path.absolute()
def find_file(
file_name: str,
directories: Iterable[str] | None = None,
extensions: Iterable[str] | None = None
) -> str:
) -> Path:
# Check if this is a file online first, and if so, download
# it to a temporary directory
if validators.url(file_name):
import urllib.request
from manimlib.utils.directories import get_downloads_dir
stem, name = os.path.split(file_name)
folder = get_downloads_dir()
path = os.path.join(folder, name)
suffix = Path(file_name).suffix
file_hash = hash_string(file_name)
folder = manimlib.utils.directories.get_downloads_dir()
path = Path(folder, file_hash).with_suffix(suffix)
urllib.request.urlretrieve(file_name, path)
return path
# Check if what was passed in is already a valid path to a file
if os.path.exists(file_name):
return file_name
return Path(file_name)
# Otherwise look in local file system
directories = directories or [""]
extensions = extensions or [""]
possible_paths = (
os.path.join(directory, file_name + extension)
Path(directory, file_name + extension)
for directory in directories
for extension in extensions
)
for path in possible_paths:
if os.path.exists(path):
if path.exists():
return path
raise IOError(f"{file_name} not Found")
def get_sorted_integer_files(
directory: str,
min_index: float = 0,
max_index: float = np.inf,
remove_non_integer_files: bool = False,
remove_indices_greater_than: float | None = None,
extension: str | None = None,
) -> list[str]:
indexed_files = []
for file in os.listdir(directory):
if '.' in file:
index_str = file[:file.index('.')]
else:
index_str = file
full_path = os.path.join(directory, file)
if index_str.isdigit():
index = int(index_str)
if remove_indices_greater_than is not None:
if index > remove_indices_greater_than:
os.remove(full_path)
continue
if extension is not None and not file.endswith(extension):
continue
if index >= min_index and index < max_index:
indexed_files.append((index, file))
elif remove_non_integer_files:
os.remove(full_path)
indexed_files.sort(key=lambda p: p[0])
return list(map(lambda p: os.path.join(directory, p[1]), indexed_files))

View File

@@ -1,154 +0,0 @@
from __future__ import annotations
import importlib
import inspect
import os
import yaml
from rich import box
from rich.console import Console
from rich.prompt import Confirm
from rich.prompt import Prompt
from rich.rule import Rule
from rich.table import Table
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
def get_manim_dir() -> str:
manimlib_module = importlib.import_module("manimlib")
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
return os.path.abspath(os.path.join(manimlib_dir, ".."))
def remove_empty_value(dictionary: dict[str, Any]) -> None:
for key in list(dictionary.keys()):
if dictionary[key] == "":
dictionary.pop(key)
elif isinstance(dictionary[key], dict):
remove_empty_value(dictionary[key])
def init_customization() -> None:
configuration = {
"directories": {
"mirror_module_path": False,
"output": "",
"raster_images": "",
"vector_images": "",
"sounds": "",
"temporary_storage": "",
},
"universal_import_line": "from manimlib import *",
"style": {
"tex_template": "",
"font": "Consolas",
"background_color": "",
},
"window_position": "UR",
"window_monitor": 0,
"full_screen": False,
"break_into_partial_movies": False,
"camera_resolutions": {
"low": "854x480",
"medium": "1280x720",
"high": "1920x1080",
"4k": "3840x2160",
"default_resolution": "",
},
"fps": 30,
}
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"
)
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
)
console.print("[bold]Styles:[/bold]")
style_config = configuration["style"]
tex_template = Prompt.ask(
" Select a TeX template to compile a LaTeX source file",
default="default"
)
style_config["tex_template"] = tex_template
style_config["background_color"] = Prompt.ask(
" Which [bold]background color[/bold] do you want [italic](hex code)",
default="#333333"
)
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_resolutions"]["default_resolution"] = Prompt.ask(
" Which one to choose as the default rendering quality",
choices=["low", "medium", "high", "ultra_high"],
default="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
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]")
except KeyboardInterrupt:
console.print("\n[green]Exit configuration guide[/green]")

View File

@@ -13,10 +13,7 @@ from manimlib.utils.file_ops import find_file
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Sequence, Optional, Tuple
from manimlib.typing import UniformDict
from moderngl.vertex_array import VertexArray
from moderngl.framebuffer import Framebuffer
from typing import Sequence, Optional
# Global maps to reflect uniform status

View File

@@ -36,12 +36,6 @@ def get_num_args(function: Callable) -> int:
def get_parameters(function: Callable) -> Iterable[str]:
return inspect.signature(function).parameters.keys()
# Just to have a less heavyweight name for this extremely common operation
#
# We may wish to have more fine-grained control over division by zero behavior
# in the future (separate specifiable values for 0/0 and x/0 with x != 0),
# but for now, we just allow the option to handle indeterminate 0/0.
def clip(a: float, min_a: float, max_a: float) -> float:
if a < min_a:
@@ -58,6 +52,10 @@ def arr_clip(arr: np.ndarray, min_a: float, max_a: float) -> np.ndarray:
def fdiv(a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None) -> Scalable:
"""
Less heavyweight name for np.true_divide, enabling
default behavior for 0/0
"""
if zero_over_zero_value is not None:
out = np.full_like(a, zero_over_zero_value)
where = np.logical_or(a != 0, b != 0)
@@ -68,11 +66,13 @@ def fdiv(a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None)
return np.true_divide(a, b, out=out, where=where)
def binary_search(function: Callable[[float], float],
target: float,
lower_bound: float,
upper_bound: float,
tolerance:float = 1e-4) -> float | None:
def binary_search(
function: Callable[[float], float],
target: float,
lower_bound: float,
upper_bound: float,
tolerance:float = 1e-4
) -> float | None:
lh = lower_bound
rh = upper_bound
mh = (lh + rh) / 2
@@ -96,7 +96,6 @@ def binary_search(function: Callable[[float], float],
return mh
def hash_string(string: str) -> str:
# Truncating at 16 bytes for cleanliness
def hash_string(string: str, n_bytes=16) -> str:
hasher = hashlib.sha256(string.encode())
return hasher.hexdigest()[:16]
return hasher.hexdigest()[:n_bytes]

View File

@@ -1,5 +1,9 @@
from __future__ import annotations
import subprocess
import threading
import platform
from manimlib.utils.directories import get_sound_dir
from manimlib.utils.file_ops import find_file
@@ -10,3 +14,27 @@ def get_full_sound_file_path(sound_file_name: str) -> str:
directories=[get_sound_dir()],
extensions=[".wav", ".mp3", ""]
)
def play_sound(sound_file):
"""Play a sound file using the system's audio player"""
full_path = get_full_sound_file_path(sound_file)
system = platform.system()
if system == "Windows":
# Windows
subprocess.Popen(
["powershell", "-c", f"(New-Object Media.SoundPlayer '{full_path}').PlaySync()"],
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
elif system == "Darwin":
# macOS
subprocess.Popen(
["afplay", full_path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
else:
subprocess.Popen(
["aplay", full_path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)

View File

@@ -48,6 +48,10 @@ def get_norm(vect: VectN | List[float]) -> float:
return sum((x**2 for x in vect))**0.5
def get_dist(vect1: VectN, vect2: VectN):
return get_norm(vect2 - vect1)
def normalize(
vect: VectN | List[float],
fall_back: VectN | List[float] | None = None

View File

@@ -1,40 +1,41 @@
from __future__ import annotations
import re
from functools import lru_cache
from manimlib.utils.tex_to_symbol_count import TEX_TO_SYMBOL_COUNT
@lru_cache
def num_tex_symbols(tex: str) -> int:
tex = remove_tex_environments(tex)
commands_pattern = r"""
(?P<sqrt>\\sqrt\[[0-9]+\])| # Special sqrt with number
(?P<escaped_brace>\\[{}])| # Escaped braces
(?P<cmd>\\[a-zA-Z!,-/:;<>]+) # Regular commands
"""
This function attempts to estimate the number of symbols that
a given string of tex would produce.
Warning, it may not behave perfectly
"""
# First, remove patterns like \begin{align}, \phantom{thing},
# \begin{array}{cc}, etc.
pattern = "|".join(
rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?"
for s in ["begin", "end", "phantom"]
)
tex = re.sub(pattern, "", tex)
# Progressively count the symbols associated with certain tex commands,
# and remove those commands from the string, adding the number of symbols
# that command creates
total = 0
pos = 0
for match in re.finditer(commands_pattern, tex, re.VERBOSE):
# Count normal characters up to this command
total += sum(1 for c in tex[pos:match.start()] if c not in "^{} \n\t_$\\&")
# Start with the special case \sqrt[number]
for substr in re.findall(r"\\sqrt\[[0-9]+\]", tex):
total += len(substr) - 5 # e.g. \sqrt[3] is 3 symbols
tex = tex.replace(substr, " ")
general_command = r"\\[a-zA-Z!,-/:;<>]+"
for substr in re.findall(general_command, tex):
total += TEX_TO_SYMBOL_COUNT.get(substr, 1)
tex = tex.replace(substr, " ")
if match.group("sqrt"):
total += len(match.group()) - 5
elif match.group("escaped_brace"):
total += 1 # Count escaped brace as one symbol
else:
total += TEX_TO_SYMBOL_COUNT.get(match.group(), 1)
pos = match.end()
# Count remaining characters
total += sum(map(lambda c: c not in "^{} \n\t_$\\&", tex))
total += sum(1 for c in tex[pos:] if c not in "^{} \n\t_$\\&")
return total
def remove_tex_environments(tex: str) -> str:
# Handle \phantom{...} with any content
tex = re.sub(r"\\phantom\{[^}]*\}", "", tex)
# Handle other environment commands
tex = re.sub(r"\\(begin|end)(\{\w+\})?(\{\w+\})?(\[\w+\])?", "", tex)
return tex

View File

@@ -1,72 +1,44 @@
from __future__ import annotations
from contextlib import contextmanager
import os
import re
import yaml
import subprocess
from functools import lru_cache
from manimlib.config import get_custom_config
from pathlib import Path
import tempfile
from manimlib.utils.cache import cache_on_disk
from manimlib.config import manim_config
from manimlib.config import get_manim_dir
from manimlib.logger import log
from manimlib.utils.directories import get_tex_dir
from manimlib.utils.simple_functions import hash_string
SAVED_TEX_CONFIG = {}
def get_tex_template_config(template_name: str) -> dict[str, str]:
name = template_name.replace(" ", "_").lower()
with open(os.path.join(
get_manim_dir(), "manimlib", "tex_templates.yml"
), encoding="utf-8") as tex_templates_file:
template_path = os.path.join(get_manim_dir(), "manimlib", "tex_templates.yml")
with open(template_path, encoding="utf-8") as tex_templates_file:
templates_dict = yaml.safe_load(tex_templates_file)
if name not in templates_dict:
log.warning(
"Cannot recognize template '%s', falling back to 'default'.",
name
)
log.warning(f"Cannot recognize template {name}, falling back to 'default'.")
name = "default"
return templates_dict[name]
def get_tex_config() -> dict[str, str]:
@lru_cache
def get_tex_config(template: str = "") -> tuple[str, str]:
"""
Returns a dict which should look something like this:
{
"template": "default",
"compiler": "latex",
"preamble": "..."
}
Returns a compiler and preamble to use for rendering LaTeX
"""
# Only load once, then save thereafter
if not SAVED_TEX_CONFIG:
template_name = get_custom_config()["style"]["tex_template"]
template_config = get_tex_template_config(template_name)
SAVED_TEX_CONFIG.update({
"template": template_name,
"compiler": template_config["compiler"],
"preamble": template_config["preamble"]
})
return SAVED_TEX_CONFIG
template = template or manim_config.tex.template
config = get_tex_template_config(template)
return config["compiler"], config["preamble"]
def tex_content_to_svg_file(
content: str, template: str, additional_preamble: str,
short_tex: str
) -> str:
tex_config = get_tex_config()
if not template or template == tex_config["template"]:
compiler = tex_config["compiler"]
preamble = tex_config["preamble"]
else:
config = get_tex_template_config(template)
compiler = config["compiler"]
preamble = config["preamble"]
if additional_preamble:
preamble += "\n" + additional_preamble
full_tex = "\n\n".join((
def get_full_tex(content: str, preamble: str = ""):
return "\n\n".join((
"\\documentclass[preview]{standalone}",
preamble,
"\\begin{document}",
@@ -74,90 +46,105 @@ def tex_content_to_svg_file(
"\\end{document}"
)) + "\n"
svg_file = os.path.join(
get_tex_dir(), hash_string(full_tex) + ".svg"
)
if not os.path.exists(svg_file):
# If svg doesn't exist, create it
with display_during_execution("Writing " + short_tex):
create_tex_svg(full_tex, svg_file, compiler)
return svg_file
@lru_cache(maxsize=128)
def latex_to_svg(
latex: str,
template: str = "",
additional_preamble: str = "",
short_tex: str = "",
show_message_during_execution: bool = True,
) -> str:
"""Convert LaTeX string to SVG string.
Args:
latex: LaTeX source code
template: Path to a template LaTeX file
additional_preamble: String including any added "\\usepackage{...}" style imports
Returns:
str: SVG source code
Raises:
LatexError: If LaTeX compilation fails
NotImplementedError: If compiler is not supported
"""
if show_message_during_execution:
message = f"Writing {(short_tex or latex)[:70]}..."
else:
message = ""
compiler, preamble = get_tex_config(template)
preamble = "\n".join([preamble, additional_preamble])
full_tex = get_full_tex(latex, preamble)
return full_tex_to_svg(full_tex, compiler, message)
def create_tex_svg(full_tex: str, svg_file: str, compiler: str) -> None:
@cache_on_disk
def full_tex_to_svg(full_tex: str, compiler: str = "latex", message: str = ""):
if message:
print(message, end="\r")
if compiler == "latex":
program = "latex"
dvi_ext = ".dvi"
elif compiler == "xelatex":
program = "xelatex -no-pdf"
dvi_ext = ".xdv"
else:
raise NotImplementedError(
f"Compiler '{compiler}' is not implemented"
raise NotImplementedError(f"Compiler '{compiler}' is not implemented")
# Write intermediate files to a temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
tex_path = Path(temp_dir, "working").with_suffix(".tex")
dvi_path = tex_path.with_suffix(dvi_ext)
# Write tex file
tex_path.write_text(full_tex)
# Run latex compiler
process = subprocess.run(
[
compiler,
*(['-no-pdf'] if compiler == "xelatex" else []),
"-interaction=batchmode",
"-halt-on-error",
f"-output-directory={temp_dir}",
tex_path
],
capture_output=True,
text=True
)
# Write tex file
root, _ = os.path.splitext(svg_file)
with open(root + ".tex", "w", encoding="utf-8") as tex_file:
tex_file.write(full_tex)
if process.returncode != 0:
# Handle error
error_str = ""
log_path = tex_path.with_suffix(".log")
if log_path.exists():
content = log_path.read_text()
error_match = re.search(r"(?<=\n! ).*\n.*\n", content)
if error_match:
error_str = error_match.group()
raise LatexError(error_str or "LaTeX compilation failed")
# tex to dvi
if os.system(" ".join((
program,
"-interaction=batchmode",
"-halt-on-error",
f"-output-directory=\"{os.path.dirname(svg_file)}\"",
f"\"{root}.tex\"",
">",
os.devnull
))):
log.error(
"LaTeX Error! Not a worry, it happens to the best of us."
# Run dvisvgm and capture output directly
process = subprocess.run(
[
"dvisvgm",
dvi_path,
"-n", # no fonts
"-v", "0", # quiet
"--stdout", # output to stdout instead of file
],
capture_output=True
)
error_str = ""
with open(root + ".log", "r", encoding="utf-8") as log_file:
error_match_obj = re.search(r"(?<=\n! ).*\n.*\n", log_file.read())
if error_match_obj:
error_str = error_match_obj.group()
log.debug(
f"The error could be:\n`{error_str}`",
)
raise LatexError(error_str)
# dvi to svg
os.system(" ".join((
"dvisvgm",
f"\"{root}{dvi_ext}\"",
"-n",
"-v",
"0",
"-o",
f"\"{svg_file}\"",
">",
os.devnull
)))
# Return SVG string
result = process.stdout.decode('utf-8')
# Cleanup superfluous documents
for ext in (".tex", dvi_ext, ".log", ".aux"):
try:
os.remove(root + ext)
except FileNotFoundError:
pass
if message:
print(" " * len(message), end="\r")
# TODO, perhaps this should live elsewhere
@contextmanager
def display_during_execution(message: str):
# Merge into a single line
to_print = message.replace("\n", " ")
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
finally:
print(" " * len(to_print), end="\r")
return result
class LatexError(Exception):

View File

@@ -104,6 +104,7 @@ TEX_TO_SYMBOL_COUNT = {
R"\mapsto": 2,
R"\markright": 0,
R"\mathds": 0,
R"\mathcal": 0,
R"\max": 3,
R"\mbox": 0,
R"\medskip": 0,

View File

@@ -5,17 +5,20 @@ import numpy as np
import moderngl_window as mglw
from moderngl_window.context.pyglet.window import Window as PygletWindow
from moderngl_window.timers.clock import Timer
from screeninfo import get_monitors
from functools import wraps
import screeninfo
from manimlib.constants import ASPECT_RATIO
from manimlib.constants import FRAME_SHAPE
from manimlib.utils.customization import get_customization
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, TypeVar, Optional
from manimlib.scene.scene import Scene
T = TypeVar("T")
class Window(PygletWindow):
fullscreen: bool = False
@@ -26,28 +29,84 @@ class Window(PygletWindow):
def __init__(
self,
scene: Scene,
size: tuple[int, int] = (1280, 720),
scene: Optional[Scene] = None,
position_string: str = "UR",
monitor_index: int = 1,
full_screen: bool = False,
size: Optional[tuple[int, int]] = None,
position: Optional[tuple[int, int]] = None,
samples: int = 0
):
scene.window = self
super().__init__(size=size, samples=samples)
self.default_size = size
self.default_position = self.find_initial_position(size)
self.scene = scene
self.monitor = self.get_monitor(monitor_index)
self.default_size = size or self.get_default_size(full_screen)
self.default_position = position or self.position_from_string(position_string)
self.pressed_keys = set()
self.title = str(scene)
self.size = size
super().__init__(samples=samples)
self.to_default_position()
if self.scene:
self.init_for_scene(scene)
def init_for_scene(self, scene: Scene):
"""
Resets the state and updates the scene associated to this window.
This is necessary when we want to reuse an *existing* window after a
`scene.reload()` was requested, which will create new scene instances.
"""
self.pressed_keys.clear()
self._has_undrawn_event = True
mglw.activate_context(window=self)
self.scene = scene
self.title = str(scene)
self.init_mgl_context()
self.timer = Timer()
self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer)
mglw.activate_context(window=self, ctx=self.ctx)
self.timer.start()
self.to_default_position()
# This line seems to resync the viewport
self.on_resize(*self.size)
def get_monitor(self, index):
try:
monitors = screeninfo.get_monitors()
return monitors[min(index, len(monitors) - 1)]
except screeninfo.ScreenInfoError:
# Default fallback
return screeninfo.Monitor(width=1920, height=1080)
def get_default_size(self, full_screen=False):
width = self.monitor.width // (1 if full_screen else 2)
height = int(width // ASPECT_RATIO)
return (width, height)
def position_from_string(self, position_string):
# Alternatively, it might be specified with a string like
# UR, OO, DL, etc. specifying what corner it should go to
char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2}
size = self.default_size
width_diff = self.monitor.width - size[0]
height_diff = self.monitor.height - size[1]
x_step = char_to_n[position_string[1]] * width_diff // 2
y_step = char_to_n[position_string[0]] * height_diff // 2
return (self.monitor.x + x_step, -self.monitor.y + y_step)
def focus(self):
"""
Puts focus on this window by hiding and showing it again.
Note that the pyglet `activate()` method didn't work as expected here,
so that's why we have to use this workaround. This will produce a small
flicker on the window but at least reliably focuses it. It may also
offset the window position slightly.
"""
self._window.set_visible(False)
self._window.set_visible(True)
def to_default_position(self):
self.position = self.default_position
@@ -57,27 +116,6 @@ class Window(PygletWindow):
self.size = (w - 1, h - 1)
self.size = (w, h)
def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]:
custom_position = get_customization()["window_position"]
monitors = get_monitors()
mon_index = get_customization()["window_monitor"]
monitor = monitors[min(mon_index, len(monitors) - 1)]
window_width, window_height = size
# Position might be specified with a string of the form
# x,y for integers x and y
if "," in custom_position:
return tuple(map(int, custom_position.split(",")))
# Alternatively, it might be specified with a string like
# UR, OO, DL, etc. specifying what corner it should go to
char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2}
width_diff = monitor.width - window_width
height_diff = monitor.height - window_height
return (
monitor.x + char_to_n[custom_position[1]] * width_diff // 2,
-monitor.y + char_to_n[custom_position[0]] * height_diff // 2,
)
# Delegate event handling to scene
def pixel_coords_to_space_coords(
self,
@@ -85,7 +123,7 @@ class Window(PygletWindow):
py: int,
relative: bool = False
) -> np.ndarray:
if not hasattr(self.scene, "frame"):
if self.scene is None or not hasattr(self.scene, "frame"):
return np.zeros(3)
pixel_shape = np.array(self.size)
@@ -116,6 +154,8 @@ class Window(PygletWindow):
@note_undrawn_event
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
super().on_mouse_motion(x, y, dx, dy)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
self.scene.on_mouse_motion(point, d_point)
@@ -123,6 +163,8 @@ class Window(PygletWindow):
@note_undrawn_event
def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None:
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True)
self.scene.on_mouse_drag(point, d_point, buttons, modifiers)
@@ -130,18 +172,24 @@ class Window(PygletWindow):
@note_undrawn_event
def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None:
super().on_mouse_press(x, y, button, mods)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
self.scene.on_mouse_press(point, button, mods)
@note_undrawn_event
def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None:
super().on_mouse_release(x, y, button, mods)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
self.scene.on_mouse_release(point, button, mods)
@note_undrawn_event
def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None:
super().on_mouse_scroll(x, y, x_offset, y_offset)
if not self.scene:
return
point = self.pixel_coords_to_space_coords(x, y)
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
self.scene.on_mouse_scroll(point, offset, x_offset, y_offset)
@@ -150,32 +198,44 @@ class Window(PygletWindow):
def on_key_press(self, symbol: int, modifiers: int) -> None:
self.pressed_keys.add(symbol) # Modifiers?
super().on_key_press(symbol, modifiers)
if not self.scene:
return
self.scene.on_key_press(symbol, modifiers)
@note_undrawn_event
def on_key_release(self, symbol: int, modifiers: int) -> None:
self.pressed_keys.difference_update({symbol}) # Modifiers?
super().on_key_release(symbol, modifiers)
if not self.scene:
return
self.scene.on_key_release(symbol, modifiers)
@note_undrawn_event
def on_resize(self, width: int, height: int) -> None:
super().on_resize(width, height)
if not self.scene:
return
self.scene.on_resize(width, height)
@note_undrawn_event
def on_show(self) -> None:
super().on_show()
if not self.scene:
return
self.scene.on_show()
@note_undrawn_event
def on_hide(self) -> None:
super().on_hide()
if not self.scene:
return
self.scene.on_hide()
@note_undrawn_event
def on_close(self) -> None:
super().on_close()
if not self.scene:
return
self.scene.on_close()
def is_key_pressed(self, symbol: int) -> bool:

View File

@@ -1,8 +1,12 @@
addict
appdirs
audioop-lts; python_version>='3.13'
colour
diskcache
ipython>=8.18.0
isosurfaces
fontTools
manimpango>=0.4.0.post0,<0.5.0
manimpango>=0.6.0
mapbox-earcut
matplotlib
moderngl
@@ -13,11 +17,11 @@ pydub
pygments
PyOpenGL
pyperclip
pyrr
pyyaml
rich
scipy
screeninfo
setuptools
skia-pathops
svgelements>=1.8.1
sympy

View File

@@ -1,6 +1,6 @@
[metadata]
name = manimgl
version = 1.6.1
version = 1.7.2
author = Grant Sanderson
author_email= grant@3blue1brown.com
description = Animation engine for explanatory math videos
@@ -29,10 +29,15 @@ classifiers =
packages = find:
include_package_data = True
install_requires =
addict
appdirs
audioop-lts; python_version >= "3.13"
colour
ipython
diskcache
ipython>=8.18.0
isosurfaces
manimpango>=0.4.0.post0,<0.5.0
fontTools
manimpango>=0.6.0
mapbox-earcut
matplotlib
moderngl
@@ -47,10 +52,12 @@ install_requires =
rich
scipy
screeninfo
setuptools
skia-pathops
svgelements>=1.8.1
sympy
tqdm
typing-extensions; python_version < "3.11"
validators
[options.entry_points]