1981 Commits

Author SHA1 Message Date
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
Grant Sanderson
d499544366 Merge pull request #2180 from fofoni/fix-ipython-scope
Fix `NameError` when a pasted function attempts to access a variable in its outer scope
2024-10-13 20:57:19 -05:00
Pedro Fonini
2dd0256af6 Instantiate the IPython shell without a local ns 2024-10-12 22:02:43 -03:00
Grant Sanderson
d4080b8978 Merge pull request #2179 from 3b1b/3b1b-patch-1
Reference workflow documentation
2024-10-12 15:42:35 -07:00
Grant Sanderson
23c9e67fa4 Reference workflow documentation 2024-10-12 15:42:25 -07:00
Grant Sanderson
cce4ffbb60 Merge pull request #2177 from 3b1b/video-work
Fix issue with Difference
2024-10-10 13:20:53 -07:00
Grant Sanderson
99493fc4f7 Fix issue with Difference
https://github.com/3b1b/manim/issues/2174
2024-10-10 15:13:33 -05:00
Grant Sanderson
81b17dd63e Merge pull request #2168 from zhujisheng/patch-1
Update scene.py to make scene.time more accurate
2024-10-10 12:24:05 -07:00
Grant Sanderson
15e5d8a07a Merge pull request #2176 from 3b1b/video-work
Video work
2024-10-10 12:23:19 -07:00
Grant Sanderson
154a473a12 Get rid of globals update locals hack
It seems this issues is no longer there in the case of list constructors(?). Although it still exists for functions defined within a cell, that can be circumvented with more explicit function arguments.
2024-10-10 14:05:43 -05:00
Grant Sanderson
29cb6f76fe Write scene insertions into a subdirectory 2024-10-02 07:24:33 -05:00
Grant Sanderson
09e9e65ba4 Merge branch 'master' of github.com:3b1b/manim into video-work 2024-10-01 13:29:42 -05:00
Grant Sanderson
f69b189f2c Merge pull request #2171 from 3b1b/fix-vert-index-issue
Remove use of gl_VertexID
2024-10-01 11:27:57 -07:00
Grant Sanderson
641c03a95b Change condition for updating VMobject. outer_vert_indices 2024-10-01 13:27:02 -05:00
Grant Sanderson
f737823bac Add VMobject. get_shader_vert_indices 2024-10-01 13:20:23 -05:00
Grant Sanderson
95bb67c47f Change return type for Mobject.get_shader_data 2024-10-01 13:18:40 -05:00
Grant Sanderson
512fb89726 Save VMobject. outer_vert_indices 2024-10-01 13:14:24 -05:00
Grant Sanderson
cf37f34e1f Add copy cursor position option 2024-10-01 13:05:00 -05:00
Grant Sanderson
bddd9c35ea Tiny formatting 2024-10-01 13:04:50 -05:00
Grant Sanderson
f0bf50eb7f Remove use of gl_VertexID 2024-09-28 09:54:28 -05:00
Grant Sanderson
fea7096cbe Change default animation behavior to suspend_mobject_updating=False 2024-09-28 09:48:20 -05:00
zhujisheng
ccb9977a67 Update scene.py to make scene.time more accurate
When the time intervals are (0, dt, 2dt, ...), during the first frame processing, only the video is inserted, but the update parameter is 0, which causes scene.time to be inaccurate. The correct time intervals should be (dt, 2dt, ...)
2024-09-23 23:48:18 +08:00
Grant Sanderson
1f8ad5be16 Fix pfp for null curves 2024-09-21 12:17:10 -04:00
Grant Sanderson
217eb6b486 Move new VectorField from optics projects into main repo 2024-09-21 12:16:09 -04:00
Grant Sanderson
0804109301 Flatten uniform arrays 2024-09-21 12:15:37 -04:00
Grant Sanderson
62a4ea5165 Update description of remove_list_redundancies 2024-09-21 12:15:29 -04:00
Grant Sanderson
3e7244b90b Fix bad argument 2024-09-21 12:15:06 -04:00
Grant Sanderson
95fca885c9 Push clip plane functionality up to all Mobjects 2024-09-17 17:20:19 -05:00
Grant Sanderson
8eac976c8d Tweak moderngl import in ImageMobject 2024-09-06 13:35:32 -05:00
Grant Sanderson
9eda000a97 Merge pull request #1816 from TurkeyBilly/patch-8
fix label squeezed bug
2024-09-06 11:03:38 -07:00
Grant Sanderson
bcf610d1ad Merge branch 'master' into patch-8 2024-09-06 11:03:19 -07:00
Grant Sanderson
df9acfb4d5 Merge pull request #2009 from Varniex/patch-1
Correction of indices
2024-09-06 10:59:04 -07:00
Grant Sanderson
b6e5b0f34a Merge pull request #2036 from LinZezh1/fix_animation
Remove duplicate items
2024-09-06 10:57:32 -07:00
Grant Sanderson
6d23df0497 Clean up changes associated with fixing aspect ratio issue 2024-09-06 12:50:19 -05:00
Grant Sanderson
05a89d754e Merge pull request #2056 from germanzhu/fix-aspect-ratio
fix aspect_ratio other than 16:9 issue
2024-09-06 10:26:07 -07:00
Grant Sanderson
2178ec2b85 Merge branch 'master' into fix-aspect-ratio 2024-09-06 10:24:42 -07:00
Grant Sanderson
1f55832a6a Clean up around z_index 2024-09-06 12:20:26 -05:00
Grant Sanderson
aebf2220a6 Merge pull request #2057 from germanzhu/add-zorder-mobject
add zorder to mobject
2024-09-06 10:11:40 -07:00
Grant Sanderson
08f7cb8d3e zorder -> z_index 2024-09-06 10:11:31 -07:00
Grant Sanderson
c8326d1cce zorder -> z_index 2024-09-06 10:11:26 -07:00
Grant Sanderson
133cec9725 zorder -> z_index 2024-09-06 10:11:21 -07:00
Grant Sanderson
9812503597 Merge pull request #2059 from germanzhu/fix_vshowpassingflash
fix VShowPassingFlash index out of bounds issue
2024-09-06 09:39:06 -07:00
Grant Sanderson
9011c864fd Merge pull request #2061 from floatwoozy/dev
Error effect when rendering the same ImageMobject
2024-09-06 09:35:31 -07:00
Grant Sanderson
6b88947151 Merge pull request #2079 from TangentSquared/master
Modifying requirements.txt to include the latest version of IPython.
2024-09-06 09:31:47 -07:00
Grant Sanderson
30e6c357ed Merge pull request #2138 from MarkHoo/master
Update tex_templates.yml
2024-09-06 09:27:22 -07:00
Grant Sanderson
a5137a05f1 Merge pull request #2134 from jkjkil4/fix-there_and_back_with_pause
fix: `there_and_back_with_pause`
2024-09-06 09:25:31 -07:00
Grant Sanderson
21f9df8ccd Merge pull request #2163 from 3b1b/unify-vbo
Refactor ShaderWrapper
2024-09-06 09:17:48 -07:00
Grant Sanderson
8f1299929f Swap window buffer after blit from another fbo 2024-09-06 11:15:38 -05:00
Grant Sanderson
e7c540f415 Move texture id tracking to ShaderWrapper
Rather than having a globally unique id for each texture, dynamically allocate new texure ids within each ShaderWrapper, so that there is no upper bound on how many textures can be used.
2024-09-06 11:07:38 -05:00
Grant Sanderson
76fdd02db0 Go back to default fill border width of 0, except for String 2024-09-06 09:17:28 -05:00
Grant Sanderson
a713868f3d Add Mobject.get_opacities 2024-09-06 09:12:23 -05:00
Grant Sanderson
2836acc3c7 Have Write default stroke color match that of the mobject 2024-09-06 09:12:14 -05:00
Grant Sanderson
f378d33d01 Use doubled fill canvas so antialiasing doesn't depend on border width 2024-08-30 14:09:38 -05:00
Grant Sanderson
054261d86f Add zero point edge case for point_from_proportion 2024-08-29 14:10:37 -05:00
Grant Sanderson
04d77f2bec Fix joint angle for lines 2024-08-28 11:57:23 -05:00
Grant Sanderson
1e996dcd27 Fix Arrow.set_perpendicular_to_camera 2024-08-28 11:56:25 -05:00
Grant Sanderson
e85a1ce1b7 Add Arrow.set_perpendicular_to_camera 2024-08-28 10:50:51 -05:00
Grant Sanderson
22a6b47ec9 Always recompute unit normals on rotate 2024-08-26 11:46:43 -05:00
Grant Sanderson
55a798676a Always stop skipping after checkpoint_paste 2024-08-26 11:41:50 -05:00
Grant Sanderson
5cf5e497e7 Don't have joint_angles, unit_normal and path_end_indices get refreshed for shift/scale/rotate
Change unit_normal directly in rotate
In general, don't trigger refresh on apply_points_function
2024-08-26 11:41:32 -05:00
Grant Sanderson
7519ce15da Merge branch 'master' of github.com:3b1b/manim into unify-vbo 2024-08-26 10:20:44 -05:00
Grant Sanderson
f86245517d Merge pull request #2147 from osMrPigHead/fix-animation-time_span
fix(animation): `time_span` doesn't work for mobjects with submobjects
2024-08-26 08:20:11 -07:00
Grant Sanderson
36ea70d990 Remove whitespace 2024-08-26 09:14:53 -05:00
Grant Sanderson
cd5c436ce4 Tighter (albeit arbitrary) constraint on path end detection
This is really not great
2024-08-26 09:14:42 -05:00
Grant Sanderson
827f4db5e2 Clarify where normal is being inverted 2024-08-26 09:13:39 -05:00
Grant Sanderson
441ac77eae Change Euler axis limits for xz flat plane 2024-08-26 08:22:29 -05:00
Grant Sanderson
51de1fb650 For some reason RenderGroups must be groups for camera reorientation to function 2024-08-23 14:56:35 -05:00
Grant Sanderson
8785eb1844 Don't have quiet default changes to stroke_behind 2024-08-23 14:46:34 -05:00
Grant Sanderson
a823901b98 Only form render groups from Mobjects of the same type
I.e. bias against forming such groups
2024-08-23 14:45:27 -05:00
Grant Sanderson
513de19657 Clarify that ShaderWrapper id is an int 2024-08-23 14:44:00 -05:00
Grant Sanderson
fcc5dc00f9 Treat objects fixed in frame as having flat stroke 2024-08-23 14:17:26 -05:00
Grant Sanderson
566fc87a60 Don't keep an outer_vert_indices attribute 2024-08-23 14:11:05 -05:00
Grant Sanderson
499803159c Remove methods and tracking associated with triangulation and non-winding fill 2024-08-23 14:08:56 -05:00
Grant Sanderson
487c714d9b Remove stray new line 2024-08-23 13:56:16 -05:00
Grant Sanderson
e939e1de09 Tweak the correction for angle_diffs in get_joint_angles 2024-08-23 13:56:08 -05:00
Grant Sanderson
c26ebfc10f Have is_closed use the last path 2024-08-23 13:55:42 -05:00
Grant Sanderson
5d6a1f30c4 Count joints near 180 degrees as straight 2024-08-23 13:55:10 -05:00
Grant Sanderson
a08523d746 Fix get_subpath_end_indices_from_points 2024-08-23 13:29:31 -05:00
Grant Sanderson
bcafcbf490 Don't have VMobject refresh joint angles on shift, scale and rotate 2024-08-23 12:32:17 -05:00
Grant Sanderson
e4007f6915 Add glow dot to show light in SurfaceExample 2024-08-22 15:31:22 -05:00
Grant Sanderson
ada66ee8fb Remove stray comment 2024-08-22 15:31:10 -05:00
Grant Sanderson
bd2947be28 Use preferred group type for FadeTransform 2024-08-22 15:31:03 -05:00
Grant Sanderson
9a7bfdd1c9 For Surface, calculate normals with neighboring points 2024-08-22 15:21:40 -05:00
Grant Sanderson
c8d5e91422 Rename shader_dtype -> data_dtype 2024-08-22 14:52:35 -05:00
Grant Sanderson
09bed1f8f4 Allow more file_writer configuration in default_config.yml 2024-08-22 14:24:26 -05:00
Grant Sanderson
eda7f81fb9 Update VMobject.is_smooth 2024-08-21 17:22:34 -05:00
Grant Sanderson
d5575cf1ef Change how joint_angles are computed 2024-08-21 17:19:42 -05:00
Grant Sanderson
11df256369 Fix error rect 2024-08-21 16:49:08 -05:00
Grant Sanderson
cfe70ca869 Don't let Write override stroke_behind default 2024-08-21 16:47:51 -05:00
Grant Sanderson
bda894959b Keeps stroke width as a function of pixels, independent of zoom level 2024-08-21 16:12:56 -05:00
Grant Sanderson
d870bb29de Fix the non-flat stroke edge case for tangents near line of sight 2024-08-21 16:12:13 -05:00
Grant Sanderson
442206faad Go back to non-flat-stroke as the default 2024-08-21 16:05:02 -05:00
Grant Sanderson
ceac4fbe1a Fix up FillArrow 2024-08-21 15:58:57 -05:00
Grant Sanderson
22ee13a884 Have FillArrow take the place of StrokeArrow as the main Arrow 2024-08-21 14:52:54 -05:00
Grant Sanderson
eea8416d57 Default to allowing null lines, except for SVGPaths 2024-08-21 14:44:33 -05:00
Grant Sanderson
b3386ad7a7 Add an option for VMobject.add_line to add a null line 2024-08-21 14:29:40 -05:00
Grant Sanderson
c83d03aeb7 Simplify get_subpath_end_indices_from_points 2024-08-21 14:29:16 -05:00
Grant Sanderson
0a89725090 Tweaking what triggers refreshes 2024-08-21 13:57:54 -05:00
Grant Sanderson
0cb7a8f691 Only recompute subpath_end_indices as needed 2024-08-21 13:44:24 -05:00
Grant Sanderson
43f1704f69 Fix computation of joint_angles to handle zero division case 2024-08-21 13:44:00 -05:00
Grant Sanderson
941513d68c Only recompute VMobject.get_unit_normal as needed 2024-08-21 12:26:42 -05:00
Grant Sanderson
9a5386b022 Instead of passing in joint_products to stroke shader, just track joint_angles and pass in global unit normal 2024-08-21 11:59:48 -05:00
Grant Sanderson
67bedc6d1f Fix fill_depth_vert_format 2024-08-21 09:14:23 -05:00
Grant Sanderson
79ec791fc2 Move get_fill_canvas into VShaderWrapper 2024-08-21 09:02:22 -05:00
Grant Sanderson
748780378b Have stroke_behind default to True for VMobjects with fill and no stroke 2024-08-21 08:43:57 -05:00
Grant Sanderson
dfc5f152dd Have border width pre-multiply by alpha, and don't use a separate texture for that border width 2024-08-20 22:03:45 -05:00
Grant Sanderson
d0cb5b4eea Fix normal orientation on Surface 2024-08-20 14:47:47 -05:00
Grant Sanderson
35ce4c6704 Use Mobject.set_uniform in Mobject.set_shading 2024-08-20 14:44:48 -05:00
Grant Sanderson
7ddbd13e38 Ensure mobject uniforms get passed to ShaderWrapper on init 2024-08-20 14:44:09 -05:00
Grant Sanderson
304856e6e0 Remove indices_list argument on ShaderWrapper.read_in 2024-08-20 14:21:37 -05:00
Grant Sanderson
82582d08bd Fewer parameters for stroke frag shader 2024-08-20 13:32:52 -05:00
Grant Sanderson
a8784692e8 Reference new shader file structure 2024-08-20 12:58:53 -05:00
Grant Sanderson
0a313eb119 Reorganize quadratic bezier shader files 2024-08-20 12:58:34 -05:00
Grant Sanderson
5eb5a11499 Don't have absolute stroke width change with frame size (at least temporarily) 2024-08-20 12:36:28 -05:00
Grant Sanderson
0a585b123c Use stash_mobject_pointers on Mobject.deepcopy 2024-08-20 12:15:37 -05:00
Grant Sanderson
910f28f52e Don't have a default flat stroke in set_style 2024-08-20 12:13:18 -05:00
Grant Sanderson
7474ae17b0 Change default to flat stroke, except in 3d situations 2024-08-20 11:52:05 -05:00
Grant Sanderson
b8931e7b9c When shader_id is updated, have ancestors mark data as changed 2024-08-20 11:36:37 -05:00
Grant Sanderson
0414f8786c Use Mobject.set_uniform to reassign flat_stroke 2024-08-20 11:35:42 -05:00
Grant Sanderson
e0191d81d9 Instead of tracking _shaders_initialized, just check if self.shader_wrapper is None 2024-08-20 10:48:43 -05:00
Grant Sanderson
0ac9ee1fbf Don't deepcopy ShaderWrapper 2024-08-20 10:15:53 -05:00
Grant Sanderson
b9645ad196 Only update shader wrapper when stroke_behind genuinely changes 2024-08-20 10:00:52 -05:00
Grant Sanderson
87ca6e56aa Don't treat font size as a uniform 2024-08-20 10:00:07 -05:00
Grant Sanderson
e61957a4e0 Fix wrong check for path_func in last commit 2024-08-20 09:25:30 -05:00
Grant Sanderson
a8ef9629eb More direct lerp in Mobject.interpolate 2024-08-20 09:10:33 -05:00
Grant Sanderson
e796a0c6d6 Remove stray TODO 2024-08-20 09:10:14 -05:00
Grant Sanderson
5ff80ffc6c Fix matplotlib color map import 2024-08-20 08:54:06 -05:00
Grant Sanderson
f12b143d16 Unify get_shader_wrapper_list, and and better subdivide render groups by ShaderWrapper ids 2024-08-20 08:53:51 -05:00
Grant Sanderson
08e33faab8 Ensure scene properly updates on an embed 2024-08-19 21:49:39 -05:00
Grant Sanderson
0b2c59ac6b More explicitly unpack v_base_normal 2024-08-19 21:45:26 -05:00
Grant Sanderson
6223623b40 Ensure border width blends better within filled VMobject for opacity < 1 2024-08-19 21:39:53 -05:00
Grant Sanderson
7217c9fca5 Reorganize VShaderWrapper.render_fill 2024-08-19 20:51:37 -05:00
Grant Sanderson
b288d5301e Get rid of cached _has_stroke and _has_fill parameters 2024-08-19 17:27:47 -05:00
Grant Sanderson
058914fdd2 Mildly more efficient CameraFrame.get_view_matrix() 2024-08-19 16:54:29 -05:00
Grant Sanderson
c064b11e2a Interleave base_point and unit_normal in the same array 2024-08-19 16:50:49 -05:00
Grant Sanderson
b7337f0781 Remove some ShaderWrapper methods which are no longer needed 2024-08-19 14:45:12 -05:00
Grant Sanderson
195264f079 Stop pretending to support non-winding fill in the fill shader.
Possibly, if we want to reintroduce it later on, it should have its own dedicated shader, and maybe a distinct Mobject type too
2024-08-19 14:37:11 -05:00
Grant Sanderson
09d147c8ef Set default border width to 0.5, and keep it for opacities < 1 2024-08-19 14:35:19 -05:00
Grant Sanderson
39bcead679 Clean up 2024-08-19 14:22:10 -05:00
Grant Sanderson
28eba26bee Remove stray new line 2024-08-19 14:09:23 -05:00
Grant Sanderson
3b5d63d2fa Add depth shader to handle winding fill depth test. 2024-08-19 14:09:07 -05:00
Grant Sanderson
4cb16dfc0b Don't apply depth test during winding fill 2024-08-19 11:28:58 -05:00
Grant Sanderson
a12fa0c03d Remove attributes from VMobject which are now handled in VShaderWrapper 2024-08-19 10:04:28 -05:00
Grant Sanderson
4174f314b4 Reorganize, and ensure get_shader_wrapper_list works for general Groups 2024-08-19 09:17:58 -05:00
Grant Sanderson
f2bca0045f Add border width and backstroke 2024-08-19 08:18:45 -05:00
Grant Sanderson
24b160f9f9 Update VMobject shader wrapper
Use a combined VBO
Render with TRIANGLE_STRIP, and ignore every other
2024-08-19 08:05:32 -05:00
Grant Sanderson
f9b9cf69fd Update so that vbo is not recreated on each from, but is read into 2024-08-17 07:11:56 -05:00
Grant Sanderson
0efa96e399 Just use L-inf norm for point equality 2024-08-16 16:18:37 -05:00
Grant Sanderson
12d39ef37c Merge pull request #2161 from 3b1b/video-work
Adjustments to fill antialiasing
2024-08-16 13:25:40 -07:00
Grant Sanderson
ccc84f4ab1 Give Numbers a default border width 2024-08-16 15:21:29 -05:00
Grant Sanderson
bb42b66201 Include fill border width in style 2024-08-16 15:21:20 -05:00
Grant Sanderson
c20ce8d633 Have arrows default to flat stroke 2024-08-16 15:10:40 -05:00
Grant Sanderson
0267740bde Change STROKE_WIDTH_CONVERSION width back to older value 2024-08-16 15:03:47 -05:00
Grant Sanderson
d8edccdab4 Fix zero stroke width issue 2024-08-16 14:59:18 -05:00
Grant Sanderson
108db87087 Rather than ignoring border with for non-one opacity, go back to having set_fill default to setting 0 border width for intermediate opacities 2024-08-16 12:38:11 -05:00
Grant Sanderson
21c0bcb8b6 Don't wait for animations while skipping 2024-08-16 12:20:49 -05:00
Grant Sanderson
902a4f264e Add white space after assert statements 2024-08-16 12:15:55 -05:00
Grant Sanderson
3f15715ff1 Use _data_defaults for initializing fill border width 2024-08-16 12:10:37 -05:00
Grant Sanderson
174f318602 Push _data_default initialization into init_data 2024-08-16 12:10:07 -05:00
Grant Sanderson
430a88cf13 Change default border width to 0, and only draw it for opacity 1 2024-08-16 12:05:47 -05:00
Grant Sanderson
4a6e6ca646 Double the size of the fill canvas, to effectively do msaa 2024-08-16 12:04:27 -05:00
Grant Sanderson
e2b0c2b9bf Merge pull request #2160 from 3b1b/video-work
Fix low resolution thin stroke issue
2024-08-15 15:13:20 -07:00
Grant Sanderson
04347e7876 Fix thin stroke issue for low resolutions 2024-08-15 17:11:02 -05:00
Grant Sanderson
df0ae6fdc9 Fix missing ignore_skipping -> force_draw 2024-08-15 16:40:16 -05:00
Grant Sanderson
59235d3eed Include flat stroke in get_style, set_style, match_style 2024-08-15 15:37:55 -05:00
Grant Sanderson
0fa74a7921 Merge pull request #2159 from 3b1b/video-work
Energy savings
2024-08-15 12:08:51 -07:00
Grant Sanderson
65d5947966 Change ignore_skipping name to force_draw 2024-08-15 14:05:32 -05:00
Grant Sanderson
a5ba721f96 Cease useless rendering
Change so that with a live window, rendering only happens if there has been an event (mouse motion, key press, etc.) to respond to.
2024-08-15 14:03:16 -05:00
Grant Sanderson
c7acbe5de6 Brighten up fill 2024-08-15 10:53:46 -05:00
osMrPigHead
644084d9a7 Merge branch '3b1b:master' into fix-animation-time_span 2024-08-15 15:11:30 +08:00
Grant Sanderson
af8e5236d2 Merge pull request #2157 from 3b1b/video-work
3d stroke bug fix
2024-08-13 16:05:01 -07:00
Grant Sanderson
a4858918dd Small reformatting 2024-08-08 13:59:50 -07:00
Grant Sanderson
31b6affabb Push up pointwise styling update 2024-08-08 13:56:16 -07:00
Grant Sanderson
cff3bdf8d4 Remove unnecessary flat stroke option 2024-08-08 13:55:48 -07:00
Grant Sanderson
9f54b85c4e Change miter threshold to global constant 2024-08-08 13:55:25 -07:00
Grant Sanderson
c345d76de0 Patch for glitches associated with non-flat stroke when tangency direction lines up with camera view 2024-08-08 11:36:28 -05:00
Grant Sanderson
aad2bded14 Merge pull request #2155 from 3b1b/video-work
Update how stroke is rendered
2024-08-07 13:13:17 -07:00
Grant Sanderson
1ff758dea8 Remove (no longer necessary) specifications of non-flat stroke for 3d things 2024-08-07 15:06:10 -05:00
Grant Sanderson
bf43a648a4 Allow for setting flat stroke in VMobject.set_stroke 2024-08-07 14:54:37 -05:00
Grant Sanderson
099aaaee43 Increase polyline factor 2024-08-07 14:46:26 -05:00
Grant Sanderson
70862a068f Don't buff out stroke width by antialias width 2024-08-07 14:46:17 -05:00
Grant Sanderson
f677a02036 Allow for manually setting miter or bevel joints 2024-08-07 14:45:55 -05:00
Grant Sanderson
bc91e91634 In get_euler_angles, add edge case for gimbal lock on the low side 2024-08-07 14:11:10 -05:00
Grant Sanderson
0a43a3ff9a Remove unnecessary stroke vert input 2024-08-07 14:10:50 -05:00
Grant Sanderson
e130625b9b Handle edge case of single point passed into approx_smooth_quadratic_bezier_handles 2024-08-07 12:12:29 -05:00
Grant Sanderson
0dcf630222 Change default to non-flat stroke rendering 2024-08-07 11:23:37 -05:00
Grant Sanderson
61a2b4d0da Improve flat stroke for sharp corners with a smooth transition to miter joints 2024-08-07 10:34:47 -05:00
Grant Sanderson
edb438e5e2 Further clean up to stroke shader 2024-08-06 15:45:21 -05:00
Grant Sanderson
ec88673e92 Fix kink issue in flat stroke 2024-08-06 10:41:31 -05:00
Grant Sanderson
44ec9933b7 Small format fix 2024-08-06 10:39:23 -05:00
Grant Sanderson
4ff61ed561 Default approximate smoothing (non-approx has a bug in 3d) 2024-08-06 10:27:35 -05:00
Grant Sanderson
4223bb6320 Small cleanup on TracedPath 2024-08-06 10:23:46 -05:00
Grant Sanderson
b45c71d3c2 Account for Gimbal lock in panning 2024-08-05 16:58:03 -05:00
Grant Sanderson
2b6ec2d95f Add spacing on assert lines 2024-08-05 15:01:20 -05:00
Grant Sanderson
a5926195ee Clean up stroke shader 2024-08-05 15:01:02 -05:00
Grant Sanderson
557819ad03 Remove pre-computation of curve points and joint products 2024-08-05 14:25:15 -05:00
Grant Sanderson
f363eaa2fd Add stand in for a ribboning effect 2024-08-05 13:37:34 -05:00
Grant Sanderson
c61c18486c Don't bevel corners on inner joints of quadratic bezier curves 2024-08-05 13:37:17 -05:00
Grant Sanderson
26249c34bb Have non-flat stroke operate based on projecting tangents 2024-08-05 09:15:06 -05:00
Grant Sanderson
b3bbc31ea9 Small clean up to stroke shaders 2024-08-03 08:12:00 -05:00
Grant Sanderson
71814a118b Fix dot updater in graph example 2024-08-01 07:56:26 -05:00
Grant Sanderson
d644e3b184 Merge branch 'master' of github.com:3b1b/manim 2024-08-01 07:55:11 -05:00
Grant Sanderson
78ddfe29c0 Merge pull request #2153 from 3b1b/simpler-stroke
Simpler stroke
2024-08-01 05:53:10 -07:00
Grant Sanderson
8f78e2e127 Merge pull request #2152 from 3b1b/video-work
Video work
2024-08-01 05:43:29 -07:00
Grant Sanderson
5decf810e7 Change default anti_alias_width to 1.5 2024-08-01 07:41:45 -05:00
Grant Sanderson
aea747b6d3 Final refinements on polyline stroke implementations 2024-08-01 07:41:32 -05:00
Grant Sanderson
361817b506 Pass fewer values to frag shader 2024-08-01 07:17:26 -05:00
Grant Sanderson
a3469c236e Simpler compute_subdivisions 2024-08-01 06:32:04 -05:00
Grant Sanderson
c6a6503544 Cleaning up first pass implementation 2024-07-31 16:27:37 -04:00
Grant Sanderson
3ea8393e9a First pass at a polyline implementation for stroke 2024-07-31 15:51:06 -04:00
Grant Sanderson
5aeb457bb1 Hot fix for Traicing Tail 2024-07-31 15:32:17 +02:00
Grant Sanderson
bbc89d13e9 Don't save state after each embed cell call 2024-07-31 15:32:08 +02:00
Grant Sanderson
a105216a47 Small clean up 2024-07-31 15:31:26 +02:00
Grant Sanderson
c1efd14904 Add touch and notouch to embed vocabulary 2024-07-31 15:24:50 +02:00
Grant Sanderson
a7765dcac3 Change copy frame position command 2024-07-31 15:24:25 +02:00
Grant Sanderson
57d4732ef1 Remove unused lines 2024-07-31 15:23:59 +02:00
Grant Sanderson
d1314e5a3c Catch screeninfo.ScreenInfoError error 2024-07-31 15:23:41 +02:00
Grant Sanderson
79c89ad34d Remove (outdated) background rectangle arg from example scene 2024-07-31 15:23:16 +02:00
osMrPigHead
0eae42977a fix(animation): time_span doesn't work for mobjects with submobjects 2024-07-23 15:55:06 +08:00
Grant Sanderson
a07ccf4aca Include *args, **kwargs in embed shell event pre_cell and post_cell functions 2024-06-24 15:22:22 -07:00
Grant Sanderson
4feb831a11 Pass group parameter in LaggedStartMap -> AnimationGroup 2024-06-24 15:21:09 -07:00
Michael
63e98eee94 Update tex_templates.yml
Fix the error when using Chinese, and add international typesetting support
2024-06-04 23:47:19 +08:00
jkjkil4
ab28804ae5 fix: there_and_back_with_pause 2024-05-22 16:37:07 +08:00
Grant Sanderson
88c7e9d2c9 Merge pull request #2132 from pdancstep/example-correction
Removed old argument from example
2024-05-03 11:34:56 -07:00
Grant Sanderson
3c374c3e92 Merge pull request #2108 from kubanemil/integer_matrix
remove include_background_rectangle
2024-05-03 11:34:32 -07:00
pdancstep
c970f776bb Removed old argument from example 2024-05-03 11:29:57 -07:00
Grant Sanderson
772a328302 Have FadeTransform target match all uniforms of source 2024-04-12 21:52:08 -04:00
Grant Sanderson
f5d1a9c449 Keep track of original float matrix in DecimalMatrix 2024-03-25 19:10:42 -03:00
Grant Sanderson
920f2407e0 Revert default underline stroke width 2024-03-25 19:10:30 -03:00
Grant Sanderson
7565e936fa Better bubble flipping 2024-03-25 19:10:16 -03:00
Grant Sanderson
1d6aa47933 Reimplement SpeechBubble and ThoughtBubble 2024-03-21 14:36:17 -03:00
Grant Sanderson
0509e824c6 Have border_width default to 0 for lower opacity 2024-03-21 14:36:06 -03:00
Grant Sanderson
ec42326618 Fix remover=True case for FadeTransform 2024-03-21 14:35:33 -03:00
Grant Sanderson
223d671eea Remove redundancy 2024-03-21 14:34:39 -03:00
Grant Sanderson
7e6a37d499 Typo fix: make_number_changable -> make_number_changeable 2024-03-21 10:03:41 -03:00
Grant Sanderson
6b3834739c Undo redundant previous tweak to LaggedStartMap input type 2024-03-21 10:00:11 -03:00
Grant Sanderson
b26feb7045 Adjust Underline configuration 2024-03-21 09:59:29 -03:00
Grant Sanderson
7db69e32aa Update input type to LaggedStartMap 2024-03-16 11:10:52 -03:00
Grant Sanderson
fa99eafe2b Account for rgba case in point_to_rgb 2024-03-16 11:10:42 -03:00
emil
8235607b2a remove include_background_rectangle 2024-03-11 17:18:52 +06:00
Grant Sanderson
4729e44e05 Merge pull request #2106 from 3b1b/video-work
Video work
2024-03-07 11:53:20 -08:00
Grant Sanderson
27f397e0a6 Have stroke width change continuously with fixed_in_frame status 2024-03-07 16:47:24 -03:00
Grant Sanderson
226d649ee6 Change UpdatersExample scene to feature .always and .f_always syntax 2024-03-07 16:42:40 -03:00
Grant Sanderson
d3ba101ee5 Change from tracking time_based_updater and non_time_updater lists separately to just tracking one list 2024-03-07 16:39:45 -03:00
Grant Sanderson
83cd5d6246 Clean up updater matters, prune unused functions 2024-03-07 16:07:39 -03:00
Grant Sanderson
70b839e188 Change to only compute has_updater status as needed 2024-03-07 15:34:26 -03:00
Grant Sanderson
fd35433a62 Change name note_updated_family -> note_changed_family 2024-03-07 15:32:15 -03:00
Grant Sanderson
4b14c11e4b Only reconstruct family as needed 2024-03-07 13:49:07 -03:00
Grant Sanderson
e124aecd6b Clarify Mobject.needs_new_bounding_box is private 2024-03-07 13:27:29 -03:00
Grant Sanderson
3c778ba678 Add comment to Mobject.animate 2024-03-07 13:27:09 -03:00
Grant Sanderson
a6b46c641b Add Mobject.always and Mobject.f_always
For nicer syntax in creating updaters
2024-03-07 10:17:01 -03:00
Grant Sanderson
2380ffd616 Allow LaggedStart to accept an iterable as an argument 2024-03-07 09:41:47 -03:00
Grant Sanderson
1372cf101c Allow VGroup and Group to accept generators and iterables as arguments 2024-03-07 09:23:02 -03:00
Grant Sanderson
4d67361800 Add shuffled 2024-03-07 08:40:19 -03:00
Grant Sanderson
a5f2ac689f Spacing 2024-03-07 08:40:10 -03:00
Grant Sanderson
2e9c89502d Restrict alpha in CountInFrom 2024-03-07 08:39:45 -03:00
Grant Sanderson
9432a73a9f Let checkpoint_paste work on methods of the current scene 2024-03-02 16:49:54 -05:00
Grant Sanderson
ffbe5c8114 Add type hints for affects_mobject_list 2024-03-02 16:49:35 -05:00
Grant Sanderson
7edc4b64ad Replace numbers_with_elongated_ticks -> big_tick_numbers 2024-02-22 11:47:28 -08:00
Grant Sanderson
e784c42f0d Add big_tick_spacing option NumberLine 2024-02-22 11:45:31 -08:00
Grant Sanderson
4a89376fdd Add method for resetting which plane in 3d space behaves like the floor when panning 2024-02-21 12:42:59 -08:00
Grant Sanderson
712fa30174 Ensure Brace.get_tex uses buff key word arg 2024-02-21 12:25:43 -08:00
Grant Sanderson
5632fee9a3 Ensure get_opacity returns float 2024-02-21 12:25:26 -08:00
Grant Sanderson
7b577e9fc1 Have matrix keep track of elements and ellipses as lists instead of VGroups 2024-02-13 14:52:16 -06:00
Grant Sanderson
ed3ac74d67 Add option to change Euler axes 2024-02-13 14:48:56 -06:00
Grant Sanderson
4ce8a3ba9d Fix computation of normals for Surface 2024-02-13 14:48:00 -06:00
Grant Sanderson
d44e248277 Specify type of argument in Mobject.add 2024-02-09 17:48:25 -06:00
Grant Sanderson
578427543c Add defaults for Matrix object to have ellipses 2024-02-09 17:48:02 -06:00
Grant Sanderson
c531e56a2f Fix DecimalNumber.set_value font size issue 2024-02-08 14:55:12 -06:00
Grant Sanderson
45f8ca7643 Specify output type for DecimalMatrix.element_to_mobject 2024-02-08 14:44:12 -06:00
Grant Sanderson
2966f358a3 Pull type definitions used for Generic[SubmobjectType] outside of if TYPE_CHECKING block 2024-02-08 14:43:37 -06:00
Grant Sanderson
8417369da1 Performance improvement for DecimalNumber.set_value 2024-02-08 14:42:46 -06:00
Grant Sanderson
f3571cf2cb Add random import 2024-02-08 14:37:58 -06:00
Grant Sanderson
e4c824e672 Update random_bright_color to operate based on hsl ranges 2024-02-08 14:37:46 -06:00
Grant Sanderson
31b2bcd9e6 Add interpolate_color_by_hsl 2024-02-08 14:37:30 -06:00
Grant Sanderson
100b108ad1 Treat Group and VGroup more like list types
This may not be the best way to address it, but at least temporarily it prevents linting issues for calls like VGroup(Circle())[0].get_radius()
2024-02-05 15:02:13 -06:00
Grant Sanderson
7009f0f53e Add space after assert 2024-02-03 19:11:18 -06:00
Grant Sanderson
dfa96c2047 Update copy_frame_positioning 2024-02-03 18:49:04 -06:00
Grant Sanderson
ebe689dede Treat is_fixed_in_frame as a float in uniforms, to allow for transformations between 2024-02-03 18:48:54 -06:00
Grant Sanderson
4aef0d1bf5 Add add_ambient_rotation 2024-02-03 18:48:16 -06:00
Grant Sanderson
661814deea Add all orientation options into CameraFrame.reorient 2024-02-03 18:48:07 -06:00
Grant Sanderson
45d9049405 Minor clean up 2024-02-03 18:47:39 -06:00
Grant Sanderson
711438f625 Update the types in decorator methods using @wraps
This is method to address issues flagged by pyright
2024-02-03 18:00:47 -06:00
Grant Sanderson
cde709fcfa Replace fix_in_frame matching for DecimalNumber.set_value to general uniform matching 2024-01-19 17:42:52 -06:00
Grant Sanderson
1c72059725 Have surrounding rectangle match framed fixed status of what it surrounds 2024-01-19 17:42:29 -06:00
Grant Sanderson
d3dee240c3 Allow for smooth transitions between mobjects fixed and unfixed from the frame 2024-01-19 17:42:12 -06:00
Grant Sanderson
60b762ca43 Remove redundant and unused get_gl_Position.glsl file 2024-01-19 17:41:46 -06:00
Grant Sanderson
8179ba88d0 Reformat defaults for n_rows and n_cols in Mobject.arrange_in_grid 2024-01-18 11:13:12 -06:00
Grant Sanderson
855ef9be8d Refactor Matrix, DecimalMatrix, MobjectMatrix, etc. 2024-01-18 11:12:42 -06:00
Grant Sanderson
2c110790d2 Merge pull request #2094 from 3b1b/video-work
Video work
2024-01-17 13:08:43 -08:00
Grant Sanderson
41ece958fd Explicitly call out global naure of ID_TO_TEXTURE map 2024-01-17 15:02:19 -06:00
Grant Sanderson
88672a21ff Include texture id in shader wrapper id
This ensure that, among other things, ImageMobjects appearing in groups don't get lumped together in rendering.
2024-01-17 15:01:49 -06:00
Grant Sanderson
9ba684d35f Merge pull request #2028 from rudransh61/master
Update installation.rst added FFmpeg install
2024-01-01 12:55:18 -08:00
Grant Sanderson
f8fedffa4c Use rate function on MoveAlongPath 2023-12-02 21:28:22 -06:00
TangentSquared
fa017b94d9 Update requirements.txt
Now, the ipython version needs to be 8.18.0 or greater
2023-12-02 19:23:54 +05:30
Grant Sanderson
dcf3eb8416 Ignore pyright configuration 2023-11-06 12:36:34 -05:00
Grant Sanderson
8a4d7b4e8c Add a small hack to ensure Window resets properly in non-primary monitors 2023-11-06 12:34:39 -05:00
Grant Sanderson
246a010799 Add default border width to StringMobject 2023-11-06 12:33:56 -05:00
Grant Sanderson
17cd597904 Have Arrow track what stroke width it was set to 2023-11-06 12:33:26 -05:00
Grant Sanderson
2cdb85cae9 Don't assign a fixed default depth to ThreeDAxes 2023-11-06 12:32:47 -05:00
Grant Sanderson
0d046a7eab Add an option for a graph to continually update to its defining function 2023-11-06 12:32:27 -05:00
Grant Sanderson
cbc32468bf Note chanted stroke and fill after DrawBorderThenFill has complete 2023-11-06 12:31:52 -05:00
Grant Sanderson
f4778b57ef Have Animation keep track of whether a mobject had had it's updating suspended before resuming it at the end 2023-11-06 12:31:16 -05:00
Grant Sanderson
916ab94efd Remove white space 2023-10-09 14:17:44 -05:00
Grant Sanderson
a8b1791ff5 Small tweaks to arrow tip implementation 2023-09-04 21:16:36 -07:00
Grant Sanderson
39e5d24858 Factor out partial results from point_from_proportion 2023-09-04 21:16:18 -07:00
Grant Sanderson
295a0f76cc Formatting tweak 2023-09-04 21:15:53 -07:00
Grant Sanderson
2b00a9cf80 Fix add_curve_to 2023-09-04 21:15:43 -07:00
Grant Sanderson
b53ab02675 Simplify initialization of Line with path arc 2023-09-04 20:49:51 -04:00
Grant Sanderson
5f41e238ba Improve VMobject.add_arc_to 2023-09-04 20:49:36 -04:00
Grant Sanderson
690eb24562 Add VMobject.add_arc_to method 2023-09-04 19:31:40 -04:00
Grant Sanderson
60a4f0e167 Factor out Arc.create_quadratic_bezier_points to quadratic_bezier_points_for_arc 2023-09-04 19:08:13 -04:00
Grant Sanderson
0a642133ad Add scale_radii method for DotCloud 2023-09-04 18:46:28 -04:00
Grant Sanderson
87e4a71ca3 Add surround method for Rectangles and SurroundingRectangles 2023-09-04 18:46:11 -04:00
Fuwuwuwu
7278095921 Change ImageMobject rendering mode to TRIANGLES
And add vertices
2023-09-03 08:55:03 +08:00
german2020
f0a61beaf5 fix VShowPassingFlash index out of bounds issue 2023-08-28 10:10:51 +08:00
german2020
0b5e9d4a8b add zorder to mobject 2023-08-27 15:35:22 +08:00
german2020
65e7943ff7 fix aspect_ratio other than 16:9 issue 2023-08-27 13:51:54 +08:00
Grant Sanderson
fa798a2018 Add \dots and \mathds to tex_to_symbol_count 2023-08-15 20:40:39 -07:00
Grant Sanderson
13d4ab1eb0 Stylistic change 2023-08-15 20:40:24 -07:00
Grant Sanderson
c8cf83eedf Add cartesian_product 2023-08-15 20:40:15 -07:00
Grant Sanderson
eafd09549d Handle make_jagged for empty VMobjects 2023-08-15 20:40:01 -07:00
Grant Sanderson
f2ad9a70f7 Small format fix 2023-08-15 20:39:27 -07:00
Grant Sanderson
4be7f611ec Fix issues with stroke opacities passed as numpy arrays 2023-08-15 20:38:55 -07:00
Grant Sanderson
d21b05ae0d Arrow fix 2023-08-15 20:37:54 -07:00
Grant Sanderson
fc522e5278 Change width_to_tip_len -> tip_len_to_width 2023-08-15 20:37:13 -07:00
Grant Sanderson
ddf2f7d9bd Fix typo 2023-08-15 20:36:45 -07:00
Grant Sanderson
2337be2318 Remove num_sampled_graph_points_per_tick in Axes __init__ 2023-08-15 20:36:36 -07:00
Grant Sanderson
7954ba14ef Use rate_function appropriately in ShowIncreasingSubsets 2023-08-15 20:35:42 -07:00
LinZeZhi
c65b7242e4 Remove duplicate items 2023-06-29 10:18:44 +08:00
Grant Sanderson
7ff45b4637 Having changing decimal match fixed_in_frame status 2023-06-23 10:57:57 -07:00
Grant Sanderson
4f42ebeb4f Small formatting tweaks 2023-06-10 09:26:20 -07:00
Grant Sanderson
21d20541b5 Add texture_names_to_ids to ShaderWrapper 2023-06-10 09:25:44 -07:00
Grant Sanderson
0609c1bfa8 Change default saturation and gamma to each be 1 2023-06-10 09:25:03 -07:00
Grant Sanderson
162fd4a92b Change defaults for where videos are saved
Save them directly to the relevant output directory, rather than to a "videos" subdirectory within it.
2023-06-10 09:24:50 -07:00
Grant Sanderson
cb02066f22 Add always_depth_test option to ThreeDScene, default to true 2023-06-10 09:23:19 -07:00
Grant Sanderson
3e64111952 Change default on VMobject to no depth test 2023-06-10 09:22:55 -07:00
Grant Sanderson
6f8ea7433d Small formatting tweaks 2023-06-10 09:22:34 -07:00
Grant Sanderson
bae3b98c0b Fixes to Surface 2023-06-10 09:22:15 -07:00
Grant Sanderson
63f6e9d84f Add Dartboard 2023-06-10 09:21:57 -07:00
Grant Sanderson
f01b990c2e Add default Mobject.match_style 2023-06-10 09:21:45 -07:00
Grant Sanderson
fa1080d59a Be sure reverse_points changes data in place 2023-06-10 09:21:32 -07:00
Grant Sanderson
ce7422f8af Add ThreeDAxes.get_graph and .get_parametric_surface 2023-06-10 09:21:02 -07:00
Grant Sanderson
16f5890fd3 Add CoordianteSystem.get_area_under_graph
This is not perfect, since one could optionally add a different color for negative area.
2023-06-10 09:20:28 -07:00
Grant Sanderson
5d9a7f49e6 Add taper_width argument to FlashAround 2023-06-10 09:19:48 -07:00
Grant Sanderson
f33b8d1d2f Add stretch_factor in FlashUnder Underline 2023-06-10 09:19:32 -07:00
Rudransh Bhardwaj
71ab276e05 Update installation.rst added FFmpeg install
Added how to install ffmpeg
2023-05-26 16:24:30 +05:30
Dishant Varshney
3b2904b4c7 Correction of indices 2023-04-02 03:16:09 +05:30
Grant Sanderson
de8e9e5ec1 Fix error with setting bubble direction 2023-03-14 10:35:04 -07:00
Grant Sanderson
6b24860bbf Tweak default configuration for Underline 2023-03-14 10:34:44 -07:00
Grant Sanderson
0d415036a9 Ensure exact integers are displays for large values in Integer 2023-03-14 10:34:33 -07:00
Grant Sanderson
80fb1a98a9 Add \text to TEX_TO_SYMBOL_COUNT 2023-03-10 11:05:50 -08:00
Grant Sanderson
d1e2a7a157 Make sure \text{...} is counted correctly in num_tex_symbols 2023-03-10 11:05:30 -08:00
Grant Sanderson
b644bb51de Update submobjects before parents 2023-03-10 11:05:07 -08:00
Grant Sanderson
392019fc6e Specify type in bind_to_graph 2023-03-10 11:04:49 -08:00
Grant Sanderson
6d0b586597 Prevent index out of range error for ShowSubmobjectsOneByOne 2023-03-10 11:04:25 -08:00
Grant Sanderson
b216b8f7e3 Reset default in set_style to stroke_background = False 2023-02-24 08:23:24 -05:00
Grant Sanderson
1eb819363d Fix issue with ticks going beyond number line 2023-02-24 08:23:01 -05:00
Grant Sanderson
a79d4a862f Keep track of dots in DieFace 2023-02-24 08:22:24 -05:00
Grant Sanderson
3f2d15986a Make TrueDot shading in 3d work 2023-02-16 16:47:59 -08:00
Grant Sanderson
c372ef4aaa Faster VMobject.get_arc_length 2023-02-16 15:02:30 -08:00
Grant Sanderson
3a05352f73 Add poly_line_length function 2023-02-16 15:02:15 -08:00
Grant Sanderson
dcb58c1f4f Remove arg_creator arg from LaggedStartMap
Anything that enables is better done just with LaggedStart
2023-02-15 20:55:26 -08:00
Grant Sanderson
576a26493e Use quick_point_from_proportion in MoveAlongPath 2023-02-15 20:54:59 -08:00
Grant Sanderson
d8428585f8 Merge pull request #1994 from 3b1b/video-work
Several bug fixes
2023-02-15 09:40:43 -08:00
Grant Sanderson
557cb66c52 Fix transparent background videos 2023-02-15 09:38:35 -08:00
Grant Sanderson
01c51dbc6d animation.update_config -> animation.update_rate_info 2023-02-09 15:16:51 -08:00
Grant Sanderson
ad409999dc Small tweak 2023-02-09 15:16:33 -08:00
Grant Sanderson
b39fbb62f4 Ensure joint_products are refreshed for _AnimationBuilder 2023-02-09 15:16:10 -08:00
Grant Sanderson
3e3e4de5e9 Add option for gamma correction to SceneFileWriter 2023-02-08 20:09:33 -08:00
Grant Sanderson
ded06c1f88 Give SceneFileWriter an option for saturation, and set default to 1.7
The colors in a scene can look a little different in a preview window vs. in the rendered file written by ffmpeg. This is mean to bring them closer together.
2023-02-08 19:41:07 -08:00
Grant Sanderson
f2c07afe74 add 'return self' 2023-02-08 19:39:49 -08:00
Grant Sanderson
169e7a302b Give ThreeDAxes flat stroke by default 2023-02-08 19:39:37 -08:00
Grant Sanderson
0ce972991b Remove num_axis_pieces arg from ThreeDAxes 2023-02-08 19:39:22 -08:00
Grant Sanderson
41f0239e9d Merge pull request #1992 from 3b1b/bug-fix
Fix for index -3 error
2023-02-08 10:40:40 -08:00
Grant Sanderson
1844f7fd64 Fix for https://github.com/3b1b/manim/issues/1991 2023-02-08 10:39:07 -08:00
Grant Sanderson
66b78d01a9 Merge pull request #1989 from 3b1b/video-work
A few more fixes and tweaks.
2023-02-04 16:57:13 -08:00
Grant Sanderson
d1b1df64a5 Ensure Window's scene always points back to window
Issues can arise in the few milliseconds of startup otherwise.
2023-02-04 16:51:14 -08:00
Grant Sanderson
4e90a77fcd Change type hint on LaggedStart to accept any functions outputting animations 2023-02-04 16:50:12 -08:00
Grant Sanderson
7d1330fa68 Check if mobject_uniforms is None 2023-02-04 16:49:32 -08:00
Grant Sanderson
c918e84784 Change default progress bar format 2023-02-03 18:10:29 -08:00
Grant Sanderson
e3b95276fa Update TexTransformExample, and pull out TexIndexing 2023-02-03 17:48:51 -08:00
Grant Sanderson
3bf9e40aba Add more lenient tolerance to Mobject.has_same_shape_as 2023-02-03 17:35:20 -08:00
Grant Sanderson
fab917ccee Improve TransformMatchingString to match longest common substrings by default 2023-02-03 17:28:27 -08:00
Grant Sanderson
b8fe7b0172 Note that restoring state affects the mobject list 2023-02-03 17:28:00 -08:00
Grant Sanderson
a54d1eddfc Set default pixel format to yuv420p 2023-02-03 12:48:56 -08:00
Grant Sanderson
e1bb360e0b Add CLI args for setting video codec and pixel forma 2023-02-03 12:46:01 -08:00
Grant Sanderson
12dc124d72 Revert to simple progress_description_len default 2023-02-03 11:46:03 -08:00
Grant Sanderson
bc107787cc Clean up get_scenes_to_render 2023-02-03 11:45:47 -08:00
Grant Sanderson
b25f022859 Make it an option (default to false) to prerun a scene to calculate its number of frames 2023-02-03 11:06:07 -08:00
Grant Sanderson
7c561d3757 Edit set_program_uniform 2023-02-03 11:05:40 -08:00
Grant Sanderson
ac3db9b636 Add set_program_uniform function 2023-02-02 21:13:18 -08:00
Grant Sanderson
772ea792d0 Add check for null VMobject in shader init 2023-02-02 21:12:42 -08:00
Grant Sanderson
ee08c552bf Remove ShaderWrapper.get_program_id 2023-02-02 20:49:13 -08:00
Grant Sanderson
c4777015fc FIx Mobject.replace_shader_code 2023-02-02 20:47:55 -08:00
Grant Sanderson
d10745a379 Have CameraFrame.get_view_matrix and and CameraFrame.get_implied_camera_location use _data_has_changed instead of a refresh arg 2023-02-02 20:47:12 -08:00
Grant Sanderson
88959df7a8 Use set_color instead of set_rgba_array in vector_field 2023-02-02 18:24:12 -08:00
Grant Sanderson
3d0fe27c55 Simplify VMobject.set_rgba_array 2023-02-02 18:23:41 -08:00
Grant Sanderson
4629e08769 Ensure joint_products are computed at both the start and end of an animation 2023-02-02 18:17:12 -08:00
Grant Sanderson
009f9dd18b Don't call become at the end of Transform 2023-02-02 18:16:44 -08:00
Grant Sanderson
7f940fbee4 Change how ShaderWrapper uniforms are handled 2023-02-02 17:45:52 -08:00
Grant Sanderson
fbcbbc9a58 Merge pull request #1988 from 3b1b/video-work
Several minor fixes
2023-02-02 16:28:43 -08:00
Grant Sanderson
1dcc678b2f Make sure animating a VGroup uniforms will have an effect 2023-02-02 16:18:09 -08:00
Grant Sanderson
ad2e7144b4 Lock uniform keys, the same way data keys are 2023-02-02 16:17:26 -08:00
Grant Sanderson
e36719a21b Use resize_points in Mobject.set_data 2023-02-02 15:37:10 -08:00
Grant Sanderson
c4d698a169 Have Transform only copy target_mobject if it must 2023-02-02 15:36:58 -08:00
Grant Sanderson
0e60b124eb Only compute const_data_keys for unlocked data 2023-02-02 15:02:55 -08:00
Grant Sanderson
d263fa23fa Merge pull request #1986 from 3b1b/video-work
A few small performance improvements
2023-02-02 14:49:49 -08:00
Grant Sanderson
6f2cbc4d1f Merge branch 'master' of github.com:3b1b/manim into video-work 2023-02-02 14:47:44 -08:00
Grant Sanderson
4e674e571c Merge pull request #1987 from 3b1b/interactive-scene-update
Interactive scene update
2023-02-02 14:47:32 -08:00
Grant Sanderson
be602930c3 Check that scene has a camera frame in pixel_to_point_coords 2023-02-02 14:46:07 -08:00
Grant Sanderson
9cadfa1818 Make it so that copying a mobject will copy its name, if applicable 2023-02-02 14:46:07 -08:00
Grant Sanderson
b9d37a9f7e Add copy_frame_anim_call 2023-02-02 14:46:07 -08:00
Grant Sanderson
d0c6d4d386 Accept list of Vect3 as an input to Mobject.set_points 2023-02-02 14:46:07 -08:00
Grant Sanderson
3c0d682efc Change clicking behavior in InteractiveScene 2023-02-02 14:46:07 -08:00
Grant Sanderson
63dbe3b23f More direct check for family_members_with_points 2023-02-02 14:32:55 -08:00
Grant Sanderson
bd89056c8e Only recalculate outer_vert_indices when points are resized 2023-02-02 14:29:37 -08:00
Grant Sanderson
b9d6dcd67d Save _has_fill and _has_stroke to prevent unnecessary recalculation 2023-02-02 14:26:08 -08:00
Grant Sanderson
e5eed7c36a Batch render groups by Mobject type 2023-02-02 13:28:06 -08:00
Grant Sanderson
acb4b1c6b3 Finalize color at the vertex level, rather than the fragment level, for fill 2023-02-02 12:04:23 -08:00
Grant Sanderson
a1b9eae301 Merge branch 'master' of github.com:3b1b/manim into video-work 2023-02-02 11:40:23 -08:00
Grant Sanderson
7476740980 Merge pull request #1985 from 3b1b/cleaner-winding-fill
Cleaner winding fill
2023-02-02 11:39:18 -08:00
Grant Sanderson
c3823e722d Update fill shader alpha blending, and simplify the fill canvas 2023-02-02 11:28:20 -08:00
Grant Sanderson
0cf9a35367 Have SVG subdivide intersections if winding fill is not a default 2023-02-02 11:28:20 -08:00
Grant Sanderson
594b9258da Account for unnecessary calls to use_winding_fill 2023-02-02 11:28:20 -08:00
Grant Sanderson
4ec2e8b0c5 Merge branch 'master' of github.com:3b1b/manim into video-work 2023-02-02 11:22:58 -08:00
Grant Sanderson
260815c675 Merge pull request #1984 from 3b1b/safer-self
Use typing_extensions to import Self for python versions <3.11
2023-02-02 11:22:33 -08:00
Grant Sanderson
ab6a7df4af Use typing_extensions to import Self for python versions <3.11 2023-02-02 10:54:47 -08:00
Grant Sanderson
17cef427f1 Update fill shader alpha blending, and simplify the fill canvas 2023-02-02 10:42:24 -08:00
Grant Sanderson
b499caaa45 Have SVG subdivide intersections if winding fill is not a default 2023-02-02 10:40:09 -08:00
Grant Sanderson
9c03a40d68 Account for unnecessary calls to use_winding_fill 2023-02-02 10:39:30 -08:00
Grant Sanderson
47672d3b1e Add checks for setting submobjects with existing list 2023-02-01 22:52:59 -08:00
Grant Sanderson
eeadbe4542 Small reshuffling 2023-02-01 22:52:02 -08:00
Grant Sanderson
8adf2a6e07 Partition render groups based on shader type, fixed_in_frame status, depth_test and whether the mobject is changing 2023-02-01 20:39:16 -08:00
Grant Sanderson
6eafdc63cc Merge branch 'video-work' into render-groups 2023-02-01 20:17:01 -08:00
Grant Sanderson
ebf2ee5849 Update tex patterns 2023-02-01 20:12:06 -08:00
Grant Sanderson
f4a6f99b54 Check _use_winding_fill on the submobject level 2023-02-01 20:11:50 -08:00
Grant Sanderson
8820af65ec Check that scene has a camera frame in pixel_to_point_coords 2023-02-01 20:11:31 -08:00
Grant Sanderson
f83c441210 Make it so that copying a mobject will copy its name, if applicable 2023-02-01 13:35:46 -08:00
Grant Sanderson
f293ccdff4 Add copy_frame_anim_call 2023-02-01 13:35:10 -08:00
Grant Sanderson
40bcb7e0f3 Accept list of Vect3 as an input to Mobject.set_points 2023-02-01 13:02:56 -08:00
Grant Sanderson
979589a156 Change clicking behavior in InteractiveScene 2023-02-01 13:02:34 -08:00
Grant Sanderson
9ef14c7260 Change default h_buff on Matrix 2023-02-01 11:38:54 -08:00
Grant Sanderson
c062592684 Draw border width behind fill 2023-02-01 11:36:54 -08:00
Grant Sanderson
c8b65d5621 Merge pull request #1982 from 3b1b/video-work
Various bug fixes and tweaks
2023-02-01 11:28:42 -08:00
Grant Sanderson
e76c64ad52 Merge branch 'master' into video-work 2023-02-01 11:28:27 -08:00
Grant Sanderson
5527c0706d Merge pull request #1981 from 3b1b/add-self-type
Add Self type
2023-02-01 11:27:00 -08:00
Grant Sanderson
280090a7c9 Small reorganization to VMobject.get_shader_wrapper_list, and default to fill border being drawn up front 2023-02-01 11:24:46 -08:00
Grant Sanderson
b351c9f1c8 Add smaller h_buff to matrix in OpeningManimExample 2023-02-01 11:20:42 -08:00
Grant Sanderson
04733ac32e Default to fully opaque background rectangle 2023-02-01 11:20:09 -08:00
Grant Sanderson
0c9afb65d9 Small clean up to render calls 2023-02-01 11:19:40 -08:00
Grant Sanderson
b1fb3e1d54 Add render mode and early discard for fill canvas vao 2023-02-01 11:19:22 -08:00
Grant Sanderson
7c087838a3 Change backstroke defaults in OpeningScene 2023-01-31 21:33:36 -08:00
Grant Sanderson
bc38165d44 Allow for matched_pairs arg to TransformMatchingStrings 2023-01-31 20:16:20 -08:00
Grant Sanderson
c8d1ee5c88 No longer any need for specialized invisible_copy 2023-01-31 20:16:05 -08:00
Grant Sanderson
f858a439dd Make alignment between VMobjects conducive to smoother interpolation 2023-01-31 20:15:48 -08:00
Grant Sanderson
d3a4d81a63 Remove commended code 2023-01-31 16:08:43 -08:00
Grant Sanderson
fca5770b9f Fix Camera.blit 2023-01-31 16:06:53 -08:00
Grant Sanderson
d9c85aac46 Add dict_eq 2023-01-31 15:37:30 -08:00
Grant Sanderson
93e65fa3e7 Prevent needless extra copying in Mobject.copy 2023-01-31 15:37:23 -08:00
Grant Sanderson
3b0c958189 Check case of scale = 0 in get_view_matrix 2023-01-31 15:37:03 -08:00
Grant Sanderson
f42b3bfa3e In UpdatersExample, use Tex.make_number_changable 2023-01-31 14:49:07 -08:00
Grant Sanderson
077f264890 In Mobject.become, match needs_new_bounding_box status 2023-01-31 14:48:26 -08:00
Grant Sanderson
7e78e76966 Only call become at the end of Transform if the rate func ends at 1 2023-01-31 14:46:28 -08:00
Grant Sanderson
1e46847a69 Use Iterator type for Mobject.__iter__ 2023-01-31 14:32:32 -08:00
Grant Sanderson
4c327cd5d2 Merge branch 'add-self-type' into video-work 2023-01-31 14:26:31 -08:00
Grant Sanderson
3e308d881f Merge branch 'master' of github.com:3b1b/manim into video-work 2023-01-31 14:24:45 -08:00
Grant Sanderson
8e8229b9b8 Merge pull request #1980 from 3b1b/glitch-fix
Change VMobject rendering mode to TRIANGLES
2023-01-31 14:24:08 -08:00
Grant Sanderson
9dc6cce09f Merge branch 'master' into glitch-fix 2023-01-31 14:23:57 -08:00
Grant Sanderson
e8302e6025 Merge pull request #1978 from 3b1b/type-error-fix
Possible fix for a type error
2023-01-31 14:22:15 -08:00
Grant Sanderson
015a7487e7 Remove stray import 2023-01-31 14:20:40 -08:00
Grant Sanderson
468fdf9003 Add Self type to value_tracker.py 2023-01-31 14:19:58 -08:00
Grant Sanderson
031adda503 Small fixes to three_dimensions.py 2023-01-31 14:19:24 -08:00
Grant Sanderson
0452012d54 Add Self type to shape_matchers.py 2023-01-31 14:18:02 -08:00
Grant Sanderson
576d8c996b Add Self type to numbers.py 2023-01-31 14:16:15 -08:00
Grant Sanderson
74a11bb05c Add Self type to matrix.py 2023-01-31 14:13:47 -08:00
Grant Sanderson
6a42ef846a Add Self type to geometry.py 2023-01-31 14:12:41 -08:00
Grant Sanderson
da6875ca55 Add Self type to coordinate_systems.py 2023-01-31 14:08:22 -08:00
Grant Sanderson
d8c21ff7aa Add Self type to changing.py 2023-01-31 14:08:12 -08:00
Grant Sanderson
af585ca3a1 Add Self type to dot_cloud.py and point_cloud_mobject.py 2023-01-31 13:49:48 -08:00
Grant Sanderson
3779577d9f Add Self type to surface.py 2023-01-31 13:47:25 -08:00
Grant Sanderson
b58224f6c8 Add Self type to vectorized_mobject.py 2023-01-31 13:43:54 -08:00
Grant Sanderson
50343e9629 Add Self type to mobject.py 2023-01-31 13:31:48 -08:00
Grant Sanderson
a4d9b101de Whoops, make sure deepcopy actually returns 2023-01-31 12:53:25 -08:00
Grant Sanderson
60aae748a7 Make sure animations will trigger a refresh for joint products 2023-01-31 12:49:02 -08:00
Grant Sanderson
92e4d43ca3 Make sure camera location is not computed more times than it needs to be 2023-01-31 12:29:49 -08:00
Grant Sanderson
424db4b3e4 Ensure view matrix is not computed more than it needs to be 2023-01-31 12:20:25 -08:00
Grant Sanderson
2d0bdfbdb6 Merge branch 'glitch-fix' into video-work 2023-01-31 11:59:54 -08:00
Grant Sanderson
9e5fca6750 Merge branch 'type-error-fix' into video-work 2023-01-31 11:58:09 -08:00
Grant Sanderson
2cbad30f45 Change VMobject rendering mode to TRIANGLES
And set indices appropriately when reading in to the ShaderWrapper
2023-01-31 11:45:53 -08:00
Grant Sanderson
5952f9ea74 Make sure rgbas will be resized if bigger than Mobject.data 2023-01-31 09:48:23 -08:00
Grant Sanderson
f2d71e6521 Don't rotate Laptop into place 2023-01-31 09:20:05 -08:00
Grant Sanderson
0645912765 Merge pull request #1979 from 3b1b/video-work
Various bug fixes
2023-01-31 09:11:43 -08:00
Grant Sanderson
57deab6617 Fix ControlsExample 2023-01-31 09:08:38 -08:00
Grant Sanderson
f8cfcfbc64 Fix EventListener typo 2023-01-31 09:08:24 -08:00
Grant Sanderson
76ee97adfa Possible fix for a type error 2023-01-31 08:48:57 -08:00
Grant Sanderson
e7734ca84c Fix TexturedSurface 2023-01-30 20:57:21 -08:00
Grant Sanderson
5ff44f5850 Divide by epsilon prior to normalizing 2023-01-30 20:51:04 -08:00
Grant Sanderson
a58327657c Provide an epsilon of room to Sphere at poles 2023-01-30 20:50:42 -08:00
Grant Sanderson
55da5d5d03 Remove use of dv_points and du_points, pass unit normals to shader instead 2023-01-30 20:49:32 -08:00
Grant Sanderson
c469c6b009 Prevent NumberPlane from double drawing axes 2023-01-30 20:38:23 -08:00
Grant Sanderson
9017df847d Add Camera.blit, and use it when there's a window, but the used fbo is not the window's 2023-01-30 20:38:13 -08:00
Grant Sanderson
33116f8af1 Remove stray import 2023-01-30 20:37:27 -08:00
Grant Sanderson
0de914fd01 No need for du_points, and dv_points in SurfaceMesh 2023-01-30 20:37:15 -08:00
Grant Sanderson
e950286fa4 Replace get_gl_Position -> emit_gl_Position 2023-01-30 18:43:28 -08:00
Grant Sanderson
5490b3be19 Merge pull request #1977 from 3b1b/video-work
Various clean ups associated with 3d scenes
2023-01-30 16:27:53 -08:00
Grant Sanderson
215c21babf Small renaming 2023-01-30 15:24:10 -08:00
Grant Sanderson
7e00660e47 Whoops, re-instate pre_render 2023-01-30 15:16:08 -08:00
Grant Sanderson
daaaba0a67 Use frame_scale uniform instead of frame_shape 2023-01-30 15:10:41 -08:00
Grant Sanderson
9628adc957 Ensure scroll zooming works better in 3d 2023-01-30 15:03:05 -08:00
Grant Sanderson
af69cf9c7d Track field of view instead of focal_dist_to_height 2023-01-30 15:02:33 -08:00
Grant Sanderson
71bd3edb09 Fix frame scaling 2023-01-30 15:02:04 -08:00
Grant Sanderson
277c471c90 Refactor so that view matrix incorporates frame scaling 2023-01-30 14:15:39 -08:00
Grant Sanderson
b85c3bd478 Remove stray import 2023-01-30 11:57:09 -08:00
Grant Sanderson
285953b44d Add FRAME_SHAPE constant 2023-01-30 11:57:01 -08:00
Grant Sanderson
15d8ebb572 Add Mobject.set_shape 2023-01-30 11:53:15 -08:00
Grant Sanderson
abdcb64461 Fix panning for off-center frame 2023-01-28 23:05:52 -08:00
Grant Sanderson
e58aea9e2f Change to a default where scrolling zooms, and dragging pans 2023-01-28 22:59:54 -08:00
Grant Sanderson
2705ba3afa Fix CameraFrame.to_fixed_frame_point 2023-01-28 22:58:05 -08:00
Grant Sanderson
901d40ba11 Fix scroll zooming 2023-01-28 22:53:56 -08:00
Grant Sanderson
0d9bb71d3c Increase scroll sensitivity 2023-01-28 22:42:33 -08:00
Grant Sanderson
0fe5922253 Clean up scrolling 2023-01-28 22:40:57 -08:00
Grant Sanderson
09900456f7 Clean up pixel_coords_to_space_coords to work better in 3d 2023-01-28 22:30:46 -08:00
Grant Sanderson
91f69be3e0 Add CameraFrame.to_fixed_frame_point 2023-01-28 22:30:00 -08:00
Grant Sanderson
da9610b9f9 Remove redundancy 2023-01-28 17:54:00 -08:00
Grant Sanderson
b3dec3fd51 Small fixes 2023-01-28 17:53:49 -08:00
Grant Sanderson
68255b1c9a Remove references to no-longer-need path_string_config 2023-01-28 17:53:40 -08:00
Grant Sanderson
28c4921a1a Specify ctx type 2023-01-28 15:40:58 -08:00
Grant Sanderson
0d36f17f9c Remove unused line 2023-01-28 15:40:10 -08:00
Grant Sanderson
c08e111911 Default to applying depth test for non-fixed objects added to a 3d scene 2023-01-28 15:02:00 -08:00
Grant Sanderson
368f48f8dd Make use of some glsl built-ins 2023-01-28 15:01:32 -08:00
Grant Sanderson
474a6c27e3 Counteract alpha scaling from fill frag 2023-01-28 15:00:15 -08:00
Grant Sanderson
31b937a7f1 Remove no-longer necessary PathString configuration 2023-01-28 13:19:06 -08:00
Grant Sanderson
c6db74c996 Re-order conditional blocks 2023-01-28 13:06:23 -08:00
Grant Sanderson
fbde9e8bba Check for null orientation in getting euler angles 2023-01-28 13:06:08 -08:00
Grant Sanderson
8d72340501 Remove some references to triangulation 2023-01-28 12:43:56 -08:00
Grant Sanderson
144e512952 Use active camera config, not default 2023-01-28 12:19:49 -08:00
Grant Sanderson
dec5089777 Update SurfaceExample to use ThreeDScene 2023-01-28 12:04:15 -08:00
Grant Sanderson
5deef1c249 Allow for setting a default frame orientation 2023-01-28 12:02:19 -08:00
Grant Sanderson
a1449def95 Move SampleSpaceScene to videos repo 2023-01-28 11:45:31 -08:00
Grant Sanderson
2a3f927566 Set defaults so that VMobjects will have no antialias width when depth test is turned on 2023-01-28 11:39:31 -08:00
Grant Sanderson
c7ef4eefbc Add getter/setter for anti_alias_width 2023-01-28 11:35:26 -08:00
Grant Sanderson
ab03a0cfba Add Mobject.set_uniform 2023-01-28 11:35:15 -08:00
Grant Sanderson
05a80f53a7 Make sure depth test calls map to the whole family 2023-01-28 10:37:34 -08:00
Grant Sanderson
60a27f52f1 Add depth sampling for fill 2023-01-28 10:36:41 -08:00
Grant Sanderson
1a62314719 Add a "clear" option for Mobjects
This not only sets the submobject list to 0, but removes self from the parent lists
2023-01-28 10:11:22 -08:00
Grant Sanderson
8a18967ea4 Initial implementation of render groups in Scene 2023-01-28 10:11:10 -08:00
Grant Sanderson
fc379dab18 Add a "clear" option for Mobjects
This not only sets the submobject list to 0, but removes self from the parent lists
2023-01-28 10:10:51 -08:00
Grant Sanderson
f296dd8df5 Merge pull request #1974 from 3b1b/video-work
Miscellaneous bug fixes
2023-01-27 19:28:40 -08:00
Grant Sanderson
047128a663 Make sure shader_wrapper inherits depth test 2023-01-27 19:27:42 -08:00
Grant Sanderson
38abef8871 Fix ShaderWrapper.init_textures 2023-01-27 19:27:23 -08:00
Grant Sanderson
8ecfc2b2cf add shaders to universal imports 2023-01-27 17:08:22 -08:00
Grant Sanderson
ce5d0b61f9 Add back accidentally deleted reverse_points code 2023-01-27 16:58:22 -08:00
Grant Sanderson
71ef39ea5b Remove "poor man's anti-aliasing" for Fill and instead render a small border width for fill 2023-01-27 16:15:20 -08:00
Grant Sanderson
3a01eb31bd Remove group_type arg 2023-01-27 16:02:47 -08:00
Grant Sanderson
d5b1a1725d Allow Mobject.remove to remove any family member, not just immediate submobjects 2023-01-27 15:15:16 -08:00
Grant Sanderson
1f6363821b Have VMobject inherit children uniforms when rendering 2023-01-27 14:48:57 -08:00
Grant Sanderson
35c19fe8a7 Edit is_fixed_in_frame 2023-01-27 14:48:31 -08:00
Grant Sanderson
e57ca4e1ee Track orientation for non-winding fill 2023-01-27 12:43:21 -08:00
Grant Sanderson
86fb69c5bb Track unit normal for fill 2023-01-27 12:35:43 -08:00
Grant Sanderson
1c432dd6dc Small refactor to stroke geom shader 2023-01-27 10:48:06 -08:00
Grant Sanderson
3a0916fe3a Reorganize fbo initialization 2023-01-27 10:12:53 -08:00
Grant Sanderson
40ae481979 Marginally better vbo/ibo tracking 2023-01-27 10:01:37 -08:00
Grant Sanderson
97e4c25453 Add comment 2023-01-27 08:29:41 -08:00
Grant Sanderson
1707958e0f Clean up fill shader a bit 2023-01-27 08:26:54 -08:00
Grant Sanderson
746b52cda5 Okay, actually fix Arrow 2023-01-26 23:51:05 -08:00
Grant Sanderson
86fb1d82f5 Typo fix 2023-01-26 23:44:33 -08:00
Grant Sanderson
de7545e5fa Tiny tweak to array_is_constant 2023-01-26 23:43:34 -08:00
Grant Sanderson
b21e470e69 In append_vectorized_mobject, append data as well as points 2023-01-26 23:43:21 -08:00
Grant Sanderson
79039bde61 Fix Arrow 2023-01-26 23:42:03 -08:00
Grant Sanderson
2863672740 Small clean up 2023-01-26 23:05:09 -08:00
Grant Sanderson
422c9cebd2 Only trigger triangulation for non-winding fill 2023-01-26 23:05:01 -08:00
Grant Sanderson
6388647860 Change to using glBlendFuncSeparate
To please type checkers
2023-01-26 22:51:14 -08:00
Grant Sanderson
eab8edd51d Remove needless list(...) 2023-01-26 22:41:36 -08:00
Grant Sanderson
8f6c14ad5f Increase threshold for discarding fill fragments 2023-01-26 22:41:23 -08:00
Grant Sanderson
7a59cc2f03 Use gl.MAX to blend alphas in fill 2023-01-26 22:40:29 -08:00
Grant Sanderson
0f89349bb8 Small clean up 2023-01-26 21:00:20 -08:00
Grant Sanderson
ab57b0acf0 Ensure positive orientation on all SVG, not just PathStrings 2023-01-26 21:00:07 -08:00
Grant Sanderson
1bd6a77151 Don't use @stash_mobject_pointers on copy, be more explicit 2023-01-26 20:59:36 -08:00
Grant Sanderson
bf2a609246 Have animation group collect parts as a VGroup if it can 2023-01-26 20:39:05 -08:00
Grant Sanderson
e9333a908c Move camera.clear call into 'capture' 2023-01-26 20:38:38 -08:00
Grant Sanderson
5803a00598 Use smaller fill_texture, adjusting winding-fill blending hack as is necessary 2023-01-26 20:14:22 -08:00
Grant Sanderson
9ee9e1946a Use non-window fbo in scene.get_image 2023-01-26 20:02:50 -08:00
Grant Sanderson
3a175c1a4c Note that sorting surface indices affects data 2023-01-26 20:02:28 -08:00
Grant Sanderson
258bc2256a Provide a check that shader uniforms really need updating before setting value 2023-01-26 20:01:59 -08:00
Grant Sanderson
adfef48418 Merge pull request #1973 from 3b1b/video-work
Refactor render out of camera (plus winding fill blending fix)
2023-01-26 16:54:44 -08:00
Grant Sanderson
acdc2654d3 Account for 'target_mobject is None' case 2023-01-26 16:52:25 -08:00
Grant Sanderson
9696827213 Allow for adding null subpath 2023-01-26 16:50:22 -08:00
Grant Sanderson
6d4782506a Account for null fill cases in invinisble_copy 2023-01-26 16:49:36 -08:00
Grant Sanderson
28c875c2c3 Finish Transforms with a call to Mobject.become 2023-01-26 16:49:13 -08:00
Grant Sanderson
164c9ba754 Use copy in set_data 2023-01-26 16:46:05 -08:00
Grant Sanderson
8ef71bb930 Don't use 'become' for interpolate at alpha = 0 or 1 2023-01-26 16:43:40 -08:00
Grant Sanderson
a8da171adb Make sure a group inherits the fixed_in_frame status of its parts 2023-01-26 15:58:56 -08:00
Grant Sanderson
65afed1bd1 Move shading from fill geom to fill frag shader 2023-01-26 15:50:27 -08:00
Grant Sanderson
14cda7e908 Don't show progress bar in embed by default 2023-01-26 15:28:10 -08:00
Grant Sanderson
37f0bf8c11 Fix winding fill blending
(Using somewhat of a hack)
2023-01-26 15:27:48 -08:00
Grant Sanderson
3f5df432ce Consider winding_fill alphas pre-multiplied 2023-01-26 12:17:21 -08:00
Grant Sanderson
a33b24310e Fix non-winding-fill orientation 2023-01-26 12:15:30 -08:00
Grant Sanderson
c6c23a1fe7 Unnecessary refresh_bounding_box 2023-01-26 12:06:13 -08:00
Grant Sanderson
f5cb2bfa52 Check for mismatched keys in uniform interpolation 2023-01-26 12:06:05 -08:00
Grant Sanderson
05dd399270 Ensure svgs have positive orientation by default 2023-01-26 12:04:58 -08:00
Grant Sanderson
72590a8fef Note that using winding fill changes data 2023-01-26 12:04:34 -08:00
Grant Sanderson
a1595a9e2f Use separate fbo for writing to file when window is active 2023-01-25 22:34:11 -08:00
Grant Sanderson
a68bc1271b Have FillShaders all share the same texture used for intermediary rendering 2023-01-25 19:43:16 -08:00
Grant Sanderson
cb36fda6d7 In interpolate, only update data status if some keys are unlocked 2023-01-25 19:24:19 -08:00
Grant Sanderson
88590e5a05 Remove serializing deepcopy 2023-01-25 19:23:55 -08:00
Grant Sanderson
a601384211 Remove stray imports 2023-01-25 19:23:22 -08:00
Grant Sanderson
f96a697ee3 Use become for interpolating at 0 or 1 2023-01-25 19:21:24 -08:00
Grant Sanderson
3c8e3792e7 Remove references to refresh_static_mobjects 2023-01-25 19:20:11 -08:00
Grant Sanderson
2beb55727f Change naming logic for recorded inserts 2023-01-25 17:30:12 -08:00
Grant Sanderson
7609b1db78 Set up by-hand anti-aliasing for FillShaderWrapper 2023-01-25 17:19:44 -08:00
Grant Sanderson
018b07212f Change how joint_products are updated in pointwise_become_partial 2023-01-25 16:50:52 -08:00
Grant Sanderson
d2af6a5f4b Keep track of when Mobject data has changed, and used that to determine when ShaderWrapper generates new buffers 2023-01-25 16:43:47 -08:00
Grant Sanderson
4dfabc1c28 Make sure FillShaderWrapper works without a window 2023-01-25 14:20:36 -08:00
Grant Sanderson
424707d035 Move rendering more fully away from Camera to Mobject and ShaderWrapper 2023-01-25 14:13:56 -08:00
Grant Sanderson
2c737ed540 Move most of rendering logic to ShaderWrapper 2023-01-25 13:45:18 -08:00
Grant Sanderson
c94d8fd3b0 Move Texture handling and vao creation outside of Camera 2023-01-25 12:10:39 -08:00
Grant Sanderson
3299741359 Move program code to ShaderWrapper, away from Camera 2023-01-25 11:23:31 -08:00
Grant Sanderson
10047773f7 Have ShaderWrapper track OpenGL context 2023-01-25 10:49:30 -08:00
Grant Sanderson
16d773f1b3 Remove refresh_shader_data 2023-01-25 10:48:59 -08:00
Grant Sanderson
80729c0cb8 Only initialize ShaderWrappers as needed 2023-01-25 10:37:12 -08:00
Grant Sanderson
8c1e5f3b42 Change use_clip_plane to be a function 2023-01-25 10:31:05 -08:00
Grant Sanderson
3339aad29e Separate CameraFrame into its own file 2023-01-25 10:19:44 -08:00
Grant Sanderson
a9a3ca08cd Merge pull request #1972 from 3b1b/winding-fill
Winding fill
2023-01-25 10:01:31 -08:00
Grant Sanderson
bc5c78de83 Add winding fill to VMobject args 2023-01-25 09:56:07 -08:00
Grant Sanderson
0ea91f22b2 Merge pull request #1971 from 3b1b/video-work
Video work
2023-01-25 09:54:55 -08:00
Grant Sanderson
7deaf4cb11 Small clean up 2023-01-25 09:51:27 -08:00
Grant Sanderson
93dd9f687b Ensure align_family works well with VMobject fill 2023-01-25 09:50:16 -08:00
Grant Sanderson
4cb9c9c2fc Remove unnecessary normalize 2023-01-25 08:18:10 -08:00
Grant Sanderson
6cf8c8d2e8 Do refresh in pointwise_become_partial 2023-01-24 21:47:12 -08:00
Grant Sanderson
272925fa19 Change winding fill blend_func 2023-01-24 21:46:52 -08:00
Grant Sanderson
0e2d21bed3 Don't necessarily use VGroup with FadeTransform 2023-01-24 21:46:34 -08:00
Grant Sanderson
346d252451 Don't save triangulation, but do orient svg paths positively 2023-01-24 21:33:00 -08:00
Grant Sanderson
088a2f65a3 Misc. clean up 2023-01-24 21:10:57 -08:00
Grant Sanderson
307487e087 Don't pre-normalize joint_products 2023-01-24 21:10:26 -08:00
Grant Sanderson
98eccab977 Ensure background rectangle matches orientation 2023-01-24 20:03:33 -08:00
Grant Sanderson
f0df5c759d Make winding fill optional, and make winding additive rather than toggling 2023-01-24 20:03:23 -08:00
Grant Sanderson
e9c70dbfd9 Ensure vert_indices are always of type int 2023-01-24 16:58:47 -08:00
Grant Sanderson
72da9786a3 Use null array for vert indices in place of None 2023-01-24 15:53:43 -08:00
Grant Sanderson
516fe9155e Small tweaks 2023-01-24 15:29:35 -08:00
Grant Sanderson
b93e284695 In aligning families, scale inserted submobjects to 0 2023-01-24 15:29:09 -08:00
Grant Sanderson
88ed1a2fdb Have init_fill_fbo take in ctx as an argument 2023-01-24 15:28:09 -08:00
Grant Sanderson
945aa9713f Fix aligned subpaths bug 2023-01-24 15:02:06 -08:00
Grant Sanderson
aa6c321a0a Change InteractiveScene dot config 2023-01-24 14:25:02 -08:00
Grant Sanderson
87afdac6a4 Small clean up 2023-01-24 14:09:41 -08:00
Grant Sanderson
6e56c31d67 Use gl_InstanceID instead of hacking triangle_strip 2023-01-24 13:49:43 -08:00
Grant Sanderson
4774d2bc3b First pass at a winding-based fill approach 2023-01-24 13:29:34 -08:00
Grant Sanderson
d01658bc5b Fix multi-color setting 2023-01-24 13:04:13 -08:00
Grant Sanderson
97789fff35 Swap buffers when resetting to default position 2023-01-24 12:05:39 -08:00
Grant Sanderson
b1f0270316 Change threshold for bevel reduction 2023-01-24 12:05:25 -08:00
Grant Sanderson
b99b88fd25 Update Scene.get_image to resize window if needed 2023-01-24 12:05:08 -08:00
Grant Sanderson
1dda706335 Small cleanup 2023-01-24 12:04:43 -08:00
Grant Sanderson
8a6deb4068 Enable recording during a Scene embed 2023-01-23 17:10:18 -08:00
Grant Sanderson
e2421a650c Don't disable clip plane 2023-01-23 17:06:49 -08:00
Grant Sanderson
b0cca9e4b6 Camera pixel_shape should reflect the drawn fbo 2023-01-23 17:04:44 -08:00
Grant Sanderson
c13495deeb Blit based on window's viewport, when in preview mode 2023-01-23 17:03:46 -08:00
Grant Sanderson
03080a10a7 Small style tweaks 2023-01-23 15:05:10 -08:00
Grant Sanderson
8d729eef5a Rename perspective to view_matrix 2023-01-23 14:41:17 -08:00
Grant Sanderson
e8b75941e0 Get rid of pixel_width and pixel_height attrs on Camera 2023-01-23 14:02:06 -08:00
Grant Sanderson
1d4fcf020b Refer directly to fbo viewports in get_raw_fbo_data 2023-01-23 11:54:01 -08:00
Grant Sanderson
8ce5dc7e84 Add DieFace to drawings.py 2023-01-23 11:53:30 -08:00
Benjamin Bastian
b934ee5f50 Make isinstance check work for python 3.7-3.9 (#1969) 2023-01-23 11:36:51 +08:00
Grant Sanderson
ba1b43df1a Merge pull request #1967 from 3b1b/video-work
Miscellaneous fixes
2023-01-22 09:48:50 -08:00
Grant Sanderson
847c27ad23 Remove stray import 2023-01-22 09:45:50 -08:00
Grant Sanderson
8b786311af Rename set_ctx_clip_distance -> set_ctx_clip_plane 2023-01-22 09:45:41 -08:00
Grant Sanderson
39cda62b66 Remove texture_id == 15 hack 2023-01-22 09:07:26 -08:00
Grant Sanderson
24864a3d61 Small tweak 2023-01-21 11:13:22 -08:00
Grant Sanderson
9e02796c9a Fix set_width for variable stroke width 2023-01-20 21:38:48 -08:00
Grant Sanderson
8e1fdd5a79 Change crosshair style 2023-01-20 21:38:36 -08:00
Grant Sanderson
0d66981ac7 Fix issue with variable stroke width 2023-01-20 21:38:28 -08:00
Grant Sanderson
2e2e8dfee2 Fix the fact that Write messes up joint_products 2023-01-20 20:33:47 -08:00
Grant Sanderson
917481cb23 Always refresh in pointwise_become_partial 2023-01-20 20:33:23 -08:00
Grant Sanderson
8fee4d1a66 Fix straight line fill anti-alias issue 2023-01-20 17:35:36 -08:00
Grant Sanderson
f2d4313bcf Update SurfaceExample to specify samples at scene level 2023-01-20 17:05:59 -08:00
Grant Sanderson
d08a16a5fb Make Camera.fbo the entity rendered to, with a separate fbo for emitted frames 2023-01-20 16:31:09 -08:00
Grant Sanderson
66d12a1687 Specify number of samples at the scene level, and be sure it works for Window as well 2023-01-20 16:30:39 -08:00
Grant Sanderson
9249433144 Don't pass uv_func from SGroup 2023-01-20 16:26:06 -08:00
Grant Sanderson
23b4e3e03b Use cubic formula in stroke frag for large stroke width case 2023-01-20 13:22:18 -08:00
Grant Sanderson
6c262f63b1 Reduce is_linear threshold 2023-01-20 10:10:39 -08:00
Grant Sanderson
b7ea24f9ea Tweak stroke frag 2023-01-20 10:09:58 -08:00
Grant Sanderson
7df6efb55f Handle edge case of false endpoints in make_smooth 2023-01-20 10:08:12 -08:00
Grant Sanderson
fe7dc3c459 Tiny cleanup 2023-01-20 10:07:28 -08:00
Grant Sanderson
2290f810ca Small tweaks to number_to_point 2023-01-19 20:25:29 -08:00
Grant Sanderson
debc68a3b4 Just moving some lines around 2023-01-19 20:24:59 -08:00
Grant Sanderson
909e515a2f Only compute xyz-to-uv matrix in non-linear case 2023-01-19 20:24:32 -08:00
Grant Sanderson
1c2ec03f7d Replace VMobject.make_approximately_smooth with VMobject.make_smooth(approx=True) 2023-01-19 11:34:13 -08:00
Grant Sanderson
6839de9a31 Remove rotate.glsl 2023-01-19 10:52:53 -08:00
Grant Sanderson
c873d073e2 Factor out rotation matrix 2023-01-19 10:47:21 -08:00
Grant Sanderson
1eae7c06ba Add conditions for resize_with_interpolation 2023-01-19 09:56:40 -08:00
Grant Sanderson
dbeef42600 In VMobject.set_stroke, Don't use resize_with_interpolation for non-list args 2023-01-19 09:56:26 -08:00
Grant Sanderson
93f3c6535f Have DecimalMobject save pre-generated characters 2023-01-19 09:55:56 -08:00
Grant Sanderson
763967281f Don't use resize_with_interpolation by default 2023-01-19 09:51:19 -08:00
Grant Sanderson
1367e31439 Fix bug with polygon start angle = 0 2023-01-19 09:50:41 -08:00
Grant Sanderson
e6abff4299 Speed up bind_graph_to_func 2023-01-19 09:50:24 -08:00
Grant Sanderson
148898f983 Fix insert_n_curves 2023-01-18 23:43:51 -08:00
Grant Sanderson
981fe009e5 Fix radius on dots 2023-01-18 23:43:36 -08:00
Grant Sanderson
22d2819ecf Improvements to make_smooth 2023-01-18 22:39:02 -08:00
Grant Sanderson
781e0a9805 Merge pull request #1966 from 3b1b/video-work
Various render improvements
2023-01-18 16:26:06 -08:00
Grant Sanderson
3820e098c0 Tweak to type hints 2023-01-18 16:25:32 -08:00
Grant Sanderson
8e2cf04b71 Simplify true_dot shaders 2023-01-18 15:36:00 -08:00
Grant Sanderson
cd3c5031fa Fix get_perspective_transform to shift before rotation 2023-01-18 13:59:50 -08:00
Grant Sanderson
6c2544098b Store pixel_size instead of pixel_shape 2023-01-18 13:50:09 -08:00
Grant Sanderson
b667d89e9b Simplify get_gl_Position 2023-01-18 13:44:41 -08:00
Grant Sanderson
fa525b494c Increase threshold for bevel tweaking 2023-01-18 13:07:37 -08:00
Grant Sanderson
13c41be17f Small clean up 2023-01-18 13:07:18 -08:00
Grant Sanderson
e20efda3df Revert away from using curve_to_quadratic 2023-01-18 13:03:23 -08:00
Grant Sanderson
8b3aa8f5c6 Account for edge cases on curve_to_quadratic 2023-01-18 12:52:47 -08:00
Grant Sanderson
d39fea0d4d A few small fixes 2023-01-18 12:52:05 -08:00
Grant Sanderson
96b0ec9094 Use fontTools.cu2qu.cu2qu import curve_to_quadratic 2023-01-18 12:29:25 -08:00
Grant Sanderson
40436d6370 Slightly cleaner xs_on_clean_parabola 2023-01-18 11:03:22 -08:00
Grant Sanderson
44e5f15ae9 Default to non-flat stroke for meshes 2023-01-18 10:58:37 -08:00
Grant Sanderson
72e5bde274 Rename xy-to-uv -> xyz-to-uv 2023-01-18 10:58:25 -08:00
Grant Sanderson
1a663943c9 Simpler xyz-to-uv map for linear case 2023-01-18 10:57:14 -08:00
Grant Sanderson
874906bedf Replace 'light_source_position' with 'light_position' 2023-01-18 10:04:51 -08:00
Grant Sanderson
0b72bc5d08 Fix joint normal issue 2023-01-18 10:04:05 -08:00
Grant Sanderson
c7e32e847d Delete no-longer used functions 2023-01-17 17:46:23 -08:00
Grant Sanderson
5e1a02d2ce Use xyz-to-uv matrix for fill 2023-01-17 17:45:25 -08:00
Grant Sanderson
9ed8dd5439 Clean up 2023-01-17 17:39:32 -08:00
Grant Sanderson
b7831ef3f1 Go back to computing xyz-to-uv before repositioning 2023-01-17 17:27:20 -08:00
Grant Sanderson
c2587de691 Apply xyz_to_uv to pre-positioned points 2023-01-17 17:22:00 -08:00
Grant Sanderson
c563ec2036 Simplifications 2023-01-17 17:20:30 -08:00
Grant Sanderson
870e88f8c9 First attempt at finding uv coords from 3d space instead of 2d 2023-01-17 15:46:09 -08:00
Grant Sanderson
7fe84d9263 Don't recompute cross(v01, v12) 2023-01-17 13:18:32 -08:00
Grant Sanderson
1b3bc7a27c For linearity, check cosine of angle instead of angle 2023-01-17 13:16:58 -08:00
Grant Sanderson
b16f0981f6 No need to set flat stroke defaults in Polygon/Polyline 2023-01-17 11:37:06 -08:00
Grant Sanderson
abbe131e8d Track full cross product and dot product of tangent vectors at joints
And alter the convention of what flat_stroke means to be more sensible in 3d
2023-01-17 11:36:47 -08:00
Grant Sanderson
4de0d098ea Allow cross to take an 'out' array 2023-01-17 11:35:50 -08:00
Grant Sanderson
ed2dbfd9b9 Remove unused imports 2023-01-16 21:48:11 -08:00
Grant Sanderson
8d277af47c Go back the convention of positioning stroke vertices in space before geom shader 2023-01-16 21:46:43 -08:00
Grant Sanderson
8ac0aa484b Slight tweak to get_unit_normal 2023-01-16 19:34:37 -08:00
Grant Sanderson
1f613953d6 Don't pre-rotate light source 2023-01-16 19:34:20 -08:00
Grant Sanderson
c3cd64f68c Package reflectiveness, gloss and shadow into a single uniform "shading" 2023-01-16 19:33:57 -08:00
Grant Sanderson
6e6a30c95a Be sure joint_angles are updated immediately before being read into a shader wrapper 2023-01-16 14:28:53 -08:00
Grant Sanderson
20222bc7e9 Update imports 2023-01-16 14:18:49 -08:00
Grant Sanderson
f15ac81131 Pull out helper functions from shader_wrapper.py 2023-01-16 14:18:35 -08:00
Grant Sanderson
7050c7e7b0 Change type for Surface.shader_dtype 2023-01-16 14:11:30 -08:00
Grant Sanderson
161bf7377d Merge pull request #1965 from 3b1b/data-arrays
Data arrays
2023-01-16 14:06:56 -08:00
Grant Sanderson
bd2d45ebc6 Handle edge case of low ring end 2023-01-16 14:00:53 -08:00
Grant Sanderson
dd0e91015c Account for null family case 2023-01-16 13:56:03 -08:00
Grant Sanderson
1a15756330 Make sure Mobject.become works with bounding_box 2023-01-16 13:55:53 -08:00
Grant Sanderson
8ef93b0f9d Treat font_size as a float 2023-01-16 13:43:48 -08:00
Grant Sanderson
a46e580fa4 Make sure resize_preserving_order preserves data type 2023-01-16 13:37:06 -08:00
Grant Sanderson
3b40ccc987 In Mobject.append_points, have most data default to the last value 2023-01-16 13:29:35 -08:00
Grant Sanderson
ae50748717 Default to resizing_preserving_order in set_points 2023-01-16 13:28:09 -08:00
Grant Sanderson
c23f020d9a Add Mobject.const_data_keys so that interpolations can be faster 2023-01-16 13:27:20 -08:00
Grant Sanderson
db45d9e646 Add array_is_constant 2023-01-16 13:26:44 -08:00
Grant Sanderson
f5480d02ff Tidy up ShaderWrapper.read_in 2023-01-16 13:26:34 -08:00
Grant Sanderson
3f8c861973 Rename draw_stroke_behind_fill -> stroke_behind 2023-01-16 11:53:07 -08:00
Grant Sanderson
3a09acd28c Update type hint for ShaderWrapper.uniforms 2023-01-16 11:50:48 -08:00
Grant Sanderson
bdcfbc39ec Cleanup VMobject shader wrapper methods
Deleting those which are no longer needed
2023-01-16 11:50:31 -08:00
Grant Sanderson
74b42a6eb5 Small renaming 2023-01-16 08:59:50 -08:00
Grant Sanderson
5a95bfa70f Delete align_stroke_width_data_to_points 2023-01-15 21:28:19 -08:00
Grant Sanderson
eba86be35b Use resize_preserving_order in aligning VMobjects 2023-01-15 21:23:56 -08:00
Grant Sanderson
2ca8848007 Merge branch 'master' of github.com:3b1b/manim into data-arrays 2023-01-15 21:08:55 -08:00
Grant Sanderson
afbc624ac4 Merge pull request #1964 from 3b1b/video-work
Fix index buffer bug
2023-01-15 21:08:25 -08:00
Grant Sanderson
77a3984683 Fix index buffer bug 2023-01-15 21:06:55 -08:00
Grant Sanderson
29f51a7c6a Check if joint_angles are in locked_data_keys before computing 2023-01-15 20:31:27 -08:00
Grant Sanderson
ba9f61b50b Have ShaderWrapper read in data rather than other shader wrappers 2023-01-15 20:27:19 -08:00
Grant Sanderson
f63331eb24 Use Mobject.data in place of shader_data, remove read_data_into_shader 2023-01-15 20:01:37 -08:00
Grant Sanderson
6f9f83fb1b Unify shader_dtype and data_dtype 2023-01-15 19:09:29 -08:00
Grant Sanderson
3f2fd5b142 Update calculation of path ends to not include adjacent pairs 2023-01-15 18:27:08 -08:00
Grant Sanderson
90ac1fc0bf Rename 'points' -> 'point' 2023-01-15 18:23:41 -08:00
Grant Sanderson
3ba5237f9b Account for edge case with end of loop near end 2023-01-15 18:10:38 -08:00
Grant Sanderson
408890e0d9 Update TexturedSurface for using a data array 2023-01-15 18:02:16 -08:00
Grant Sanderson
9704f063c9 Use pointlike_data_keys for interpolation and rotation 2023-01-15 18:01:37 -08:00
Grant Sanderson
d267c00761 Update Surface to have a data array 2023-01-15 17:40:46 -08:00
Grant Sanderson
ca5e119425 Use _data_defaults for DotCloud radius 2023-01-15 17:40:22 -08:00
Grant Sanderson
7e45558c55 Allow Mobject subclasses to specify which parts of data should act like points 2023-01-15 17:40:05 -08:00
Grant Sanderson
e55434925e Fix get_style for null point case 2023-01-15 16:50:00 -08:00
Grant Sanderson
d868f685dc Account for case of setting stroke width with null array 2023-01-15 16:49:48 -08:00
Grant Sanderson
e37b667c8b Modify Mobject.interpolate 2023-01-15 16:49:24 -08:00
Grant Sanderson
2dafcb3e63 Remove check_data_alignment 2023-01-15 16:48:13 -08:00
Grant Sanderson
801f449ca0 Fix lock_matching_data 2023-01-15 16:46:27 -08:00
Grant Sanderson
f4c50f61b8 Change rgbas -> rgba 2023-01-15 16:46:03 -08:00
Grant Sanderson
2815f60616 First pass at changing data to structure numpy array
This doesn't yet tackle Surface
2023-01-15 16:05:18 -08:00
Grant Sanderson
286b8fb6c3 Set the stage for data to be treated as a structure numpy array 2023-01-15 12:34:59 -08:00
Grant Sanderson
f2e91ef66f Use uniform for ValueTracker value instead of data 2023-01-15 12:34:28 -08:00
Grant Sanderson
da15eb4ad2 Remove a few direct references to data["points"] 2023-01-15 10:00:05 -08:00
Grant Sanderson
ea943de557 Store font size in uniforms not data 2023-01-15 09:50:52 -08:00
Grant Sanderson
8e1b23ee98 Merge pull request #1955 from IcedMonk/yearUpdate
update the year
2023-01-15 09:39:56 -08:00
Grant Sanderson
de38b56d0d Merge pull request #1959 from 3b1b/video-work
Simplify quadratic bezier shaders
2023-01-15 09:39:16 -08:00
Grant Sanderson
51a5086093 Don't default to smoothing for implicit curves 2023-01-15 09:26:56 -08:00
Grant Sanderson
bf726667a8 Don't make TexAndNumbersExample an InteractiveScene by default 2023-01-15 09:26:17 -08:00
Grant Sanderson
4582f5d331 Clean up sort_faces_back_to_front 2023-01-14 16:03:27 -08:00
Grant Sanderson
5b8fb1828f Use ibo for single use render groups 2023-01-14 16:03:16 -08:00
Grant Sanderson
702bb2776c Remove (no-longer needed) filename_to_code_map 2023-01-14 09:41:32 -08:00
Grant Sanderson
c9ba32b568 No need to call tobytes 2023-01-14 08:31:53 -08:00
Grant Sanderson
19a7721661 Fix unit normal issue with bezier fill 2023-01-14 08:00:21 -08:00
Grant Sanderson
c8238f6b39 Consolidate lighting uniforms 2023-01-14 07:57:47 -08:00
Grant Sanderson
d19b244ee1 Move uniform pixel_shape declaration outside of get_gl_Position 2023-01-13 22:11:13 -08:00
Grant Sanderson
a004c88e02 Consolidate functions associated with computing gl_Position 2023-01-13 22:07:28 -08:00
Grant Sanderson
fa81d9f6ea Move comment 2023-01-13 21:43:01 -08:00
Grant Sanderson
3555936c4d Check for valid inputs to arccos 2023-01-13 21:42:54 -08:00
Grant Sanderson
dae51abc17 Allow for a clip plane with Surface 2023-01-13 21:42:34 -08:00
Grant Sanderson
1c1325ff8d More coloring work of Surface to vert shader 2023-01-13 21:04:36 -08:00
Grant Sanderson
28d5baeeff Small stylistic tweaks 2023-01-13 21:04:15 -08:00
Grant Sanderson
51efe0d18e Merge branch 'master' of github.com:3b1b/manim into video-work 2023-01-13 20:42:03 -08:00
Grant Sanderson
12c3af9647 Update to get_subpath_end_indices_from_points 2023-01-13 20:35:54 -08:00
Grant Sanderson
42909a94ac No need for np.repeat in normalize_along_axis 2023-01-13 20:34:18 -08:00
Grant Sanderson
071e7f1a74 Small tweak for get_joint_angles 2023-01-13 20:33:36 -08:00
Grant Sanderson
2aa2eedbbd Calculate unit normal more directly 2023-01-13 17:42:59 -08:00
Grant Sanderson
e534206eb6 Small style change to get_xy_to_uv 2023-01-13 17:37:08 -08:00
Grant Sanderson
394d87effb Small style tweaks to quadratic_bezier_fill/geom 2023-01-13 17:26:59 -08:00
Grant Sanderson
e832bb775f Fix small bug with null triangles 2023-01-13 16:53:22 -08:00
Grant Sanderson
31cc2671e5 Have line_intersects_path not assume closed path 2023-01-13 16:35:18 -08:00
Grant Sanderson
937b894826 Clean up find_intersection 2023-01-13 16:35:01 -08:00
Grant Sanderson
599f74c749 Refresh bounding boxes after selection 2023-01-13 14:59:53 -08:00
Grant Sanderson
91f976f7e9 Don't include bounding box in Mobject.data
In principle, Mobject.data should just carry information that will pass to shaders
2023-01-13 14:58:52 -08:00
Grant Sanderson
cbfe82579f Make sure VMobject.reverse_points works with new path convention 2023-01-13 14:16:04 -08:00
Grant Sanderson
ae99c8cd2e Go back to use_simple_quadratic_approx being set at the Text level 2023-01-13 14:08:29 -08:00
Grant Sanderson
da2b13aee9 Fix triangulation issue for Annulus 2023-01-13 13:22:26 -08:00
Grant Sanderson
a88b56bb04 Be sure get_triangulation is called before orientation data is read in 2023-01-13 13:22:06 -08:00
Grant Sanderson
73a894c136 Set selection animating status after adding mobjects 2023-01-13 13:07:29 -08:00
Grant Sanderson
c820cb4775 Don't propagate animating status to full extended family 2023-01-13 13:06:50 -08:00
Grant Sanderson
69ac946e63 Simpler get_gl_Position 2023-01-13 12:41:09 -08:00
Grant Sanderson
42d612f253 Clean up VMobject.get_joint_angles 2023-01-13 09:57:22 -08:00
Grant Sanderson
440138aac5 Allow for tracking which data keys should always have the same size as points 2023-01-13 09:47:06 -08:00
Grant Sanderson
108bb3da44 Have SVGs default to use_simple_quadratic_approx 2023-01-13 08:16:46 -08:00
Grant Sanderson
10d4db64c8 Merge pull request #1962 from LucaCappelletti94/tqdm_jupyter_notebook
Updated tqdm to support also notebooks
2023-01-13 08:09:14 -08:00
Grant Sanderson
e1abae1d96 Fix typo 2023-01-13 00:24:26 -08:00
Grant Sanderson
c8a77a352f Fix Piano 2023-01-13 00:24:19 -08:00
Grant Sanderson
d2800b6c96 Make sure cross behaves as expected on 2d arrays 2023-01-13 00:05:29 -08:00
Grant Sanderson
c9a2971433 Remove stray constant 2023-01-12 23:58:49 -08:00
Grant Sanderson
a12b5cca67 Add angle check in addition to use_simple_quadratic_approx 2023-01-12 23:58:43 -08:00
Grant Sanderson
aa8fba5b02 Change subdivide_intersections default to one subdivision, remove second check 2023-01-12 23:58:19 -08:00
Grant Sanderson
2c6e8692ce Add condition for calling subdivide_intersections 2023-01-12 23:45:26 -08:00
Grant Sanderson
a44fc2e6d5 Add optional number of subdivision to subdivide_intersections 2023-01-12 23:45:03 -08:00
Grant Sanderson
190b9e4603 Tiny tweak 2023-01-12 21:24:34 -08:00
Grant Sanderson
f39fd92e9e Add condition to account for end of string 2023-01-12 21:24:27 -08:00
Grant Sanderson
f0edc6628b Push default of use_simple_quadratic_approx being True to Text 2023-01-12 21:24:03 -08:00
Grant Sanderson
9f1ab09749 Make usage of simpler quadratic approximations an option for VMobject 2023-01-12 21:06:35 -08:00
Grant Sanderson
a2f3758a7a Add VMobject.subdivide_intersections 2023-01-12 21:05:19 -08:00
Grant Sanderson
de4a56849e Add line_intersects_path 2023-01-12 21:04:46 -08:00
Grant Sanderson
923066db2b Minor style changes to stroke geom shader 2023-01-12 19:54:32 -08:00
Grant Sanderson
de5198196b Fix anti-alias width bug on DotCloud 2023-01-12 19:44:10 -08:00
Grant Sanderson
a47b95044d Add assertions to assure VMobject has a valid number of points 2023-01-12 19:31:08 -08:00
Grant Sanderson
db8b0e7bce Change simple quadratic approximation threshold 2023-01-12 19:24:56 -08:00
Grant Sanderson
dcdf74a715 Clean up stroke geometry shader to better function in 3d 2023-01-12 19:24:42 -08:00
Grant Sanderson
9778c3e085 Remove no-longer necessary correction for path ends 2023-01-12 18:29:44 -08:00
Grant Sanderson
644ea41443 Default to simpler quadratic approximation of cubic beziers 2023-01-12 18:29:19 -08:00
Grant Sanderson
9464f83d18 Track indices for fill shader, but prevent ibo usage at the Camera level 2023-01-12 15:56:44 -08:00
Grant Sanderson
a07701e295 Use triangle strip for stroke shader 2023-01-12 15:56:12 -08:00
Grant Sanderson
24afb2a24f Fix angle_between 2023-01-12 14:43:52 -08:00
Grant Sanderson
c6d3a9646c Merge branch 'reduced-bezier-paths' into video-work 2023-01-12 13:38:55 -08:00
Grant Sanderson
64578df603 Don't save SVG Path data to file
It's more trouble than it's worth
2023-01-12 13:23:48 -08:00
Grant Sanderson
e676bd957b Do inner joint angle computation in geom shader 2023-01-12 13:08:35 -08:00
Grant Sanderson
9320dad45f Have get_subpath_end_indices_from_points ask for exact equality between h and a0 2023-01-12 11:54:25 -08:00
Grant Sanderson
ab30f085b4 Simpler VMobject.get_num_curves 2023-01-12 10:40:18 -08:00
Grant Sanderson
f3e91db581 Small comment change 2023-01-12 10:39:50 -08:00
Grant Sanderson
04fd50491e More consistent corners computation for stroke geom shader 2023-01-12 10:14:14 -08:00
Grant Sanderson
7d90a82317 Fix fill shader vert data 2023-01-12 10:13:42 -08:00
LucaCappelletti94
1708fbd672 Updated tqdm to support also notebooks 2023-01-12 12:03:14 +01:00
Grant Sanderson
1a8e923ef2 Small speed ups 2023-01-12 00:40:27 -08:00
Grant Sanderson
a8fb05a44a Don't use index buffer for fill
It turns out to be strangely slower
2023-01-12 00:23:22 -08:00
Grant Sanderson
c0b3c246de Revert "Revert "Go back to fill shader tracing vertex index manually""
This reverts commit 387de61119.
2023-01-12 00:16:05 -08:00
Grant Sanderson
ae42f6244e Don't use an index buffer for stroke 2023-01-12 00:14:07 -08:00
Grant Sanderson
131e1c2eeb Have corners emitted by stroke geom shader better line up with bezier control points
This is helpful for, say, setting variable stroke width
2023-01-11 21:30:12 -08:00
Grant Sanderson
1f04ba92fb Fix Polygon.round_corners 2023-01-11 20:47:04 -08:00
Grant Sanderson
ed26fdfab8 Have Polyline default to flat stroke 2023-01-11 20:31:38 -08:00
Grant Sanderson
bf84b1933b Remove unnecessary flat stroke specification 2023-01-11 20:31:30 -08:00
Grant Sanderson
5a56a2a5ec Remove bad solution to bevel issue 2023-01-11 20:28:00 -08:00
Grant Sanderson
f04d0ad350 Update add_cubic_bezier_curve 2023-01-11 20:27:37 -08:00
Grant Sanderson
8c435d6181 Default to flat stroke for polygons 2023-01-11 20:27:17 -08:00
Grant Sanderson
31715ec98a Fix Arrow 2023-01-11 20:09:28 -08:00
Grant Sanderson
a17a57825e Fix Annulus and sector 2023-01-11 20:01:54 -08:00
Grant Sanderson
61437b2a7f Remove (no longer used) n_points_per_curve 2023-01-11 19:53:00 -08:00
Grant Sanderson
333db992ed Minor cleanup to Prism 2023-01-11 19:52:45 -08:00
Grant Sanderson
8a08b62f7c Remove n_points_per_curve reference 2023-01-11 19:52:33 -08:00
Grant Sanderson
5d7e923ac6 Remove stray imports 2023-01-11 19:40:21 -08:00
Grant Sanderson
6a18a05a3b Fix arrow for new path behavior 2023-01-11 19:40:15 -08:00
Grant Sanderson
5fac213fee Change crosshair initialization 2023-01-11 19:27:34 -08:00
Grant Sanderson
557e57d95b Change taper width default 2023-01-11 19:27:23 -08:00
Grant Sanderson
d3a40eb1ac Update VShowPassingFlash for new path mode 2023-01-11 16:40:40 -08:00
Grant Sanderson
032a8fd030 Save answer for VMobject.get_outer_vert_indices 2023-01-11 16:25:42 -08:00
Grant Sanderson
387de61119 Revert "Go back to fill shader tracing vertex index manually"
This reverts commit 7847ff1a9d.
2023-01-11 16:22:12 -08:00
Grant Sanderson
5e459d57c6 Revert "Don't use index buffer"
This reverts commit d9dc956137.
2023-01-11 16:21:55 -08:00
Grant Sanderson
d9dc956137 Don't use index buffer
It's strangely longer
2023-01-11 15:59:46 -08:00
Grant Sanderson
7847ff1a9d Go back to fill shader tracing vertex index manually 2023-01-11 15:32:27 -08:00
Grant Sanderson
f2370afea0 Treat stroke indices properly 2023-01-11 15:20:05 -08:00
Grant Sanderson
dba70ceded Clean up Arc init 2023-01-11 14:55:03 -08:00
Grant Sanderson
da125c1072 Add saved outer_vert_indices 2023-01-11 14:42:06 -08:00
Grant Sanderson
c0fba529d9 Update crosshair for new path structure 2023-01-11 14:20:33 -08:00
Grant Sanderson
86b756ab1f No need for subdividing sharp curves 2023-01-11 14:20:20 -08:00
Grant Sanderson
55bc8464b9 Update Arc to make sense with new path structure 2023-01-11 14:19:57 -08:00
Grant Sanderson
21908a48de Change behavior of get_quadratic_approximation_of_cubic to return quintuplets instead of sextuplets 2023-01-11 14:19:33 -08:00
Grant Sanderson
40b9e22b6e Update some type hints in bezier 2023-01-11 14:19:17 -08:00
Grant Sanderson
2808710d60 Add VectNArray 2023-01-11 14:18:57 -08:00
Grant Sanderson
c4e1db7f9d Don't draw paths where the handle equals the first anchor 2023-01-11 14:18:45 -08:00
Grant Sanderson
2a7b787ef6 Test the orientation in the middle of the curve 2023-01-11 14:18:12 -08:00
Grant Sanderson
681fa513a7 First pass at changing VMobject use simpler path strings
Remove the redundancy of treating each quadratic bezier triplet separately, where most ends overlap the next beginning
2023-01-11 14:17:56 -08:00
Grant Sanderson
b967c04c2c Actually ensure get_joint_angle works for start_new_path case 2023-01-10 18:33:49 -08:00
Grant Sanderson
0205a37209 More tiny tweaks for joint_angles 2023-01-10 17:02:12 -08:00
Grant Sanderson
eccaa8681e Ensure get_joint_angles works in start-new-path case 2023-01-10 16:48:08 -08:00
Grant Sanderson
9d9e000c63 Remove stray line 2023-01-10 16:41:12 -08:00
Grant Sanderson
886fd193f0 Use three shader wrappers to account for backstroke 2023-01-10 16:41:03 -08:00
Grant Sanderson
bfaf81c6b3 Refresh shader data at the start of an animation 2023-01-10 16:40:31 -08:00
Grant Sanderson
470e7bee1e Recompute joint angle at the end of align_points 2023-01-10 16:01:52 -08:00
Grant Sanderson
e5b17aad69 Small bug fix for TransformMatchingShape 2023-01-10 16:01:34 -08:00
Grant Sanderson
22420b7724 Ensure joint angles are computed (if need be) during interpolation 2023-01-10 15:32:31 -08:00
Grant Sanderson
e189df81b1 Change default endpoint angle to be 0 2023-01-10 15:32:05 -08:00
Grant Sanderson
0c3367f27b First-pass fix for joints in 3d 2023-01-10 15:17:08 -08:00
Grant Sanderson
131ecce7c4 Add rotate function 2023-01-10 15:16:41 -08:00
Grant Sanderson
7212a98d65 Small clean up 2023-01-10 12:59:10 -08:00
Grant Sanderson
802bd58aa7 Small cleanup 2023-01-10 12:26:16 -08:00
Grant Sanderson
116d6fe244 Revert emit_pentagon to work on 3d vectors 2023-01-10 11:51:05 -08:00
Grant Sanderson
6ec3d9f4a5 Add 'no_joint' to joint types 2023-01-10 11:06:41 -08:00
Grant Sanderson
ab1227a908 Ensure joint angles are up to date in align_points 2023-01-10 10:26:30 -08:00
Grant Sanderson
27db1c5987 Add arrays_match function 2023-01-10 10:25:59 -08:00
Grant Sanderson
8cd59f852d Add small comments 2023-01-10 09:55:06 -08:00
Grant Sanderson
8175c2d408 Rename quadratic_bezier_geometry_functions to get_xy_to_uv 2023-01-10 09:53:17 -08:00
Grant Sanderson
b31ad49850 A little clean up to fill shader 2023-01-10 09:49:51 -08:00
Grant Sanderson
70113d5a48 Tiny cleanup 2023-01-10 08:58:08 -08:00
Grant Sanderson
4b652be492 Delete get_xyz_to_uv 2023-01-10 08:57:38 -08:00
Grant Sanderson
05796654f4 Replace "bezier_degree" with "is_linear" 2023-01-10 08:54:02 -08:00
Grant Sanderson
2b90f0b244 Add condition for auto-miter joints 2023-01-09 20:30:49 -08:00
Grant Sanderson
dcb166e21b Refresh joint_angles on almost any change 2023-01-09 20:30:29 -08:00
Grant Sanderson
02a0ffe04e Delete quadratic_bezier_distance.glsl 2023-01-09 20:17:10 -08:00
Grant Sanderson
98e358f87d Move a line 2023-01-09 20:10:34 -08:00
Grant Sanderson
494e04405c Refresh all family shader data in align_data 2023-01-09 20:10:06 -08:00
Grant Sanderson
480cc6759f Small cleanup 2023-01-09 19:54:21 -08:00
Grant Sanderson
b4544052d9 Change fill shader to use simpler uv space 2023-01-09 19:54:09 -08:00
Grant Sanderson
f75df1e26e Clean up a few messes from previous commit 2023-01-09 18:51:41 -08:00
Grant Sanderson
c67ae08b62 Have stroke geometry shader pass to a coordinate system where the curve is some segment of y = x^2 2023-01-09 16:46:38 -08:00
Grant Sanderson
d87db65344 Add stand-in for better unit normal treatment 2023-01-09 12:02:50 -08:00
Grant Sanderson
d1c765353d Small comment change 2023-01-09 11:56:27 -08:00
Grant Sanderson
fc86bf7f9e Specify that uniforms can be numpy arrays 2023-01-09 11:56:21 -08:00
Grant Sanderson
ccf7a503c1 Add ANGLE_THRESHOLD constant 2023-01-09 10:52:12 -08:00
Grant Sanderson
ccbb5534fa Remove find_intersection 2023-01-09 10:52:02 -08:00
Grant Sanderson
80c0e88133 Put joint_angle information in VMobject.data 2023-01-09 10:08:03 -08:00
Grant Sanderson
5870274adb Delete unused functions 2023-01-09 00:07:46 -05:00
Grant Sanderson
08c02b21aa Small comment 2023-01-09 00:02:30 -05:00
Grant Sanderson
a6e5f25912 Delete get_reduced_control_points 2023-01-09 00:00:31 -05:00
Grant Sanderson
47d0dca087 Check for null curve 2023-01-08 23:58:45 -05:00
Grant Sanderson
16390283cf Small cleanup for fill shader 2023-01-08 23:34:02 -05:00
Grant Sanderson
89d9e260eb Various cleanups for stroke shader 2023-01-08 23:33:39 -05:00
Grant Sanderson
ae2a253fb1 Just pass one bevel parameter rather than two 2023-01-08 21:56:38 -05:00
Grant Sanderson
0136cef1d9 Minor simplifications for stroke shader 2023-01-08 21:53:43 -05:00
Grant Sanderson
2bf1f5eb40 Make sure pointwise_become_partial doesn't needlessly refresh joint angles 2023-01-08 21:29:35 -05:00
Grant Sanderson
21051ce289 Performance improvement for get_joint_angles 2023-01-08 21:29:08 -05:00
Grant Sanderson
9e7cd1399d Add arr_clip (faster alternative to np.clip) 2023-01-08 21:27:56 -05:00
Grant Sanderson
d5fdc75164 Remove the need to track previous and next joints in stroke shader 2023-01-08 20:29:31 -05:00
Grant Sanderson
cae13aa1f0 Add space 2023-01-08 10:15:22 -05:00
Grant Sanderson
9b2495abb1 Remove "orientation" from locked_keys when recomputed
Rather than re-reading into fill_data
2023-01-05 08:46:12 -05:00
Grant Sanderson
f5455bb554 Avoid using set_points in Rotation 2023-01-05 08:29:59 -05:00
Grant Sanderson
0d433b075e Fix bug associated with orientation data not always getting written
This might reflect a deeper issue with the locked_data sometimes being _too_ locked.
2023-01-04 17:18:10 -08:00
Grant Sanderson
0c1abebd95 Don't save last frame by default 2023-01-04 16:39:59 -08:00
Grant Sanderson
1cf89abf53 No need to track vertex index manually 2023-01-04 16:39:59 -08:00
Grant Sanderson
c30b102458 Use re.sub instead of looping over re.findall 2023-01-04 16:39:59 -08:00
Grant Sanderson
bf5d587204 Merge pull request #1957 from 3b1b/video-work
Refactor TransformMatchingStrings/TransformMatchingShapes
2023-01-02 09:38:08 -08:00
Grant Sanderson
abf1dd3d8b Typo fix 2023-01-02 09:32:10 -08:00
Samyek
333fd2676d update the year 2023-01-01 09:24:42 +00:00
Grant Sanderson
a31c4ae3c2 Add more indexing details to TexTransformExample 2022-12-30 15:19:29 -08:00
Grant Sanderson
4c1f1f8749 Clean up TransformMatchingStrings 2022-12-30 15:09:07 -08:00
Grant Sanderson
3aa9eb6372 Add String.get_symbol_substrings and Tex.get_symbol_substrings 2022-12-30 15:07:41 -08:00
Grant Sanderson
4335e85659 Abstract away logic of TransformMatchingStrings to a new version of TransformMatchingParts 2022-12-30 13:54:55 -08:00
Grant Sanderson
96bc95ef38 Allow FadeTransform to group parts as VGroup if applicable 2022-12-30 13:53:32 -08:00
Grant Sanderson
cec43dfe51 Use Vect3 type in fading.py 2022-12-30 13:53:12 -08:00
Grant Sanderson
50960eefd4 Merge pull request #1953 from 3b1b/video-work
More tweaks and fixes
2022-12-29 21:00:00 -08:00
Grant Sanderson
66f0a57c6b Clean up looks_identical 2022-12-29 20:58:37 -08:00
Grant Sanderson
dddaef0e6c Add lru_cache for get_shader_code_from_file 2022-12-29 20:51:11 -08:00
Grant Sanderson
7895a2cfee No need to immediately compute triangulation for SVG paths 2022-12-29 20:51:00 -08:00
Grant Sanderson
7dde368eeb Ensure error-flash rect is fixed in frame 2022-12-29 20:18:19 -08:00
Grant Sanderson
8e3378f798 Orient Camera.get_pixel_array correctly 2022-12-29 20:17:54 -08:00
Grant Sanderson
632819dd6d Show examples of indexing by substrings and regular expressions 2022-12-29 19:45:29 -08:00
Grant Sanderson
fdccfd51fc Add TexAndNumbersExample 2022-12-29 19:43:12 -08:00
Grant Sanderson
5d87f3f954 Update parents of new_mob in Mobject.replace 2022-12-29 19:42:54 -08:00
Grant Sanderson
3b4a233bb1 Fix LaggedStartMap 2022-12-29 18:53:26 -08:00
Grant Sanderson
4db01fd221 Fix Mobject.looks_identical 2022-12-29 18:53:06 -08:00
Grant Sanderson
5c33c7e4a8 Remove "None" output type for set_animating_status 2022-12-29 18:52:37 -08:00
Grant Sanderson
7df12c68dc Tiny cleanup 2022-12-29 18:52:13 -08:00
Grant Sanderson
0f9adbf91c Add Tex.make_number_changable 2022-12-29 18:52:00 -08:00
Grant Sanderson
04d3e6a47c Interpolate colors using square of rgbs 2022-12-29 16:07:28 -08:00
Grant Sanderson
124c83d94e Merge pull request #1952 from 3b1b/video-work
More miscellaneous tweaks and fixes
2022-12-29 15:53:02 -08:00
Grant Sanderson
d6d75d8f9a Change from np.all(arr) to arr.all() 2022-12-29 15:50:35 -08:00
Grant Sanderson
2dbb9367c4 Default to removing null curves 2022-12-29 15:40:32 -08:00
Grant Sanderson
4bc7e3a8f2 Remove redundant long_line specification 2022-12-29 15:40:18 -08:00
Grant Sanderson
ba68505c18 Flash around border on exceptions in Scene.embed 2022-12-29 15:08:57 -08:00
Grant Sanderson
71815fd7de Add (optional) error sound for exceptions in Scene.embed 2022-12-29 14:58:40 -08:00
Grant Sanderson
cba101995f Allow for customizable exception display mode with Scene.embed 2022-12-29 14:38:25 -08:00
Grant Sanderson
4e014d7a8f Have get_scene_config automatically find intersection of Scene parameters and config keys
Previously it required duplicating any new parameter
2022-12-29 14:36:50 -08:00
Grant Sanderson
13fc8daba9 Extend StringMobject.select_unisolated_substring to work for regular expressions 2022-12-29 14:18:46 -08:00
Grant Sanderson
a54a81744d Have ShaderWrapper.combine_with do nothing for empty arg list 2022-12-29 12:04:35 -08:00
Grant Sanderson
3165a28cd0 Clean up and fix VMobject.get_shader_wrapper_list 2022-12-29 12:04:01 -08:00
Grant Sanderson
25ac5f3507 Use Mobject.1.has_same_shape_as in TransformMatchingStrings 2022-12-29 12:03:17 -08:00
Grant Sanderson
ef09d6fce2 Allow for AnimationGroup to specify that parts belong to a VGroup 2022-12-29 12:02:48 -08:00
Grant Sanderson
3738f0a48e Add Mobject.has_same_shape_as 2022-12-29 12:02:28 -08:00
Grant Sanderson
9018357d20 Fix Mobject.looks_identical 2022-12-29 12:02:20 -08:00
Grant Sanderson
4330f78ed6 Enable lag_ratio for TransformMatchingStrings 2022-12-29 10:44:52 -08:00
Grant Sanderson
1feae23566 Improve num_tex_symbols 2022-12-29 10:37:46 -08:00
Grant Sanderson
aedde4dffc Merge pull request #1951 from 3b1b/video-work
Small tweaks and fixes
2022-12-28 21:37:36 -08:00
Grant Sanderson
53f19b6620 Remove @staticmethod from @wraps functions 2022-12-28 21:36:21 -08:00
Grant Sanderson
42f2461acb Revert "Allow for file_writing to use multiple thread"
This reverts commit 7b0b31e8d9.
2022-12-28 21:22:04 -08:00
Grant Sanderson
4c39c1abd6 Use arr.all() instead of np.all(arr) 2022-12-28 21:06:51 -08:00
Grant Sanderson
596aea3bf5 Update to bezier 2022-12-28 21:06:36 -08:00
Grant Sanderson
7b0b31e8d9 Allow for file_writing to use multiple thread 2022-12-28 21:05:03 -08:00
Grant Sanderson
d6b308ed47 Update elements of VMobject.shader_wrapper_list with depth_test and appropriate uniforms 2022-12-28 19:40:33 -08:00
Grant Sanderson
cc9a4501ad Have **kwargs in matrix pass on to elements 2022-12-28 19:40:02 -08:00
Grant Sanderson
8226396382 Merge pull request #1950 from 3b1b/video-work
Miscellaneous fixes and tweaks
2022-12-28 19:26:17 -08:00
Grant Sanderson
e76b673e63 Change axes height in GraphExample 2022-12-28 19:22:49 -08:00
Grant Sanderson
a6ee54488b Add deep option to VMobject.copy 2022-12-28 19:22:41 -08:00
Grant Sanderson
4f37486655 Have VMobject keep track of a shader_wrapper_list 2022-12-28 19:18:20 -08:00
Grant Sanderson
a92a506224 Don't distinguish stroke uniforms from fill uniforms 2022-12-28 19:17:52 -08:00
Grant Sanderson
8fc243e398 Make anti_alias_width a Mobject uniform, rather than a camera attribute 2022-12-28 18:52:05 -08:00
Grant Sanderson
1180932026 Fix antialiasing issue with straight lines in text 2022-12-28 16:37:44 -08:00
Grant Sanderson
e59b3d2ac0 Update TexTransformExample 2022-12-28 13:39:53 -08:00
Grant Sanderson
926f3515bf Write new TransformMatchingStrings 2022-12-28 13:39:46 -08:00
Grant Sanderson
c7ba775845 In StringMobject.select_parts, default to using select_unisolated_substring if substring was not explicitly isolated 2022-12-28 13:39:13 -08:00
Grant Sanderson
05f02f5154 Fix ShaderWrapper.combine_with 2022-12-28 13:38:27 -08:00
Grant Sanderson
34d8ab81f9 Fix for num_tex_symbols 2022-12-28 13:38:15 -08:00
Grant Sanderson
44dc22e5e4 Rename local_unit_normal to unit_normal 2022-12-28 11:17:55 -08:00
Grant Sanderson
4d65ceabf7 Include zero in orientation checks 2022-12-28 11:17:45 -08:00
Grant Sanderson
b96a65d576 Tweaks to quadratic_bezier_geometry_functions 2022-12-28 11:16:53 -08:00
Grant Sanderson
c8c96fe645 Tweaks to get_unit_normal 2022-12-28 11:16:24 -08:00
Grant Sanderson
6204011fe4 Don't check point equality in triggers_refreshed_triangulation 2022-12-28 10:15:03 -08:00
Grant Sanderson
083de38e4c Remove "arbitrary-seeming branching" 2022-12-28 10:05:44 -08:00
Grant Sanderson
7dc1fe21bd Change angle threshold for determining bezier degree 2022-12-28 09:31:20 -08:00
Grant Sanderson
a6e21b2ccd Change Mobject.append_points
For future plans, it may be nicest for data["points"] to only ever get redefined by Mobject.resize_points
2022-12-28 09:22:22 -08:00
Grant Sanderson
97be203b57 Add type hint 2022-12-28 08:45:18 -08:00
Grant Sanderson
61155f5c72 Simplify Mobject.set_points 2022-12-28 08:45:05 -08:00
Grant Sanderson
966e2c9790 Update VMobject.has_fill and has_stroke 2022-12-27 22:18:54 -08:00
Grant Sanderson
e73ae78987 Update Mobject.has_points 2022-12-27 22:18:41 -08:00
Grant Sanderson
816f6eb297 Use ShaderWrapper.read_in instead of combine_with
Improves speed
2022-12-27 21:40:34 -08:00
Grant Sanderson
e54e04a5ce Add ShaderWrapper.read_in
This will function similarly to combine_with, but without necessarily having to allocate new memory
2022-12-27 21:39:20 -08:00
Grant Sanderson
49d4472e7e Add StringMobject.get_specified_substrings 2022-12-27 19:13:09 -08:00
Grant Sanderson
6044e475a4 Tiny VMobject.has_stroke update 2022-12-27 19:12:55 -08:00
Grant Sanderson
8c44834554 Compute triangulation before replicating 2022-12-27 19:12:36 -08:00
Grant Sanderson
795f6e2490 Remove unit_normal attribute 2022-12-27 19:12:00 -08:00
Grant Sanderson
6d50be55d3 Small inconsequential edits to get_triangulation 2022-12-27 15:54:12 -08:00
Grant Sanderson
f2859a9a8c Give default curve orientation of 1 2022-12-27 15:53:24 -08:00
Grant Sanderson
33a92d3ab3 Small cleanup in camera.py 2022-12-27 14:53:55 -08:00
Grant Sanderson
934a73ddb8 Remove stray import 2022-12-27 14:29:13 -08:00
Grant Sanderson
2b67aa0e01 Type hints for Window class variables 2022-12-26 09:40:11 -07:00
Grant Sanderson
3bf5ce5776 Fix TexMobject.select_unisolated_substring for substrings with escape characters 2022-12-26 09:02:00 -07:00
Grant Sanderson
db0770a4fd Remove **kwargs from Mobject.__init__ 2022-12-26 07:58:40 -07:00
Grant Sanderson
a2bdf54025 Remove unused parameter 2022-12-26 07:58:30 -07:00
Grant Sanderson
22c67df2ad Simplify Scene.remove to not require a Mobject.get_ancestors call 2022-12-26 07:46:40 -07:00
Grant Sanderson
fcff44a66b Merge pull request #1943 from 3b1b/string-mobject-refactor
StringMobject refactor
2022-12-23 17:11:30 -08:00
Grant Sanderson
62c9e2b58f Fix data["orientation"] alignment issue a separate way 2022-12-23 18:09:09 -07:00
Grant Sanderson
db52d0a73f Add type hints and @staticmethod decorators to wraps functions 2022-12-23 17:45:35 -07:00
Grant Sanderson
580d57a45c Add type hints and @staticmethod decorators to wraps functions 2022-12-23 17:44:00 -07:00
Grant Sanderson
453b863738 Be sure to align data in FadeTransform 2022-12-23 17:24:33 -07:00
Grant Sanderson
7f203d1611 Remove triangulation cachine 2022-12-23 13:33:07 -07:00
Grant Sanderson
dd2fb6ae74 Delete refresh_unit_normal 2022-12-23 10:23:57 -07:00
Grant Sanderson
40bf1fd6a9 Stop tracking unit_normal in data
Track orientation of each curve instead
2022-12-23 10:08:23 -07:00
Grant Sanderson
3878b8c077 Fix rotation_between_vectors 2022-12-23 10:04:27 -07:00
Grant Sanderson
ef04b9eb01 Revert "Simplify get_unit_normal to always recompute"
This reverts commit 96d391d9fd.
2022-12-22 18:07:46 -07:00
Grant Sanderson
baf2690d77 In rotation_between_vectors, account for the case where vectors align 2022-12-22 17:57:47 -07:00
Grant Sanderson
c36d178856 Trigger unit normal refresh for all bounding_box refreshes 2022-12-22 17:51:59 -07:00
Grant Sanderson
96d391d9fd Simplify get_unit_normal to always recompute 2022-12-22 17:51:22 -07:00
Grant Sanderson
8c5d4db411 Ensure VMobject.get_area_vector works with uneven number of points 2022-12-22 12:09:54 -07:00
Grant Sanderson
b7d473ff43 Revert "Revert back from the needs_new_unit_normal change in favor of recomputation"
This reverts commit d8deec8f81.
2022-12-22 12:03:33 -07:00
Grant Sanderson
fca7c0609a Factor out epsilon in earclip_triangulation 2022-12-22 11:53:14 -07:00
Grant Sanderson
a4d47f64b0 Fix normalize_along_axis 2022-12-22 11:52:55 -07:00
Grant Sanderson
5af4b9cc65 No need to refresh triangulation in flip 2022-12-22 11:52:30 -07:00
Grant Sanderson
ff090c016f Use only anchors to compute area vector 2022-12-22 11:46:51 -07:00
Grant Sanderson
33682b7199 MTex is the new Tex, Tex is now OldTex
Global replace
Tex -> OldTex
TexText -> OldTexText
MTex -> Tex
MTexText -> TexText
2022-12-21 13:18:20 -08:00
Grant Sanderson
805236337e Remove stray import 2022-12-21 12:52:32 -08:00
Grant Sanderson
958c34c705 Push functionality for selecting unisolated substrings up into StringMobject 2022-12-21 12:47:48 -08:00
Grant Sanderson
10d53c82e1 Improved num_tex_symbols
Based on data gathered for tex commands
2022-12-21 12:47:18 -08:00
Grant Sanderson
6277e28373 Have MTex.__getitem__ call MTex.select_parts 2022-12-20 22:37:23 -08:00
Grant Sanderson
4b73140435 Have MTex.select_parts fall back on dirty_select with a warning 2022-12-20 22:37:01 -08:00
Grant Sanderson
ef941b4040 Factor out num_tex_symbols 2022-12-20 22:35:41 -08:00
Grant Sanderson
81e6ab5b1d Add MTex.dirty_select
A more general way to select subparts by tex. Not as reliable as select_parts, but useful in many cases.
2022-12-20 17:02:29 -08:00
Grant Sanderson
e357885da0 Replace TexText with MTexText 2022-12-20 15:24:38 -08:00
Grant Sanderson
6d5b980d4a Replace Tex and MTex throughout library 2022-12-20 15:22:34 -08:00
Grant Sanderson
0d525baf29 Clean up special_tex contents, make each class use MTexText instead of TexText 2022-12-20 15:08:54 -08:00
Grant Sanderson
3293f72adc Move BulletedList, TexTextFromPresetString and Title to their own file 2022-12-20 14:48:54 -08:00
Grant Sanderson
e0725c111e Allow MTex to accept multiple strings as an argument 2022-12-20 14:32:04 -08:00
Grant Sanderson
6176bcd45a Add option for StringMobject to only render one svg 2022-12-20 14:32:04 -08:00
Grant Sanderson
9c106eb873 Add option for StringMobject to only render a single svg
Then set as the default behavior for MTex and Text
2022-12-20 14:31:17 -08:00
Grant Sanderson
1a485ddd19 Ensure color_to_hex always returns 6 character hex codes 2022-12-20 14:31:17 -08:00
Grant Sanderson
d4417d3d07 Add t2c option to MTex 2022-12-20 14:31:17 -08:00
Grant Sanderson
dd662b0d12 Move Selector and Span to manimlib.typing 2022-12-20 14:29:17 -08:00
Grant Sanderson
6beea2a7eb Merge pull request #1942 from 3b1b/video-work
Refactor config.py
2022-12-20 09:36:46 -08:00
Grant Sanderson
623aef41f8 Rename get_camera_configuration -> get_camera_config 2022-12-20 09:36:07 -08:00
Grant Sanderson
9f71f87278 Refactor config.py 2022-12-20 09:31:02 -08:00
Grant Sanderson
67912e26d3 Merge pull request #1941 from 3b1b/video-work
Small bug fixes and refactors
2022-12-19 21:20:01 -08:00
Grant Sanderson
a26fe605b3 Slight refactor for inserted_embed_line 2022-12-19 21:17:44 -08:00
Grant Sanderson
5c7caee902 Go back to writing a new file for insert_embed, but edit the module's __file__ attribute 2022-12-19 21:14:29 -08:00
Grant Sanderson
c605ac1c83 Better height, width, unit_size behavior for CoordinateSystems 2022-12-19 20:34:42 -08:00
Grant Sanderson
d8deec8f81 Revert back from the needs_new_unit_normal change in favor of recomputation 2022-12-19 18:17:25 -08:00
Grant Sanderson
75a98a8936 Change color_to_hex implementation 2022-12-19 18:03:53 -08:00
Grant Sanderson
073a62bf03 Factor rgb_to_hex, hex_to_int and int_to_hex away from StringMobject and to utils/color 2022-12-19 17:28:22 -08:00
Grant Sanderson
78fd6d3f35 Merge pull request #1938 from 3b1b/video-work
Small refactors and bug fixes
2022-12-19 17:08:08 -08:00
Grant Sanderson
e0e7e24351 Move display_during_execturion call
Such that it only gets called when a new svg needs to be written
2022-12-19 17:01:06 -08:00
Grant Sanderson
c2c8149627 Revert "Move display_during_exection associated with Latex rendering"
This reverts commit ab470c3ee5.
2022-12-19 16:54:55 -08:00
Grant Sanderson
a53867d8a1 Fix TexText bug 2022-12-19 16:54:44 -08:00
Grant Sanderson
b2fd22c539 Small cleanups 2022-12-19 16:21:40 -08:00
Grant Sanderson
5c0232a5e0 Have LatexError show line of error and the next line
This is where undefined control sequence errors will show up.
2022-12-19 16:20:19 -08:00
Grant Sanderson
ab470c3ee5 Move display_during_exection associated with Latex rendering
Only have it display when a new SVG is being written
2022-12-19 16:19:43 -08:00
Grant Sanderson
71c9144952 Use io.BytesIO rather than writing to a temp file 2022-12-19 16:03:16 -08:00
Grant Sanderson
8d05431b7b Add points in reverse order to AngularSector 2022-12-19 14:43:57 -08:00
Grant Sanderson
96d9e41a35 Refresh unit normal on init of brace 2022-12-19 14:43:27 -08:00
Grant Sanderson
2c20a1509e Remoe height defaults form __init__args of SingleStringTex and String 2022-12-19 14:43:10 -08:00
Grant Sanderson
ef64b90ed3 Allow for default height/width of SVGMobject specified as class variables 2022-12-19 14:42:04 -08:00
Grant Sanderson
f158d3e751 Give unit_normal refreshing the same behavior as triangulation
That is, don't actually compute it until it needs to be. This is necessary so that VMobject.append_points can refresh both of those, without effecting SVG initialization.
2022-12-19 14:41:06 -08:00
Grant Sanderson
4632228cac Merge pull request #1937 from 3b1b/video-work
Small bug fixes and refactorings
2022-12-19 11:40:41 -08:00
Grant Sanderson
d79e1d6ed8 Move shell instantiation to beginning of checkpoint_paste 2022-12-19 11:36:28 -08:00
Grant Sanderson
6615a912bd Add default_camera_config class variable to Scene, likewise for window and file_writer configuration 2022-12-19 11:35:43 -08:00
Grant Sanderson
99fa3ee620 Refactor Scene.embed and checkpoint_paste 2022-12-19 10:51:26 -08:00
Grant Sanderson
c330dfddae Add space 2022-12-19 10:38:06 -08:00
Grant Sanderson
ba93bd0cbf Replace unit_size with get_unit_size() 2022-12-18 19:52:59 -08:00
Grant Sanderson
b1f363d5a9 Expand input type for AnimationGroup 2022-12-18 19:52:48 -08:00
Grant Sanderson
e5c13ba9d7 Use shell.run_cell instead of paste line magic
Because %paste -q line magic introduces issues with IPython 8.2 and above
2022-12-18 12:20:14 -08:00
Grant Sanderson
3c04ffc513 Better functionality for default axis configuration on coordinate systems 2022-12-18 12:19:26 -08:00
Grant Sanderson
4f6c387a03 Edit interact message 2022-12-18 12:01:33 -08:00
Grant Sanderson
38c4fd8770 Remove unnecessary save 2022-12-18 11:54:57 -08:00
Grant Sanderson
3e4b6a7fb0 Add show_progress and skip options to checkpoint_paste 2022-12-18 11:21:57 -08:00
Grant Sanderson
af471161ea Fix 'Failed to get module" warning with Scene embed 2022-12-18 11:11:08 -08:00
Grant Sanderson
e728196814 Remove implicit string concatenation 2022-12-18 10:50:34 -08:00
Grant Sanderson
99dbf6b8c3 Edited insert_embed_line to write over existing file rather than creating a new one
Otherwise behavior of things like __file__ is not as expected.
2022-12-18 10:50:17 -08:00
Grant Sanderson
07feb33cbb Merge pull request #1932 from 3b1b/kill-config
Kill config
2022-12-18 10:27:15 -08:00
Grant Sanderson
44b7d33784 Delete config.rst 2022-12-18 09:40:51 -08:00
Grant Sanderson
0433cd727c Tweak type hints in matrix.py 2022-12-18 09:38:46 -08:00
Grant Sanderson
c96cdf43a1 Make sure mutability of dict arguments won't cause problems
One could argue that a pattern of "arg: dict | None = None" followed by "self.param = arg or dict()" is better, but that would make for an inconsistent pattern in cases where the default argument is not None.
2022-12-18 09:38:28 -08:00
Grant Sanderson
903e140719 Add from __future__ import annotations 2022-12-18 09:13:11 -08:00
Grant Sanderson
f8b39f2ff1 Allow Scalable type to be any FloatArray 2022-12-18 09:11:16 -08:00
Grant Sanderson
6f0020950f Make Scalable a Typevar 2022-12-17 22:28:53 -08:00
Grant Sanderson
7ac78f3dbb Updating type hints for paths.py 2022-12-17 22:14:53 -08:00
Grant Sanderson
f53fad1a96 Avoid implicit string concatenation 2022-12-17 22:14:43 -08:00
Grant Sanderson
c96a698713 Remove unused drag_pixels 2022-12-17 22:14:30 -08:00
Grant Sanderson
f6ff226cd4 Remove stray imports 2022-12-17 22:14:20 -08:00
Grant Sanderson
4dec67f9fe Move unused (and slightly absurd) dict-related helper functions to 3b1b/videos/once_useful_constructs 2022-12-17 22:00:37 -08:00
Grant Sanderson
0dab04080e No need to have a get_runtime helper when many other options exist 2022-12-17 21:53:15 -08:00
Grant Sanderson
fd20ead11b Update output type for make_even 2022-12-17 21:51:01 -08:00
Grant Sanderson
3f024175d4 Add check for invalid entries to Mobject.set_points 2022-12-17 19:52:34 -08:00
Grant Sanderson
26ff1a9716 Fix bug associated with GlowDot having None points list 2022-12-17 19:49:04 -08:00
Grant Sanderson
d4a29df99c Remove needless data["points"] reference 2022-12-17 19:48:47 -08:00
Grant Sanderson
75979eb7d1 Add NULL_POINTS constant 2022-12-17 19:48:27 -08:00
Grant Sanderson
365bb12dce Change type hints for set_points and append_points to Vect3Array 2022-12-17 19:31:43 -08:00
Grant Sanderson
0115037c82 Include types in universal imports 2022-12-17 19:31:27 -08:00
Grant Sanderson
38db5ca9b9 Replace handle_play_like_call with pre_play and post_play method 2022-12-17 18:59:05 -08:00
Grant Sanderson
24fd6d890e Tweak type hints for bezier.py 2022-12-17 18:35:26 -08:00
Grant Sanderson
810f2c67ab Make handle_play_like_call a static method 2022-12-17 18:11:38 -08:00
Grant Sanderson
bd537afe72 Remove skip and show_progress options of checkpoint_paste 2022-12-17 18:11:25 -08:00
Grant Sanderson
3e8738de2a Fix output type of VMobject.get_subpaths_from_points 2022-12-17 18:04:55 -08:00
Grant Sanderson
b817e6f15f Tweak type hints in color.py 2022-12-17 17:29:49 -08:00
Grant Sanderson
5b697b3782 Remove stray import 2022-12-17 17:29:39 -08:00
Grant Sanderson
0e558db122 Add a few type hints to specify VMobject family always consists of VMobjects 2022-12-17 17:03:34 -08:00
Grant Sanderson
1f0427d685 Add height as attribute to SVGMobject 2022-12-17 13:17:12 -08:00
Grant Sanderson
534770180d Remove stray import 2022-12-17 13:16:55 -08:00
Grant Sanderson
97f28b34f3 Distinguish Vect3 from Vect3Array types 2022-12-17 13:16:48 -08:00
Grant Sanderson
8db20cc460 Small fix 2022-12-16 20:59:14 -08:00
Grant Sanderson
5625f63ca2 Clean up SVG.__Init__ and fix style issue 2022-12-16 20:59:02 -08:00
Grant Sanderson
cef6506920 Add better types + Small refactors on space_ops 2022-12-16 20:35:45 -08:00
Grant Sanderson
dec11a4b17 Rename np_vector type to Vect3 or Vect4 to make context clearer 2022-12-16 20:35:26 -08:00
Grant Sanderson
43fd5e1aea Move custom type to manimlib.typing 2022-12-16 20:19:18 -08:00
Grant Sanderson
c00af3c1bf Rename config_ops -> dict_ops 2022-12-16 18:59:23 -08:00
Grant Sanderson
ef0999cc09 Rename config_ops -> dict_ops 2022-12-16 15:23:43 -08:00
Grant Sanderson
a4272d11a2 Finish last(?) digest_config vestige 2022-12-16 15:21:31 -08:00
Grant Sanderson
a6db0877de Remove stray imports 2022-12-16 15:21:14 -08:00
Grant Sanderson
2bab16133d Fix textured path bug 2022-12-16 15:04:15 -08:00
Grant Sanderson
d798f5ebf0 Small bug fix 2022-12-16 15:02:42 -08:00
Grant Sanderson
7510c9808e expand type for ParametricFunction function 2022-12-16 15:02:35 -08:00
Grant Sanderson
1a0eff05fa Ensure t_range always has three entries 2022-12-16 15:02:21 -08:00
Grant Sanderson
123f7e5a30 Better default color behavior 2022-12-16 14:48:13 -08:00
Grant Sanderson
15f03dae7b Small fixes to Axes configuration 2022-12-16 14:47:56 -08:00
Grant Sanderson
c6fc8dcf45 Remove stray import 2022-12-16 11:41:49 -08:00
Grant Sanderson
b21f5bad00 Fix small t2c issue 2022-12-16 11:41:42 -08:00
Grant Sanderson
afab37c2d2 Remove stray digest_config references 2022-12-16 11:00:59 -08:00
Grant Sanderson
a1cbff46b8 Remove CONFIG vestiges 2022-12-16 10:54:28 -08:00
Grant Sanderson
ea4a47aeef Kill CONFIG in mtex_mobject.py 2022-12-16 10:54:14 -08:00
Grant Sanderson
9ad370a04b Move all contents of once_useful_constructs out of this repo
And into 3b1b/videos
2022-12-16 10:43:59 -08:00
Grant Sanderson
e4aebaf791 Remove old constructs that were too specific to old 3b1b videos and are not worth fixing
Or rather, move them over to 3b1b/videos repo
2022-12-16 10:26:04 -08:00
Grant Sanderson
0ad2f18ca6 Delete ShowPassingFlashWithThinningStrokeWidth 2022-12-16 10:16:52 -08:00
Grant Sanderson
89770158de Kill CONFIG in vector_field.py 2022-12-16 10:16:13 -08:00
Grant Sanderson
271e2f0865 Kill CONFIG in value_tracker.py 2022-12-16 10:02:12 -08:00
Grant Sanderson
ae52f19a4a Remove stray CONFIG vestiges 2022-12-16 10:02:03 -08:00
Grant Sanderson
c2cf261c81 Kill CONFIG in three_dimensions.py 2022-12-16 09:56:38 -08:00
Grant Sanderson
880aaf913f Change default fill from WHITE to GREY_C 2022-12-16 09:56:29 -08:00
Grant Sanderson
72aaed57d5 Push depth_test into __init__ args 2022-12-16 09:56:17 -08:00
Grant Sanderson
f64cae1db4 Push depth_test into __init__ args 2022-12-16 09:56:03 -08:00
Grant Sanderson
9e077b29db Specify fill_color/stroke_color instead of color 2022-12-16 09:55:50 -08:00
Grant Sanderson
a04d4c0d79 Kill CONFIG in shape_matchers 2022-12-15 20:48:53 -08:00
Grant Sanderson
fa7ee22c46 Kill CONFIG in probability.py 2022-12-15 20:40:13 -08:00
Grant Sanderson
7e46c87fc5 Kill CONFIG in numbers.py 2022-12-15 20:33:58 -08:00
Grant Sanderson
9039fe69e4 Kill CONFIG in number_line.py 2022-12-15 20:26:10 -08:00
Grant Sanderson
451f1df830 Small nudge to import order 2022-12-15 20:11:32 -08:00
Grant Sanderson
a7d7ed0793 Kill CONFIG, and slightly refactor, matrix.py 2022-12-15 20:09:52 -08:00
Grant Sanderson
9d65ef3cae Kill CONFIG in interactives.py 2022-12-15 18:29:05 -08:00
Grant Sanderson
2a645b27f8 Kill CONFIG in geometry.py 2022-12-15 18:19:09 -08:00
Grant Sanderson
c244f8738f Remove stray import 2022-12-15 18:18:59 -08:00
Grant Sanderson
3be43119cb Remove stray import 2022-12-15 18:18:52 -08:00
Grant Sanderson
37786edc99 Small type fix 2022-12-15 18:18:44 -08:00
Grant Sanderson
25388b4ad3 Kill CONFIG in functions.py 2022-12-15 16:47:03 -08:00
Grant Sanderson
00403fe5b4 Change default stroke color 2022-12-15 16:46:51 -08:00
Grant Sanderson
02143001a4 Move RangeSpecifier to constants 2022-12-15 16:46:42 -08:00
Grant Sanderson
a05820b7c7 Kill CONFIG in frame.py 2022-12-15 16:28:15 -08:00
Grant Sanderson
5b5b3a7d20 Kill CONFIG in coordinate_system 2022-12-15 16:19:03 -08:00
Grant Sanderson
57875875c1 Kill CONFIG in changing.py 2022-12-15 15:40:56 -08:00
Grant Sanderson
a78e2b6ad2 Specify ManimColor type 2022-12-15 15:40:46 -08:00
Grant Sanderson
da90c5e297 Allow for multivalued style arguments 2022-12-15 15:40:36 -08:00
Grant Sanderson
4c894077d3 Add np_vector types to constants 2022-12-15 15:24:08 -08:00
Grant Sanderson
c56968fd09 Cleaner imports 2022-12-15 15:23:59 -08:00
Grant Sanderson
c8d01e7a43 Kill CONFIG in drawings.py 2022-12-15 14:28:34 -08:00
Grant Sanderson
3b5181d1a3 Kill CONFIG in StringMobject and Text 2022-12-15 13:35:13 -08:00
Grant Sanderson
588c3fce02 Allow style configuration to be None 2022-12-15 12:48:43 -08:00
Grant Sanderson
a3215d0354 Fix small height bug 2022-12-15 12:48:27 -08:00
Grant Sanderson
c55374245d Kill config in svg_mobject 2022-12-15 12:48:06 -08:00
Grant Sanderson
c2766c9837 Remove stray import 2022-12-15 12:47:53 -08:00
Grant Sanderson
5a309d41b7 Undo potentially undefined typing 2022-12-15 11:33:53 -08:00
Grant Sanderson
10c0f4b694 Don't pass kwargs to Animation from LaggedStart 2022-12-15 11:33:41 -08:00
Grant Sanderson
2a89e84538 Kill CONFIG in brace.py 2022-12-15 11:33:27 -08:00
Grant Sanderson
aac320aa98 Update Checkmark and Exmark definitions 2022-12-15 11:33:14 -08:00
Grant Sanderson
7aa5c83a14 Kill CONFIG in tex_mobject.py 2022-12-15 11:33:00 -08:00
Grant Sanderson
d4b6bf40e5 Small type fixes 2022-12-15 10:17:57 -08:00
Grant Sanderson
002129e7a2 Remove stray import 2022-12-15 10:17:43 -08:00
Grant Sanderson
1aaa3b4ad5 Remove unnecessary import 2022-12-15 10:12:10 -08:00
Grant Sanderson
a39c65cb5c Fix type 2022-12-15 10:11:58 -08:00
Grant Sanderson
6c4e028eab Kill CONFIG in surface.py 2022-12-15 10:11:50 -08:00
Grant Sanderson
bab1f964bb Add types to constants 2022-12-15 09:56:08 -08:00
Grant Sanderson
3a65eb4d2c Kill CONFIG in ImageMobject 2022-12-15 09:56:01 -08:00
Grant Sanderson
98c53151ad Correct type for texture_paths 2022-12-15 09:55:50 -08:00
Grant Sanderson
b373e33a22 Kill CONFIG in dot_cloud.py 2022-12-15 09:37:32 -08:00
Grant Sanderson
a715a5bc3f Kill CONFIG in vectorized_mobject.py 2022-12-15 09:19:05 -08:00
Grant Sanderson
97a5861ccf Remove stray "kwargs" 2022-12-15 09:18:53 -08:00
Grant Sanderson
e8c220a3f2 Add np_vector shorthand type to constants 2022-12-15 09:18:41 -08:00
Grant Sanderson
133ac8bb26 Kill CONFIG in mobject.py 2022-12-15 09:18:22 -08:00
Grant Sanderson
a817364a0e Remove three_d_scene import 2022-12-15 08:14:35 -08:00
Grant Sanderson
33b4e617a6 Move ThreeDScene to once_useful_constructs 2022-12-15 08:09:31 -08:00
Grant Sanderson
c57f1f997a Kill CONFIG in vector_space_scene and move to once_useful_constructs 2022-12-15 08:08:40 -08:00
Grant Sanderson
0b994db0ec Make camera_config changable as a class variable 2022-12-14 17:14:53 -08:00
Grant Sanderson
69b0b0727e Kill CONFIG in scene_file_writer.py 2022-12-14 17:07:05 -08:00
Grant Sanderson
013bf8b639 Kill CONFIG in scene.py 2022-12-14 17:01:46 -08:00
Grant Sanderson
2f8fe689d9 Kill CONFIG in camera.py 2022-12-14 16:41:19 -08:00
Grant Sanderson
a0a17be6ea Remove digest_config 2022-12-14 16:27:33 -08:00
Grant Sanderson
4e8b80fe86 Ensure shift continues to be second arg of FadeOut 2022-12-14 16:27:25 -08:00
Grant Sanderson
53994f0650 Add shorthand for type np.ndarray[int, np.dtype[np.float64]] 2022-12-14 16:17:15 -08:00
Grant Sanderson
ca1ba67a85 Kill config in update.py 2022-12-14 16:02:15 -08:00
Grant Sanderson
4aa7d439f1 Kill CONFIG in transform_matching_parts animations 2022-12-14 15:55:02 -08:00
Grant Sanderson
0aa451396d Kill CONFIG in specialized.py 2022-12-14 15:13:30 -08:00
Grant Sanderson
a6744a19d3 Kill CONFIG in rotation.py 2022-12-14 15:08:20 -08:00
Grant Sanderson
02c3243f98 Kill CONFIG in numbers.py 2022-12-14 15:02:25 -08:00
Grant Sanderson
81615d9f4b Kill CONFIG in movement.py 2022-12-14 14:58:25 -08:00
Grant Sanderson
7dcf5eff8e Kill CONFIG in indication.py 2022-12-14 14:40:05 -08:00
Grant Sanderson
a901704b31 Return self in Circle.surround 2022-12-14 14:39:57 -08:00
Grant Sanderson
c8442c404e Refresh triangulation after reversing points 2022-12-14 14:39:49 -08:00
Grant Sanderson
7474a98752 Kill config in growing.py 2022-12-14 12:08:18 -08:00
Grant Sanderson
5571c7d576 Kill config in transform.py 2022-12-14 12:05:33 -08:00
Grant Sanderson
adf886dced Add 'Callable' import 2022-12-14 11:27:13 -08:00
Grant Sanderson
f6858778c4 Kill config in fading 2022-12-14 11:27:00 -08:00
Grant Sanderson
98a969242a Add (optional) quieting for checkpoint_paste 2022-12-14 11:02:51 -08:00
Grant Sanderson
44a7cfa12e Update output types for VMobject.get_family and VMobject.replicate 2022-12-14 11:02:16 -08:00
Grant Sanderson
187de0163f Kill config in creation.py 2022-12-14 10:58:35 -08:00
Grant Sanderson
958002152e Define ManimColor type in constants 2022-12-14 10:55:32 -08:00
Grant Sanderson
a7bf10c570 Kill composition CONFIGs 2022-12-13 16:52:32 -08:00
Grant Sanderson
6f470679f7 Kill Animation CONFIG 2022-12-13 15:45:57 -08:00
Ikko Ashimine
155cde26da Fix typo in vectorized_mobject.py (#1913)
lenth -> length
2022-12-13 14:57:23 +08:00
Grant Sanderson
a801aefcea Merge pull request #1906 from 3b1b/video-work
Video work
2022-11-18 09:16:18 -08:00
Grant Sanderson
5281a83e9d Add overshoot rate func 2022-11-18 09:12:52 -08:00
Grant Sanderson
7de03e2541 Change/refactor progress display defaults in scene file writing 2022-11-18 09:12:40 -08:00
Grant Sanderson
8cc7616271 Bind 'u' to unselection 2022-11-18 09:12:12 -08:00
Grant Sanderson
15a446977f Added type for ValueTracker 2022-11-18 09:11:59 -08:00
Grant Sanderson
12d4b48508 Make sure animating status propagates through ancestors 2022-11-18 09:11:47 -08:00
Grant Sanderson
a4ffe9b4e5 Allow group_by_rows and group_bg_cols option for creating grids 2022-11-18 09:11:29 -08:00
Grant Sanderson
b6dd640fe7 Add Mobject.reverse_submobjects 2022-11-18 09:10:44 -08:00
Grant Sanderson
83393abb22 Fix bug deleting i from coordinate labels 2022-11-18 09:10:24 -08:00
Grant Sanderson
90e8a397b8 add_axis_labels for ThreeDAxes 2022-11-18 09:09:21 -08:00
Grant Sanderson
6683c9dbca Add get_graph for 3d surfaces off of ThreeDAxes 2022-11-18 09:09:06 -08:00
Grant Sanderson
004b7427f5 Allow bound graphs to track discontinuities 2022-11-18 09:08:30 -08:00
Grant Sanderson
d8e4c1d698 Account for updated rate_func usage in Rotate 2022-11-18 09:07:33 -08:00
Grant Sanderson
4e7f06dea8 Change rate_func to be used in submobject update
This allows for rate functions like overshoot and running start who have outputs outside of [0, 1] to still be used effectively.
2022-11-18 09:07:18 -08:00
Grant Sanderson
e55dd01081 Merge pull request #1890 from 3b1b/video-work
Fix Restore
2022-11-04 12:28:40 -07:00
Grant Sanderson
b94e9f3a24 Fix Restore 2022-11-04 12:27:50 -07:00
Grant Sanderson
764dec20eb Merge pull request #1889 from 3b1b/bug-fixes
Bug fixes
2022-11-03 16:49:40 -07:00
Grant Sanderson
8adf99b8a7 Fix Circle.point_at_angle
https://github.com/3b1b/manim/issues/1875
2022-11-03 16:48:30 -07:00
Grant Sanderson
a2606c7e37 Make sure find_intersection returns a result matching the shape of inputs 2022-11-03 16:48:14 -07:00
Grant Sanderson
84fa3de435 By default, don't let Mobject.become match updaters
This causes the use of Mobject.become in an updater function to make the mobject immediately lose its updater.

https://github.com/3b1b/manim/issues/1877
2022-11-03 16:35:41 -07:00
Grant Sanderson
f878537814 Don't have Matrix try to convert lists of Mobject into numpy arrays
https://github.com/3b1b/manim/issues/1878
2022-11-03 16:31:19 -07:00
Grant Sanderson
8e6265d35e Give set_color_by_gradient more expected behavior
https://github.com/3b1b/manim/issues/1882
2022-11-03 16:17:17 -07:00
Grant Sanderson
834c806e83 Merge pull request #1888 from 3b1b/video-work
Video work
2022-11-03 16:05:34 -07:00
Grant Sanderson
471dcdbaf1 Merge pull request #1879 from zaharkogan/patch-1
Update LICENSE.md
2022-11-03 15:45:00 -07:00
Grant Sanderson
ee668dc741 Merge pull request #1858 from IcyChlorine/bugfix_progress_bar
Fix bug with rendering progress bar
2022-11-03 15:44:32 -07:00
Grant Sanderson
d28ba53f22 Merge branch 'video-work' of github.com:3b1b/manim into video-work 2022-11-03 11:34:01 -07:00
Grant Sanderson
faa37844e7 Add unit_tex option for NumberLine.add_numbers 2022-11-03 11:32:25 -07:00
Grant Sanderson
80d34547db Typo fix 2022-11-03 11:32:25 -07:00
Grant Sanderson
ce77f38bf1 Add CoordinateSystem.bind_graph_to_func 2022-11-03 11:32:25 -07:00
Grant Sanderson
1bd3d61b08 Add unit_tex option for NumberLine.add_numbers 2022-11-03 11:30:20 -07:00
Grant Sanderson
c3c1d3ec35 Typo fix 2022-11-03 11:29:49 -07:00
Grant Sanderson
e89f193c56 Add CoordinateSystem.bind_graph_to_func 2022-11-03 11:29:43 -07:00
Zakhar Kogan
8dfa6415dc Update LICENSE.md
Source: https://softwareengineering.stackexchange.com/questions/210472/is-renewal-of-mit-license-needed-on-github-at-the-beginning-of-each-year
2022-10-11 15:33:50 +03:00
Maciej Musielik
fb50e4eb55 Fixing a typo ("termnial" -> "terminal") (#1872) 2022-10-01 16:07:09 +08:00
Grant Sanderson
abe52a61d9 Merge pull request #1864 from 3b1b/video-work
Video work
2022-09-13 14:12:05 -07:00
Grant Sanderson
88f2ae6d0d Merge branch 'master' of github.com:3b1b/manim into video-work 2022-09-13 14:08:15 -07:00
Grant Sanderson
d2e570eb19 Merge pull request #1862 from widcardw/dev
Add `set_anim_args` to `.animate` method
2022-09-13 12:42:36 -07:00
Grant Sanderson
d48957c312 Merge pull request #1818 from YishiMichael/refactor
Add `template` and `additional_preamble` parameters to `Tex`
2022-09-13 12:42:15 -07:00
Grant Sanderson
0c75d79080 Have window default to half monitor width instead of half height 2022-09-13 11:46:33 -07:00
widcardw
603a773847 fix: pass args by calling animate that borrowed from CE 2022-09-11 22:59:43 +08:00
widcardw
2f691355db chore: add doc-string of set_anim_args 2022-09-11 10:31:30 +08:00
widcardw
a613099b1d feat: add set_anim_args to .animate method 2022-09-11 10:22:08 +08:00
YishiMichael
fa1d938af1 chore: Optimize warning info 2022-09-10 23:55:52 +08:00
YishiMichael
455d6604be chore: Restrict version of svgelements 2022-09-07 13:28:27 +08:00
YishiMichael
4a575afdde chore: Remove unused namespace constants 2022-09-07 13:23:02 +08:00
YishiMichael
ae115d9992 refactor: Remove expansion of <use> elements 2022-09-07 13:19:08 +08:00
IcyChlorine
8882030136 thumbnails 2022-08-27 12:11:55 +08:00
IcyChlorine
8719107b18 fix the bug that progress bar can't be correctly showed while rendering if scene class name isn't passed in args explicitly. 2022-08-27 11:25:36 +08:00
YishiMichael
53c43ee8ea fix: recover default_config.yml 2022-08-23 11:41:21 +08:00
YishiMichael
3c0abb0b40 fix: add missed imports 2022-08-23 11:40:19 +08:00
YishiMichael
eadf611f1e fix: fix an incorrect typing 2022-08-22 22:05:33 +08:00
YishiMichael
4dfe8aff86 refactor: refactor StringMobject 2022-08-22 21:52:48 +08:00
YishiMichael
8bd01d60e4 fix: add protect attribute to hash_seed 2022-08-22 17:08:58 +08:00
YishiMichael
c2a75e15cc refactor: refactor StringMobject 2022-08-22 16:55:46 +08:00
YishiMichael
19c757ec90 refactor: refactor StringMobject 2022-08-20 13:01:59 +08:00
YishiMichael
7ffc7b33f7 Refactor StringMobject and relevant classes 2022-08-07 23:57:54 +08:00
YishiMichael
28e4240475 Refactor StringMobject and relevant classes 2022-08-07 11:29:31 +08:00
YishiMichael
f434eb93e2 Refactor StringMobject and relevant classes 2022-08-07 00:50:29 +08:00
Grant Sanderson
69bb4f026c Make sure Window matches aspect ratio of Camera 2022-07-19 12:57:25 -07:00
Grant Sanderson
0406557b5c Make sure CLI resolution updates will work 2022-07-19 12:46:45 -07:00
Grant Sanderson
de7d9ce8c9 Don't let stroke width vary with zoom levels 2022-07-19 12:38:45 -07:00
Grant Sanderson
eff9e6f732 Animate resetting 2022-07-19 12:38:02 -07:00
Grant Sanderson
cc81cc5cf5 Move unit normal refreshing to VMobject 2022-07-19 12:37:34 -07:00
Grant Sanderson
af7c58dbe8 Import DEGREES 2022-07-19 12:36:50 -07:00
Grant Sanderson
7c4bb9cbbd Include upper endpoint on graphs 2022-07-19 12:36:24 -07:00
beccare
650d49c031 fixed broken link to community edition (versions) (#1840) 2022-07-17 18:08:44 +08:00
TonyCrane
3c7a38660a small fix 2022-06-28 09:16:06 +08:00
TonyCrane
606ee5e4f1 update docs 2022-06-28 09:13:41 +08:00
TonyCrane
844d139ed4 update changelog 2022-06-28 09:04:57 +08:00
YishiMichael
093af347aa Refactor LabelledString and relevant classes 2022-06-11 15:15:39 +08:00
YishiMichael
93265c7341 Remove a twice-defined method 2022-05-30 10:19:52 +08:00
Grant Sanderson
0d845d5bba Bubble tweaks 2022-05-29 16:37:53 -07:00
Grant Sanderson
dfa019fcde Make sure Mobject.become remaps any attributes pointing to family members appropriately. 2022-05-29 16:37:44 -07:00
Grant Sanderson
77309a634b Fix to embed insertion 2022-05-29 16:37:00 -07:00
Grant Sanderson
1b1ba606ed Remove unnecessary blend_func definition (which just sets it to the default value) 2022-05-29 16:36:46 -07:00
YishiMichael
a73bd5d4fe Fix popping bug 2022-05-29 16:48:30 +08:00
YishiMichael
97ac8c9953 Expand use elements 2022-05-29 16:26:40 +08:00
YishiMichael
bc939fdd5b Rename font to template 2022-05-28 23:18:56 +08:00
YishiMichael
3faa21cadd Add blank template 2022-05-28 22:44:30 +08:00
YishiMichael
bf530db2ed Add ctex_basic template 2022-05-28 22:01:36 +08:00
YishiMichael
f0447d7739 Small refactors on StringMobject and relevant classes 2022-05-28 21:43:37 +08:00
YishiMichael
59eba943e5 Resolve conflicts 2022-05-28 12:43:53 +08:00
YishiMichael
22a3bef670 Resolve conflicts 2022-05-28 12:40:29 +08:00
YishiMichael
cbffbfa019 Add back mismatched indices (#1820)
* Fix a logistic bug

* Add back mismatched indices
2022-05-25 14:12:54 +08:00
Grant Sanderson
07a8274cb1 Merge pull request #1821 from 3b1b/video-work
Video work
2022-05-24 15:20:07 -07:00
Grant Sanderson
83b4aa6b88 Let defaullt text alignment be decided in default_config 2022-05-24 15:16:59 -07:00
Grant Sanderson
117a34dc67 Change crosshair behavior 2022-05-23 11:08:08 -07:00
Grant Sanderson
ca523c8a5e Fix bubble direction 2022-05-23 11:07:51 -07:00
Grant Sanderson
ad58a9e6c1 Tiny refactor 2022-05-23 11:07:39 -07:00
YishiMichael
9386461d27 Add font attribute for Tex 2022-05-22 23:47:45 +08:00
YishiMichael
1596356385 Reuse hash_string function 2022-05-22 10:29:20 +08:00
YishiMichael
f0984487ea Construct TexTemplate class to convert tex to svg 2022-05-21 15:56:03 +08:00
Bill Xi
3108b49f55 Update coordinate_systems.py 2022-05-21 15:52:56 +08:00
YishiMichael
edca4a93fa Merge branch '3b1b:master' into refactor 2022-05-20 18:58:19 +08:00
YishiMichael
cfded00f13 Fix a logistic bug (#1815) 2022-05-20 18:56:40 +08:00
YishiMichael
26de7c9ce5 Fix a logistic bug 2022-05-20 18:49:38 +08:00
YishiMichael
49723f54cb Add necessary imports (#1804)
* Add necessary imports

* Add necessary imports

* Add necessary imports

* Add `from __future__ import annotations`
2022-05-19 22:07:06 +08:00
Grant Sanderson
cece830349 Merge branch 'master' of github.com:3b1b/manim into video-work 2022-05-17 09:19:47 -07:00
Grant Sanderson
cd240f2a80 Merge pull request #1795 from YishiMichael/refactor
Refactor StringMobject and relevant classes
2022-05-17 09:17:53 -07:00
Grant Sanderson
6decb0c32a Rename frame_rate -> fps 2022-05-14 17:47:31 -07:00
Grant Sanderson
fb3cf308df Make sure init_customization matches default_config.yml 2022-05-14 17:42:07 -07:00
Grant Sanderson
dd5d239971 Change convention for how camera_qualities are represented in default_config.yml 2022-05-14 17:29:07 -07:00
Grant Sanderson
25de729bb3 Only lock data for mobjects without updaters 2022-05-14 17:28:31 -07:00
Grant Sanderson
5f56778cdf Don't update frame during window closing on cell execution 2022-05-14 17:28:11 -07:00
Grant Sanderson
2e26d66454 Merge branch 'master' of github.com:3b1b/manim into video-work 2022-05-11 12:48:59 -07:00
Grant Sanderson
f741217c34 Make sure keyboard interrupted renders don't overwrite pre-existing video files 2022-05-11 12:48:08 -07:00
Grant Sanderson
11d19b6d57 Update frame at the end of each checkpoint_paste 2022-05-11 12:47:42 -07:00
Grant Sanderson
97643d788d Default to showing animation progress in embedded runs 2022-05-11 12:47:21 -07:00
Grant Sanderson
cef7c383a5 Add scene time to information that can be displayed with the appropriate key press 2022-05-11 12:46:56 -07:00
Grant Sanderson
0060a4860c Bug fix to VMobject.match_style 2022-05-11 12:46:15 -07:00
Grant Sanderson
4fdaeb1547 Fix(?) issue with numbers forgetting style info when resetting value 2022-05-11 12:45:51 -07:00
Grant Sanderson
48689c8c7b Add DecimalNumber.get_tex 2022-05-11 12:45:27 -07:00
Grant Sanderson
cd866573b5 Add about_edge argument to arrange_to_fit_width, etc. 2022-05-11 12:45:06 -07:00
Grant Sanderson
584e259b44 Bug fix for mirrored directories 2022-05-11 12:44:51 -07:00
YishiMichael
cdadaf8a8c Rename LabelledString to StringMobject 2022-05-06 22:09:58 +08:00
YishiMichael
7cf0e0ba10 Refactor LabelledString and relevant classes 2022-05-06 17:56:27 +08:00
YishiMichael
b509f62010 Refactor LabelledString and relevant classes 2022-05-06 16:43:20 +08:00
YishiMichael
642602155d [WIP] Refactor LabelledString and relevant classes 2022-05-05 23:03:02 +08:00
Grant Sanderson
41b811a5e7 Update frame on all play calls when skipping animations, so as to provide a rapid preview during scene loading 2022-05-04 21:22:48 -07:00
Grant Sanderson
93fc81ac9d Add show_progress option to checkpoint_paste 2022-05-04 21:22:22 -07:00
Grant Sanderson
d662971559 Larger cursor location label 2022-05-04 21:22:00 -07:00
Grant Sanderson
c4d452248a Move mirror output path logic to config.py 2022-05-04 21:21:49 -07:00
Grant Sanderson
181038e2f3 More curves to cross by default 2022-05-04 09:30:54 -07:00
YishiMichael
511a3aab3d [WIP] Remove comments 2022-05-04 22:18:19 +08:00
YishiMichael
1cb7401141 [WIP] Refactor LabelledString and relevant classes 2022-05-04 21:56:13 +08:00
Grant Sanderson
a9a151d4ef Have presenter mode hold before first play call 2022-05-03 12:45:16 -07:00
Grant Sanderson
22c5e79f5f Some cleanups to the way scenes end 2022-05-03 12:41:44 -07:00
Grant Sanderson
a87d3b5f59 Add Mobject.arrange_to_fit_dim 2022-05-03 12:40:43 -07:00
YishiMichael
ab8f78f40f [WIP] Refactor LabelledString and relevant classes 2022-05-03 23:39:37 +08:00
Grant Sanderson
a6fcfa3b40 Add time_span option to Animation 2022-05-02 11:40:42 -07:00
Grant Sanderson
fddb0b29e1 Remove unnecessary import 2022-05-02 11:38:58 -07:00
Grant Sanderson
57b7af3bf1 Remove old-style method building from ExampleScenes 2022-05-02 11:13:18 -07:00
Grant Sanderson
a09c440281 Slight tweaks to crosshair 2022-05-02 11:13:05 -07:00
Grant Sanderson
c019210015 Have InteractiveScene ignore state of crosshair and selection_highlight 2022-05-02 11:12:04 -07:00
Grant Sanderson
75e1cff579 Reorganize how scene state is managed 2022-05-02 11:11:18 -07:00
Grant Sanderson
feab79c260 Get rid of overly complicated anims_from_play_args (which means old style method building is no longer supported) 2022-05-02 11:10:57 -07:00
Grant Sanderson
308aadcec5 Make show_animation_progress effective 2022-05-02 11:09:09 -07:00
YishiMichael
03cb42ba15 [WIP] Refactor LabelledString and relevant classes 2022-05-02 22:40:06 +08:00
Grant Sanderson
4a8e8e5447 Clear later checkpoints 2022-05-01 15:31:42 -04:00
Grant Sanderson
602fbd1a9f Fix -e for first line of scene 2022-05-01 15:31:31 -04:00
Grant Sanderson
33ffd4863a Add crosshair 2022-05-01 15:31:07 -04:00
Grant Sanderson
6a664ece78 Rename InteractiveScene.colors -> InteractiveScene.palette_colors 2022-04-28 19:16:11 -04:00
Grant Sanderson
354030464e Bug patch Tex sometimes rendering black 2022-04-28 19:15:53 -04:00
Grant Sanderson
a791a82111 Bug fix 2022-04-28 19:15:26 -04:00
Grant Sanderson
7f94a401a8 Wait on start for presenter mode 2022-04-28 12:15:00 -06:00
Grant Sanderson
ed5a435852 Default to more highlight layers 2022-04-28 12:14:49 -06:00
Grant Sanderson
a4b38fd420 Clean up DecimalNumber constructor 2022-04-28 12:14:36 -06:00
Grant Sanderson
c1b222c233 Set default buff for is_point_touching to 0 2022-04-28 11:59:56 -06:00
Grant Sanderson
5d59f945ca FillArrow tweak 2022-04-28 11:59:38 -06:00
Grant Sanderson
ac08963fef Have selection_highlight refresh with an updater 2022-04-28 11:59:21 -06:00
Grant Sanderson
e83ad785ca Handle quitting during scene more gracefully 2022-04-27 11:19:44 -07:00
Grant Sanderson
52259af5df Don't show animation progress bar by default 2022-04-27 11:19:20 -07:00
Grant Sanderson
7c233123a1 Tweaks and fixes to InteractiveScene 2022-04-27 09:55:46 -07:00
Grant Sanderson
c311204993 Tweaks to VHighlight 2022-04-27 09:55:29 -07:00
Grant Sanderson
2b104a46fd Add error message to LatexError 2022-04-27 09:55:15 -07:00
Grant Sanderson
d75439a60e Hacky fix to lambda namespace issues with IPython embeds 2022-04-27 09:54:29 -07:00
Grant Sanderson
1b589e336f Add checkpoints to Scene 2022-04-27 09:53:56 -07:00
Grant Sanderson
ec9ed32d78 Organize get_ancestors from top to bottom 2022-04-27 09:53:23 -07:00
Grant Sanderson
0e45b41fea Match updaters in Mobject.become 2022-04-27 09:52:44 -07:00
Grant Sanderson
c498b88750 Small tweaks to Mobject.looks_identical for marginal speed 2022-04-27 09:52:27 -07:00
Grant Sanderson
2dcc989bb4 (whitespace) 2022-04-27 09:51:43 -07:00
YishiMichael
065900c6ac Some refactors 2022-04-27 23:04:24 +08:00
YishiMichael
69db53d612 Merge branch '3b1b:master' into refactor 2022-04-26 10:07:40 +08:00
Grant Sanderson
b920e7be7b Rewrite remove_list_redundancies based on (ordered) dicts 2022-04-25 10:45:35 -07:00
Grant Sanderson
3584926036 Merge pull request #1802 from 3b1b/video-work
Video work
2022-04-25 10:29:02 -07:00
Grant Sanderson
4c1210b3ab Add smarter default radius to round_corners 2022-04-25 10:26:29 -07:00
Grant Sanderson
aaea3f40f6 Don't copy Mobject attrs which are mobject but not family members 2022-04-25 10:26:07 -07:00
Grant Sanderson
d6bf9f00a1 Slight tweaks 2022-04-25 10:25:35 -07:00
Grant Sanderson
42d1f48c60 Only leave wait notes in presenter mode 2022-04-25 09:55:49 -07:00
Grant Sanderson
01f0dd30d0 Have Scene.remove look at extended ancestry 2022-04-25 09:55:00 -07:00
Grant Sanderson
40b432a29b Add extended option to Mobject.get_ancestors 2022-04-25 09:54:32 -07:00
Grant Sanderson
d43b5c9bdc Fix shift + s gather selection bug 2022-04-24 13:32:26 -07:00
Grant Sanderson
f2b4245c13 Slight speed-up to InteractiveScene.gather_selection 2022-04-24 13:24:55 -07:00
Grant Sanderson
e49e4b8373 Speed-ups to Mobject.copy 2022-04-24 13:24:20 -07:00
Grant Sanderson
66f695a1ed Ensure ShaderWrapper.copy copies uniforms which are numpy arrays 2022-04-24 13:23:30 -07:00
Grant Sanderson
cc8922155d Make sure Scene.remove clears internal mobject list of family members of args 2022-04-24 13:23:02 -07:00
Grant Sanderson
6310e2fb64 Clean up Scene.remove function, delete restructure_list_to_exclude_certain_family_members 2022-04-24 10:29:31 -07:00
Grant Sanderson
db884b0a67 Add Mobject.get_ancestors 2022-04-24 10:29:02 -07:00
Grant Sanderson
efe051b8e1 Revert to -e to create a new temporary file, rather than writing over the original 2022-04-24 10:28:53 -07:00
Grant Sanderson
205116b8ce Fix refresh_selection_highlight 2022-04-23 18:52:44 -07:00
Grant Sanderson
bd2dce0830 When scene saves state, have it only copy mobjects which have changed 2022-04-23 18:52:26 -07:00
Grant Sanderson
3ae0a4e81b Add equality for ShaderWrapper 2022-04-23 18:51:03 -07:00
Grant Sanderson
c3c5717dde Add Mobject.looks_identical 2022-04-23 18:50:45 -07:00
YishiMichael
b8efdea6ec Merge pull request #2 from YishiMichael/master
Update 'refactor' branch
2022-04-24 08:30:38 +08:00
YishiMichael
97edc2d6cf Merge branch 'master' into refactor 2022-04-24 08:26:22 +08:00
YishiMichael
30e33b1baa Merge branch 'refactor' into master 2022-04-24 08:24:27 +08:00
Grant Sanderson
304cf88451 Merge pull request #1797 from 3b1b/video-work
Video work
2022-04-23 10:18:25 -07:00
Grant Sanderson
d9475a6860 Remove unnecessary imports 2022-04-23 10:16:48 -07:00
Grant Sanderson
902c2c002d Slight copy refactor 2022-04-23 10:16:35 -07:00
Grant Sanderson
587bc4d0bd Add necessary import 2022-04-23 10:16:23 -07:00
Grant Sanderson
d733687834 Have -e write over original source file, then correct 2022-04-23 10:16:11 -07:00
Grant Sanderson
0fd8491c51 Move Command + z and Command + shift + z behavior to Scene 2022-04-23 09:20:44 -07:00
Grant Sanderson
753ef3b74a Merge pull request #1796 from 3b1b/video-work
Improved embed, fixes to Mobject.copy
2022-04-23 09:16:46 -07:00
Grant Sanderson
2ba9243067 Merge branch 'master' of github.com:3b1b/manim into video-work 2022-04-23 09:03:53 -07:00
Grant Sanderson
669182944d Merge pull request #1789 from YishiMichael/master
Sort imports
2022-04-23 08:53:14 -07:00
YishiMichael
e085c2e214 Refactor LabelledString and relevant classes 2022-04-23 17:17:43 +08:00
Grant Sanderson
f70e91348c Remove Mobject.interaction_allowed, in favor of using _is_animating for multiple purposes 2022-04-22 23:14:57 -07:00
Grant Sanderson
754316bf58 Factor out event handling 2022-04-22 23:14:19 -07:00
Grant Sanderson
04bca6cafb Refresh static mobjects on undo's and redo's 2022-04-22 23:14:00 -07:00
Grant Sanderson
62289045cc Fix animating Mobject.restore bug 2022-04-22 19:42:47 -07:00
Grant Sanderson
3961005fd7 Rename is_movable to interaction_allowed 2022-04-22 19:17:39 -07:00
Grant Sanderson
7b342a2759 Remove unnecessary lines 2022-04-22 19:03:00 -07:00
Grant Sanderson
59506b89cc Revert to original copying scheme 2022-04-22 19:02:44 -07:00
Grant Sanderson
71c14969df Refactor -e flag hackiness 2022-04-22 15:41:23 -07:00
Grant Sanderson
b2e0aee93e Get rid of ctrl + shift + e embed option 2022-04-22 11:46:18 -07:00
Grant Sanderson
cf466006fa Add undo and redo stacks for scene, together with Command + Z functionality 2022-04-22 11:44:28 -07:00
Grant Sanderson
4d8698a0e8 Add Mobject.deserialize 2022-04-22 11:42:26 -07:00
Grant Sanderson
b9751e9d06 Add cursor location label 2022-04-22 10:17:29 -07:00
Grant Sanderson
bb7fa2c8aa Update behavior of -e flag to take in (optional) strings as inputs 2022-04-22 10:17:15 -07:00
Grant Sanderson
e0f5686d66 Fix bug with trying to close window during embed 2022-04-22 10:16:43 -07:00
Grant Sanderson
581228b08f Have scene keep track of a map from mobject ids to mobjects for all it's ever seen 2022-04-22 08:33:57 -07:00
Grant Sanderson
2737d9a736 Have BlankScene inherit from InteractiveScene 2022-04-22 08:33:18 -07:00
Grant Sanderson
c96bdc243e Update Scene.embed to play nicely with gui interactions 2022-04-22 08:16:17 -07:00
Grant Sanderson
5927f6a1cd Default to "" for scene_file_writer output dir 2022-04-22 08:14:29 -07:00
Grant Sanderson
1b2460f02a Remove refresh_shader_wrapper_id from Mobject.become 2022-04-22 08:14:05 -07:00
YishiMichael
37075590b5 Sort imports 2022-04-22 16:42:45 +08:00
YishiMichael
bf5cec7dba Revert some files 2022-04-22 15:41:57 +08:00
YishiMichael
f8c8a399c9 Revert some files 2022-04-22 15:31:13 +08:00
YishiMichael
f226aa7314 Merge branch '3b1b:master' into master 2022-04-22 15:02:59 +08:00
Grant Sanderson
b4b72d1b68 Allow stretched-resizing 2022-04-21 15:31:46 -07:00
Grant Sanderson
78a7078772 Move saved mobject directory logic to scene_file_writer.py 2022-04-21 15:02:11 -07:00
Grant Sanderson
4caa033323 Allow for sweeping selection 2022-04-21 15:01:54 -07:00
Grant Sanderson
3a60ab144b Remove saved mobject directory logic from InteractiveScene 2022-04-21 15:01:30 -07:00
Grant Sanderson
f53f202dcd A few small cleanups 2022-04-21 15:00:58 -07:00
Grant Sanderson
9d5e2b32fa Add VHighlight 2022-04-21 14:32:39 -07:00
Grant Sanderson
fe3e10acd2 Updates to copying based on pickle serializing 2022-04-21 14:32:27 -07:00
Grant Sanderson
c04615c4e9 In Mobject.set_uniforms, copy uniforms that are numpy arrays 2022-04-21 14:30:39 -07:00
Grant Sanderson
6474e25fcd A few small updates to InteractiveScene 2022-04-21 00:28:37 -07:00
Grant Sanderson
996d71c49e Add fallback for Mobject copying for unpicklable objects 2022-04-20 22:53:49 -07:00
Grant Sanderson
d24b8ff48f Merge branch 'master' into master 2022-04-20 22:40:11 -07:00
Grant Sanderson
485a4ca33a Merge pull request #1794 from 3b1b/video-work
InteractiveScene, etc.
2022-04-20 22:22:09 -07:00
Grant Sanderson
cc563bf5e2 Merge pull request #1787 from lakscastro/patch-1
Remove unused import
2022-04-20 22:19:17 -07:00
Grant Sanderson
19881f3e2d Remove pickle from requirements (as it's in standard library) 2022-04-20 22:17:25 -07:00
Grant Sanderson
a0507c5277 Update to use new keybinding 2022-04-20 22:07:28 -07:00
Grant Sanderson
1b009a4b03 Simplify Mobject.copy to just use pickle serialization 2022-04-20 22:07:10 -07:00
Grant Sanderson
c3afc84bfe Add a rudimentary InteractiveScene to allow for Mobject editing in a GUI fashion 2022-04-20 21:54:16 -07:00
Grant Sanderson
e579f4c955 Add pickle and pyperclip to requirements 2022-04-20 21:53:25 -07:00
Grant Sanderson
4f2e3456e2 Raise Specific exception type when running into latex errors 2022-04-20 21:53:05 -07:00
Grant Sanderson
47636686cb Cleanup extract_mobject_family_members 2022-04-20 21:51:56 -07:00
Grant Sanderson
eae7dbbe6e Change default transparent background codec to be prores 2022-04-20 21:51:36 -07:00
Grant Sanderson
a3579eab41 Have SceneFileWriter handle a location for saved mobjects 2022-04-20 21:51:18 -07:00
Grant Sanderson
5a34ca1fba Add MANIM_COLORS 2022-04-20 21:50:44 -07:00
Grant Sanderson
68e2909af1 Mild cleanup to Scene interactivity 2022-04-20 21:50:37 -07:00
Grant Sanderson
777b6d3778 Allow for saving and loading mobjects from file at the Scene level 2022-04-20 21:49:57 -07:00
Grant Sanderson
97400a5cf2 Update Scene.save_state and Scene.restore 2022-04-20 21:49:38 -07:00
Grant Sanderson
cb768c26a0 Add functionality for recovering mobjects from their ids (to enable copying and pasting) 2022-04-20 21:48:58 -07:00
Grant Sanderson
fdeab8ca95 Make sure AnimationGroup plays nicely with setting mobject animation status 2022-04-20 21:47:47 -07:00
Grant Sanderson
b09e6916dc Remove VMobject.get_highlight 2022-04-20 21:47:12 -07:00
Grant Sanderson
a0c46ef3bf Have set_animating_status recurse over family 2022-04-20 21:46:43 -07:00
Grant Sanderson
4839037503 Update Mobject.make_movable to recurse over family 2022-04-20 21:44:42 -07:00
Grant Sanderson
f636199d9a Add Mobject.get_all_corners 2022-04-20 21:43:16 -07:00
Grant Sanderson
50f5d20cc3 Allow for saving and loading mobjects from file 2022-04-20 21:42:59 -07:00
Grant Sanderson
2dd2fb500e Remove Mobject.get_highlight 2022-04-20 21:42:22 -07:00
Grant Sanderson
c1716895c0 Add Mobject.is_touching 2022-04-20 21:42:07 -07:00
Grant Sanderson
135f68de35 Update Mobject.is_point_touching 2022-04-20 21:41:47 -07:00
YishiMichael
8852921b3d Refactor double brace parsing 2022-04-18 19:44:32 +08:00
YishiMichael
cbb7e69f68 Refactor LabelledString and relevant classes 2022-04-18 18:47:57 +08:00
YishiMichael
0e0244128c Refactor LabelledString and relevant classes 2022-04-17 13:57:03 +08:00
YishiMichael
e9298c5faf Remove sorting key 2022-04-16 16:31:55 +08:00
YishiMichael
4f5173b633 Adjust typing 2022-04-16 15:45:55 +08:00
YishiMichael
58127e7511 import Iterables 2022-04-16 15:34:32 +08:00
YishiMichael
b387bc0c95 Adjust typings 2022-04-16 15:29:23 +08:00
YishiMichael
0406ef70bb Adjust typings for sounds.py and tex_file_writing.py 2022-04-16 14:37:28 +08:00
YishiMichael
654da85cf6 Adjust typings 2022-04-16 14:09:59 +08:00
YishiMichael
bc18894040 Remove empty results in LabelledString.select_parts 2022-04-16 13:59:42 +08:00
YishiMichael
ac4620483c Support flexible selector types 2022-04-16 12:53:43 +08:00
YishiMichael
4690edec3e Refactor LabelledString 2022-04-16 00:24:55 +08:00
YishiMichael
a1e77b0ce2 Refactor LabelledString 2022-04-15 23:58:06 +08:00
YishiMichael
dbefc3b256 Refactor LabelledString 2022-04-15 23:30:42 +08:00
YishiMichael
14dfd776dc Refactor LabelledString 2022-04-15 23:26:41 +08:00
YishiMichael
0a810bb4f1 Refactor LabelledString 2022-04-15 22:54:06 +08:00
YishiMichael
09952756ce Support hashing Color type in hash_obj 2022-04-15 13:48:24 +08:00
YishiMichael
020bd87271 Add back base_color attribute 2022-04-15 13:27:50 +08:00
Grant Sanderson
50565fcd7a Change the way changing-vs-static mobjects are tracked
Previously, Camera would keep track of which mobjects are supposed to be "static", so that it could generated their render groups once and not repeat unnecessarily. This had an awkward dependence where Scene would then need to keep track of which mobjects should and should not be considered static.

This update pushes that logic to the Mobject level, where it keeps track internally of whether it's being animated, has an updater, or can be moved around by the mouse.
2022-04-14 16:27:58 -07:00
Grant Sanderson
5a91c73b23 Merge branch 'master' of github.com:3b1b/manim into video-work 2022-04-14 14:40:14 -07:00
Grant Sanderson
5e49f20294 Add VMobject.get_highlight 2022-04-14 14:37:50 -07:00
Grant Sanderson
29816fa74c Add get_highlight 2022-04-14 14:37:38 -07:00
Grant Sanderson
95f56f5e80 Be sure has_updater_status is properly updated after clear 2022-04-14 14:37:12 -07:00
Grant Sanderson
6a01e36b36 Minor cleanup 2022-04-14 14:36:17 -07:00
YishiMichael
4c324767bd Recover Mobject.scale method 2022-04-15 00:55:02 +08:00
YishiMichael
eec6b01a72 Refactor labelled_string.py 2022-04-14 21:07:31 +08:00
YishiMichael
0c1e5b337b Support passing in complete environment tags 2022-04-13 22:51:55 +08:00
TonyCrane
bda7f98d2e release: ready to release v1.6.1 2022-04-13 10:36:38 +08:00
TonyCrane
9d74e8bce3 docs: update changelog for #1783 #1785 #1788 #1791 2022-04-13 10:34:59 +08:00
YishiMichael
bff9f74b04 Prevent from passing an empty string 2022-04-12 23:19:10 +08:00
Grant Sanderson
845ee83f71 Merge pull request #1791 from 3b1b/fix_image_mobject
Fix `ImageMobject` by overriding `set_color` method
2022-04-12 08:13:01 -07:00
YishiMichael
42444d090e Add missing import 2022-04-12 21:09:25 +08:00
YishiMichael
b11ce7ff7c Adjust annotation 2022-04-12 20:22:13 +08:00
YishiMichael
296ab84b46 Adjust annotation 2022-04-12 20:21:25 +08:00
TonyCrane
55684af27d fix: fix ImageMobject by overriding set_color method 2022-04-12 20:20:03 +08:00
YishiMichael
93790cde64 Add import annotations statement 2022-04-12 20:03:48 +08:00
YishiMichael
fbebaf0c75 Sort imports 2022-04-12 19:39:19 +08:00
YishiMichael
9ef9961d0e Sort imports 2022-04-12 19:19:59 +08:00
YishiMichael
0cf3199578 Adjust return type 2022-04-12 11:26:19 +08:00
YishiMichael
f307c2a298 Add type annotations for color.py 2022-04-12 11:13:05 +08:00
Grant Sanderson
859680d5ab Merge pull request #1788 from 3b1b/interpolate-fix
Interpolate fix
2022-04-11 10:51:05 -07:00
Grant Sanderson
dc4b9bc93c Use outer_interpolate for NumberLine.number_to_point 2022-04-11 10:47:26 -07:00
Grant Sanderson
705f1a528b Separate functionality of ordinary linear interpolation from that using np.outer on arrays 2022-04-11 10:47:11 -07:00
Grant Sanderson
e8ac25903e Add case for zero vectors on angle_between_vectors 2022-04-11 09:59:24 -07:00
Grant Sanderson
773520bcd9 Merge pull request #1785 from YishiMichael/master
Fix bug when handling multi-line tex
2022-04-11 09:52:51 -07:00
Grant Sanderson
d26b8a826c Merge pull request #1783 from EbbDrop/more-special-string
Added a \overset as a special string
2022-04-11 09:50:33 -07:00
YishiMichael
12bfe88f40 Some refactors 2022-04-11 23:44:33 +08:00
lask
31cbf2d905 Remove unused improt 2022-04-10 20:00:10 -03:00
YishiMichael
36d62ae1a3 Add regex parameter 2022-04-10 09:23:53 +08:00
YishiMichael
e23f667c3d Fix bug when handling multi-line tex 2022-04-10 08:36:13 +08:00
EbbDrop
2277679111 Added a \overset as a special string 2022-04-08 23:06:08 +02:00
TonyCrane
9d7db7aacd release: ready to release v1.6.0 2022-04-07 11:00:43 +08:00
TonyCrane
e8430b38b2 docs: update changelog for #1781 2022-04-07 10:57:21 +08:00
鹤翔万里
1f32a9e674 Some fix (#1781)
* fix: reduce warning from numpy

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

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

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

* Add refresh_locked_data

* Add presenter mode to scenes with -p option

* Allow for an embed by hitting e during interaction

* Add set_min_height, etc.

* Make sure null parametric curve has at least one point

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

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

* Simplify choose, and add gen_choose for fractional amounts

* Default to no top on axes

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

* Allow wait calls to ignore presenter mode

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

* Use generator instead of list in bezier

* Bubble init_colors should override

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

* Stop displaying when writing is happening

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

* Refactor MTex

* Implement TransformMatchingMTex

* Some refactors

* Some refactors

* Some small refactors

* Strip strings before matching

* Implement get_submob_tex

* Use RGB color mode

* Some small refactors

* Try refactoring SVGMobject with svglib

* Refactor SVGMobject using svgelements

* Refactor SVGMobject using svgelements

* Use functions instead of func names as dict values

* style: modify import order to conform to PEP8

* Set default values to None

* modify import order

* Remove unused import

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

* Refactor MTex

* Implement TransformMatchingMTex

* Some refactors

* Some refactors

* Some small refactors

* Strip strings before matching

* Implement get_submob_tex

* Use RGB color mode

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

* add classifiers to metadata
2022-02-05 22:13:34 +08:00
TonyCrane
3883f57bf8 release: ready to release v1.4.1 2022-02-04 11:03:37 +08:00
TonyCrane
d2e0811285 import Iterable from collections.abc instead of collections 2022-02-04 10:55:59 +08:00
Grant Sanderson
1e2a6ffb8a Merge pull request #1724 from TurkeyBilly/patch-2
Temporarily fix boolean operation bug
2022-01-31 08:06:27 -08:00
Bill Xi
56e5696163 Update boolean_ops.py 2022-01-31 23:29:36 +08:00
TonyCrane
1ec00629a5 release: ready to release v1.4.0 2022-01-30 13:06:22 +08:00
TonyCrane
aa6335cd90 docs: update changelog for #1719 #1720 #1721 and #1723 2022-01-30 13:00:57 +08:00
YishiMichael
7093f7d02d Some small refactors to MTex (#1723) 2022-01-30 12:42:35 +08:00
Grant Sanderson
fad9ed2df7 Merge pull request #1720 from YishiMichael/master
Handle explicit color-related commands
2022-01-29 08:01:48 -08:00
YishiMichael
725155409b Some small refactors 2022-01-29 21:06:54 +08:00
YishiMichael
a6675eb043 Some small refactors 2022-01-29 14:35:27 +08:00
YishiMichael
5d2dcec307 Fix color-related bugs 2022-01-29 14:05:52 +08:00
Grant Sanderson
f60dc7cd07 Merge pull request #1719 from 3b1b/parse-style
Parse style from <style> tag and Add support to <line> tag
2022-01-27 13:33:14 -08:00
YishiMichael
6c39cac62b Remove redundant attribute 2022-01-28 01:19:02 +08:00
鹤翔万里
2bd25a55fa add back override parameter to init_colors 2022-01-28 00:20:13 +08:00
鹤翔万里
0e4edfdd79 improve config helper (#1721) 2022-01-28 00:16:19 +08:00
YishiMichael
277256a407 Merge branch '3b1b:master' into master 2022-01-27 23:11:19 +08:00
YishiMichael
831b7d455c Handle explicit color-related commands 2022-01-27 23:09:05 +08:00
TonyCrane
1d14a23af9 docs: update changelog for #1712 #1717 and #1716 2022-01-27 22:21:40 +08:00
TonyCrane
dffa70ea15 docs: update changelog for #1704 and #1709 2022-01-27 22:09:57 +08:00
TonyCrane
31976063df add dependency cssselect2 2022-01-27 17:31:14 +08:00
TonyCrane
aa135280ac support <line> tag in SVG 2022-01-27 17:23:58 +08:00
TonyCrane
f0160822ba fix bug of ref map 2022-01-27 17:17:19 +08:00
TonyCrane
48e07d1817 parse style attribute using tinycss 2022-01-27 17:16:52 +08:00
TonyCrane
3ef5899a24 some cleanups 2022-01-27 16:43:45 +08:00
TonyCrane
f895455264 add parser for <style> tag of SVG 2022-01-27 16:37:51 +08:00
Grant Sanderson
3baa14103e Merge pull request #1716 from YishiMichael/master
Refactor MTex and some clean-ups
2022-01-26 08:56:44 -08:00
Grant Sanderson
c315300ff1 Merge branch 'master' into master 2022-01-26 08:54:18 -08:00
Grant Sanderson
3b17d6d0eb Merge pull request #1718 from 3b1b/text-fix
Text fix
2022-01-26 08:21:36 -08:00
Grant Sanderson
8a29de5ef0 Override style for Text 2022-01-26 08:21:00 -08:00
Grant Sanderson
ecb729850a Small style fixes 2022-01-26 08:20:45 -08:00
Grant Sanderson
a770291053 Include style in MTex.get_mobjects_from 2022-01-26 08:20:38 -08:00
Grant Sanderson
27c666fab5 Merge pull request #1717 from 3b1b/svg-style
Parse and generate style for SVG
2022-01-26 07:56:03 -08:00
YishiMichael
942a7e71b8 Update MTex 2022-01-26 23:46:13 +08:00
TonyCrane
ebb75d1235 cached SVGMobject in SingleStringTex with default color 2022-01-26 20:37:44 +08:00
TonyCrane
9af23415a2 synchronize SingleStringTex's color to SVGMobject 2022-01-26 20:20:48 +08:00
TonyCrane
19778e405a some cleanups 2022-01-26 19:55:47 +08:00
TonyCrane
833e40c2d4 fix default style 2022-01-26 19:50:27 +08:00
TonyCrane
9df53b8a18 fix the bug of M command with more than 2 args 2022-01-26 14:05:01 +08:00
TonyCrane
ff86b0e378 fix the bug of outdated relative_point after command Z 2022-01-26 13:56:42 +08:00
TonyCrane
92adcd75d4 add style support to svg 2022-01-26 13:53:53 +08:00
YishiMichael
240f5020b4 Add back default_config.yml 2022-01-26 13:21:27 +08:00
YishiMichael
e8205a5049 Some refactors for MTex 2022-01-26 13:03:14 +08:00
TonyCrane
6c8dd14adc some clean 2022-01-26 11:00:57 +08:00
Grant Sanderson
07f84e2676 Merge pull request #1712 from 3b1b/fix-svg
Improve handling of SVG transform and Some refactors
2022-01-25 13:26:40 -08:00
TonyCrane
8db1164ece some refactors 2022-01-25 21:48:04 +08:00
TonyCrane
790bf0a104 fix typo 2022-01-25 20:25:30 +08:00
TonyCrane
8205edcc4c fix a small bug 2022-01-25 20:13:20 +08:00
TonyCrane
05b3c9852e fix add_smooth_cubic_curve_to when have only one point 2022-01-25 20:06:00 +08:00
TonyCrane
925f2e123f add comments 2022-01-25 19:54:19 +08:00
TonyCrane
565763a2ff reconstruct path parser 2022-01-25 19:44:42 +08:00
TonyCrane
6a74c241b8 fix bug of node which is not an element 2022-01-25 16:28:23 +08:00
TonyCrane
416cc8e6d5 add warning for unsupported element type 2022-01-25 14:41:11 +08:00
TonyCrane
d694aed452 add support for skewX and skewY transform 2022-01-25 14:40:02 +08:00
TonyCrane
11379283aa add support for rotate transform 2022-01-25 14:29:47 +08:00
TonyCrane
dd13559b11 replace warnings.warn with log.warning 2022-01-25 14:09:05 +08:00
TonyCrane
1658438fef allow Mobject.scale receive iterable scale_factor 2022-01-25 14:05:32 +08:00
TonyCrane
f4eb2724c5 refactor SVGMobject.handle_transforms 2022-01-25 14:04:35 +08:00
TonyCrane
33f720c73a fix typo 2022-01-25 13:15:53 +08:00
TonyCrane
bbb4fa155c fix the depth of svg tag 2022-01-25 13:14:19 +08:00
Grant Sanderson
2318c9e716 Merge pull request #1709 from TurkeyBilly/patch-4
Fix "Code is unreachable Pylance" warning for NumberPlane
2022-01-17 08:56:08 -08:00
Bill Xi
e80dd243f1 Added abstract method decorator and override 2022-01-17 20:27:34 +08:00
Grant Sanderson
3ffe300f96 Merge pull request #1704 from TurkeyBilly/patch-2
Adding "label_buff" config parameter for Brace
2022-01-03 08:53:34 -08:00
Bill Xi
24e3caa072 fix no "import copy" bug
added import copy
2022-01-03 16:49:00 +08:00
Bill Xi
9efd02c500 Remove spelling mistake
I misspelled "label"
2022-01-03 16:37:26 +08:00
Bill Xi
0a318486c5 Adding "lable_buff" config parameter for Brace 2022-01-03 14:57:16 +08:00
鹤翔万里
919133c6bf Merge pull request #1702 from Suji04/patch-2
removed extra 'all' from comments
2021-12-31 18:18:50 +08:00
Sujan Dutta
066a2ed5dc removed extra 'all' from comments 2021-12-31 00:10:57 -05:00
TonyCrane
09ced7ce9a docs: update changelog for #1694 and #1697 2021-12-23 10:34:15 +08:00
Grant Sanderson
505b229117 Merge pull request #1697 from 3b1b/video-work
Video work
2021-12-21 10:59:50 -08:00
Grant Sanderson
5aa8d15d85 Use FFMPEG_BIN instead of "ffmpeg" for sound incorporation 2021-12-21 10:58:58 -08:00
Grant Sanderson
7aa05572ab Remove unnecessary import 2021-12-21 10:58:41 -08:00
Grant Sanderson
f1996f8479 Small hack for the lightbulb, needs to be fixed properly later 2021-12-21 10:58:33 -08:00
Grant Sanderson
37b63ca956 Merge pull request #1694 from DangGiaChi/BarChart_modified
Add option to add ticks on x-axis in BarChart()
2021-12-17 09:30:53 -08:00
DangGiaChi
84fd657d9b Change variables names: x_tick, x_ticks, y_tick, y_ticks 2021-12-17 15:02:10 +07:00
DangGiaChi
b489490f41 Fixed things as suggestions 2021-12-17 07:14:37 +07:00
DangGiaChi
bbf45f95c6 Add option to add ticks on x-axis in BarChart() 2021-12-16 22:03:29 +07:00
159 changed files with 16555 additions and 14447 deletions

View File

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

1
.gitignore vendored
View File

@@ -91,6 +91,7 @@ ipython_config.py
# pyenv
.python-version
pyrightconfig.json
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 3Blue1Brown LLC
Copyright (c) 2020-2023 3Blue1Brown LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -12,14 +12,16 @@
Manim is an engine for precise programmatic animations, designed for creating explanatory math videos.
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/installation/versions.html?highlight=OpenGL#which-version-to-use) for more details.
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.6 or higher (Python 3.8 is recommended).
Manim runs on Python 3.7 or higher.
System requirements are [FFmpeg](https://ffmpeg.org/), [OpenGL](https://www.opengl.org/) and [LaTeX](https://www.latex-project.org) (optional, if you want to use LaTeX).
For Linux, [Pango](https://pango.gnome.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
@@ -91,7 +93,9 @@ manimgl example_scenes.py OpeningManimExample
```
This should pop up a window playing a simple scene.
Some useful flags include:
Look through the [example scenes](https://3b1b.github.io/manim/getting_started/example_scenes.html) to see examples of the library's syntax, animation types and object types. In the [3b1b/videos](https://github.com/3b1b/videos) repo, you can see all the code for 3blue1brown videos, though code from older videos may not be compatible with the most recent version of manim. The readme of that repo also outlines some details for how to set up a more interactive workflow, as shown in [this manim demo video](https://www.youtube.com/watch?v=rbu7Zu5X1zI) for example.
When running in the CLI, some useful flags include:
* `-w` to write the scene to a file
* `-o` to write the scene to a file and open the result
* `-s` to skip to the end and just show the final frame.
@@ -101,8 +105,6 @@ Some useful flags include:
Take a look at custom_config.yml for further configuration. To add your customization, you can either edit this file, or add another file by the same name "custom_config.yml" to whatever directory you are running manim from. For example [this is the one](https://github.com/3b1b/videos/blob/master/custom_config.yml) for 3blue1brown videos. There you can specify where videos should be output to, where manim should look for image files and sounds you want to read in, and other defaults regarding style and video quality.
Look through the [example scenes](https://3b1b.github.io/manim/getting_started/example_scenes.html) to get a sense of how it is used, and feel free to look through the code behind [3blue1brown videos](https://github.com/3b1b/videos) for a much larger set of example. Note, however, that developments are often made to the library without considering backwards compatibility with those old videos. To run an old project with a guarantee that it will work, you will have to go back to the commit which completed that project.
### Documentation
Documentation is in progress at [3b1b.github.io/manim](https://3b1b.github.io/manim/). And there is also a Chinese version maintained by [**@manim-kindergarten**](https://manim.org.cn): [docs.manim.org.cn](https://docs.manim.org.cn/) (in Chinese).

View File

@@ -1,69 +1,304 @@
Changelog
=========
Unreleased
----------
Breaking Changes
^^^^^^^^^^^^^^^^
- Added ``InteractiveScene`` (`#1794 <https://github.com/3b1b/manim/pull/1794>`__)
Fixed bugs
^^^^^^^^^^
- Fixed ``ImageMobject`` by overriding ``set_color`` method (`#1791 <https://github.com/3b1b/manim/pull/1791>`__)
- Fixed bug with trying to close window during embed (`#1796 <https://github.com/3b1b/manim/commit/e0f5686d667152582f052021cd62bd2ef8c6b470>`__)
- Fixed animating ``Mobject.restore`` bug (`#1796 <https://github.com/3b1b/manim/commit/62289045cc8e102121cfe4d7739f3c89102046fb>`__)
- Fixed ``InteractiveScene.refresh_selection_highlight`` (`#1802 <https://github.com/3b1b/manim/commit/205116b8cec964b5619416f6e8acf0d8ac7df828>`__)
- Fixed ``VMobject.match_style`` (`#1821 <https://github.com/3b1b/manim/commit/0060a4860c9d6b073a60cd839269c213446bba7b>`__)
New Features
^^^^^^^^^^^^
- Added specific euler angle getters (`#1794 <https://github.com/3b1b/manim/commit/df2d465140e25fee265f602608aebbbaa2898c7e>`__)
- Added start angle option to ``Circle`` (`#1794 <https://github.com/3b1b/manim/commit/217c1d7bb02f23a61722bf7275c40802be808563>`__)
- Added ``Mobject.is_touching`` (`#1794 <https://github.com/3b1b/manim/commit/c1716895c0d9f36e23487322a18963991100bb95>`__)
- Added ``Mobject.get_highlight`` (`#1794 <https://github.com/3b1b/manim/commit/29816fa74c7aa6ca060b63ab4165c89987e58d8b>`__)
- Allowed for saving and loading mobjects from file (`#1794 <https://github.com/3b1b/manim/commit/50f5d20cc379947d7253d841c060dd7c55fa7787>`__)
- Added ``Mobject.get_all_corners`` (`#1794 <https://github.com/3b1b/manim/commit/f636199d9a5d1e87ab861bcb6aebae6c9d96a133>`__)
- Added ``Scene.id_to_mobject`` and ``Scene.ids_to_group`` (`#1794 <https://github.com/3b1b/manim/commit/cb768c26a0bc63e02c3035b4af31ba5cbc2e9dda>`__)
- Added ``Scene.save_mobject`` and ``Scene.load_mobject`` to allow for saving and loading mobjects from file at the Scene level (`#1794 <https://github.com/3b1b/manim/commit/777b6d37783f8592df8a8abc3d62af972bc5a0c6>`__)
- Added ``InteractiveScene`` (`#1794 <https://github.com/3b1b/manim/commit/c3afc84bfeb3a76ea8ede4ec4d9f36df0d4d9a28>`__)
- Added ``VHighlight`` (`#1794 <https://github.com/3b1b/manim/commit/9d5e2b32fa9215219d11a601829126cea40410d1>`__)
- Allowed for sweeping selection (`#1796 <https://github.com/3b1b/manim/commit/4caa03332367631d2fff15afd7e56b15fe8701ee>`__)
- Allowed stretched-resizing (`#1796 <https://github.com/3b1b/manim/commit/b4b72d1b68d0993b96a6af76c4bb6816f77f0f12>`__)
- Added cursor location label (`#1796 <https://github.com/3b1b/manim/commit/b9751e9d06068f27a327b419c52fd3c9d68db2e6>`__)
- Added ``Mobject.deserialize`` (`#1796 <https://github.com/3b1b/manim/commit/4d8698a0e88333f6481c08d1b84b6e44f9dc4543>`__)
- Added undo and redo stacks for scene (`#1796 <https://github.com/3b1b/manim/commit/cf466006faa00fc12dc22f5732dc21ccedaa5a63>`__)
- Added ``Mobject.looks_identical`` (`#1802 <https://github.com/3b1b/manim/commit/c3c5717dde543b172b928b516d80a29bbd12651f>`__)
- Added equality for ``ShaderWrapper`` (`#1802 <https://github.com/3b1b/manim/commit/3ae0a4e81b7790194bcf27142a1deb29fa548b9d>`__)
- Added ``Mobject.get_ancestors`` (`#1802 <https://github.com/3b1b/manim/commit/db884b0a67fcee1ad7009f1869c475015fa886c7>`__)
- Added smarter default radius to ``Polygon.round_corners`` (`#1802 <https://github.com/3b1b/manim/commit/4c1210b3ab1bf66b161f3d00cb859d36068c2fbb>`__)
- Added checkpoints to ``Scene`` (`#1821 <https://github.com/3b1b/manim/commit/1b589e336f8151f2914ff00e8956baea8a95abc5>`__)
- Added ``crosshair`` to ``InteractiveScene`` (`#1821 <https://github.com/3b1b/manim/commit/33ffd4863aaa7ecf950b7044181a8e8e3c643698>`__)
- Added ``SceneState`` (`#1821 <https://github.com/3b1b/manim/commit/75e1cff5792065aa1c7fb3eb02e6ee0fa0e8e18d>`__)
- Added ``time_span`` option to ``Animation`` (`#1821 <https://github.com/3b1b/manim/commit/a6fcfa3b4053b7f68f7b029eae87dbd207d97ad2>`__)
- Added ``Mobject.arrange_to_fit_dim`` (`#1821 <https://github.com/3b1b/manim/commit/a87d3b5f59a64ce5a89ce6e17310bdbf62166157>`__)
- Added ``DecimalNumber.get_tex`` (`#1821 <https://github.com/3b1b/manim/commit/48689c8c7bc0029bf5c1b540c11f647e857d419b>`__)
Refactor
^^^^^^^^
- Updated parent updater status when adding updaters (`#1794 <https://github.com/3b1b/manim/commit/3b847da9eaad7391e779c5dbce63ad9257d8c773>`__)
- Added case for zero vectors on ``angle_between_vectors`` (`#1794 <https://github.com/3b1b/manim/commit/e8ac25903e19cbb2b2c2037c988baafce4ddcbbc>`__)
- Refactored ``Mobject.clear_updaters`` (`#1794 <https://github.com/3b1b/manim/commit/95f56f5e80106443d705c68fa220850ec38daee0>`__)
- Changed the way changing-vs-static mobjects are tracked (more details see `#1794 <https://github.com/3b1b/manim/commit/50565fcd7a43ed13dc532f17515208edf97f64d0>`__)
- Refactored ``Mobject.is_point_touching`` (`#1794 <https://github.com/3b1b/manim/commit/135f68de35712be266a1a85261d6d44234fc0056>`__)
- Refactored ``Mobject.make_movable`` and ``Mobject.set_animating_status`` to recurse over family (`#1794 <https://github.com/3b1b/manim/commit/48390375037f745c9cb82b03d1cb3a1de6c530f3>`__)
- Refactored ``AnimationGroup`` (`#1794 <https://github.com/3b1b/manim/commit/fdeab8ca953b46a902b531febcf132739ca194d4>`__)
- Refactored ``Scene.save_state`` and ``Scene.restore`` (`#1794 <https://github.com/3b1b/manim/commit/97400a5cf26f33ed507ddeeb9b9a7f1a558d4f17>`__)
- Added ``MANIM_COLORS`` (`#1794 <https://github.com/3b1b/manim/commit/5a34ca1fba8b4724eda0caa11b271d74e49f468c>`__)
- Changed default transparent background codec to be prores (`#1794 <https://github.com/3b1b/manim/commit/eae7dbbe6eaf4344374713052aae694e69b62c28>`__)
- Simplified ``Mobject.copy`` (`#1794 <https://github.com/3b1b/manim/commit/1b009a4b035244bd6a0b48bc4dc945fd3b4236ef>`__)
- Refactored ``StringMobject`` and relevant classes (`#1795 <https://github.com/3b1b/manim/pull/1795>`__)
- Updates to copying based on pickle serializing (`#1796 <https://github.com/3b1b/manim/commit/fe3e10acd29a3dd6f8b485c0e36ead819f2d937b>`)
- Removed ``refresh_shader_wrapper_id`` from ``Mobject.become`` (`#1796 <https://github.com/3b1b/manim/commit/1b2460f02a694314897437b9b8755443ed290cc1>`__)
- Refactored ``Scene.embed`` to play nicely with gui interactions (`#1796 <https://github.com/3b1b/manim/commit/c96bdc243e57c17bb75bf12d73ab5bf119cf1464>`__)
- Made ``BlankScene`` inherit from ``InteractiveScene`` (`#1796 <https://github.com/3b1b/manim/commit/2737d9a736885a594dd101ffe07bb82e00069333>`__)
- Updated behavior of -e flag to take in (optional) strings as inputs (`#1796 <https://github.com/3b1b/manim/commit/bb7fa2c8aa68d7c7992517cfde3c7d0e804e13e8>`__)
- Refactor -e flag (`#1796 <https://github.com/3b1b/manim/commit/71c14969dffc8762a43f9646a0c3dc024a51b8df>`__)
- Reverted to original copying scheme (`#1796 <https://github.com/3b1b/manim/commit/59506b89cc73fff3b3736245dd72e61dcebf9a2c>`__)
- Renamed ``Mobject.is_movable`` to ``Mobject.interaction_allowed`` (`#1796 <https://github.com/3b1b/manim/commit/3961005fd708333a3e77856d10e78451faa04075>`__)
- Refreshed static mobjects on undo's and redo's (`#1796 <https://github.com/3b1b/manim/commit/04bca6cafbb1482b8f25cfb34ce83316d8a095c9>`__)
- Factored out event handling (`#1796 <https://github.com/3b1b/manim/commit/754316bf586be5a59839f8bac6fb9fcc47da0efb>`__)
- Removed ``Mobject.interaction_allowed``, in favor of using ``_is_animating`` for multiple purposes (`#1796 <https://github.com/3b1b/manim/commit/f70e91348c8241bcb96470e7881dd92d9d3386d3>`__)
- Moved Command + z and Command + shift + z behavior to Scene (`#1797 <https://github.com/3b1b/manim/commit/0fd8491c515ad23ca308099abe0f39fc38e2dd0e>`__)
- Slight copy refactor (`#1797 <https://github.com/3b1b/manim/commit/902c2c002d6ca03c8080b2bd02ca36f2b8a748b6>`__)
- When scene saves state, have it only copy mobjects which have changed (`#1802 <https://github.com/3b1b/manim/commit/bd2dce08300e5b110c6668bd6763f3918fcdc65e>`__)
- Cleaned up ``Scene.remove`` function (`#1802 <https://github.com/3b1b/manim/commit/6310e2fb6414b01b3fe4be1d4d98525e34356b5e>`__)
- Speed-ups to ``Mobject.copy`` (`#1802 <https://github.com/3b1b/manim/commit/e49e4b8373c13c7a888193aaf61955470acbe5d6>`__)
- Slight speed-up to ``InteractiveScene.gather_selection`` (`#1802 <https://github.com/3b1b/manim/commit/f2b4245c134da577a2854732ec0331768d93ffbe>`__)
- Only leave wait notes in presenter mode (`#1802 <https://github.com/3b1b/manim/commit/42d1f48c60d11caa043d5458e64bfceb31ea203f>`__)
- Refactored ``remove_list_redundancies`` and ``list_update`` (`#1821 <https://github.com/3b1b/manim/commit/b920e7be7b85bc0bb0577e2f71c4320bb97b42d4>`__)
- Match updaters in ``Mobject.become`` (`#1821 <https://github.com/3b1b/manim/commit/0e45b41fea5f22d136f62f4af2e0d892e61a12ce>`__)
- Don't show animation progress bar by default (`#1821 <https://github.com/3b1b/manim/commit/52259af5df619d3f44fbaff4c43402b93d01be2f>`__)
- Handle quitting during scene more gracefully (`#1821 <https://github.com/3b1b/manim/commit/e83ad785caaa1a1456e07b23f207469d335bbc0d>`__)
- Made ``selection_highlight`` refresh with an updater (`#1821 <https://github.com/3b1b/manim/commit/ac08963feff24a1dd2e57f604b44ea0a18ab01f3>`__)
- Refactored ``anims_from_play_args`` to ``prepare_animations`` which deprecating old style ``self.play(mob.method, ...)`` (`#1821 <https://github.com/3b1b/manim/commit/feab79c260498fd7757a304e24c617a4e51ba1df>`__)
- Made presenter mode hold before first play call (`#1821 <https://github.com/3b1b/manim/commit/a9a151d4eff80cc37b9db0fe7117727aac45ba09>`__)
- Update frame on all play calls when skipping animations, so as to provide a rapid preview during scene loading (`#1821 <https://github.com/3b1b/manim/commit/41b811a5e7c03f528d41555217106e62b287ca3b>`__)
- Renamed frame_rate to fps (`#1821 <https://github.com/3b1b/manim/commit/6decb0c32aec21c09007f9a2b91aaa8e642ca848>`__)
- Let default text alignment be decided in default_config (`#1821 <https://github.com/3b1b/manim/commit/83b4aa6b88b6c3defb19f204189681f5afbb219e>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on ``pyperclip`` (`#1794 <https://github.com/3b1b/manim/commit/e579f4c955844fba415b976c313f64d1bb0376d0>`__)
v1.6.1
------
Fixed bugs
^^^^^^^^^^
- Fixed the bug of ``MTex`` with multi-line tex string (`#1785 <https://github.com/3b1b/manim/pull/1785>`__)
- Fixed ``interpolate`` (`#1788 <https://github.com/3b1b/manim/pull/1788>`__)
- Fixed ``ImageMobject`` (`#1791 <https://github.com/3b1b/manim/pull/1791>`__)
Refactor
^^^^^^^^
- Added ``\overset`` as a special string in ``Tex`` (`#1783 <https://github.com/3b1b/manim/pull/1783>`__)
- Added ``outer_interpolate`` to perform interpolation using ``np.outer`` on arrays (`#1788 <https://github.com/3b1b/manim/pull/1788>`__)
v1.6.0
------
Breaking changes
^^^^^^^^^^^^^^^^
- **Python 3.6 is no longer supported** (`#1736 <https://github.com/3b1b/manim/pull/1736>`__)
Fixed bugs
^^^^^^^^^^
- Fixed the width of riemann rectangles (`#1762 <https://github.com/3b1b/manim/pull/1762>`__)
- Bug fixed in cases where empty array is passed to shader (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/fa38b56fd87f713657c7f778f39dca7faf15baa8>`__)
- Fixed ``AddTextWordByWord`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
- Fixed ``ControlsExample`` (`#1781 <https://github.com/3b1b/manim/pull/1781>`__)
New features
^^^^^^^^^^^^
- Added more functions to ``Text`` (details: `#1751 <https://github.com/3b1b/manim/pull/1751>`__)
- Allowed ``interpolate`` to work on an array of alpha values (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/bf2d9edfe67c7e63ac0107d1d713df7ae7c3fb8f>`__)
- Allowed ``Numberline.number_to_point`` and ``CoordinateSystem.coords_to_point`` to work on an array of inputs (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/c3e13fff0587d3bb007e71923af7eaf9e4926560>`__)
- Added a basic ``Prismify`` to turn a flat ``VMobject`` into something with depth (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/f249da95fb65ed5495cd1db1f12ece7e90061af6>`__)
- Added ``GlowDots``, analogous to ``GlowDot`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/e19f35585d817e74b40bc30b1ab7cee84b24da05>`__)
- Added ``TransformMatchingStrings`` which is compatible with ``Text`` and ``MTex`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
- Added support for ``substring`` and ``case_sensitive`` parameters for ``LabelledString.get_parts_by_string`` (`#1780 <https://github.com/3b1b/manim/pull/1780>`__)
Refactor
^^^^^^^^
- Added type hints (`#1736 <https://github.com/3b1b/manim/pull/1736>`__)
- Specifid UTF-8 encoding for tex files (`#1748 <https://github.com/3b1b/manim/pull/1748>`__)
- Refactored ``Text`` with the latest manimpango (`#1751 <https://github.com/3b1b/manim/pull/1751>`__)
- Reorganized getters for ``ParametricCurve`` (`#1757 <https://github.com/3b1b/manim/pull/1757>`__)
- Refactored ``CameraFrame`` to use ``scipy.spatial.transform.Rotation`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/625460467fdc01fc1b6621cbb3d2612195daedb9>`__)
- Refactored rotation methods to use ``scipy.spatial.transform.Rotation`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/7bf3615bb15cc6d15506d48ac800a23313054c8e>`__)
- Used ``stroke_color`` to init ``Arrow`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/c0b7b55e49f06b75ae133b5a810bebc28c212cd6>`__)
- Refactored ``Mobject.set_rgba_array_by_color`` (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/8b1f0a8749d91eeda4b674ed156cbc7f8e1e48a8>`__)
- Made panning more sensitive to mouse movements (`#1764 <https://github.com/3b1b/manim/pull/1764/commits/9d0cc810c5fcb4252990e706c6bf880d571cb1a2>`__)
- Added loading progress for large SVGs (`#1766 <https://github.com/3b1b/manim/pull/1766>`__)
- Added getter/setter of ``field_of_view`` for ``CameraFrame`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0610f331a4f7a126a3aae34f8a2a86eabcb692f4>`__)
- Renamed ``focal_distance`` to ``focal_dist_to_height`` and added getter/setter (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0610f331a4f7a126a3aae34f8a2a86eabcb692f4>`__)
- Added getter and setter for ``VMobject.joint_type`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/2a7a7ac5189a14170f883533137e8a2ae09aac41>`__)
- Refactored ``VCube`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0f8d7ed59751d42d5011813ba5694ecb506082f7>`__)
- Refactored ``Prism`` to receive ``width height depth`` instead of ``dimensions`` (`#1770 <https://github.com/3b1b/manim/pull/1770/commits/0f8d7ed59751d42d5011813ba5694ecb506082f7>`__)
- Refactored ``Text``, ``MarkupText`` and ``MTex`` based on ``LabelledString`` (`#1772 <https://github.com/3b1b/manim/pull/1772>`__)
- Refactored ``LabelledString`` and relevant classes (`#1779 <https://github.com/3b1b/manim/pull/1779>`__)
v1.5.0
------
Fixed bugs
^^^^^^^^^^
- Bug fix for the case of calling ``Write`` on a null object (`#1740 <https://github.com/3b1b/manim/pull/1740>`__)
New features
^^^^^^^^^^^^
- Added ``TransformMatchingMTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
- Added ``ImplicitFunction`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
- Added ``Polyline`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
- Allowed ``Mobject.set_points`` to take in an empty list, and added ``Mobject.add_point`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/a64259158538eae6043566aaf3d3329ff4ac394b>`__)
- Added ``Scene.refresh_locked_data`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/33d2894c167c577a15fdadbaf26488ff1f5bff87>`__)
- Added presenter mode to scenes with ``-p`` option (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9a9cc8bdacb7541b7cd4a52ad705abc21f3e27fe>`__ and `#1742 <https://github.com/3b1b/manim/pull/1742>`__)
- Allowed for an embed by hitting ``ctrl+shift+e`` during interaction (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/9df12fcb7d8360e51cd7021d6877ca1a5c31835e>`__ and `#1746 <https://github.com/3b1b/manim/pull/1746>`__)
- Added ``Mobject.set_min_width/height/depth`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2798d15591a0375ae6bb9135473e6f5328267323>`__)
- Allowed ``Mobject.match_coord/x/y/z`` to take in a point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/29a4d3e82ba94c007c996b2d1d0f923941452698>`__)
- Added ``text_config`` to ``DecimalNumber`` (`#1744 <https://github.com/3b1b/manim/pull/1744>`__)
Refactor
^^^^^^^^
- Refactored ``MTex`` (`#1725 <https://github.com/3b1b/manim/pull/1725>`__)
- Refactored ``SVGMobject`` with svgelements (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
- Made sure ``ParametricCurve`` has at least one point (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/2488b9e866fb1ecb842a27dd9f4956ec167e3dee>`__)
- Set default to no tips on ``Axes`` (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/6c6d387a210756c38feca7d34838aa9ac99bb58a>`__)
- Stopped displaying when writing tex string is happening (`#1739 <https://github.com/3b1b/manim/pull/1739/commits/58e06e8f6b7c5059ff315d51fd0018fec5cfbb05>`__)
- Reorganize inheriting order and refactor SVGMobject (`#1745 <https://github.com/3b1b/manim/pull/1745>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on ``isosurfaces`` (`#1727 <https://github.com/3b1b/manim/pull/1727>`__)
- Removed dependency on ``argparse`` since it's a built-in module (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
- Removed dependency on ``pyreadline`` (`#1728 <https://github.com/3b1b/manim/pull/1728>`__)
- Removed dependency on ``cssselect2`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
- Added dependency on ``svgelements`` (`#1731 <https://github.com/3b1b/manim/pull/1731>`__)
v1.4.1
------
Fixed bugs
^^^^^^^^^^
- Temporarily fixed boolean operations' bug (`#1724 <https://github.com/3b1b/manim/pull/1724>`__)
- Import ``Iterable`` from ``collections.abc`` instead of ``collections`` which is deprecated since python 3.9 (`d2e0811 <https://github.com/3b1b/manim/commit/d2e0811285f7908e71a65e664fec88b1af1c6144>`__)
v1.4.0
------
Fixed bugs
^^^^^^^^^^
- Temporarily fixed ``Lightbulb`` (`f1996f8 <https://github.com/3b1b/manim/pull/1697/commits/f1996f8479f9e33d626b3b66e9eb6995ce231d86>`__)
- Fixed some bugs of ``SVGMobject`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Fixed some bugs of SVG path string parser (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
- Fixed some bugs of ``MTex`` (`#1720 <https://github.com/3b1b/manim/pull/1720>`__)
New features
^^^^^^^^^^^^
- Added option to add ticks on x-axis in ``BarChart`` (`#1694 <https://github.com/3b1b/manim/pull/1694>`__)
- Added ``lable_buff`` config parameter for ``Brace`` (`#1704 <https://github.com/3b1b/manim/pull/1704>`__)
- Added support for ``rotate skewX skewY`` transform in SVG (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Added style support to ``SVGMobject`` (`#1717 <https://github.com/3b1b/manim/pull/1717>`__)
- Added parser to <style> element of SVG (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
- Added support for <line> element in ``SVGMobject`` (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
Refactor
^^^^^^^^
- Used ``FFMPEG_BIN`` instead of ``"ffmpeg"`` for sound incorporation (`5aa8d15 <https://github.com/3b1b/manim/pull/1697/commits/5aa8d15d85797f68a8f169ca69fd90d441a3abbe>`__)
- Decorated ``CoordinateSystem.get_axes`` and ``.get_all_ranges`` as abstract method (`#1709 <https://github.com/3b1b/manim/pull/1709>`__)
- Refactored SVG path string parser (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Allowed ``Mobject.scale`` to receive iterable ``scale_factor`` (`#1712 <https://github.com/3b1b/manim/pull/1712>`__)
- Refactored ``MTex`` (`#1716 <https://github.com/3b1b/manim/pull/1716>`__)
- Improved config helper (``manimgl --config``) (`#1721 <https://github.com/3b1b/manim/pull/1721>`__)
- Refactored ``MTex`` (`#1723 <https://github.com/3b1b/manim/pull/1723>`__)
Dependencies
^^^^^^^^^^^^
- Added dependency on python package `cssselect2 <https://github.com/Kozea/cssselect2>`__ (`#1719 <https://github.com/3b1b/manim/pull/1719>`__)
v1.3.0
------
Fixed bugs
^^^^^^^^^^
- `#1653 <https://github.com/3b1b/manim/pull/1653>`__: Fixed ``Mobject.stretch_to_fit_depth``
- `#1655 <https://github.com/3b1b/manim/pull/1655>`__: Fixed the bug of rotating camera
- `c73d507 <https://github.com/3b1b/manim/pull/1688/commits/c73d507c76af5c8602d4118bc7538ba04c03ebae>`__: Fixed ``SurfaceMesh`` to be evenly spaced
- `82bd02d <https://github.com/3b1b/manim/pull/1688/commits/82bd02d21fbd89b71baa21e077e143f440df9014>`__: Fixed ``angle_between_vectors`` add ``rotation_between_vectors``
- `a717314 <https://github.com/3b1b/manim/pull/1688/commits/a7173142bf93fd309def0cc10f3c56f5e6972332>`__: Fixed ``VMobject.fade``
- `fbc329d <https://github.com/3b1b/manim/pull/1688/commits/fbc329d7ce3b11821d47adf6052d932f7eff724a>`__: Fixed ``angle_between_vectors``
- `bcd0990 <https://github.com/3b1b/manim/pull/1688/commits/bcd09906bea5eaaa5352e7bee8f3153f434cf606>`__: Fixed bug in ``ShowSubmobjectsOneByOne``
- `7023548 <https://github.com/3b1b/manim/pull/1691/commits/7023548ec62c4adb2f371aab6a8c7f62deb7c33c>`__: Fixed bug in ``TransformMatchingParts``
- Fixed ``Mobject.stretch_to_fit_depth`` (`#1653 <https://github.com/3b1b/manim/pull/1653>`__)
- Fixed the bug of rotating camera (`#1655 <https://github.com/3b1b/manim/pull/1655>`__)
- Fixed ``SurfaceMesh`` to be evenly spaced (`c73d507 <https://github.com/3b1b/manim/pull/1688/commits/c73d507c76af5c8602d4118bc7538ba04c03ebae>`__)
- Fixed ``angle_between_vectors`` add ``rotation_between_vectors`` (`82bd02d <https://github.com/3b1b/manim/pull/1688/commits/82bd02d21fbd89b71baa21e077e143f440df9014>`__)
- Fixed ``VMobject.fade`` (`a717314 <https://github.com/3b1b/manim/pull/1688/commits/a7173142bf93fd309def0cc10f3c56f5e6972332>`__)
- Fixed ``angle_between_vectors`` (`fbc329d <https://github.com/3b1b/manim/pull/1688/commits/fbc329d7ce3b11821d47adf6052d932f7eff724a>`__)
- Fixed bug in ``ShowSubmobjectsOneByOne`` (`bcd0990 <https://github.com/3b1b/manim/pull/1688/commits/bcd09906bea5eaaa5352e7bee8f3153f434cf606>`__)
- Fixed bug in ``TransformMatchingParts`` (`7023548 <https://github.com/3b1b/manim/pull/1691/commits/7023548ec62c4adb2f371aab6a8c7f62deb7c33c>`__)
New Features
New features
^^^^^^^^^^^^
- `e10f850 <https://github.com/3b1b/manim/commit/e10f850d0d9f971931cc85d44befe67dc842af6d>`__: Added CLI flag ``--log-level`` to specify log level
- `#1667 <https://github.com/3b1b/manim/pull/1667>`__: Added operations (``+`` and ``*``) for ``Mobject``
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added 4 boolean operations for ``VMobject`` in ``manimlib/mobject/boolean_ops.py``
- Added CLI flag ``--log-level`` to specify log level (`e10f850 <https://github.com/3b1b/manim/commit/e10f850d0d9f971931cc85d44befe67dc842af6d>`__)
- Added operations (``+`` and ``*``) for ``Mobject`` (`#1667 <https://github.com/3b1b/manim/pull/1667>`__)
- Added 4 boolean operations for ``VMobject`` in ``manimlib/mobject/boolean_ops.py`` (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
- ``Union(*vmobjects, **kwargs)``
- ``Difference(subject, clip, **kwargs)``
- ``Intersection(*vmobjects, **kwargs)``
- ``Exclusion(*vmobjects, **kwargs)``
- `81c3ae3 <https://github.com/3b1b/manim/pull/1688/commits/81c3ae30372e288dc772633dbd17def6e603753e>`__: Added reflectiveness
- `2c7689e <https://github.com/3b1b/manim/pull/1688/commits/2c7689ed9e81229ce87c648f97f26267956c0bc9>`__: Enabled ``glow_factor`` on ``DotCloud``
- `d065e19 <https://github.com/3b1b/manim/pull/1688/commits/d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772>`__: Added option ``-e`` to insert embed line from the command line
- `0e78027 <https://github.com/3b1b/manim/pull/1688/commits/0e78027186a976f7e5fa8d586f586bf6e6baab8d>`__: Improved ``point_from_proportion`` to account for arc length
- `781a993 <https://github.com/3b1b/manim/pull/1688/commits/781a9934fda6ba11f22ba32e8ccddcb3ba78592e>`__: Added shortcut ``set_backstroke`` for setting black background stroke
- `0b898a5 <https://github.com/3b1b/manim/pull/1688/commits/0b898a5594203668ed9cad38b490ab49ba233bd4>`__: Added ``Suface.always_sort_to_camera``
- `e899604 <https://github.com/3b1b/manim/pull/1688/commits/e899604a2d05f78202fcb3b9824ec34647237eae>`__: Added getter methods for specific euler angles
- `407c53f <https://github.com/3b1b/manim/pull/1688/commits/407c53f97c061bfd8a53beacd88af4c786f9e9ee>`__: Hade ``rotation_between_vectors`` handle identical/similar vectors
- `49743da <https://github.com/3b1b/manim/pull/1688/commits/49743daf3244bfa11a427040bdde8e2bb79589e8>`__: Added ``Mobject.insert_submobject`` method
- `9dd1f47 <https://github.com/3b1b/manim/pull/1688/commits/9dd1f47dabca1580d6102e34e44574b0cba556e7>`__: Created single progress display for full scene render
- `264f7b1 <https://github.com/3b1b/manim/pull/1691/commits/264f7b11726e9e736f0fe472f66e38539f74e848>`__: Added ``Circle.get_radius``
- `83841ae <https://github.com/3b1b/manim/pull/1691/commits/83841ae41568a9c9dff44cd163106c19a74ac281>`__: Added ``Dodecahedron``
- `a1d5147 <https://github.com/3b1b/manim/pull/1691/commits/a1d51474ea1ce3b7aa3efbe4c5e221be70ee2f5b>`__: Added ``GlowDot``
- `#1678 <https://github.com/3b1b/manim/pull/1678>`__: Added ``MTex`` , see `#1678 <https://github.com/3b1b/manim/pull/1678>`__ for details
- Added reflectiveness (`81c3ae3 <https://github.com/3b1b/manim/pull/1688/commits/81c3ae30372e288dc772633dbd17def6e603753e>`__)
- Enabled ``glow_factor`` on ``DotCloud`` (`2c7689e <https://github.com/3b1b/manim/pull/1688/commits/2c7689ed9e81229ce87c648f97f26267956c0bc9>`__)
- Added option ``-e`` to insert embed line from the command line (`d065e19 <https://github.com/3b1b/manim/pull/1688/commits/d065e1973d1d6ebd2bece81ce4bdf0c2fff7c772>`__)
- Improved ``point_from_proportion`` to account for arc length (`0e78027 <https://github.com/3b1b/manim/pull/1688/commits/0e78027186a976f7e5fa8d586f586bf6e6baab8d>`__)
- Added shortcut ``set_backstroke`` for setting black background stroke (`781a993 <https://github.com/3b1b/manim/pull/1688/commits/781a9934fda6ba11f22ba32e8ccddcb3ba78592e>`__)
- Added ``Suface.always_sort_to_camera`` (`0b898a5 <https://github.com/3b1b/manim/pull/1688/commits/0b898a5594203668ed9cad38b490ab49ba233bd4>`__)
- Added getter methods for specific euler angles (`e899604 <https://github.com/3b1b/manim/pull/1688/commits/e899604a2d05f78202fcb3b9824ec34647237eae>`__)
- Hade ``rotation_between_vectors`` handle identical/similar vectors (`407c53f <https://github.com/3b1b/manim/pull/1688/commits/407c53f97c061bfd8a53beacd88af4c786f9e9ee>`__)
- Added ``Mobject.insert_submobject`` method (`49743da <https://github.com/3b1b/manim/pull/1688/commits/49743daf3244bfa11a427040bdde8e2bb79589e8>`__)
- Created single progress display for full scene render (`9dd1f47 <https://github.com/3b1b/manim/pull/1688/commits/9dd1f47dabca1580d6102e34e44574b0cba556e7>`__)
- Added ``Circle.get_radius`` (`264f7b1 <https://github.com/3b1b/manim/pull/1691/commits/264f7b11726e9e736f0fe472f66e38539f74e848>`__)
- Added ``Dodecahedron`` (`83841ae <https://github.com/3b1b/manim/pull/1691/commits/83841ae41568a9c9dff44cd163106c19a74ac281>`__)
- Added ``GlowDot`` (`a1d5147 <https://github.com/3b1b/manim/pull/1691/commits/a1d51474ea1ce3b7aa3efbe4c5e221be70ee2f5b>`__)
- Added ``MTex`` , see `#1678 <https://github.com/3b1b/manim/pull/1678>`__ for details (`#1678 <https://github.com/3b1b/manim/pull/1678>`__)
Refactor
^^^^^^^^
- `#1662 <https://github.com/3b1b/manim/pull/1662>`__: Refactored support for command ``A`` in path of SVG
- `#1662 <https://github.com/3b1b/manim/pull/1662>`__: Refactored ``SingleStringTex.balance_braces``
- `8b454fb <https://github.com/3b1b/manim/pull/1688/commits/8b454fbe9335a7011e947093230b07a74ba9c653>`__: Slight tweaks to how saturation_factor works on newton-fractal
- `317a5d6 <https://github.com/3b1b/manim/pull/1688/commits/317a5d6226475b6b54a78db7116c373ef84ea923>`__: Made it possible to set full screen preview as a default
- `e764da3 <https://github.com/3b1b/manim/pull/1688/commits/e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc>`__: Used ``quick_point_from_proportion`` for graph points
- `d2182b9 <https://github.com/3b1b/manim/pull/1688/commits/d2182b9112300558b6c074cefd685f97c10b3898>`__: Made sure ``Line.set_length`` returns self
- `eea3c6b <https://github.com/3b1b/manim/pull/1688/commits/eea3c6b29438f9e9325329c4355e76b9f635e97a>`__: Better align ``SurfaceMesh`` to the corresponding surface polygons
- `ee1594a <https://github.com/3b1b/manim/pull/1688/commits/ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3>`__: Match ``fix_in_frame`` status for ``FlashAround`` mobject
- `ba23fbe <https://github.com/3b1b/manim/pull/1688/commits/ba23fbe71e4a038201cd7df1d200514ed1c13bc2>`__: Made sure ``Mobject.is_fixed_in_frame`` stays updated with uniforms
- `98b0d26 <https://github.com/3b1b/manim/pull/1691/commits/98b0d266d2475926a606331923cca3dc1dea97ad>`__: Made sure ``skip_animations`` and ``start_at_animation_number`` play well together
- `f8e6e7d <https://github.com/3b1b/manim/pull/1691/commits/f8e6e7df3ceb6f3d845ced4b690a85b35e0b8d00>`__: Updated progress display for full scene render
- `8f1dfab <https://github.com/3b1b/manim/pull/1691/commits/8f1dfabff04a8456f5c4df75b0f97d50b2755003>`__: ``VectorizedPoint`` should call ``__init__`` for both super classes
- `758f329 <https://github.com/3b1b/manim/pull/1691/commits/758f329a06a0c198b27a48c577575d94554305bf>`__: Used array copy when checking need for refreshing triangulation
- Refactored support for command ``A`` in path of SVG (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
- Refactored ``SingleStringTex.balance_braces`` (`#1662 <https://github.com/3b1b/manim/pull/1662>`__)
- Slight tweaks to how saturation_factor works on newton-fractal (`8b454fb <https://github.com/3b1b/manim/pull/1688/commits/8b454fbe9335a7011e947093230b07a74ba9c653>`__)
- Made it possible to set full screen preview as a default (`317a5d6 <https://github.com/3b1b/manim/pull/1688/commits/317a5d6226475b6b54a78db7116c373ef84ea923>`__)
- Used ``quick_point_from_proportion`` for graph points (`e764da3 <https://github.com/3b1b/manim/pull/1688/commits/e764da3c3adc5ae2a4ce877b340d2b6abcddc2fc>`__)
- Made sure ``Line.set_length`` returns self (`d2182b9 <https://github.com/3b1b/manim/pull/1688/commits/d2182b9112300558b6c074cefd685f97c10b3898>`__)
- Better align ``SurfaceMesh`` to the corresponding surface polygons (`eea3c6b <https://github.com/3b1b/manim/pull/1688/commits/eea3c6b29438f9e9325329c4355e76b9f635e97a>`__)
- Match ``fix_in_frame`` status for ``FlashAround`` mobject (`ee1594a <https://github.com/3b1b/manim/pull/1688/commits/ee1594a3cb7a79b8fc361e4c4397a88c7d20c7e3>`__)
- Made sure ``Mobject.is_fixed_in_frame`` stays updated with uniforms (`ba23fbe <https://github.com/3b1b/manim/pull/1688/commits/ba23fbe71e4a038201cd7df1d200514ed1c13bc2>`__)
- Made sure ``skip_animations`` and ``start_at_animation_number`` play well together (`98b0d26 <https://github.com/3b1b/manim/pull/1691/commits/98b0d266d2475926a606331923cca3dc1dea97ad>`__)
- Updated progress display for full scene render (`f8e6e7d <https://github.com/3b1b/manim/pull/1691/commits/f8e6e7df3ceb6f3d845ced4b690a85b35e0b8d00>`__)
- ``VectorizedPoint`` should call ``__init__`` for both super classes (`8f1dfab <https://github.com/3b1b/manim/pull/1691/commits/8f1dfabff04a8456f5c4df75b0f97d50b2755003>`__)
- Used array copy when checking need for refreshing triangulation (`758f329 <https://github.com/3b1b/manim/pull/1691/commits/758f329a06a0c198b27a48c577575d94554305bf>`__)
Dependencies
^^^^^^^^^^^^
- `#1675 <https://github.com/3b1b/manim/pull/1675>`__: Added dependency on python packages `skia-pathops <https://github.com/fonttools/skia-pathops>`__
- Added dependency on python package `skia-pathops <https://github.com/fonttools/skia-pathops>`__ (`#1675 <https://github.com/3b1b/manim/pull/1675>`__)
v1.2.0
------
@@ -71,57 +306,57 @@ v1.2.0
Fixed bugs
^^^^^^^^^^
- `#1592 <https://github.com/3b1b/manim/pull/1592>`__: Fixed ``put_start_and_end_on`` in 3D
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Fixed ``DecimalNumber``'s scaling issue
- `56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__: Fixed bug with common range array used for all coordinate systems
- `8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__: Fixed ``CoordinateSystem`` init bug
- `0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__: Fixed bug for single-valued ``ValueTracker``
- `54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__: Fixed bug with SVG rectangles
- `d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__: Fixed ``DotCloud.set_radii``
- `b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__: Temporarily fixed bug for ``PMobject`` array resizing
- `5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__: Fixed ``match_style``
- `719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__: Fixed negative ``path_arc`` case
- `c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__: Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis``
- `7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__: Fixed ``ComplexPlane`` -i display bug
- Fixed ``put_start_and_end_on`` in 3D (`#1592 <https://github.com/3b1b/manim/pull/1592>`__)
- Fixed ``DecimalNumber``'s scaling issue (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
- Fixed bug with common range array used for all coordinate systems (`56df154 <https://github.com/3b1b/manim/commit/56df15453f3e3837ed731581e52a1d76d5692077>`__)
- Fixed ``CoordinateSystem`` init bug (`8645894 <https://github.com/3b1b/manim/commit/86458942550c639a241267d04d57d0e909fcf252>`__)
- Fixed bug for single-valued ``ValueTracker`` (`0dc096b <https://github.com/3b1b/manim/commit/0dc096bf576ea900b351e6f4a80c13a77676f89b>`__)
- Fixed bug with SVG rectangles (`54ad355 <https://github.com/3b1b/manim/commit/54ad3550ef0c0e2fda46b26700a43fa8cde0973f>`__)
- Fixed ``DotCloud.set_radii`` (`d45ea28 <https://github.com/3b1b/manim/commit/d45ea28dc1d92ab9c639a047c00c151382eb0131>`__)
- Temporarily fixed bug for ``PMobject`` array resizing (`b543cc0 <https://github.com/3b1b/manim/commit/b543cc0e32d45399ee81638b6d4fb631437664cd>`__)
- Fixed ``match_style`` (`5f878a2 <https://github.com/3b1b/manim/commit/5f878a2c1aa531b7682bd048468c72d2835c7fe5>`__)
- Fixed negative ``path_arc`` case (`719c81d <https://github.com/3b1b/manim/commit/719c81d72b00dcf49f148d7c146774b22e0fe348>`__)
- Fixed bug with ``CoordinateSystem.get_lines_parallel_to_axis`` (`c726eb7 <https://github.com/3b1b/manim/commit/c726eb7a180b669ee81a18555112de26a8aff6d6>`__)
- Fixed ``ComplexPlane`` -i display bug (`7732d2f <https://github.com/3b1b/manim/commit/7732d2f0ee10449c5731499396d4911c03e89648>`__)
New Features
New features
^^^^^^^^^^^^
- `#1598 <https://github.com/3b1b/manim/pull/1598>`__: Supported the elliptical arc command ``A`` for ``SVGMobject``
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Added ``FlashyFadeIn``
- `#1607 <https://github.com/3b1b/manim/pull/1607>`__: Save triangulation
- `#1625 <https://github.com/3b1b/manim/pull/1625>`__: Added new ``Code`` mobject
- `#1637 <https://github.com/3b1b/manim/pull/1637>`__: Add warnings and use rich to display log
- `bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__: Added ``VCube``
- `6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__: Supported ``ValueTracker`` to track vectors
- `3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__: Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject``
- `a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__: Added ``TracgTail``
- `acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__: Added ``Scene.point_to_mobject``
- `f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__: Added poly_fractal shader
- `b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__: Added kwargs to ``TipableVMobject.set_length``
- `17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__: Added ``Mobject.replicate``
- `33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__: Added mandelbrot_fractal shader
- `f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__: Saved state before each embed
- `e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__: Allowed releasing of Textures
- `14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__: Consolidated and renamed newton_fractal shader
- `6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__: Hade ``ImageMoject`` remember the filepath to the Image
- Supported the elliptical arc command ``A`` for ``SVGMobject`` (`#1598 <https://github.com/3b1b/manim/pull/1598>`__)
- Added ``FlashyFadeIn`` (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
- Save triangulation (`#1607 <https://github.com/3b1b/manim/pull/1607>`__)
- Added new ``Code`` mobject (`#1625 <https://github.com/3b1b/manim/pull/1625>`__)
- Add warnings and use rich to display log (`#1637 <https://github.com/3b1b/manim/pull/1637>`__)
- Added ``VCube`` (`bd356da <https://github.com/3b1b/manim/commit/bd356daa99bfe3134fcb192a5f72e0d76d853801>`__)
- Supported ``ValueTracker`` to track vectors (`6d72893 <https://github.com/3b1b/manim/commit/6d7289338234acc6658b9377c0f0084aa1fa7119>`__)
- Added ``set_max_width``, ``set_max_height``, ``set_max_depth`` to ``Mobject`` (`3bb8f3f <https://github.com/3b1b/manim/commit/3bb8f3f0422a5dfba0da6ef122dc0c01f31aff03>`__)
- Added ``TracgTail`` (`a35dd5a <https://github.com/3b1b/manim/commit/a35dd5a3cbdeffa3891d5aa5f80287c18dba2f7f>`__)
- Added ``Scene.point_to_mobject`` (`acba13f <https://github.com/3b1b/manim/commit/acba13f4991b78d54c0bf93cce7ca3b351c25476>`__)
- Added poly_fractal shader (`f84b8a6 <https://github.com/3b1b/manim/commit/f84b8a66fe9e8b3872e5c716c5c240c14bb555ee>`__)
- Added kwargs to ``TipableVMobject.set_length`` (`b24ba19 <https://github.com/3b1b/manim/commit/b24ba19dec48ba4e38acbde8eec6d3a308b6ab83>`__)
- Added ``Mobject.replicate`` (`17c2772 <https://github.com/3b1b/manim/commit/17c2772b84abf6392a4170030e36e981de4737d0>`__)
- Added mandelbrot_fractal shader (`33fa76d <https://github.com/3b1b/manim/commit/33fa76dfac36e70bb5fad69dc6a336800c6dacce>`__)
- Saved state before each embed (`f22a341 <https://github.com/3b1b/manim/commit/f22a341e8411eae9331d4dd976b5e15bc6db08d9>`__)
- Allowed releasing of Textures (`e10a752 <https://github.com/3b1b/manim/commit/e10a752c0001e8981038faa03be4de2603d3565f>`__)
- Consolidated and renamed newton_fractal shader (`14fbed7 <https://github.com/3b1b/manim/commit/14fbed76da4b493191136caebb8a955e2d41265b>`__)
- Hade ``ImageMoject`` remember the filepath to the Image (`6cdbe0d <https://github.com/3b1b/manim/commit/6cdbe0d67a11ab14a6d84840a114ae6d3af10168>`__)
Refactor
^^^^^^^^
- `#1601 <https://github.com/3b1b/manim/pull/1601>`__: Changed back to simpler ``Mobject.scale`` implementation
- `b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__: Simplified ``Square``
- `40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__: Removed unused parameter ``triangulation_locked``
- `8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__: Reimplemented ``Arrow``
- `d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__: Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default
- `7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__: Refactored to call ``_handle_scale_side_effects`` after scaling takes place
- `7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__: Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end``
- `0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__: Made sure framerate is 30 for previewed scenes
- `c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__: Pushed ``pixel_coords_to_space_coords`` to ``Window``
- `d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__: Refactored to pass tuples and not arrays to uniforms
- `9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__: Refactored to copy uniform arrays in ``Mobject.copy``
- `ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__: Added ``bounding_box`` as exceptional key to point_cloud mobject
- `329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__: Made sure stroke width is always a float
- Changed back to simpler ``Mobject.scale`` implementation (`#1601 <https://github.com/3b1b/manim/pull/1601>`__)
- Simplified ``Square`` (`b667db2 <https://github.com/3b1b/manim/commit/b667db2d311a11cbbca2a6ff511d2c3cf1675486>`__)
- Removed unused parameter ``triangulation_locked`` (`40290ad <https://github.com/3b1b/manim/commit/40290ada8343f10901fa9151cbdf84689667786d>`__)
- Reimplemented ``Arrow`` (`8647a64 <https://github.com/3b1b/manim/commit/8647a6429dd0c52cba14e971b8c09194a93cfd87>`__)
- Used ``make_approximately_smooth`` for ``set_points_smoothly`` by default (`d8378d8 <https://github.com/3b1b/manim/commit/d8378d8157040cd797cc47ef9576beffd8607863>`__)
- Refactored to call ``_handle_scale_side_effects`` after scaling takes place (`7b4199c <https://github.com/3b1b/manim/commit/7b4199c674e291f1b84678828b63b6bd4fcc6b17>`__)
- Refactored to only call ``throw_error_if_no_points`` once for ``get_start_and_end`` (`7356a36 <https://github.com/3b1b/manim/commit/7356a36fa70a8279b43ae74e247cbd43b2bfd411>`__)
- Made sure framerate is 30 for previewed scenes (`0787c4f <https://github.com/3b1b/manim/commit/0787c4f36270a6560b50ce3e07b30b0ec5f2ba3e>`__)
- Pushed ``pixel_coords_to_space_coords`` to ``Window`` (`c635f19 <https://github.com/3b1b/manim/commit/c635f19f2a33e916509e53ded46f55e2afa8f5f2>`__)
- Refactored to pass tuples and not arrays to uniforms (`d5a88d0 <https://github.com/3b1b/manim/commit/d5a88d0fa457cfcf4cb9db417a098c37c95c7051>`__)
- Refactored to copy uniform arrays in ``Mobject.copy`` (`9483f26 <https://github.com/3b1b/manim/commit/9483f26a3b056de0e34f27acabd1a946f1adbdf9>`__)
- Added ``bounding_box`` as exceptional key to point_cloud mobject (`ed1fc4d <https://github.com/3b1b/manim/commit/ed1fc4d5f94467d602a568466281ca2d0368b506>`__)
- Made sure stroke width is always a float (`329d2c6 <https://github.com/3b1b/manim/commit/329d2c6eaec3d88bfb754b555575a3ea7c97a7e0>`__)
v1.1.0
@@ -147,7 +382,7 @@ Fixed bugs
- Rewrote ``earclip_triangulation`` to fix triangulation
- Allowed sound_file_name to be taken in without extensions
New Features
New features
^^^^^^^^^^^^
- Added :class:`~manimlib.animation.indication.VShowPassingFlash`

View File

@@ -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_FRAME_RATE = 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,30 +74,23 @@ Mathematical constant
PI = np.pi
TAU = 2 * PI
DEGREES = TAU / 360
DEG = TAU / 360
Text
----
.. code-block:: python
START_X = 30
START_Y = 20
NORMAL = "NORMAL"
ITALIC = "ITALIC"
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,79 +70,107 @@ 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.
``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
``text``
-------
- ``font``
Default font of Text
- ``text_alignment``
Default text alignment for LaTeX
``tex``
-------
- ``executable``
The executable program used to compile LaTeX (``latex`` or ``xelatex -no-pdf``
is recommended)
- ``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.
- ``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)
``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.
``style``
---------
- ``font``
Default font of Text
``ignore_manimlib_modules_on_reload``
-------------------------------------
- ``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)
``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_qualities``
--------------------
Export quality
- ``low``
Low quality (default is 480p15)
- ``medium``
Medium quality (default is 720p30)
- ``high``
High quality (default is 1080p30)
- ``ultra_high``
Ultra high quality (default is 4K60)
- ``default_quality``
Default quality (one of the above four)
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

@@ -1,85 +0,0 @@
CONFIG dictionary
=================
What's CONFIG
-------------
``CONFIG`` dictionary is a feature of manim, which facilitates the inheritance
and modification of parameters between parent and child classes.
| ``CONFIG`` dictionary 's processing is in ``manimlib/utils/config_ops.py``
| It can convert the key-value pairs in the ``CONFIG`` dictionary into class attributes and values
Generally, the first line of the ``.__init__()`` method in some basic class (``Mobject``, ``Animation``,
etc.) will call this function ``digest_config(self, kwargs)`` to convert both
the ``CONFIG`` dictionary and ``kwargs`` into attributes. Then it can be accessed
directly through ``self.``, which simplifies the handling of inheritance between classes.
**An example**:
There are many class inheritance relationships in ``manimlib/mobject/geometry.py``
.. code-block:: python
# Line 279
class Circle(Arc):
CONFIG = {
"color": RED,
"close_new_points": True,
"anchors_span_full_range": False
}
.. code-block:: python
# Line 304
class Dot(Circle):
CONFIG = {
"radius": DEFAULT_DOT_RADIUS,
"stroke_width": 0,
"fill_opacity": 1.0,
"color": WHITE
}
The ``Circle`` class uses the key-value pair ``"color": RED`` in the ``CONFIG``
dictionary to add the attribute ``self.color``.
At the same time, the ``Dot`` class also contains the key ``color`` in the
``CONFIG`` dictionary, but the value is different. At this time, the priority will
modify the attribute ``self.color`` to ``WHITE``.
CONFIG nesting
--------------
The ``CONFIG`` dictionary supports nesting, that is, the value of the key is also
a dictionary, for example:
.. code-block:: python
class Camera(object):
CONFIG = {
# configs
}
.. code-block:: python
class Scene(object):
CONFIG = {
"window_config": {},
"camera_class": Camera,
"camera_config": {},
"file_writer_config": {},
# other configs
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
# some lines
self.camera = self.camera_class(**self.camera_config)
The ``CONFIG`` dictionary of the ``Camera`` class contains many key-value pairs,
and this class needs to be instantiated in the ``Scene`` class. For more convenient
control, there is a special key-value pair in the Scene class ``"camera_config": {}``,
Its value is a dictionary, passed in as ``kwargs`` when initializing the ``Camera`` class
to modify the value of the properties of the ``Camera`` class.
So the nesting of the ``CONFIG`` dictionary **essentially** passes in the value as ``kwargs``.

View File

@@ -32,10 +32,11 @@ Some useful flags
All supported flags
^^^^^^^^^^^^^^^^^^^
========================================================== ====== =================================================================================================================================================================================================
========================================================== ====== =====================================================================================================================================================================================================
flag abbr function
========================================================== ====== =================================================================================================================================================================================================
========================================================== ====== =====================================================================================================================================================================================================
``--help`` ``-h`` Show the help message and exit
``--version`` ``-v`` Display the version of manimgl
``--write_file`` ``-w`` Render the scene as a movie file
``--skip_animations`` ``-s`` Skip to the last frame
``--low_quality`` ``-l`` Render at a low quality (for faster rendering)
@@ -43,8 +44,9 @@ flag abbr function
``--hd`` Render at a 1080p quality
``--uhd`` Render at a 4k quality
``--full_screen`` ``-f`` Show window in full screen
``--presenter_mode`` ``-p`` Scene will stay paused during wait calls until space bar or right arrow is hit, like a slide show
``--save_pngs`` ``-g`` Save each frame as a png
``--save_as_gif`` ``-i`` Save the video as gif
``--gif`` ``-i`` Save the video as gif
``--transparent`` ``-t`` Render to a movie file with an alpha channel
``--quiet`` ``-q``
``--write_all`` ``-a`` Write all the scenes from a file
@@ -53,14 +55,16 @@ flag abbr function
``--config`` Guide for automatic configuration
``--file_name FILE_NAME`` Name for the movie or image file
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value.
``--embed LINENO`` ``-e`` Takes a line number as an argument, and results in the scene being called as if the line ``self.embed()`` was inserted into the scene code at that line number
``--embed [EMBED]`` ``-e`` 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.
``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080"
``--frame_rate FRAME_RATE`` Frame rate, as an integer
``--fps FPS`` Frame rate, as an integer
``--color COLOR`` ``-c`` Background color
``--leave_progress_bars`` Leave progress bars displayed in terminal
``--video_dir VIDEO_DIR`` directory to write video
``--video_dir VIDEO_DIR`` Directory to write video
``--config_file CONFIG_FILE`` Path to the custom configuration file
========================================================== ====== =================================================================================================================================================================================================
``--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 different files
========================================================== ====== =====================================================================================================================================================================================================
custom_config
--------------

View File

@@ -23,7 +23,7 @@ InteractiveDevlopment
self.play(ShowCreation(square))
self.wait()
# This opens an iPython termnial where you can keep writing
# This opens an iPython terminal where you can keep writing
# lines as if they were part of this construct method.
# In particular, 'square', 'circle' and 'self' will all be
# part of the local namespace in that terminal.
@@ -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("""
@@ -70,7 +70,7 @@ AnimatingMethods
class AnimatingMethods(Scene):
def construct(self):
grid = Tex(r"\pi").get_grid(10, 10, height=4)
grid = OldTex(r"\pi").get_grid(10, 10, height=4)
self.add(grid)
# You can animate the application of mobject methods with the
@@ -192,16 +192,16 @@ TexTransformExample
# each of these strings. For example, the Tex mobject
# below will have 5 subjects, corresponding to the
# expressions [A^2, +, B^2, =, C^2]
Tex("A^2", "+", "B^2", "=", "C^2"),
OldTex("A^2", "+", "B^2", "=", "C^2"),
# Likewise here
Tex("A^2", "=", "C^2", "-", "B^2"),
OldTex("A^2", "=", "C^2", "-", "B^2"),
# Alternatively, you can pass in the keyword argument
# "isolate" with a list of strings that should be out as
# their own submobject. So the line below is equivalent
# to the commented out line below it.
Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
# Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
OldTex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
# OldTex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
OldTex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
)
lines.arrange(DOWN, buff=LARGE_BUFF)
for line in lines:
@@ -221,7 +221,7 @@ TexTransformExample
self.play(
TransformMatchingTex(
lines[0].copy(), lines[1],
path_arc=90 * DEGREES,
path_arc=90 * DEG,
),
**play_kw
)
@@ -260,7 +260,7 @@ TexTransformExample
# new_line2 and the "\sqrt" from the final line. By passing in,
# transform_mismatches=True, it will transform this "^2" part into
# the "\sqrt" part.
new_line2 = Tex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
new_line2 = OldTex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
new_line2.replace(lines[2])
new_line2.match_style(lines[2])
@@ -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

@@ -1,7 +1,7 @@
Installation
============
Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
Manim runs on Python 3.7 or higher.
System requirements are
@@ -11,6 +11,32 @@ System requirements are
- `Pango <https://pango.org>`__ (only for Linux)
Install FFmpeg
--------------
Install FFmpeg Windows
------------------------
.. code-block:: cmd
choco install ffmpeg
# Install FFmepeg Linux
----------------------------
.. code-block:: sh
$ sudo apt update
$ sudo apt install ffmpeg
$ ffmpeg -version
# Install FFmpeg MacOS
----------------------------
- Download This ZIP file `https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z`(if the link is not working download this zip file from there original website)
Directly
--------

View File

@@ -96,14 +96,13 @@ Below is the directory structure of manim:
└── utils/ # Some useful utility functions
├── bezier.py # For bezier curve
├── color.py # For color
├── config_ops.py # Process CONFIG
├── dict_ops.py # Functions related to dictionary processing
├── customization.py # Read from custom_config.yml
├── debug.py # Utilities for debugging in program
├── directories.py # Read directories from config file
├── 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

@@ -26,12 +26,12 @@ class OpeningManimExample(Scene):
matrix = [[1, 1], [0, 1]]
linear_transform_words = VGroup(
Text("This is what the matrix"),
IntegerMatrix(matrix, include_background_rectangle=True),
IntegerMatrix(matrix),
Text("looks like")
)
linear_transform_words.arrange(RIGHT)
linear_transform_words.to_edge(UP)
linear_transform_words.set_stroke(BLACK, 10, background=True)
linear_transform_words.set_backstroke(width=5)
self.play(
ShowCreation(grid),
@@ -52,7 +52,7 @@ class OpeningManimExample(Scene):
this is the map $z \\rightarrow z^2$
""")
complex_map_words.to_corner(UR)
complex_map_words.set_stroke(BLACK, 5, background=True)
complex_map_words.set_backstroke(width=5)
self.play(
FadeOut(grid),
@@ -70,17 +70,13 @@ class OpeningManimExample(Scene):
class AnimatingMethods(Scene):
def construct(self):
grid = Tex(r"\pi").get_grid(10, 10, height=4)
grid = Tex(R"\pi").get_grid(10, 10, height=4)
self.add(grid)
# You can animate the application of mobject methods with the
# ".animate" syntax:
self.play(grid.animate.shift(LEFT))
# Alternatively, you can use the older syntax by passing the
# method and then the arguments to the scene's "play" function:
self.play(grid.shift, LEFT)
# Both of those will interpolate between the mobject's initial
# state and whatever happens when you apply that method.
# For this example, calling grid.shift(LEFT) would shift the
@@ -159,140 +155,145 @@ class TextExample(Scene):
class TexTransformExample(Scene):
def construct(self):
to_isolate = ["B", "C", "=", "(", ")"]
# Tex to color map
t2c = {
"A": BLUE,
"B": TEAL,
"C": GREEN,
}
# Configuration to pass along to each Tex mobject
kw = dict(font_size=72, t2c=t2c)
lines = VGroup(
# Passing in muliple arguments to Tex will result
# in the same expression as if those arguments had
# been joined together, except that the submobject
# hierarchy of the resulting mobject ensure that the
# Tex mobject has a subject corresponding to
# each of these strings. For example, the Tex mobject
# below will have 5 subjects, corresponding to the
# expressions [A^2, +, B^2, =, C^2]
Tex("A^2", "+", "B^2", "=", "C^2"),
# Likewise here
Tex("A^2", "=", "C^2", "-", "B^2"),
# Alternatively, you can pass in the keyword argument
# "isolate" with a list of strings that should be out as
# their own submobject. So the line below is equivalent
# to the commented out line below it.
Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
# Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
Tex("A^2 + B^2 = C^2", **kw),
Tex("A^2 = C^2 - B^2", **kw),
Tex("A^2 = (C + B)(C - B)", **kw),
Tex(R"A = \sqrt{(C + B)(C - B)}", **kw),
)
lines.arrange(DOWN, buff=LARGE_BUFF)
for line in lines:
line.set_color_by_tex_to_color_map({
"A": BLUE,
"B": TEAL,
"C": GREEN,
})
play_kw = {"run_time": 2}
self.add(lines[0])
# The animation TransformMatchingTex will line up parts
# of the source and target which have matching tex strings.
# Here, giving it a little path_arc makes each part sort of
# rotate into their final positions, which feels appropriate
# for the idea of rearranging an equation
# The animation TransformMatchingStrings will line up parts
# of the source and target which have matching substring strings.
# Here, giving it a little path_arc makes each part rotate into
# their final positions, which feels appropriate for the idea of
# rearranging an equation
self.play(
TransformMatchingTex(
TransformMatchingStrings(
lines[0].copy(), lines[1],
path_arc=90 * DEGREES,
# matched_keys specifies which substring should
# line up. If it's not specified, the animation
# will align the longest matching substrings.
# In this case, the substring "^2 = C^2" would
# trip it up
matched_keys=["A^2", "B^2", "C^2"],
# When you want a substring from the source
# to go to a non-equal substring from the target,
# use the key map.
key_map={"+": "-"},
path_arc=90 * DEG,
),
**play_kw
)
self.wait()
# Now, we could try this again on the next line...
self.play(
TransformMatchingTex(lines[1].copy(), lines[2]),
**play_kw
)
self.play(TransformMatchingStrings(
lines[1].copy(), lines[2],
matched_keys=["A^2"]
))
self.wait()
# ...and this looks nice enough, but since there's no tex
# in lines[2] which matches "C^2" or "B^2", those terms fade
# out to nothing while the C and B terms fade in from nothing.
# If, however, we want the C^2 to go to C, and B^2 to go to B,
# we can specify that with a key map.
self.play(FadeOut(lines[2]))
self.play(
TransformMatchingTex(
lines[1].copy(), lines[2],
key_map={
"C^2": "C",
"B^2": "B",
}
TransformMatchingStrings(
lines[2].copy(), lines[3],
key_map={"2": R"\sqrt"},
path_arc=-30 * DEG,
),
**play_kw
)
self.wait()
self.wait(2)
self.play(LaggedStartMap(FadeOut, lines, shift=2 * RIGHT))
# And to finish off, a simple TransformMatchingShapes would work
# just fine. But perhaps we want that exponent on A^2 to transform into
# the square root symbol. At the moment, lines[2] treats the expression
# A^2 as a unit, so we might create a new version of the same line which
# separates out just the A. This way, when TransformMatchingTex lines up
# all matching parts, the only mismatch will be between the "^2" from
# new_line2 and the "\sqrt" from the final line. By passing in,
# transform_mismatches=True, it will transform this "^2" part into
# the "\sqrt" part.
new_line2 = Tex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
new_line2.replace(lines[2])
new_line2.match_style(lines[2])
self.play(
TransformMatchingTex(
new_line2, lines[3],
transform_mismatches=True,
),
**play_kw
)
self.wait(3)
self.play(FadeOut(lines, RIGHT))
# Alternatively, if you don't want to think about breaking up
# the tex strings deliberately, you can TransformMatchingShapes,
# which will try to line up all pieces of a source mobject with
# those of a target, regardless of the submobject hierarchy in
# each one, according to whether those pieces have the same
# shape (as best it can).
# TransformMatchingShapes will try to line up all pieces of a
# source mobject with those of a target, regardless of the
# what Mobject type they are.
source = Text("the morse code", height=1)
target = Text("here come dots", height=1)
saved_source = source.copy()
self.play(Write(source))
self.wait()
kw = {"run_time": 3, "path_arc": PI / 2}
kw = dict(run_time=3, path_arc=PI / 2)
self.play(TransformMatchingShapes(source, target, **kw))
self.wait()
self.play(TransformMatchingShapes(target, source, **kw))
self.play(TransformMatchingShapes(target, saved_source, **kw))
self.wait()
class TexIndexing(Scene):
def construct(self):
# You can index into Tex mobject (or other StringMobjects) by substrings
equation = Tex(R"e^{\pi i} = -1", font_size=144)
self.add(equation)
self.play(FlashAround(equation["e"]))
self.wait()
self.play(Indicate(equation[R"\pi"]))
self.wait()
self.play(TransformFromCopy(
equation[R"e^{\pi i}"].copy().set_opacity(0.5),
equation["-1"],
path_arc=-PI / 2,
run_time=3
))
self.play(FadeOut(equation))
# Or regular expressions
equation = Tex("A^2 + B^2 = C^2", font_size=144)
self.play(Write(equation))
for part in equation[re.compile(r"\w\^2")]:
self.play(FlashAround(part))
self.wait()
self.play(FadeOut(equation))
# Indexing by substrings like this may not work when
# the order in which Latex draws symbols does not match
# the order in which they show up in the string.
# For example, here the infinity is drawn before the sigma
# so we don't get the desired behavior.
equation = Tex(R"\sum_{n = 1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}", font_size=72)
self.play(FadeIn(equation))
self.play(equation[R"\infty"].animate.set_color(RED)) # Doesn't hit the infinity
self.wait()
self.play(FadeOut(equation))
# However you can always fix this by explicitly passing in
# a string you might want to isolate later. Also, using
# \over instead of \frac helps to avoid the issue for fractions
equation = Tex(
R"\sum_{n = 1}^\infty {1 \over n^2} = {\pi^2 \over 6}",
# Explicitly mark "\infty" as a substring you might want to access
isolate=[R"\infty"],
font_size=72
)
self.play(FadeIn(equation))
self.play(equation[R"\infty"].animate.set_color(RED)) # Got it!
self.wait()
self.play(FadeOut(equation))
class UpdatersExample(Scene):
def construct(self):
square = Square()
square.set_fill(BLUE_E, 1)
# On all all frames, the constructor Brace(square, UP) will
# On all frames, the constructor Brace(square, UP) will
# be called, and the mobject brace will set its data to match
# that of the newly constructed object
brace = always_redraw(Brace, square, UP)
text, number = label = VGroup(
Text("Width = "),
DecimalNumber(
0,
show_ellipsis=True,
num_decimal_places=2,
include_sign=True,
)
)
label.arrange(RIGHT)
label = TexText("Width = 0.00")
number = label.make_number_changeable("0.00")
# This ensures that the method deicmal.next_to(square)
# is called on every frame
always(label.next_to, brace, UP)
label.always.next_to(brace, UP)
# You could also write the following equivalent line
# label.add_updater(lambda m: m.next_to(brace, UP))
@@ -301,7 +302,7 @@ class UpdatersExample(Scene):
# should be functions returning arguments to that method.
# The following line ensures thst decimal.set_value(square.get_y())
# is called every frame
f_always(number.set_value, square.get_width)
number.f_always.set_value(square.get_width)
# You could also write the following equivalent line
# number.add_updater(lambda m: m.set_value(square.get_width()))
@@ -350,15 +351,16 @@ class CoordinateSystemExample(Scene):
width=10,
# Axes is made of two NumberLine mobjects. You can specify
# their configuration with axis_config
axis_config={
"stroke_color": GREY_A,
"stroke_width": 2,
},
axis_config=dict(
stroke_color=GREY_A,
stroke_width=2,
numbers_to_exclude=[0],
),
# Alternatively, you can specify configuration for just one
# of them, like this.
y_axis_config={
"include_tip": False,
}
y_axis_config=dict(
big_tick_numbers=[-2, 2],
)
)
# Keyword arguments of add_coordinate_labels can be used to
# configure the DecimalNumber mobjects which it creates and
@@ -417,7 +419,7 @@ class CoordinateSystemExample(Scene):
class GraphExample(Scene):
def construct(self):
axes = Axes((-3, 10), (-1, 8))
axes = Axes((-3, 10), (-1, 8), height=6)
axes.add_coordinate_labels()
self.play(Write(axes, lag_ratio=0.01, run_time=1))
@@ -486,21 +488,82 @@ class GraphExample(Scene):
# with the intent of having other mobjects update based
# on the parameter
x_tracker = ValueTracker(2)
f_always(
dot.move_to,
lambda: axes.i2gp(x_tracker.get_value(), parabola)
)
dot.add_updater(lambda d: d.move_to(axes.i2gp(x_tracker.get_value(), parabola)))
self.play(x_tracker.animate.set_value(4), run_time=3)
self.play(x_tracker.animate.set_value(-2), run_time=3)
self.wait()
class SurfaceExample(Scene):
CONFIG = {
"camera_class": ThreeDCamera,
}
class TexAndNumbersExample(Scene):
def construct(self):
axes = Axes((-3, 3), (-3, 3), unit_size=1)
axes.to_edge(DOWN)
axes.add_coordinate_labels(font_size=16)
circle = Circle(radius=2)
circle.set_stroke(YELLOW, 3)
circle.move_to(axes.get_origin())
self.add(axes, circle)
# When numbers show up in tex, they can be readily
# replaced with DecimalMobjects so that methods like
# get_value and set_value can be called on them, and
# animations like ChangeDecimalToValue can be called
# on them.
tex = Tex("x^2 + y^2 = 4.00")
tex.next_to(axes, UP, buff=0.5)
value = tex.make_number_changeable("4.00")
# This will tie the right hand side of our equation to
# the square of the radius of the circle
value.add_updater(lambda v: v.set_value(circle.get_radius()**2))
self.add(tex)
text = Text("""
You can manipulate numbers
in Tex mobjects
""", font_size=30)
text.next_to(tex, RIGHT, buff=1.5)
arrow = Arrow(text, tex)
self.add(text, arrow)
self.play(
circle.animate.set_height(2.0),
run_time=4,
rate_func=there_and_back,
)
# By default, tex.make_number_changeable replaces the first occurance
# 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)
self.play(
LaggedStartMap(
FlashAround, exponents,
lag_ratio=0.2, buff=0.1, color=RED
),
exponents.animate.set_color(RED)
)
def func(x, y):
# Switch from manim coords to axes coords
xa, ya = axes.point_to_coords(np.array([x, y, 0]))
return xa**4 + ya**4 - 4
new_curve = ImplicitFunction(func)
new_curve.match_style(circle)
circle.rotate(angle_of_vector(new_curve.get_start())) # Align
value.clear_updaters()
self.play(
*(ChangeDecimalToValue(exp, 4) for exp in exponents),
ReplacementTransform(circle.copy(), new_curve),
circle.animate.set_stroke(width=1, opacity=0.5),
)
class SurfaceExample(ThreeDScene):
def construct(self):
surface_text = Text("For 3d scenes, try using surfaces")
surface_text.fix_in_frame()
@@ -532,13 +595,6 @@ class SurfaceExample(Scene):
mob.mesh = SurfaceMesh(mob)
mob.mesh.set_stroke(BLUE, 1, opacity=0.5)
# Set perspective
frame = self.camera.frame
frame.set_euler_angles(
theta=-30 * DEGREES,
phi=70 * DEGREES,
)
surface = surfaces[0]
self.play(
@@ -560,12 +616,12 @@ class SurfaceExample(Scene):
self.play(
Transform(surface, surfaces[2]),
# Move camera frame during the transition
frame.animate.increment_phi(-10 * DEGREES),
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
frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
self.frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
# Play around with where the light is
light_text = Text("You can move around the light source")
@@ -574,12 +630,14 @@ class SurfaceExample(Scene):
self.play(FadeTransform(surface_text, light_text))
light = self.camera.light_source
self.add(light)
light_dot = GlowDot(color=WHITE, radius=0.5)
light_dot.always.move_to(light)
self.add(light, light_dot)
light.save_state()
self.play(light.animate.move_to(3 * IN), run_time=5)
self.play(light.animate.shift(10 * OUT), run_time=5)
drag_text = Text("Try moving the mouse while pressing d or s")
drag_text = Text("Try moving the mouse while pressing d or f")
drag_text.move_to(light_text)
drag_text.fix_in_frame()
@@ -597,7 +655,7 @@ class InteractiveDevelopment(Scene):
self.play(ShowCreation(square))
self.wait()
# This opens an iPython termnial where you can keep writing
# This opens an iPython terminal where you can keep writing
# lines as if they were part of this construct method.
# In particular, 'square', 'circle' and 'self' will all be
# part of the local namespace in that terminal.
@@ -608,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("""
@@ -634,6 +692,8 @@ class InteractiveDevelopment(Scene):
class ControlsExample(Scene):
drag_to_pan = False
def setup(self):
self.textbox = Textbox()
self.checkbox = Checkbox()
@@ -650,7 +710,7 @@ class ControlsExample(Scene):
def text_updater(old_text):
assert(isinstance(old_text, Text))
new_text = Text(self.textbox.get_value(), size=old_text.size)
new_text = Text(self.textbox.get_value(), font_size=old_text.font_size)
# new_text.align_data_and_family(old_text)
new_text.move_to(old_text)
if self.checkbox.get_value():

View File

@@ -2,8 +2,15 @@ import pkg_resources
__version__ = pkg_resources.get_distribution("manimgl").version
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import *
from manimlib.constants import *
from manimlib.window import *
from manimlib.animation.animation import *
from manimlib.animation.composition import *
from manimlib.animation.creation import *
@@ -20,52 +27,52 @@ from manimlib.animation.update import *
from manimlib.camera.camera import *
from manimlib.window import *
from manimlib.mobject.boolean_ops import *
from manimlib.mobject.coordinate_systems import *
from manimlib.mobject.changing import *
from manimlib.mobject.coordinate_systems import *
from manimlib.mobject.frame import *
from manimlib.mobject.functions import *
from manimlib.mobject.geometry import *
from manimlib.mobject.interactive import *
from manimlib.mobject.matrix import *
from manimlib.mobject.mobject import *
from manimlib.mobject.mobject_update_utils import *
from manimlib.mobject.number_line import *
from manimlib.mobject.numbers import *
from manimlib.mobject.probability import *
from manimlib.mobject.shape_matchers import *
from manimlib.mobject.svg.brace import *
from manimlib.mobject.svg.drawings import *
from manimlib.mobject.svg.mtex_mobject import *
from manimlib.mobject.svg.string_mobject import *
from manimlib.mobject.svg.svg_mobject import *
from manimlib.mobject.svg.special_tex import *
from manimlib.mobject.svg.tex_mobject import *
from manimlib.mobject.svg.text_mobject import *
from manimlib.mobject.three_dimensions import *
from manimlib.mobject.types.dot_cloud import *
from manimlib.mobject.types.image_mobject import *
from manimlib.mobject.types.point_cloud_mobject import *
from manimlib.mobject.types.surface import *
from manimlib.mobject.types.vectorized_mobject import *
from manimlib.mobject.types.dot_cloud import *
from manimlib.mobject.mobject_update_utils import *
from manimlib.mobject.value_tracker import *
from manimlib.mobject.vector_field import *
from manimlib.scene.interactive_scene import *
from manimlib.scene.scene import *
from manimlib.scene.three_d_scene import *
from manimlib.utils.bezier import *
from manimlib.utils.cache import *
from manimlib.utils.color import *
from manimlib.utils.config_ops import *
from manimlib.utils.customization import *
from manimlib.utils.dict_ops import *
from manimlib.utils.debug import *
from manimlib.utils.directories import *
from manimlib.utils.file_ops import *
from manimlib.utils.images import *
from manimlib.utils.iterables import *
from manimlib.utils.file_ops import *
from manimlib.utils.paths import *
from manimlib.utils.rate_functions import *
from manimlib.utils.simple_functions import *
from manimlib.utils.shaders import *
from manimlib.utils.sounds import *
from manimlib.utils.space_ops import *
from manimlib.utils.strings import *
from manimlib.utils.tex import *

View File

@@ -1,28 +1,64 @@
#!/usr/bin/env python
import manimlib.config
import manimlib.logger
import manimlib.extract_scene
import manimlib.utils.init_config
from addict import Dict
from manimlib import __version__
from manimlib.config import manim_config
from manimlib.config import parse_cli
import manimlib.extract_scene
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

@@ -1,135 +1,173 @@
from __future__ import annotations
from copy import deepcopy
from manimlib.mobject.mobject import _AnimationBuilder
from manimlib.mobject.mobject import Mobject
from manimlib.utils.config_ops import digest_config
from manimlib.utils.iterables import remove_list_redundancies
from manimlib.utils.rate_functions import smooth
from manimlib.utils.simple_functions import clip
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
from manimlib.scene.scene import Scene
DEFAULT_ANIMATION_RUN_TIME = 1.0
DEFAULT_ANIMATION_LAG_RATIO = 0
class Animation(object):
CONFIG = {
"run_time": DEFAULT_ANIMATION_RUN_TIME,
"rate_func": smooth,
"name": None,
# Does this animation add or remove a mobject form the screen
"remover": False,
# What to enter into the update function upon completion
"final_alpha_value": 1,
# If 0, the animation is applied to all submobjects
# at the same time
def __init__(
self,
mobject: Mobject,
run_time: float = DEFAULT_ANIMATION_RUN_TIME,
# Tuple of times, between which the animation will run
time_span: tuple[float, float] | None = None,
# If 0, the animation is applied to all submobjects at the same time
# If 1, it is applied to each successively.
# If 0 < lag_ratio < 1, its applied to each
# with lagged start times
"lag_ratio": DEFAULT_ANIMATION_LAG_RATIO,
"suspend_mobject_updating": True,
}
def __init__(self, mobject, **kwargs):
assert(isinstance(mobject, Mobject))
digest_config(self, kwargs)
# If 0 < lag_ratio < 1, its applied to each with lagged start times
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
remover: bool = False,
# What to enter into the update function upon completion
final_alpha_value: float = 1.0,
# If set to True, the mobject itself will have its internal updaters called,
# but the start or target mobjects would not be suspended. To completely suspend
# updating, call mobject.suspend_updating() before the animation
suspend_mobject_updating: bool = False,
):
self.mobject = mobject
self.run_time = run_time
self.time_span = time_span
self.rate_func = rate_func
self.name = name or self.__class__.__name__ + str(self.mobject)
self.remover = remover
self.final_alpha_value = final_alpha_value
self.lag_ratio = lag_ratio
self.suspend_mobject_updating = suspend_mobject_updating
def __str__(self):
if self.name:
return self.name
return self.__class__.__name__ + str(self.mobject)
assert isinstance(mobject, Mobject)
def begin(self):
def __str__(self) -> str:
return self.name
def begin(self) -> None:
# This is called right as an animation is being
# played. As much initialization as possible,
# especially any mobject copying, should live in
# this method
if self.time_span is not None:
start, end = self.time_span
self.run_time = max(end, self.run_time)
self.mobject.set_animating_status(True)
self.starting_mobject = self.create_starting_mobject()
if self.suspend_mobject_updating:
# All calls to self.mobject's internal updaters
# during the animation, either from this Animation
# or from the surrounding scene, should do nothing.
# It is, however, okay and desirable to call
# the internal updaters of self.starting_mobject,
# or any others among self.get_all_mobjects()
self.mobject_was_updating = not self.mobject.updating_suspended
self.mobject.suspend_updating()
self.families = list(self.get_all_families_zipped())
self.interpolate(0)
def finish(self):
def finish(self) -> None:
self.interpolate(self.final_alpha_value)
if self.suspend_mobject_updating:
self.mobject.set_animating_status(False)
if self.suspend_mobject_updating and self.mobject_was_updating:
self.mobject.resume_updating()
def clean_up_from_scene(self, scene):
def clean_up_from_scene(self, scene: Scene) -> None:
if self.is_remover():
scene.remove(self.mobject)
def create_starting_mobject(self):
def create_starting_mobject(self) -> Mobject:
# Keep track of where the mobject starts
return self.mobject.copy()
def get_all_mobjects(self):
def get_all_mobjects(self) -> tuple[Mobject, Mobject]:
"""
Ordering must match the ording of arguments to interpolate_submobject
"""
return self.mobject, self.starting_mobject
def get_all_families_zipped(self):
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
return zip(*[
mob.get_family()
for mob in self.get_all_mobjects()
])
def update_mobjects(self, dt):
def update_mobjects(self, dt: float) -> None:
"""
Updates things like starting_mobject, and (for
Transforms) target_mobject. Note, since typically
(always?) self.mobject will have its updating
suspended during the animation, this will do
nothing to self.mobject.
Transforms) target_mobject.
"""
for mob in self.get_all_mobjects_to_update():
mob.update(dt)
def get_all_mobjects_to_update(self):
def get_all_mobjects_to_update(self) -> list[Mobject]:
# The surrounding scene typically handles
# updating of self.mobject. Besides, in
# most cases its updating is suspended anyway
return list(filter(
# updating of self.mobject.
items = list(filter(
lambda m: m is not self.mobject,
self.get_all_mobjects()
))
items = remove_list_redundancies(items)
return items
def copy(self):
return deepcopy(self)
def update_config(self, **kwargs):
digest_config(self, kwargs)
def update_rate_info(
self,
run_time: float | None = None,
rate_func: Callable[[float], float] | None = None,
lag_ratio: float | None = None,
):
self.run_time = run_time or self.run_time
self.rate_func = rate_func or self.rate_func
self.lag_ratio = lag_ratio or self.lag_ratio
return self
# Methods for interpolation, the mean of an Animation
def interpolate(self, alpha):
alpha = clip(alpha, 0, 1)
self.interpolate_mobject(self.rate_func(alpha))
def interpolate(self, alpha: float) -> None:
self.interpolate_mobject(alpha)
def update(self, alpha):
def update(self, alpha: float) -> None:
"""
This method shouldn't exist, but it's here to
keep many old scenes from breaking
"""
self.interpolate(alpha)
def interpolate_mobject(self, alpha):
def time_spanned_alpha(self, alpha: float) -> float:
if self.time_span is not None:
start, end = self.time_span
return clip(alpha * self.run_time - start, 0, end - start) / (end - start)
return alpha
def interpolate_mobject(self, alpha: float) -> None:
for i, mobs in enumerate(self.families):
sub_alpha = self.get_sub_alpha(alpha, i, len(self.families))
sub_alpha = self.get_sub_alpha(self.time_spanned_alpha(alpha), i, len(self.families))
self.interpolate_submobject(*mobs, sub_alpha)
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
def interpolate_submobject(
self,
submobject: Mobject,
starting_submobject: Mobject,
alpha: float
):
# Typically ipmlemented by subclass
pass
def get_sub_alpha(self, alpha, index, num_submobjects):
def get_sub_alpha(
self,
alpha: float,
index: int,
num_submobjects: int
) -> float:
# TODO, make this more understanable, and/or combine
# its functionality with AnimationGroup's method
# build_animations_with_timings
@@ -137,32 +175,35 @@ class Animation(object):
full_length = (num_submobjects - 1) * lag_ratio + 1
value = alpha * full_length
lower = index * lag_ratio
return clip((value - lower), 0, 1)
raw_sub_alpha = clip((value - lower), 0, 1)
return self.rate_func(raw_sub_alpha)
# Getters and setters
def set_run_time(self, run_time):
def set_run_time(self, run_time: float):
self.run_time = run_time
return self
def get_run_time(self):
def get_run_time(self) -> float:
if self.time_span:
return max(self.run_time, self.time_span[1])
return self.run_time
def set_rate_func(self, rate_func):
def set_rate_func(self, rate_func: Callable[[float], float]):
self.rate_func = rate_func
return self
def get_rate_func(self):
def get_rate_func(self) -> Callable[[float], float]:
return self.rate_func
def set_name(self, name):
def set_name(self, name: str):
self.name = name
return self
def is_remover(self):
def is_remover(self) -> bool:
return self.remover
def prepare_animation(anim):
def prepare_animation(anim: Animation | _AnimationBuilder):
if isinstance(anim, _AnimationBuilder):
return anim.build()

View File

@@ -1,74 +1,93 @@
import numpy as np
from __future__ import annotations
from manimlib.animation.animation import Animation, prepare_animation
from manimlib.animation.animation import Animation
from manimlib.animation.animation import prepare_animation
from manimlib.mobject.mobject import _AnimationBuilder
from manimlib.mobject.mobject import Group
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.bezier import interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.iterables import remove_list_redundancies
from manimlib.utils.rate_functions import linear
from manimlib.utils.simple_functions import clip
from typing import TYPE_CHECKING, Union, Iterable
AnimationType = Union[Animation, _AnimationBuilder]
if TYPE_CHECKING:
from typing import Callable, Optional
from manimlib.mobject.mobject import Mobject
from manimlib.scene.scene import Scene
DEFAULT_LAGGED_START_LAG_RATIO = 0.05
class AnimationGroup(Animation):
CONFIG = {
# If None, this defaults to the sum of all
# internal animations
"run_time": None,
"rate_func": linear,
# If 0, all animations are played at once.
# If 1, all are played successively.
# If >0 and <1, they start at lagged times
# from one and other.
"lag_ratio": 0,
"group": None,
}
def __init__(self, *animations, **kwargs):
digest_config(self, kwargs)
def __init__(
self,
*args: AnimationType | Iterable[AnimationType],
run_time: float = -1, # If negative, default to sum of inputed animation runtimes
lag_ratio: float = 0.0,
group: Optional[Mobject] = None,
group_type: Optional[type] = None,
**kwargs
):
animations = args[0] if isinstance(args[0], Iterable) else args
self.animations = [prepare_animation(anim) for anim in animations]
if self.group is None:
self.group = Group(*remove_list_redundancies(
[anim.mobject for anim in animations]
))
self.init_run_time()
Animation.__init__(self, self.group, **kwargs)
self.build_animations_with_timings(lag_ratio)
self.max_end_time = max((awt[2] for awt in self.anims_with_timings), default=0)
self.run_time = self.max_end_time if run_time < 0 else run_time
self.lag_ratio = lag_ratio
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:
self.group = group_type(*mobs)
elif all(isinstance(anim.mobject, VMobject) for anim in animations):
self.group = VGroup(*mobs)
else:
self.group = Group(*mobs)
def get_all_mobjects(self):
super().__init__(
self.group,
run_time=self.run_time,
lag_ratio=lag_ratio,
**kwargs
)
def get_all_mobjects(self) -> Mobject:
return self.group
def begin(self):
def begin(self) -> None:
self.group.set_animating_status(True)
for anim in self.animations:
anim.begin()
# self.init_run_time()
def finish(self):
def finish(self) -> None:
self.group.set_animating_status(False)
for anim in self.animations:
anim.finish()
def clean_up_from_scene(self, scene):
def clean_up_from_scene(self, scene: Scene) -> None:
for anim in self.animations:
anim.clean_up_from_scene(scene)
def update_mobjects(self, dt):
def update_mobjects(self, dt: float) -> None:
for anim in self.animations:
anim.update_mobjects(dt)
def init_run_time(self):
self.build_animations_with_timings()
if self.anims_with_timings:
self.max_end_time = np.max([
awt[2] for awt in self.anims_with_timings
])
else:
self.max_end_time = 0
if self.run_time is None:
def calculate_max_end_time(self) -> None:
self.max_end_time = max(
(awt[2] for awt in self.anims_with_timings),
default=0,
)
if self.run_time < 0:
self.run_time = self.max_end_time
def build_animations_with_timings(self):
def build_animations_with_timings(self, lag_ratio: float) -> None:
"""
Creates a list of triplets of the form
(anim, start_time, end_time)
@@ -81,13 +100,12 @@ class AnimationGroup(Animation):
self.anims_with_timings.append(
(anim, start_time, end_time)
)
# Start time of next animation is based on
# the lag_ratio
# Start time of next animation is based on the lag_ratio
curr_time = interpolate(
start_time, end_time, self.lag_ratio
start_time, end_time, lag_ratio
)
def interpolate(self, alpha):
def interpolate(self, alpha: float) -> None:
# Note, if the run_time of AnimationGroup has been
# set to something other than its default, these
# times might not correspond to actual times,
@@ -99,31 +117,31 @@ class AnimationGroup(Animation):
if anim_time == 0:
sub_alpha = 0
else:
sub_alpha = clip(
(time - start_time) / anim_time,
0, 1
)
sub_alpha = clip((time - start_time) / anim_time, 0, 1)
anim.interpolate(sub_alpha)
class Succession(AnimationGroup):
CONFIG = {
"lag_ratio": 1,
}
def __init__(
self,
*animations: Animation,
lag_ratio: float = 1.0,
**kwargs
):
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
def begin(self):
assert(len(self.animations) > 0)
self.init_run_time()
def begin(self) -> None:
assert len(self.animations) > 0
self.active_animation = self.animations[0]
self.active_animation.begin()
def finish(self):
def finish(self) -> None:
self.active_animation.finish()
def update_mobjects(self, dt):
def update_mobjects(self, dt: float) -> None:
self.active_animation.update_mobjects(dt)
def interpolate(self, alpha):
def interpolate(self, alpha: float) -> None:
index, subalpha = integer_interpolate(
0, len(self.animations), alpha
)
@@ -136,28 +154,29 @@ class Succession(AnimationGroup):
class LaggedStart(AnimationGroup):
CONFIG = {
"lag_ratio": DEFAULT_LAGGED_START_LAG_RATIO,
}
def __init__(
self,
*animations,
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
**kwargs
):
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
class LaggedStartMap(LaggedStart):
CONFIG = {
"run_time": 2,
}
def __init__(self, AnimationClass, mobject, arg_creator=None, **kwargs):
args_list = []
for submob in mobject:
if arg_creator:
args_list.append(arg_creator(submob))
else:
args_list.append((submob,))
def __init__(
self,
anim_func: Callable[[Mobject], Animation],
group: Mobject,
run_time: float = 2.0,
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
**kwargs
):
anim_kwargs = dict(kwargs)
if "lag_ratio" in anim_kwargs:
anim_kwargs.pop("lag_ratio")
animations = [
AnimationClass(*args, **anim_kwargs)
for args in args_list
]
super().__init__(*animations, group=mobject, **kwargs)
anim_kwargs.pop("lag_ratio", None)
super().__init__(
*(anim_func(submob, **anim_kwargs) for submob in group),
run_time=run_time,
lag_ratio=lag_ratio,
group=group
)

View File

@@ -1,122 +1,138 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.animation.composition import Succession
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.mobject import Group
from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.rate_functions import linear
from manimlib.utils.rate_functions import double_smooth
from manimlib.utils.rate_functions import smooth
from manimlib.utils.simple_functions import clip
import numpy as np
import itertools as it
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
from manimlib.mobject.mobject import Mobject
from manimlib.scene.scene import Scene
from manimlib.typing import ManimColor
class ShowPartial(Animation):
class ShowPartial(Animation, ABC):
"""
Abstract class for ShowCreation and ShowPassingFlash
"""
CONFIG = {
"should_match_start": False,
}
def __init__(self, mobject: Mobject, should_match_start: bool = False, **kwargs):
self.should_match_start = should_match_start
super().__init__(mobject, **kwargs)
def begin(self):
super().begin()
if not self.should_match_start:
self.mobject.lock_matching_data(self.mobject, self.starting_mobject)
def finish(self):
super().finish()
self.mobject.unlock_data()
def interpolate_submobject(self, submob, start_submob, alpha):
def interpolate_submobject(
self,
submob: VMobject,
start_submob: VMobject,
alpha: float
) -> None:
submob.pointwise_become_partial(
start_submob, *self.get_bounds(alpha)
)
def get_bounds(self, alpha):
@abstractmethod
def get_bounds(self, alpha: float) -> tuple[float, float]:
raise Exception("Not Implemented")
class ShowCreation(ShowPartial):
CONFIG = {
"lag_ratio": 1,
}
def __init__(self, mobject: Mobject, lag_ratio: float = 1.0, **kwargs):
super().__init__(mobject, lag_ratio=lag_ratio, **kwargs)
def get_bounds(self, alpha):
def get_bounds(self, alpha: float) -> tuple[float, float]:
return (0, alpha)
class Uncreate(ShowCreation):
CONFIG = {
"rate_func": lambda t: smooth(1 - t),
"remover": True,
"should_match_start": True,
}
def __init__(
self,
mobject: Mobject,
rate_func: Callable[[float], float] = lambda t: smooth(1 - t),
remover: bool = True,
should_match_start: bool = True,
**kwargs,
):
super().__init__(
mobject,
rate_func=rate_func,
remover=remover,
should_match_start=should_match_start,
**kwargs,
)
class DrawBorderThenFill(Animation):
CONFIG = {
"run_time": 2,
"rate_func": double_smooth,
"stroke_width": 2,
"stroke_color": None,
"draw_border_animation_config": {},
"fill_animation_config": {},
}
def __init__(self, vmobject, **kwargs):
assert(isinstance(vmobject, VMobject))
self.sm_to_index = dict([
(hash(sm), 0)
for sm in vmobject.get_family()
])
super().__init__(vmobject, **kwargs)
def begin(self):
# Trigger triangulation calculation
for submob in self.mobject.get_family():
submob.get_triangulation()
def __init__(
self,
vmobject: VMobject,
run_time: float = 2.0,
rate_func: Callable[[float], float] = double_smooth,
stroke_width: float = 2.0,
stroke_color: ManimColor = None,
draw_border_animation_config: dict = {},
fill_animation_config: dict = {},
**kwargs
):
assert isinstance(vmobject, VMobject)
self.sm_to_index = {hash(sm): 0 for sm in vmobject.get_family()}
self.stroke_width = stroke_width
self.stroke_color = stroke_color
self.draw_border_animation_config = draw_border_animation_config
self.fill_animation_config = fill_animation_config
super().__init__(
vmobject,
run_time=run_time,
rate_func=rate_func,
**kwargs
)
self.mobject = vmobject
def begin(self) -> None:
self.mobject.set_animating_status(True)
self.outline = self.get_outline()
super().begin()
self.mobject.match_style(self.outline)
self.mobject.lock_matching_data(self.mobject, self.outline)
def finish(self):
def finish(self) -> None:
super().finish()
self.mobject.unlock_data()
self.mobject.refresh_joint_angles()
def get_outline(self):
def get_outline(self) -> VMobject:
outline = self.mobject.copy()
outline.set_fill(opacity=0)
for sm in outline.get_family():
for sm in outline.family_members_with_points():
sm.set_stroke(
color=self.get_stroke_color(sm),
width=float(self.stroke_width)
color=self.stroke_color or sm.get_stroke_color(),
width=self.stroke_width,
behind=self.mobject.stroke_behind,
)
return outline
def get_stroke_color(self, vmobject):
if self.stroke_color:
return self.stroke_color
elif vmobject.get_stroke_width() > 0:
return vmobject.get_stroke_color()
return vmobject.get_color()
def get_all_mobjects(self):
def get_all_mobjects(self) -> list[Mobject]:
return [*super().get_all_mobjects(), self.outline]
def interpolate_submobject(self, submob, start, outline, alpha):
def interpolate_submobject(
self,
submob: VMobject,
start: VMobject,
outline: VMobject,
alpha: float
) -> None:
index, subalpha = integer_interpolate(0, 2, alpha)
if index == 1 and self.sm_to_index[hash(submob)] == 0:
# First time crossing over
submob.set_data(outline.data)
submob.unlock_data()
if not self.mobject.has_updaters:
submob.lock_matching_data(submob, start)
submob.needs_new_triangulation = False
self.sm_to_index[hash(submob)] = 1
if index == 0:
@@ -126,79 +142,103 @@ class DrawBorderThenFill(Animation):
class Write(DrawBorderThenFill):
CONFIG = {
# To be figured out in
# set_default_config_from_lengths
"run_time": None,
"lag_ratio": None,
"rate_func": linear,
}
def __init__(
self,
vmobject: VMobject,
run_time: float = -1, # If negative, this will be reassigned
lag_ratio: float = -1, # If negative, this will be reassigned
rate_func: Callable[[float], float] = linear,
stroke_color: ManimColor = None,
**kwargs
):
if stroke_color is None:
stroke_color = vmobject.get_color()
family_size = len(vmobject.family_members_with_points())
super().__init__(
vmobject,
run_time=self.compute_run_time(family_size, run_time),
lag_ratio=self.compute_lag_ratio(family_size, lag_ratio),
rate_func=rate_func,
stroke_color=stroke_color,
**kwargs
)
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
self.set_default_config_from_length(mobject)
super().__init__(mobject, **kwargs)
def compute_run_time(self, family_size: int, run_time: float):
if run_time < 0:
return 1 if family_size < 15 else 2
return run_time
def set_default_config_from_length(self, mobject):
length = len(mobject.family_members_with_points())
if self.run_time is None:
if length < 15:
self.run_time = 1
else:
self.run_time = 2
if self.lag_ratio is None:
self.lag_ratio = min(4.0 / length, 0.2)
def compute_lag_ratio(self, family_size: int, lag_ratio: float):
if lag_ratio < 0:
return min(4.0 / (family_size + 1.0), 0.2)
return lag_ratio
class ShowIncreasingSubsets(Animation):
CONFIG = {
"suspend_mobject_updating": False,
"int_func": np.round,
}
def __init__(self, group, **kwargs):
def __init__(
self,
group: Mobject,
int_func: Callable[[float], float] = np.round,
suspend_mobject_updating: bool = False,
**kwargs
):
self.all_submobs = list(group.submobjects)
super().__init__(group, **kwargs)
self.int_func = int_func
super().__init__(
group,
suspend_mobject_updating=suspend_mobject_updating,
**kwargs
)
def interpolate_mobject(self, alpha):
def interpolate_mobject(self, alpha: float) -> None:
n_submobs = len(self.all_submobs)
alpha = self.rate_func(alpha)
index = int(self.int_func(alpha * n_submobs))
self.update_submobject_list(index)
def update_submobject_list(self, index):
def update_submobject_list(self, index: int) -> None:
self.mobject.set_submobjects(self.all_submobs[:index])
class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
CONFIG = {
"int_func": np.ceil,
}
def __init__(
self,
group: Mobject,
int_func: Callable[[float], float] = np.ceil,
**kwargs
):
super().__init__(group, int_func=int_func, **kwargs)
def update_submobject_list(self, index):
# N = len(self.all_submobs)
def update_submobject_list(self, index: int) -> None:
index = int(clip(index, 0, len(self.all_submobs) - 1))
if index == 0:
self.mobject.set_submobjects([])
else:
self.mobject.set_submobjects([self.all_submobs[index - 1]])
# TODO, this is broken...
class AddTextWordByWord(Succession):
CONFIG = {
# If given a value for run_time, it will
# override the time_per_char
"run_time": None,
"time_per_char": 0.06,
}
class AddTextWordByWord(ShowIncreasingSubsets):
def __init__(
self,
string_mobject: StringMobject,
time_per_word: float = 0.2,
run_time: float = -1.0, # If negative, it will be recomputed with time_per_word
rate_func: Callable[[float], float] = linear,
**kwargs
):
assert isinstance(string_mobject, StringMobject)
grouped_mobject = string_mobject.build_groups()
if run_time < 0:
run_time = time_per_word * len(grouped_mobject)
super().__init__(
grouped_mobject,
run_time=run_time,
rate_func=rate_func,
**kwargs
)
self.string_mobject = string_mobject
def __init__(self, text_mobject, **kwargs):
digest_config(self, kwargs)
tpc = self.time_per_char
anims = it.chain(*[
[
ShowIncreasingSubsets(word, run_time=tpc * len(word)),
Animation(word, run_time=0.005 * len(word)**1.5),
]
for word in text_mobject
])
super().__init__(*anims, **kwargs)
def clean_up_from_scene(self, scene: Scene) -> None:
scene.remove(self.mobject)
if not self.is_remover():
scene.add(self.string_mobject)

View File

@@ -1,36 +1,41 @@
from __future__ import annotations
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.animation.transform import Transform
from manimlib.mobject.mobject import Group
from manimlib.constants import ORIGIN
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import interpolate
from manimlib.utils.rate_functions import there_and_back
from typing import TYPE_CHECKING
DEFAULT_FADE_LAG_RATIO = 0
if TYPE_CHECKING:
from typing import Callable
from manimlib.mobject.mobject import Mobject
from manimlib.scene.scene import Scene
from manimlib.typing import Vect3
class Fade(Transform):
CONFIG = {
"lag_ratio": DEFAULT_FADE_LAG_RATIO,
}
def __init__(self, mobject, shift=ORIGIN, scale=1, **kwargs):
def __init__(
self,
mobject: Mobject,
shift: np.ndarray = ORIGIN,
scale: float = 1,
**kwargs
):
self.shift_vect = shift
self.scale_factor = scale
super().__init__(mobject, **kwargs)
class FadeIn(Fade):
CONFIG = {
"lag_ratio": DEFAULT_FADE_LAG_RATIO,
}
def create_target(self) -> Mobject:
return self.mobject.copy()
def create_target(self):
return self.mobject
def create_starting_mobject(self):
def create_starting_mobject(self) -> Mobject:
start = super().create_starting_mobject()
start.set_opacity(0)
start.scale(1.0 / self.scale_factor)
@@ -39,13 +44,22 @@ class FadeIn(Fade):
class FadeOut(Fade):
CONFIG = {
"remover": True,
# Put it back in original state when done
"final_alpha_value": 0,
}
def __init__(
self,
mobject: Mobject,
shift: Vect3 = ORIGIN,
remover: bool = True,
final_alpha_value: float = 0.0, # Put it back in original state when done,
**kwargs
):
super().__init__(
mobject, shift,
remover=remover,
final_alpha_value=final_alpha_value,
**kwargs
)
def create_target(self):
def create_target(self) -> Mobject:
result = self.mobject.copy()
result.set_opacity(0)
result.shift(self.shift_vect)
@@ -54,7 +68,7 @@ class FadeOut(Fade):
class FadeInFromPoint(FadeIn):
def __init__(self, mobject, point, **kwargs):
def __init__(self, mobject: Mobject, point: Vect3, **kwargs):
super().__init__(
mobject,
shift=mobject.get_center() - point,
@@ -64,7 +78,7 @@ class FadeInFromPoint(FadeIn):
class FadeOutToPoint(FadeOut):
def __init__(self, mobject, point, **kwargs):
def __init__(self, mobject: Mobject, point: Vect3, **kwargs):
super().__init__(
mobject,
shift=point - mobject.get_center(),
@@ -74,20 +88,22 @@ class FadeOutToPoint(FadeOut):
class FadeTransform(Transform):
CONFIG = {
"stretch": True,
"dim_to_match": 1,
}
def __init__(self, mobject, target_mobject, **kwargs):
def __init__(
self,
mobject: Mobject,
target_mobject: Mobject,
stretch: bool = True,
dim_to_match: int = 1,
**kwargs
):
self.to_add_on_completion = target_mobject
mobject.save_state()
super().__init__(
Group(mobject, target_mobject.copy()),
**kwargs
)
self.stretch = stretch
self.dim_to_match = dim_to_match
def begin(self):
mobject.save_state()
super().__init__(mobject.get_group_class()(mobject, target_mobject.copy()), **kwargs)
def begin(self) -> None:
self.ending_mobject = self.mobject.copy()
Animation.begin(self)
# Both 'start' and 'end' consists of the source and target mobjects.
@@ -97,33 +113,35 @@ class FadeTransform(Transform):
for m0, m1 in ((start[1], start[0]), (end[0], end[1])):
self.ghost_to(m0, m1)
def ghost_to(self, source, target):
def ghost_to(self, source: Mobject, target: Mobject) -> None:
source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match)
source.set_uniform(**target.get_uniforms())
source.set_opacity(0)
def get_all_mobjects(self):
def get_all_mobjects(self) -> list[Mobject]:
return [
self.mobject,
self.starting_mobject,
self.ending_mobject,
]
def get_all_families_zipped(self):
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
return Animation.get_all_families_zipped(self)
def clean_up_from_scene(self, scene):
def clean_up_from_scene(self, scene: Scene) -> None:
Animation.clean_up_from_scene(self, scene)
scene.remove(self.mobject)
self.mobject[0].restore()
scene.add(self.to_add_on_completion)
if not self.remover:
scene.add(self.to_add_on_completion)
class FadeTransformPieces(FadeTransform):
def begin(self):
def begin(self) -> None:
self.mobject[0].align_family(self.mobject[1])
super().begin()
def ghost_to(self, source, target):
def ghost_to(self, source: Mobject, target: Mobject) -> None:
for sm0, sm1 in zip(source.get_family(), target.get_family()):
super().ghost_to(sm0, sm1)
@@ -132,11 +150,19 @@ class VFadeIn(Animation):
"""
VFadeIn and VFadeOut only work for VMobjects,
"""
CONFIG = {
"suspend_mobject_updating": False,
}
def __init__(self, vmobject: VMobject, suspend_mobject_updating: bool = False, **kwargs):
super().__init__(
vmobject,
suspend_mobject_updating=suspend_mobject_updating,
**kwargs
)
def interpolate_submobject(self, submob, start, alpha):
def interpolate_submobject(
self,
submob: VMobject,
start: VMobject,
alpha: float
) -> None:
submob.set_stroke(
opacity=interpolate(0, start.get_stroke_opacity(), alpha)
)
@@ -146,20 +172,42 @@ class VFadeIn(Animation):
class VFadeOut(VFadeIn):
CONFIG = {
"remover": True,
# Put it back in original state when done
"final_alpha_value": 0,
}
def __init__(
self,
vmobject: VMobject,
remover: bool = True,
final_alpha_value: float = 0.0,
**kwargs
):
super().__init__(
vmobject,
remover=remover,
final_alpha_value=final_alpha_value,
**kwargs
)
def interpolate_submobject(self, submob, start, alpha):
def interpolate_submobject(
self,
submob: VMobject,
start: VMobject,
alpha: float
) -> None:
super().interpolate_submobject(submob, start, 1 - alpha)
class VFadeInThenOut(VFadeIn):
CONFIG = {
"rate_func": there_and_back,
"remover": True,
# Put it back in original state when done
"final_alpha_value": 0.5,
}
def __init__(
self,
vmobject: VMobject,
rate_func: Callable[[float], float] = there_and_back,
remover: bool = True,
final_alpha_value: float = 0.5,
**kwargs
):
super().__init__(
vmobject,
rate_func=rate_func,
remover=remover,
final_alpha_value=final_alpha_value,
**kwargs
)

View File

@@ -1,48 +1,54 @@
from __future__ import annotations
from manimlib.animation.transform import Transform
# from manimlib.utils.paths import counterclockwise_path
from manimlib.constants import PI
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import numpy as np
from manimlib.mobject.geometry import Arrow
from manimlib.mobject.mobject import Mobject
from manimlib.typing import ManimColor
class GrowFromPoint(Transform):
CONFIG = {
"point_color": None,
}
def __init__(self, mobject, point, **kwargs):
def __init__(
self,
mobject: Mobject,
point: np.ndarray,
point_color: ManimColor = None,
**kwargs
):
self.point = point
self.point_color = point_color
super().__init__(mobject, **kwargs)
def create_target(self):
return self.mobject
def create_target(self) -> Mobject:
return self.mobject.copy()
def create_starting_mobject(self):
def create_starting_mobject(self) -> Mobject:
start = super().create_starting_mobject()
start.scale(0)
start.move_to(self.point)
if self.point_color:
if self.point_color is not None:
start.set_color(self.point_color)
return start
class GrowFromCenter(GrowFromPoint):
def __init__(self, mobject, **kwargs):
def __init__(self, mobject: Mobject, **kwargs):
point = mobject.get_center()
super().__init__(mobject, point, **kwargs)
class GrowFromEdge(GrowFromPoint):
def __init__(self, mobject, edge, **kwargs):
def __init__(self, mobject: Mobject, edge: np.ndarray, **kwargs):
point = mobject.get_bounding_box_point(edge)
super().__init__(mobject, point, **kwargs)
class GrowArrow(GrowFromPoint):
def __init__(self, arrow, **kwargs):
def __init__(self, arrow: Arrow, **kwargs):
point = arrow.get_start()
super().__init__(arrow, point, **kwargs)
class SpinInFromNothing(GrowFromCenter):
CONFIG = {
"path_arc": PI,
}

View File

@@ -1,54 +1,67 @@
import numpy as np
import math
from __future__ import annotations
import numpy as np
from manimlib.constants import *
from manimlib.animation.animation import Animation
from manimlib.animation.movement import Homotopy
from manimlib.animation.composition import AnimationGroup
from manimlib.animation.composition import Succession
from manimlib.animation.creation import ShowCreation
from manimlib.animation.creation import ShowPartial
from manimlib.animation.fading import FadeOut
from manimlib.animation.fading import FadeIn
from manimlib.animation.movement import Homotopy
from manimlib.animation.transform import Transform
from manimlib.mobject.types.vectorized_mobject import VMobject
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 DEG
from manimlib.constants import TAU
from manimlib.constants import GREY, YELLOW
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Dot
from manimlib.mobject.geometry import Line
from manimlib.mobject.shape_matchers import SurroundingRectangle
from manimlib.mobject.shape_matchers import Underline
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.geometry import Line
from manimlib.utils.bezier import interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.rate_functions import there_and_back
from manimlib.utils.rate_functions import wiggle
from manimlib.utils.rate_functions import smooth
from manimlib.utils.rate_functions import squish_rate_func
from manimlib.utils.rate_functions import there_and_back
from manimlib.utils.rate_functions import wiggle
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
from manimlib.typing import ManimColor
from manimlib.mobject.mobject import Mobject
class FocusOn(Transform):
CONFIG = {
"opacity": 0.2,
"color": GREY,
"run_time": 2,
"remover": True,
}
def __init__(self, focus_point, **kwargs):
def __init__(
self,
focus_point: np.ndarray | Mobject,
opacity: float = 0.2,
color: ManimColor = GREY,
run_time: float = 2,
remover: bool = True,
**kwargs
):
self.focus_point = focus_point
self.opacity = opacity
self.color = color
# Initialize with blank mobject, while create_target
# and create_starting_mobject handle the meat
super().__init__(VMobject(), **kwargs)
super().__init__(VMobject(), run_time=run_time, remover=remover, **kwargs)
def create_target(self):
def create_target(self) -> Dot:
little_dot = Dot(radius=0)
little_dot.set_fill(self.color, opacity=self.opacity)
little_dot.add_updater(
lambda d: d.move_to(self.focus_point)
)
little_dot.add_updater(lambda d: d.move_to(self.focus_point))
return little_dot
def create_starting_mobject(self):
def create_starting_mobject(self) -> Dot:
return Dot(
radius=FRAME_X_RADIUS + FRAME_Y_RADIUS,
stroke_width=0,
@@ -58,13 +71,19 @@ class FocusOn(Transform):
class Indicate(Transform):
CONFIG = {
"rate_func": there_and_back,
"scale_factor": 1.2,
"color": YELLOW,
}
def __init__(
self,
mobject: Mobject,
scale_factor: float = 1.2,
color: ManimColor = YELLOW,
rate_func: Callable[[float], float] = there_and_back,
**kwargs
):
self.scale_factor = scale_factor
self.color = color
super().__init__(mobject, rate_func=rate_func, **kwargs)
def create_target(self):
def create_target(self) -> Mobject:
target = self.mobject.copy()
target.scale(self.scale_factor)
target.set_color(self.color)
@@ -72,27 +91,34 @@ class Indicate(Transform):
class Flash(AnimationGroup):
CONFIG = {
"line_length": 0.2,
"num_lines": 12,
"flash_radius": 0.3,
"line_stroke_width": 3,
"run_time": 1,
}
def __init__(self, point, color=YELLOW, **kwargs):
def __init__(
self,
point: np.ndarray | Mobject,
color: ManimColor = YELLOW,
line_length: float = 0.2,
num_lines: int = 12,
flash_radius: float = 0.3,
line_stroke_width: float = 3.0,
run_time: float = 1.0,
**kwargs
):
self.point = point
self.color = color
digest_config(self, kwargs)
self.line_length = line_length
self.num_lines = num_lines
self.flash_radius = flash_radius
self.line_stroke_width = line_stroke_width
self.lines = self.create_lines()
animations = self.create_line_anims()
super().__init__(
*animations,
group=self.lines,
run_time=run_time,
**kwargs,
)
def create_lines(self):
def create_lines(self) -> VGroup:
lines = VGroup()
for angle in np.arange(0, TAU, TAU / self.num_lines):
line = Line(ORIGIN, self.line_length * RIGHT)
@@ -106,44 +132,52 @@ class Flash(AnimationGroup):
lines.add_updater(lambda l: l.move_to(self.point))
return lines
def create_line_anims(self):
def create_line_anims(self) -> list[Animation]:
return [
ShowCreationThenDestruction(line)
for line in self.lines
]
class CircleIndicate(Indicate):
CONFIG = {
"rate_func": there_and_back,
"remover": True,
"circle_config": {
"color": YELLOW,
},
}
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
circle = self.get_circle(mobject)
super().__init__(circle, **kwargs)
def get_circle(self, mobject):
circle = Circle(**self.circle_config)
circle.add_updater(lambda c: c.surround(mobject))
return circle
def interpolate_mobject(self, alpha):
super().interpolate_mobject(alpha)
self.mobject.set_stroke(opacity=alpha)
class CircleIndicate(Transform):
def __init__(
self,
mobject: Mobject,
scale_factor: float = 1.2,
rate_func: Callable[[float], float] = there_and_back,
stroke_color: ManimColor = YELLOW,
stroke_width: float = 3.0,
remover: bool = True,
**kwargs
):
circle = Circle(stroke_color=stroke_color, stroke_width=stroke_width)
circle.surround(mobject)
pre_circle = circle.copy().set_stroke(width=0)
pre_circle.scale(1 / scale_factor)
super().__init__(
pre_circle, circle,
rate_func=rate_func,
remover=remover,
**kwargs
)
class ShowPassingFlash(ShowPartial):
CONFIG = {
"time_width": 0.1,
"remover": True,
}
def __init__(
self,
mobject: Mobject,
time_width: float = 0.1,
remover: bool = True,
**kwargs
):
self.time_width = time_width
super().__init__(
mobject,
remover=remover,
**kwargs
)
def get_bounds(self, alpha):
def get_bounds(self, alpha: float) -> tuple[float, float]:
tw = self.time_width
upper = interpolate(0, 1 + tw, alpha)
lower = upper - tw
@@ -151,174 +185,163 @@ class ShowPassingFlash(ShowPartial):
lower = max(lower, 0)
return (lower, upper)
def finish(self):
def finish(self) -> None:
super().finish()
for submob, start in self.get_all_families_zipped():
submob.pointwise_become_partial(start, 0, 1)
class VShowPassingFlash(Animation):
CONFIG = {
"time_width": 0.3,
"taper_width": 0.02,
"remover": True,
}
def __init__(
self,
vmobject: VMobject,
time_width: float = 0.3,
taper_width: float = 0.05,
remover: bool = True,
**kwargs
):
self.time_width = time_width
self.taper_width = taper_width
super().__init__(vmobject, remover=remover, **kwargs)
self.mobject = vmobject
def begin(self):
self.mobject.align_stroke_width_data_to_points()
def taper_kernel(self, x):
if x < self.taper_width:
return x
elif x > 1 - self.taper_width:
return 1.0 - x
return 1.0
def begin(self) -> None:
# Compute an array of stroke widths for each submobject
# which tapers out at either end
self.submob_to_anchor_widths = dict()
self.submob_to_widths = dict()
for sm in self.mobject.get_family():
original_widths = sm.get_stroke_widths()
anchor_widths = np.array([*original_widths[0::3], original_widths[-1]])
def taper_kernel(x):
if x < self.taper_width:
return x
elif x > 1 - self.taper_width:
return 1.0 - x
return 1.0
taper_array = list(map(taper_kernel, np.linspace(0, 1, len(anchor_widths))))
self.submob_to_anchor_widths[hash(sm)] = anchor_widths * taper_array
widths = sm.get_stroke_widths()
self.submob_to_widths[hash(sm)] = np.array([
width * self.taper_kernel(x)
for width, x in zip(widths, np.linspace(0, 1, len(widths)))
])
super().begin()
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
anchor_widths = self.submob_to_anchor_widths[hash(submobject)]
def interpolate_submobject(
self,
submobject: VMobject,
starting_sumobject: None,
alpha: float
) -> None:
widths = self.submob_to_widths[hash(submobject)]
# Create a gaussian such that 3 sigmas out on either side
# will equals time_width
tw = self.time_width
sigma = tw / 6
mu = interpolate(-tw / 2, 1 + tw / 2, alpha)
xs = np.linspace(0, 1, len(widths))
zs = (xs - mu) / sigma
gaussian = np.exp(-0.5 * zs * zs)
gaussian[abs(xs - mu) > 3 * sigma] = 0
def gauss_kernel(x):
if abs(x - mu) > 3 * sigma:
return 0
z = (x - mu) / sigma
return math.exp(-0.5 * z * z)
if len(widths * gaussian) !=0:
submobject.set_stroke(width=widths * gaussian)
kernel_array = list(map(gauss_kernel, np.linspace(0, 1, len(anchor_widths))))
scaled_widths = anchor_widths * kernel_array
new_widths = np.zeros(submobject.get_num_points())
new_widths[0::3] = scaled_widths[:-1]
new_widths[2::3] = scaled_widths[1:]
new_widths[1::3] = (new_widths[0::3] + new_widths[2::3]) / 2
submobject.set_stroke(width=new_widths)
def finish(self):
def finish(self) -> None:
super().finish()
for submob, start in self.get_all_families_zipped():
submob.match_style(start)
class FlashAround(VShowPassingFlash):
CONFIG = {
"stroke_width": 4.0,
"color": YELLOW,
"buff": SMALL_BUFF,
"time_width": 1.0,
"n_inserted_curves": 20,
}
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
path = self.get_path(mobject)
if mobject.is_fixed_in_frame:
def __init__(
self,
mobject: Mobject,
time_width: float = 1.0,
taper_width: float = 0.0,
stroke_width: float = 4.0,
color: ManimColor = YELLOW,
buff: float = SMALL_BUFF,
n_inserted_curves: int = 100,
**kwargs
):
path = self.get_path(mobject, buff)
if mobject.is_fixed_in_frame():
path.fix_in_frame()
path.insert_n_curves(self.n_inserted_curves)
path.insert_n_curves(n_inserted_curves)
path.set_points(path.get_points_without_null_curves())
path.set_stroke(self.color, self.stroke_width)
super().__init__(path, **kwargs)
path.set_stroke(color, stroke_width)
super().__init__(path, time_width=time_width, taper_width=taper_width, **kwargs)
def get_path(self, mobject):
return SurroundingRectangle(mobject, buff=self.buff)
def get_path(self, mobject: Mobject, buff: float) -> SurroundingRectangle:
return SurroundingRectangle(mobject, buff=buff)
class FlashUnder(FlashAround):
def get_path(self, mobject):
return Underline(mobject, buff=self.buff)
def get_path(self, mobject: Mobject, buff: float) -> Underline:
return Underline(mobject, buff=buff, stretch_factor=1.0)
class ShowCreationThenDestruction(ShowPassingFlash):
CONFIG = {
"time_width": 2.0,
"run_time": 1,
}
def __init__(self, vmobject: VMobject, time_width: float = 2.0, **kwargs):
super().__init__(vmobject, time_width=time_width, **kwargs)
class ShowCreationThenFadeOut(Succession):
CONFIG = {
"remover": True,
}
def __init__(self, mobject, **kwargs):
def __init__(self, mobject: Mobject, remover: bool = True, **kwargs):
super().__init__(
ShowCreation(mobject),
FadeOut(mobject),
remover=remover,
**kwargs
)
class AnimationOnSurroundingRectangle(AnimationGroup):
CONFIG = {
"surrounding_rectangle_config": {},
# Function which takes in a rectangle, and spits
# out some animation. Could be some animation class,
# could be something more
"rect_animation": Animation
}
RectAnimationType: type = Animation
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
if "surrounding_rectangle_config" in kwargs:
kwargs.pop("surrounding_rectangle_config")
self.mobject_to_surround = mobject
rect = self.get_rect()
def __init__(
self,
mobject: Mobject,
stroke_width: float = 2.0,
stroke_color: ManimColor = YELLOW,
buff: float = SMALL_BUFF,
**kwargs
):
rect = SurroundingRectangle(
mobject,
stroke_width=stroke_width,
stroke_color=stroke_color,
buff=buff,
)
rect.add_updater(lambda r: r.move_to(mobject))
super().__init__(
self.rect_animation(rect, **kwargs),
)
def get_rect(self):
return SurroundingRectangle(
self.mobject_to_surround,
**self.surrounding_rectangle_config
)
super().__init__(self.RectAnimationType(rect, **kwargs))
class ShowPassingFlashAround(AnimationOnSurroundingRectangle):
CONFIG = {
"rect_animation": ShowPassingFlash
}
RectAnimationType = ShowPassingFlash
class ShowCreationThenDestructionAround(AnimationOnSurroundingRectangle):
CONFIG = {
"rect_animation": ShowCreationThenDestruction
}
RectAnimationType = ShowCreationThenDestruction
class ShowCreationThenFadeAround(AnimationOnSurroundingRectangle):
CONFIG = {
"rect_animation": ShowCreationThenFadeOut
}
RectAnimationType = ShowCreationThenFadeOut
class ApplyWave(Homotopy):
CONFIG = {
"direction": UP,
"amplitude": 0.2,
"run_time": 1,
}
def __init__(
self,
mobject: Mobject,
direction: np.ndarray = UP,
amplitude: float = 0.2,
run_time: float = 1.0,
**kwargs
):
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs, locals())
left_x = mobject.get_left()[0]
right_x = mobject.get_right()[0]
vect = self.amplitude * self.direction
vect = amplitude * direction
def homotopy(x, y, z, t):
alpha = (x - left_x) / (right_x - left_x)
@@ -330,24 +353,36 @@ class ApplyWave(Homotopy):
class WiggleOutThenIn(Animation):
CONFIG = {
"scale_value": 1.1,
"rotation_angle": 0.01 * TAU,
"n_wiggles": 6,
"run_time": 2,
"scale_about_point": None,
"rotate_about_point": None,
}
def __init__(
self,
mobject: Mobject,
scale_value: float = 1.1,
rotation_angle: float = 0.01 * TAU,
n_wiggles: int = 6,
scale_about_point: np.ndarray | None = None,
rotate_about_point: np.ndarray | None = None,
run_time: float = 2,
**kwargs
):
self.scale_value = scale_value
self.rotation_angle = rotation_angle
self.n_wiggles = n_wiggles
self.scale_about_point = scale_about_point
self.rotate_about_point = rotate_about_point
super().__init__(mobject, run_time=run_time, **kwargs)
def get_scale_about_point(self):
if self.scale_about_point is None:
return self.mobject.get_center()
def get_scale_about_point(self) -> np.ndarray:
return self.scale_about_point or self.mobject.get_center()
def get_rotate_about_point(self):
if self.rotate_about_point is None:
return self.mobject.get_center()
def get_rotate_about_point(self) -> np.ndarray:
return self.rotate_about_point or self.mobject.get_center()
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
def interpolate_submobject(
self,
submobject: Mobject,
starting_sumobject: Mobject,
alpha: float
) -> None:
submobject.match_points(starting_sumobject)
submobject.scale(
interpolate(1, self.scale_value, there_and_back(alpha)),
@@ -360,28 +395,31 @@ class WiggleOutThenIn(Animation):
class TurnInsideOut(Transform):
CONFIG = {
"path_arc": TAU / 4,
}
def __init__(self, mobject: Mobject, path_arc: float = 90 * DEG, **kwargs):
super().__init__(mobject, path_arc=path_arc, **kwargs)
def create_target(self):
return self.mobject.copy().reverse_points()
def create_target(self) -> Mobject:
result = self.mobject.copy().reverse_points()
if isinstance(result, VMobject):
result.refresh_triangulation()
return result
class FlashyFadeIn(AnimationGroup):
CONFIG = {
"fade_lag": 0,
}
def __init__(self, vmobject, stroke_width=2, **kwargs):
digest_config(self, kwargs)
def __init__(self,
vmobject: VMobject,
stroke_width: float = 2.0,
fade_lag: float = 0.0,
time_width: float = 1.0,
**kwargs
):
outline = vmobject.copy()
outline.set_fill(opacity=0)
outline.set_stroke(width=stroke_width, opacity=1)
rate_func = kwargs.get("rate_func", smooth)
super().__init__(
FadeIn(vmobject, rate_func=squish_rate_func(rate_func, self.fade_lag, 1)),
VShowPassingFlash(outline, time_width=1),
FadeIn(vmobject, rate_func=squish_rate_func(rate_func, fade_lag, 1)),
VShowPassingFlash(outline, time_width=time_width),
**kwargs
)

View File

@@ -1,40 +1,65 @@
from __future__ import annotations
from manimlib.animation.animation import Animation
from manimlib.utils.rate_functions import linear
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Sequence
import numpy as np
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.vectorized_mobject import VMobject
class Homotopy(Animation):
CONFIG = {
"run_time": 3,
"apply_function_kwargs": {},
}
apply_function_config: dict = dict()
def __init__(self, homotopy, mobject, **kwargs):
def __init__(
self,
homotopy: Callable[[float, float, float, float], Sequence[float]],
mobject: Mobject,
run_time: float = 3.0,
**kwargs
):
"""
Homotopy is a function from
(x, y, z, t) to (x', y', z')
"""
self.homotopy = homotopy
super().__init__(mobject, **kwargs)
super().__init__(mobject, run_time=run_time, **kwargs)
def function_at_time_t(self, t):
return lambda p: self.homotopy(*p, t)
def function_at_time_t(self, t: float) -> Callable[[np.ndarray], Sequence[float]]:
def result(p):
return self.homotopy(*p, t)
return result
def interpolate_submobject(self, submob, start, alpha):
def interpolate_submobject(
self,
submob: Mobject,
start: Mobject,
alpha: float
) -> None:
submob.match_points(start)
submob.apply_function(
self.function_at_time_t(alpha),
**self.apply_function_kwargs
**self.apply_function_config
)
class SmoothedVectorizedHomotopy(Homotopy):
CONFIG = {
"apply_function_kwargs": {"make_smooth": True},
}
apply_function_config: dict = dict(make_smooth=True)
class ComplexHomotopy(Homotopy):
def __init__(self, complex_homotopy, mobject, **kwargs):
def __init__(
self,
complex_homotopy: Callable[[complex, float], complex],
mobject: Mobject,
**kwargs
):
"""
Given a function form (z, t) -> w, where z and w
are complex numbers and t is time, this animates
@@ -43,21 +68,32 @@ class ComplexHomotopy(Homotopy):
def homotopy(x, y, z, t):
c = complex_homotopy(complex(x, y), t)
return (c.real, c.imag, z)
super().__init__(homotopy, mobject, **kwargs)
class PhaseFlow(Animation):
CONFIG = {
"virtual_time": 1,
"rate_func": linear,
"suspend_mobject_updating": False,
}
def __init__(self, function, mobject, **kwargs):
def __init__(
self,
function: Callable[[np.ndarray], np.ndarray],
mobject: Mobject,
virtual_time: float | None = None,
suspend_mobject_updating: bool = False,
rate_func: Callable[[float], float] = linear,
run_time: float =3.0,
**kwargs
):
self.function = function
super().__init__(mobject, **kwargs)
self.virtual_time = virtual_time or run_time
super().__init__(
mobject,
rate_func=rate_func,
run_time=run_time,
suspend_mobject_updating=suspend_mobject_updating,
**kwargs
)
def interpolate_mobject(self, alpha):
def interpolate_mobject(self, alpha: float) -> None:
if hasattr(self, "last_alpha"):
dt = self.virtual_time * (alpha - self.last_alpha)
self.mobject.apply_function(
@@ -67,14 +103,16 @@ class PhaseFlow(Animation):
class MoveAlongPath(Animation):
CONFIG = {
"suspend_mobject_updating": False,
}
def __init__(self, mobject, path, **kwargs):
def __init__(
self,
mobject: Mobject,
path: VMobject,
suspend_mobject_updating: bool = False,
**kwargs
):
self.path = path
super().__init__(mobject, **kwargs)
super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs)
def interpolate_mobject(self, alpha):
point = self.path.point_from_proportion(alpha)
def interpolate_mobject(self, alpha: float) -> None:
point = self.path.quick_point_from_proportion(self.rate_func(alpha))
self.mobject.move_to(point)

View File

@@ -1,26 +1,46 @@
from __future__ import annotations
from manimlib.animation.animation import Animation
from manimlib.mobject.numbers import DecimalNumber
from manimlib.utils.bezier import interpolate
from manimlib.utils.simple_functions import clip
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
class ChangingDecimal(Animation):
CONFIG = {
"suspend_mobject_updating": False,
}
def __init__(self, decimal_mob, number_update_func, **kwargs):
assert(isinstance(decimal_mob, DecimalNumber))
def __init__(
self,
decimal_mob: DecimalNumber,
number_update_func: Callable[[float], float],
suspend_mobject_updating: bool = False,
**kwargs
):
assert isinstance(decimal_mob, DecimalNumber)
self.number_update_func = number_update_func
super().__init__(decimal_mob, **kwargs)
super().__init__(
decimal_mob,
suspend_mobject_updating=suspend_mobject_updating,
**kwargs
)
self.mobject = decimal_mob
def interpolate_mobject(self, alpha):
def interpolate_mobject(self, alpha: float) -> None:
self.mobject.set_value(
self.number_update_func(alpha)
)
class ChangeDecimalToValue(ChangingDecimal):
def __init__(self, decimal_mob, target_number, **kwargs):
def __init__(
self,
decimal_mob: DecimalNumber,
target_number: float | complex,
**kwargs
):
start_number = decimal_mob.number
super().__init__(
decimal_mob,
@@ -30,10 +50,15 @@ class ChangeDecimalToValue(ChangingDecimal):
class CountInFrom(ChangingDecimal):
def __init__(self, decimal_mob, source_number=0, **kwargs):
start_number = decimal_mob.number
def __init__(
self,
decimal_mob: DecimalNumber,
source_number: float | complex = 0,
**kwargs
):
start_number = decimal_mob.get_value()
super().__init__(
decimal_mob,
lambda a: interpolate(source_number, start_number, a),
lambda a: interpolate(source_number, start_number, clip(a, 0, 1)),
**kwargs
)

View File

@@ -1,33 +1,54 @@
from __future__ import annotations
from manimlib.animation.animation import Animation
from manimlib.constants import OUT
from manimlib.constants import PI
from manimlib.constants import TAU
from manimlib.constants import ORIGIN
from manimlib.constants import ORIGIN, OUT
from manimlib.constants import PI, TAU
from manimlib.utils.rate_functions import linear
from manimlib.utils.rate_functions import smooth
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import numpy as np
from typing import Callable
from manimlib.mobject.mobject import Mobject
class Rotating(Animation):
CONFIG = {
# "axis": OUT,
# "radians": TAU,
"run_time": 5,
"rate_func": linear,
"about_point": None,
"about_edge": None,
"suspend_mobject_updating": False,
}
def __init__(self, mobject, angle=TAU, axis=OUT, **kwargs):
def __init__(
self,
mobject: Mobject,
angle: float = TAU,
axis: np.ndarray = OUT,
about_point: np.ndarray | None = None,
about_edge: np.ndarray | None = None,
run_time: float = 5.0,
rate_func: Callable[[float], float] = linear,
suspend_mobject_updating: bool = False,
**kwargs
):
self.angle = angle
self.axis = axis
super().__init__(mobject, **kwargs)
self.about_point = about_point
self.about_edge = about_edge
super().__init__(
mobject,
run_time=run_time,
rate_func=rate_func,
suspend_mobject_updating=suspend_mobject_updating,
**kwargs
)
def interpolate_mobject(self, alpha):
for sm1, sm2 in self.get_all_families_zipped():
sm1.set_points(sm2.get_points())
def interpolate_mobject(self, alpha: float) -> None:
pairs = zip(
self.mobject.family_members_with_points(),
self.starting_mobject.family_members_with_points(),
)
for sm1, sm2 in pairs:
for key in sm1.pointlike_data_keys:
sm1.data[key][:] = sm2.data[key]
self.mobject.rotate(
alpha * self.angle,
self.rate_func(self.time_spanned_alpha(alpha)) * self.angle,
axis=self.axis,
about_point=self.about_point,
about_edge=self.about_edge,
@@ -35,11 +56,20 @@ class Rotating(Animation):
class Rotate(Rotating):
CONFIG = {
"run_time": 1,
"rate_func": smooth,
"about_edge": ORIGIN,
}
def __init__(self, mobject, angle=PI, axis=OUT, **kwargs):
super().__init__(mobject, angle, axis, **kwargs)
def __init__(
self,
mobject: Mobject,
angle: float = PI,
axis: np.ndarray = OUT,
run_time: float = 1,
rate_func: Callable[[float], float] = smooth,
about_edge: np.ndarray = ORIGIN,
**kwargs
):
super().__init__(
mobject, angle, axis,
run_time=run_time,
rate_func=rate_func,
about_edge=about_edge,
**kwargs
)

View File

@@ -1,43 +1,55 @@
from __future__ import annotations
from manimlib.animation.composition import LaggedStart
from manimlib.animation.transform import Restore
from manimlib.constants import WHITE
from manimlib.constants import BLACK
from manimlib.constants import BLACK, WHITE
from manimlib.mobject.geometry import Circle
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.config_ops import digest_config
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import numpy as np
from manimlib.typing import ManimColor
class Broadcast(LaggedStart):
CONFIG = {
"small_radius": 0.0,
"big_radius": 5,
"n_circles": 5,
"start_stroke_width": 8,
"color": WHITE,
"remover": True,
"lag_ratio": 0.2,
"run_time": 3,
"remover": True,
}
def __init__(
self,
focal_point: np.ndarray,
small_radius: float = 0.0,
big_radius: float = 5.0,
n_circles: int = 5,
start_stroke_width: float = 8.0,
color: ManimColor = WHITE,
run_time: float = 3.0,
lag_ratio: float = 0.2,
remover: bool = True,
**kwargs
):
self.focal_point = focal_point
self.small_radius = small_radius
self.big_radius = big_radius
self.n_circles = n_circles
self.start_stroke_width = start_stroke_width
self.color = color
def __init__(self, focal_point, **kwargs):
digest_config(self, kwargs)
circles = VGroup()
for x in range(self.n_circles):
for x in range(n_circles):
circle = Circle(
radius=self.big_radius,
radius=big_radius,
stroke_color=BLACK,
stroke_width=0,
)
circle.add_updater(
lambda c: c.move_to(focal_point)
)
circle.add_updater(lambda c: c.move_to(focal_point))
circle.save_state()
circle.set_width(self.small_radius * 2)
circle.set_stroke(self.color, self.start_stroke_width)
circle.set_width(small_radius * 2)
circle.set_stroke(color, start_stroke_width)
circles.add(circle)
animations = [
Restore(circle)
for circle in circles
]
super().__init__(*animations, **kwargs)
super().__init__(
*map(Restore, circles),
run_time=run_time,
lag_ratio=lag_ratio,
remover=remover,
**kwargs
)

View File

@@ -1,34 +1,46 @@
from __future__ import annotations
import inspect
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.constants import DEFAULT_POINTWISE_FUNCTION_RUN_TIME
from manimlib.constants import DEG
from manimlib.constants import OUT
from manimlib.constants import DEGREES
from manimlib.mobject.mobject import Group
from manimlib.mobject.mobject import Mobject
from manimlib.utils.config_ops import digest_config
from manimlib.utils.paths import path_along_arc
from manimlib.utils.paths import straight_path
from manimlib.utils.rate_functions import smooth
from manimlib.utils.rate_functions import squish_rate_func
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
import numpy.typing as npt
from manimlib.scene.scene import Scene
from manimlib.typing import ManimColor
class Transform(Animation):
CONFIG = {
"path_arc": 0,
"path_arc_axis": OUT,
"path_func": None,
"replace_mobject_with_target_in_scene": False,
}
replace_mobject_with_target_in_scene: bool = False
def __init__(self, mobject, target_mobject=None, **kwargs):
super().__init__(mobject, **kwargs)
def __init__(
self,
mobject: Mobject,
target_mobject: Mobject | None = None,
path_arc: float = 0.0,
path_arc_axis: np.ndarray = OUT,
path_func: Callable | None = None,
**kwargs
):
self.target_mobject = target_mobject
self.path_arc = path_arc
self.path_arc_axis = path_arc_axis
self.path_func = path_func
super().__init__(mobject, **kwargs)
self.init_path_func()
def init_path_func(self):
def init_path_func(self) -> None:
if self.path_func is not None:
return
elif self.path_arc == 0:
@@ -39,43 +51,48 @@ class Transform(Animation):
self.path_arc_axis,
)
def begin(self):
def begin(self) -> None:
self.target_mobject = self.create_target()
self.check_target_mobject_validity()
# Use a copy of target_mobject for the align_data_and_family
# call so that the actual target_mobject stays
# preserved, since calling allign_data will potentially
# change the structure of both arguments
self.target_copy = self.target_mobject.copy()
if self.mobject.is_aligned_with(self.target_mobject):
self.target_copy = self.target_mobject
else:
# Use a copy of target_mobject for the align_data_and_family
# call so that the actual target_mobject stays
# preserved, since calling align_data will potentially
# change the structure of both arguments
self.target_copy = self.target_mobject.copy()
self.mobject.align_data_and_family(self.target_copy)
super().begin()
self.mobject.lock_matching_data(
self.starting_mobject,
self.target_copy,
)
if not self.mobject.has_updaters():
self.mobject.lock_matching_data(
self.starting_mobject,
self.target_copy,
)
def finish(self):
def finish(self) -> None:
super().finish()
self.mobject.unlock_data()
def create_target(self):
def create_target(self) -> Mobject:
# Has no meaningful effect here, but may be useful
# in subclasses
return self.target_mobject
def check_target_mobject_validity(self):
def check_target_mobject_validity(self) -> None:
if self.target_mobject is None:
raise Exception(
f"{self.__class__.__name__}.create_target not properly implemented"
)
def clean_up_from_scene(self, scene):
def clean_up_from_scene(self, scene: Scene) -> None:
super().clean_up_from_scene(scene)
if self.replace_mobject_with_target_in_scene:
scene.remove(self.mobject)
scene.add(self.target_mobject)
def update_config(self, **kwargs):
def update_config(self, **kwargs) -> None:
Animation.update_config(self, **kwargs)
if "path_arc" in kwargs:
self.path_func = path_along_arc(
@@ -83,7 +100,7 @@ class Transform(Animation):
kwargs.get("path_arc_axis", OUT)
)
def get_all_mobjects(self):
def get_all_mobjects(self) -> list[Mobject]:
return [
self.mobject,
self.starting_mobject,
@@ -91,7 +108,7 @@ class Transform(Animation):
self.target_copy,
]
def get_all_families_zipped(self):
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
return zip(*[
mob.get_family()
for mob in [
@@ -101,62 +118,48 @@ class Transform(Animation):
]
])
def interpolate_submobject(self, submob, start, target_copy, alpha):
def interpolate_submobject(
self,
submob: Mobject,
start: Mobject,
target_copy: Mobject,
alpha: float
):
submob.interpolate(start, target_copy, alpha, self.path_func)
return self
class ReplacementTransform(Transform):
CONFIG = {
"replace_mobject_with_target_in_scene": True,
}
replace_mobject_with_target_in_scene: bool = True
class TransformFromCopy(Transform):
"""
Performs a reversed Transform
"""
replace_mobject_with_target_in_scene: bool = True
def __init__(self, mobject, target_mobject, **kwargs):
super().__init__(target_mobject, mobject, **kwargs)
def interpolate(self, alpha):
super().interpolate(1 - alpha)
class ClockwiseTransform(Transform):
CONFIG = {
"path_arc": -np.pi
}
class CounterclockwiseTransform(Transform):
CONFIG = {
"path_arc": np.pi
}
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs):
super().__init__(mobject.copy(), target_mobject, **kwargs)
class MoveToTarget(Transform):
def __init__(self, mobject, **kwargs):
def __init__(self, mobject: Mobject, **kwargs):
self.check_validity_of_input(mobject)
super().__init__(mobject, mobject.target, **kwargs)
def check_validity_of_input(self, mobject):
def check_validity_of_input(self, mobject: Mobject) -> None:
if not hasattr(mobject, "target"):
raise Exception(
"MoveToTarget called on mobject"
"without attribute 'target'"
"MoveToTarget called on mobject without attribute 'target'"
)
class _MethodAnimation(MoveToTarget):
def __init__(self, mobject, methods):
def __init__(self, mobject: Mobject, methods: list[Callable], **kwargs):
self.methods = methods
super().__init__(mobject)
super().__init__(mobject, **kwargs)
class ApplyMethod(Transform):
def __init__(self, method, *args, **kwargs):
def __init__(self, method: Callable, *args, **kwargs):
"""
method is a method of Mobject, *args are arguments for
that method. Key word arguments should be passed in
@@ -170,15 +173,15 @@ class ApplyMethod(Transform):
self.method_args = args
super().__init__(method.__self__, **kwargs)
def check_validity_of_input(self, method):
def check_validity_of_input(self, method: Callable) -> None:
if not inspect.ismethod(method):
raise Exception(
"Whoops, looks like you accidentally invoked "
"the method you want to animate"
)
assert(isinstance(method.__self__, Mobject))
assert isinstance(method.__self__, Mobject)
def create_target(self):
def create_target(self) -> Mobject:
method = self.method
# Make sure it's a list so that args.pop() works
args = list(self.method_args)
@@ -193,52 +196,73 @@ class ApplyMethod(Transform):
class ApplyPointwiseFunction(ApplyMethod):
CONFIG = {
"run_time": DEFAULT_POINTWISE_FUNCTION_RUN_TIME
}
def __init__(self, function, mobject, **kwargs):
super().__init__(mobject.apply_function, function, **kwargs)
def __init__(
self,
function: Callable[[np.ndarray], np.ndarray],
mobject: Mobject,
run_time: float = 3.0,
**kwargs
):
super().__init__(mobject.apply_function, function, run_time=run_time, **kwargs)
class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction):
def __init__(self, function, mobject, **kwargs):
class ApplyPointwiseFunctionToCenter(Transform):
def __init__(
self,
function: Callable[[np.ndarray], np.ndarray],
mobject: Mobject,
**kwargs
):
self.function = function
super().__init__(mobject.move_to, **kwargs)
super().__init__(mobject, **kwargs)
def begin(self):
self.method_args = [
self.function(self.mobject.get_center())
]
super().begin()
def create_target(self) -> Mobject:
return self.mobject.copy().move_to(self.function(self.mobject.get_center()))
class FadeToColor(ApplyMethod):
def __init__(self, mobject, color, **kwargs):
def __init__(
self,
mobject: Mobject,
color: ManimColor,
**kwargs
):
super().__init__(mobject.set_color, color, **kwargs)
class ScaleInPlace(ApplyMethod):
def __init__(self, mobject, scale_factor, **kwargs):
def __init__(
self,
mobject: Mobject,
scale_factor: npt.ArrayLike,
**kwargs
):
super().__init__(mobject.scale, scale_factor, **kwargs)
class ShrinkToCenter(ScaleInPlace):
def __init__(self, mobject, **kwargs):
def __init__(self, mobject: Mobject, **kwargs):
super().__init__(mobject, 0, **kwargs)
class Restore(ApplyMethod):
def __init__(self, mobject, **kwargs):
super().__init__(mobject.restore, **kwargs)
class Restore(Transform):
def __init__(self, mobject: Mobject, **kwargs):
if not hasattr(mobject, "saved_state") or mobject.saved_state is None:
raise Exception("Trying to restore without having saved")
super().__init__(mobject, mobject.saved_state, **kwargs)
class ApplyFunction(Transform):
def __init__(self, function, mobject, **kwargs):
def __init__(
self,
function: Callable[[Mobject], Mobject],
mobject: Mobject,
**kwargs
):
self.function = function
super().__init__(mobject, **kwargs)
def create_target(self):
def create_target(self) -> Mobject:
target = self.function(self.mobject.copy())
if not isinstance(target, Mobject):
raise Exception("Functions passed to ApplyFunction must return object of type Mobject")
@@ -246,7 +270,12 @@ class ApplyFunction(Transform):
class ApplyMatrix(ApplyPointwiseFunction):
def __init__(self, matrix, mobject, **kwargs):
def __init__(
self,
matrix: npt.ArrayLike,
mobject: Mobject,
**kwargs
):
matrix = self.initialize_matrix(matrix)
def func(p):
@@ -254,7 +283,7 @@ class ApplyMatrix(ApplyPointwiseFunction):
super().__init__(func, mobject, **kwargs)
def initialize_matrix(self, matrix):
def initialize_matrix(self, matrix: npt.ArrayLike) -> np.ndarray:
matrix = np.array(matrix)
if matrix.shape == (2, 2):
new_matrix = np.identity(3)
@@ -266,12 +295,17 @@ class ApplyMatrix(ApplyPointwiseFunction):
class ApplyComplexFunction(ApplyMethod):
def __init__(self, function, mobject, **kwargs):
def __init__(
self,
function: Callable[[complex], complex],
mobject: Mobject,
**kwargs
):
self.function = function
method = mobject.apply_complex_function
super().__init__(method, function, **kwargs)
def init_path_func(self):
def init_path_func(self) -> None:
func1 = self.function(complex(1))
self.path_arc = np.log(func1).imag
super().init_path_func()
@@ -280,54 +314,18 @@ class ApplyComplexFunction(ApplyMethod):
class CyclicReplace(Transform):
CONFIG = {
"path_arc": 90 * DEGREES,
}
def __init__(self, *mobjects: Mobject, path_arc=90 * DEG, **kwargs):
super().__init__(Group(*mobjects), path_arc=path_arc, **kwargs)
def __init__(self, *mobjects, **kwargs):
self.group = Group(*mobjects)
super().__init__(self.group, **kwargs)
def create_target(self):
target = self.group.copy()
def create_target(self) -> Mobject:
group = self.mobject
target = group.copy()
cycled_targets = [target[-1], *target[:-1]]
for m1, m2 in zip(cycled_targets, self.group):
for m1, m2 in zip(cycled_targets, group):
m1.move_to(m2)
return target
class Swap(CyclicReplace):
pass # Renaming, more understandable for two entries
# TODO, this may be deprecated...worth reimplementing?
class TransformAnimations(Transform):
CONFIG = {
"rate_func": squish_rate_func(smooth)
}
def __init__(self, start_anim, end_anim, **kwargs):
digest_config(self, kwargs, locals())
if "run_time" in kwargs:
self.run_time = kwargs.pop("run_time")
else:
self.run_time = max(start_anim.run_time, end_anim.run_time)
for anim in start_anim, end_anim:
anim.set_run_time(self.run_time)
if start_anim.starting_mobject.get_num_points() != end_anim.starting_mobject.get_num_points():
start_anim.starting_mobject.align_data_and_family(end_anim.starting_mobject)
for anim in start_anim, end_anim:
if hasattr(anim, "target_mobject"):
anim.starting_mobject.align_data_and_family(anim.target_mobject)
Transform.__init__(self, start_anim.mobject,
end_anim.mobject, **kwargs)
# Rewire starting and ending mobjects
start_anim.mobject = self.starting_mobject
end_anim.mobject = self.target_mobject
def interpolate(self, alpha):
self.start_anim.interpolate(alpha)
self.end_anim.interpolate(alpha)
Transform.interpolate(self, alpha)
"""Alternate name for CyclicReplace"""
pass

View File

@@ -1,141 +1,191 @@
import numpy as np
from __future__ import annotations
import itertools as it
from difflib import SequenceMatcher
from manimlib.animation.composition import AnimationGroup
from manimlib.animation.fading import FadeTransformPieces
from manimlib.animation.fading import FadeInFromPoint
from manimlib.animation.fading import FadeOutToPoint
from manimlib.animation.transform import Transform
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Group
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config
from manimlib.mobject.svg.string_mobject import StringMobject
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterable
from manimlib.scene.scene import Scene
class TransformMatchingParts(AnimationGroup):
CONFIG = {
"mobject_type": Mobject,
"group_type": Group,
"transform_mismatches": False,
"fade_transform_mismatches": False,
"key_map": dict(),
}
def __init__(
self,
source: Mobject,
target: Mobject,
matched_pairs: Iterable[tuple[Mobject, Mobject]] = [],
match_animation: type = Transform,
mismatch_animation: type = Transform,
run_time: float = 2,
lag_ratio: float = 0,
**kwargs,
):
self.source = source
self.target = target
self.match_animation = match_animation
self.mismatch_animation = mismatch_animation
self.anim_config = dict(**kwargs)
def __init__(self, mobject, target_mobject, **kwargs):
digest_config(self, kwargs)
assert(isinstance(mobject, self.mobject_type))
assert(isinstance(target_mobject, self.mobject_type))
source_map = self.get_shape_map(mobject)
target_map = self.get_shape_map(target_mobject)
# We will progressively build up a list of transforms
# from pieces in source to those in target. These
# two lists keep track of which pieces are accounted
# for so far
self.source_pieces = source.family_members_with_points()
self.target_pieces = target.family_members_with_points()
self.anims = []
# Create two mobjects whose submobjects all match each other
# according to whatever keys are used for source_map and
# target_map
transform_source = self.group_type()
transform_target = self.group_type()
kwargs["final_alpha_value"] = 0
for key in set(source_map).intersection(target_map):
transform_source.add(source_map[key])
transform_target.add(target_map[key])
anims = [Transform(transform_source, transform_target, **kwargs)]
# User can manually specify when one part should transform
# into another despite not matching by using key_map
key_mapped_source = self.group_type()
key_mapped_target = self.group_type()
for key1, key2 in self.key_map.items():
if key1 in source_map and key2 in target_map:
key_mapped_source.add(source_map[key1])
key_mapped_target.add(target_map[key2])
source_map.pop(key1, None)
target_map.pop(key2, None)
if len(key_mapped_source) > 0:
anims.append(FadeTransformPieces(
key_mapped_source,
key_mapped_target,
for pair in matched_pairs:
self.add_transform(*pair)
# Match any pairs with the same shape
for pair in self.find_pairs_with_matching_shapes(self.source_pieces, self.target_pieces):
self.add_transform(*pair)
# Finally, account for mismatches
for source_piece in self.source_pieces:
if any([source_piece in anim.mobject.get_family() for anim in self.anims]):
continue
self.anims.append(FadeOutToPoint(
source_piece, target.get_center(),
**self.anim_config
))
for target_piece in self.target_pieces:
if any([target_piece in anim.mobject.get_family() for anim in self.anims]):
continue
self.anims.append(FadeInFromPoint(
target_piece, source.get_center(),
**self.anim_config
))
fade_source = self.group_type()
fade_target = self.group_type()
for key in set(source_map).difference(target_map):
fade_source.add(source_map[key])
for key in set(target_map).difference(source_map):
fade_target.add(target_map[key])
super().__init__(
*self.anims,
run_time=run_time,
lag_ratio=lag_ratio,
)
if self.transform_mismatches:
anims.append(Transform(fade_source.copy(), fade_target, **kwargs))
if self.fade_transform_mismatches:
anims.append(FadeTransformPieces(fade_source, fade_target, **kwargs))
else:
anims.append(FadeOutToPoint(
fade_source, target_mobject.get_center(), **kwargs
))
anims.append(FadeInFromPoint(
fade_target.copy(), mobject.get_center(), **kwargs
))
def add_transform(
self,
source: Mobject,
target: Mobject,
):
new_source_pieces = source.family_members_with_points()
new_target_pieces = target.family_members_with_points()
if len(new_source_pieces) == 0 or len(new_target_pieces) == 0:
# Don't animate null sorces or null targets
return
source_is_new = all(char in self.source_pieces for char in new_source_pieces)
target_is_new = all(char in self.target_pieces for char in new_target_pieces)
if not source_is_new or not target_is_new:
return
super().__init__(*anims)
transform_type = self.mismatch_animation
if source.has_same_shape_as(target):
transform_type = self.match_animation
self.to_remove = mobject
self.to_add = target_mobject
self.anims.append(transform_type(source, target, **self.anim_config))
for char in new_source_pieces:
self.source_pieces.remove(char)
for char in new_target_pieces:
self.target_pieces.remove(char)
def get_shape_map(self, mobject):
shape_map = {}
for sm in self.get_mobject_parts(mobject):
key = self.get_mobject_key(sm)
if key not in shape_map:
shape_map[key] = VGroup()
shape_map[key].add(sm)
return shape_map
def find_pairs_with_matching_shapes(
self,
chars1: list[Mobject],
chars2: list[Mobject]
) -> list[tuple[Mobject, Mobject]]:
result = []
for char1, char2 in it.product(chars1, chars2):
if char1.has_same_shape_as(char2):
result.append((char1, char2))
return result
def clean_up_from_scene(self, scene):
for anim in self.animations:
anim.update(0)
def clean_up_from_scene(self, scene: Scene) -> None:
super().clean_up_from_scene(scene)
scene.remove(self.mobject)
scene.remove(self.to_remove)
scene.add(self.to_add)
@staticmethod
def get_mobject_parts(mobject):
# To be implemented in subclass
return mobject
@staticmethod
def get_mobject_key(mobject):
# To be implemented in subclass
return hash(mobject)
scene.add(self.target)
class TransformMatchingShapes(TransformMatchingParts):
CONFIG = {
"mobject_type": VMobject,
"group_type": VGroup,
}
@staticmethod
def get_mobject_parts(mobject):
return mobject.family_members_with_points()
@staticmethod
def get_mobject_key(mobject):
mobject.save_state()
mobject.center()
mobject.set_height(1)
result = hash(np.round(mobject.get_points(), 3).tobytes())
mobject.restore()
return result
"""Alias for TransformMatchingParts"""
pass
class TransformMatchingTex(TransformMatchingParts):
CONFIG = {
"mobject_type": VMobject,
"group_type": VGroup,
}
class TransformMatchingStrings(TransformMatchingParts):
def __init__(
self,
source: StringMobject,
target: StringMobject,
matched_keys: Iterable[str] = [],
key_map: dict[str, str] = dict(),
matched_pairs: Iterable[tuple[VMobject, VMobject]] = [],
**kwargs,
):
matched_pairs = [
*matched_pairs,
*self.matching_blocks(source, target, matched_keys, key_map),
]
@staticmethod
def get_mobject_parts(mobject):
return mobject.submobjects
super().__init__(
source, target,
matched_pairs=matched_pairs,
**kwargs,
)
@staticmethod
def get_mobject_key(mobject):
return mobject.get_tex()
def matching_blocks(
self,
source: StringMobject,
target: StringMobject,
matched_keys: Iterable[str],
key_map: dict[str, str]
) -> list[tuple[VMobject, VMobject]]:
syms1 = source.get_symbol_substrings()
syms2 = target.get_symbol_substrings()
counts1 = list(map(source.substr_to_path_count, syms1))
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()]
# 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():
syms1[i] = "Null1"
for j in range(len(syms2)):
if target[j] in sub_target.family_members_with_points():
syms2[j] = "Null2"
# Group together longest matching substrings
while True:
matcher = SequenceMatcher(None, syms1, syms2)
match = matcher.find_longest_match(0, len(syms1), 0, len(syms2))
if match.size == 0:
break
i1 = sum(counts1[:match.a])
i2 = sum(counts2[:match.b])
size = sum(counts1[match.a:match.a + match.size])
blocks.append((source[i1:i1 + size], target[i2:i2 + size]))
for i in range(match.size):
syms1[match.a + i] = "Null1"
syms2[match.b + i] = "Null2"
return blocks
class TransformMatchingTex(TransformMatchingStrings):
"""Alias for TransformMatchingStrings"""
pass

View File

@@ -1,7 +1,14 @@
import operator as op
from __future__ import annotations
from manimlib.animation.animation import Animation
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
from manimlib.mobject.mobject import Mobject
class UpdateFromFunc(Animation):
"""
@@ -9,33 +16,51 @@ class UpdateFromFunc(Animation):
to be used when the state of one mobject is dependent
on another simultaneously animated mobject
"""
CONFIG = {
"suspend_mobject_updating": False,
}
def __init__(self, mobject, update_function, **kwargs):
def __init__(
self,
mobject: Mobject,
update_function: Callable[[Mobject], Mobject | None],
suspend_mobject_updating: bool = False,
**kwargs
):
self.update_function = update_function
super().__init__(mobject, **kwargs)
super().__init__(
mobject,
suspend_mobject_updating=suspend_mobject_updating,
**kwargs
)
def interpolate_mobject(self, alpha):
def interpolate_mobject(self, alpha: float) -> None:
self.update_function(self.mobject)
class UpdateFromAlphaFunc(UpdateFromFunc):
def interpolate_mobject(self, alpha):
class UpdateFromAlphaFunc(Animation):
def __init__(
self,
mobject: Mobject,
update_function: Callable[[Mobject, float], Mobject | None],
suspend_mobject_updating: bool = False,
**kwargs
):
self.update_function = update_function
super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs)
def interpolate_mobject(self, alpha: float) -> None:
self.update_function(self.mobject, alpha)
class MaintainPositionRelativeTo(Animation):
def __init__(self, mobject, tracked_mobject, **kwargs):
def __init__(
self,
mobject: Mobject,
tracked_mobject: Mobject,
**kwargs
):
self.tracked_mobject = tracked_mobject
self.diff = op.sub(
mobject.get_center(),
tracked_mobject.get_center(),
)
self.diff = mobject.get_center() - tracked_mobject.get_center()
super().__init__(mobject, **kwargs)
def interpolate_mobject(self, alpha):
def interpolate_mobject(self, alpha: float) -> None:
target = self.tracked_mobject.get_center()
location = self.mobject.get_center()
self.mobject.shift(target - location + self.diff)

View File

@@ -1,285 +1,152 @@
from __future__ import annotations
import moderngl
import math
from colour import Color
import OpenGL.GL as gl
from PIL import Image
import numpy as np
import itertools as it
import OpenGL.GL as gl
from PIL import Image
from manimlib.constants import *
from manimlib.camera.camera_frame import CameraFrame
from manimlib.constants import BLACK
from manimlib.constants import DEFAULT_RESOLUTION
from manimlib.constants import FRAME_HEIGHT
from manimlib.constants import FRAME_WIDTH
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Point
from manimlib.utils.config_ops import digest_config
from manimlib.utils.simple_functions import fdiv
from manimlib.utils.simple_functions import clip
from manimlib.utils.space_ops import angle_of_vector
from manimlib.utils.space_ops import rotation_matrix_transpose_from_quaternion
from manimlib.utils.space_ops import rotation_matrix_transpose
from manimlib.utils.space_ops import quaternion_from_angle_axis
from manimlib.utils.space_ops import quaternion_mult
from manimlib.utils.color import color_to_rgba
from typing import TYPE_CHECKING
class CameraFrame(Mobject):
CONFIG = {
"frame_shape": (FRAME_WIDTH, FRAME_HEIGHT),
"center_point": ORIGIN,
# Theta, phi, gamma
"euler_angles": [0, 0, 0],
"focal_distance": 2,
}
def init_data(self):
super().init_data()
self.data["euler_angles"] = np.array(self.euler_angles, dtype=float)
self.refresh_rotation_matrix()
def init_points(self):
self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP])
self.set_width(self.frame_shape[0], stretch=True)
self.set_height(self.frame_shape[1], stretch=True)
self.move_to(self.center_point)
def to_default_state(self):
self.center()
self.set_height(FRAME_HEIGHT)
self.set_width(FRAME_WIDTH)
self.set_euler_angles(0, 0, 0)
return self
def get_euler_angles(self):
return self.data["euler_angles"]
def get_inverse_camera_rotation_matrix(self):
return self.inverse_camera_rotation_matrix
def refresh_rotation_matrix(self):
# Rotate based on camera orientation
theta, phi, gamma = self.get_euler_angles()
quat = quaternion_mult(
quaternion_from_angle_axis(theta, OUT, axis_normalized=True),
quaternion_from_angle_axis(phi, RIGHT, axis_normalized=True),
quaternion_from_angle_axis(gamma, OUT, axis_normalized=True),
)
self.inverse_camera_rotation_matrix = rotation_matrix_transpose_from_quaternion(quat)
def rotate(self, angle, axis=OUT, **kwargs):
curr_rot_T = self.get_inverse_camera_rotation_matrix()
added_rot_T = rotation_matrix_transpose(angle, axis)
new_rot_T = np.dot(curr_rot_T, added_rot_T)
Fz = new_rot_T[2]
phi = np.arccos(clip(Fz[2], -1, 1))
theta = angle_of_vector(Fz[:2]) + PI / 2
partial_rot_T = np.dot(
rotation_matrix_transpose(phi, RIGHT),
rotation_matrix_transpose(theta, OUT),
)
gamma = angle_of_vector(np.dot(partial_rot_T, new_rot_T.T)[:, 0])
self.set_euler_angles(theta, phi, gamma)
return self
def set_euler_angles(self, theta=None, phi=None, gamma=None, units=RADIANS):
if theta is not None:
self.data["euler_angles"][0] = theta * units
if phi is not None:
self.data["euler_angles"][1] = phi * units
if gamma is not None:
self.data["euler_angles"][2] = gamma * units
self.refresh_rotation_matrix()
return self
def reorient(self, theta_degrees=None, phi_degrees=None, gamma_degrees=None):
"""
Shortcut for set_euler_angles, defaulting to taking
in angles in degrees
"""
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES)
return self
def set_theta(self, theta):
return self.set_euler_angles(theta=theta)
def set_phi(self, phi):
return self.set_euler_angles(phi=phi)
def set_gamma(self, gamma):
return self.set_euler_angles(gamma=gamma)
def increment_theta(self, dtheta):
self.data["euler_angles"][0] += dtheta
self.refresh_rotation_matrix()
return self
def increment_phi(self, dphi):
phi = self.data["euler_angles"][1]
new_phi = clip(phi + dphi, 0, PI)
self.data["euler_angles"][1] = new_phi
self.refresh_rotation_matrix()
return self
def increment_gamma(self, dgamma):
self.data["euler_angles"][2] += dgamma
self.refresh_rotation_matrix()
return self
def get_theta(self):
return self.data["euler_angles"][0]
def get_phi(self):
return self.data["euler_angles"][1]
def get_gamma(self):
return self.data["euler_angles"][2]
def get_shape(self):
return (self.get_width(), self.get_height())
def get_center(self):
# Assumes first point is at the center
return self.get_points()[0]
def get_width(self):
points = self.get_points()
return points[2, 0] - points[1, 0]
def get_height(self):
points = self.get_points()
return points[4, 1] - points[3, 1]
def get_focal_distance(self):
return self.focal_distance * self.get_height()
def get_implied_camera_location(self):
theta, phi, gamma = self.get_euler_angles()
dist = self.get_focal_distance()
x, y, z = self.get_center()
return (
x + dist * math.sin(theta) * math.sin(phi),
y - dist * math.cos(theta) * math.sin(phi),
z + dist * math.cos(phi)
)
def interpolate(self, *args, **kwargs):
super().interpolate(*args, **kwargs)
self.refresh_rotation_matrix()
if TYPE_CHECKING:
from typing import Optional
from manimlib.typing import ManimColor, Vect3
from manimlib.window import Window
class Camera(object):
CONFIG = {
"background_image": None,
"frame_config": {},
"pixel_width": DEFAULT_PIXEL_WIDTH,
"pixel_height": DEFAULT_PIXEL_HEIGHT,
"frame_rate": DEFAULT_FRAME_RATE,
# Note: frame height and width will be resized to match
# the pixel aspect ratio
"background_color": BLACK,
"background_opacity": 1,
def __init__(
self,
window: Optional[Window] = None,
background_image: Optional[str] = None,
frame_config: dict = dict(),
# 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
# than this value will be rescaled.
"max_allowable_norm": FRAME_WIDTH,
"image_mode": "RGBA",
"n_channels": 4,
"pixel_array_dtype": 'uint8',
"light_source_position": [-10, 10, 10],
# Measured in pixel widths, used for vector graphics
"anti_alias_width": 1.5,
max_allowable_norm: float = FRAME_WIDTH,
image_mode: str = "RGBA",
n_channels: int = 4,
pixel_array_dtype: type = np.uint8,
light_source_position: Vect3 = np.array([-10, 10, 10]),
# Although vector graphics handle antialiasing fine
# without multisampling, for 3d scenes one might want
# to set samples to be greater than 0.
"samples": 0,
}
samples: int = 0,
):
self.window = window
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
self.n_channels = n_channels
self.pixel_array_dtype = pixel_array_dtype
self.light_source_position = light_source_position
self.samples = samples
def __init__(self, ctx=None, **kwargs):
digest_config(self, kwargs, locals())
self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
self.background_rgba = [
*Color(self.background_color).get_rgb(),
self.background_opacity
]
self.init_frame()
self.init_context(ctx)
self.init_shaders()
self.init_textures()
self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max
self.background_rgba: list[float] = list(color_to_rgba(
background_color, background_opacity
))
self.uniforms = dict()
self.init_frame(**frame_config)
self.init_context()
self.init_fbo()
self.init_light_source()
self.refresh_perspective_uniforms()
self.static_mobject_to_render_group_list = {}
def init_frame(self):
self.frame = CameraFrame(**self.frame_config)
def init_frame(self, **config) -> None:
self.frame = CameraFrame(**config)
def init_context(self, ctx=None):
if ctx is None:
ctx = moderngl.create_standalone_context()
fbo = self.get_fbo(ctx, 0)
def init_context(self) -> None:
if self.window is None:
self.ctx: moderngl.Context = moderngl.create_standalone_context()
else:
fbo = ctx.detect_framebuffer()
self.ctx = ctx
self.fbo = fbo
self.set_ctx_blending()
self.ctx: moderngl.Context = self.window.ctx
# For multisample antialiasing
fbo_msaa = self.get_fbo(ctx, self.samples)
fbo_msaa.use()
self.fbo_msaa = fbo_msaa
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
self.ctx.enable(moderngl.BLEND)
def set_ctx_blending(self, enable=True):
if enable:
self.ctx.enable(moderngl.BLEND)
def init_fbo(self) -> None:
# This is the buffer used when writing to a video/image file
self.fbo_for_files = self.get_fbo(self.samples)
# This is the frame buffer we'll draw into when emitting frames
self.draw_fbo = self.get_fbo(samples=0)
if self.window is None:
self.window_fbo = None
self.fbo = self.fbo_for_files
else:
self.ctx.disable(moderngl.BLEND)
self.ctx.blend_func = (
moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
# moderngl.ONE, moderngl.ONE
)
self.window_fbo = self.ctx.detect_framebuffer()
self.fbo = self.window_fbo
def set_ctx_depth_test(self, enable=True):
if enable:
self.ctx.enable(moderngl.DEPTH_TEST)
else:
self.ctx.disable(moderngl.DEPTH_TEST)
self.fbo.use()
def init_light_source(self):
def init_light_source(self) -> None:
self.light_source = Point(self.light_source_position)
def use_window_fbo(self, use: bool = True):
assert self.window is not None
if use:
self.fbo = self.window_fbo
else:
self.fbo = self.fbo_for_files
# Methods associated with the frame buffer
def get_fbo(self, ctx, samples=0):
pw = self.pixel_width
ph = self.pixel_height
return ctx.framebuffer(
color_attachments=ctx.texture(
(pw, ph),
def get_fbo(
self,
samples: int = 0
) -> moderngl.Framebuffer:
return self.ctx.framebuffer(
color_attachments=self.ctx.texture(
self.default_pixel_shape,
components=self.n_channels,
samples=samples,
),
depth_attachment=ctx.depth_renderbuffer(
(pw, ph),
depth_attachment=self.ctx.depth_renderbuffer(
self.default_pixel_shape,
samples=samples
)
)
def clear(self):
def clear(self) -> None:
self.fbo.clear(*self.background_rgba)
self.fbo_msaa.clear(*self.background_rgba)
if self.window:
self.window.clear(*self.background_rgba)
def reset_pixel_shape(self, new_width, new_height):
self.pixel_width = new_width
self.pixel_height = new_height
self.refresh_perspective_uniforms()
def blit(self, src_fbo, dst_fbo):
"""
Copy blocks between fbo's using Blit
"""
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, src_fbo.glo)
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst_fbo.glo)
gl.glBlitFramebuffer(
*src_fbo.viewport,
*dst_fbo.viewport,
gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
)
def get_raw_fbo_data(self, dtype='f1'):
# Copy blocks from the fbo_msaa to the drawn fbo using Blit
pw, ph = (self.pixel_width, self.pixel_height)
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo_msaa.glo)
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.fbo.glo)
gl.glBlitFramebuffer(0, 0, pw, ph, 0, 0, pw, ph, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR)
return self.fbo.read(
viewport=self.fbo.viewport,
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
self.blit(self.fbo, self.draw_fbo)
return self.draw_fbo.read(
viewport=self.draw_fbo.viewport,
components=self.n_channels,
dtype=dtype,
)
def get_image(self, pixel_array=None):
def get_image(self) -> Image.Image:
return Image.frombytes(
'RGBA',
self.get_pixel_shape(),
@@ -287,15 +154,16 @@ class Camera(object):
'raw', 'RGBA', 0, -1
)
def get_pixel_array(self):
def get_pixel_array(self) -> np.ndarray:
raw = self.get_raw_fbo_data(dtype='f4')
flat_arr = np.frombuffer(raw, dtype='f4')
arr = flat_arr.reshape([*self.fbo.size, self.n_channels])
arr = flat_arr.reshape([*reversed(self.draw_fbo.size), self.n_channels])
arr = arr[::-1]
# Convert from float
return (self.rgb_max_val * arr).astype(self.pixel_array_dtype)
# Needed?
def get_texture(self):
def get_texture(self) -> moderngl.Texture:
texture = self.ctx.texture(
size=self.fbo.size,
components=4,
@@ -305,201 +173,89 @@ class Camera(object):
return texture
# Getting camera attributes
def get_pixel_shape(self):
return self.fbo.viewport[2:4]
# return (self.pixel_width, self.pixel_height)
def get_pixel_size(self) -> float:
return self.frame.get_width() / self.get_pixel_shape()[0]
def get_pixel_width(self):
def get_pixel_shape(self) -> tuple[int, int]:
return self.fbo.size
def get_pixel_width(self) -> int:
return self.get_pixel_shape()[0]
def get_pixel_height(self):
def get_pixel_height(self) -> int:
return self.get_pixel_shape()[1]
def get_frame_height(self):
def get_aspect_ratio(self):
pw, ph = self.get_pixel_shape()
return pw / ph
def get_frame_height(self) -> float:
return self.frame.get_height()
def get_frame_width(self):
def get_frame_width(self) -> float:
return self.frame.get_width()
def get_frame_shape(self):
def get_frame_shape(self) -> tuple[float, float]:
return (self.get_frame_width(), self.get_frame_height())
def get_frame_center(self):
def get_frame_center(self) -> np.ndarray:
return self.frame.get_center()
def get_location(self):
def get_location(self) -> tuple[float, float, float]:
return self.frame.get_implied_camera_location()
def resize_frame_shape(self, fixed_dimension=0):
def resize_frame_shape(self, fixed_dimension: bool = False) -> None:
"""
Changes frame_shape to match the aspect ratio
of the pixels, where fixed_dimension determines
whether frame_height or frame_width
remains fixed while the other changes accordingly.
"""
pixel_height = self.get_pixel_height()
pixel_width = self.get_pixel_width()
frame_height = self.get_frame_height()
frame_width = self.get_frame_width()
aspect_ratio = fdiv(pixel_width, pixel_height)
if fixed_dimension == 0:
aspect_ratio = self.get_aspect_ratio()
if not fixed_dimension:
frame_height = frame_width / aspect_ratio
else:
frame_width = aspect_ratio * frame_height
self.frame.set_height(frame_height)
self.frame.set_width(frame_width)
self.frame.set_height(frame_height, stretch=True)
self.frame.set_width(frame_width, stretch=True)
# Rendering
def capture(self, *mobjects, **kwargs):
self.refresh_perspective_uniforms()
def capture(self, *mobjects: Mobject) -> None:
self.clear()
self.refresh_uniforms()
self.fbo.use()
for mobject in mobjects:
for render_group in self.get_render_group_list(mobject):
self.render(render_group)
mobject.render(self.ctx, self.uniforms)
def render(self, render_group):
shader_wrapper = render_group["shader_wrapper"]
shader_program = render_group["prog"]
self.set_shader_uniforms(shader_program, shader_wrapper)
self.set_ctx_depth_test(shader_wrapper.depth_test)
render_group["vao"].render(int(shader_wrapper.render_primitive))
if render_group["single_use"]:
self.release_render_group(render_group)
if self.window:
self.window.swap_buffers()
if self.fbo is not self.window_fbo:
self.blit(self.fbo, self.window_fbo)
self.window.swap_buffers()
def get_render_group_list(self, mobject):
try:
return self.static_mobject_to_render_group_list[id(mobject)]
except KeyError:
return map(self.get_render_group, mobject.get_shader_wrapper_list())
def get_render_group(self, shader_wrapper, single_use=True):
# Data buffers
vbo = self.ctx.buffer(shader_wrapper.vert_data.tobytes())
if shader_wrapper.vert_indices is None:
ibo = None
else:
vert_index_data = shader_wrapper.vert_indices.astype('i4').tobytes()
if vert_index_data:
ibo = self.ctx.buffer(vert_index_data)
else:
ibo = None
# Program and vertex array
shader_program, vert_format = self.get_shader_program(shader_wrapper)
vao = self.ctx.vertex_array(
program=shader_program,
content=[(vbo, vert_format, *shader_wrapper.vert_attributes)],
index_buffer=ibo,
)
return {
"vbo": vbo,
"ibo": ibo,
"vao": vao,
"prog": shader_program,
"shader_wrapper": shader_wrapper,
"single_use": single_use,
}
def release_render_group(self, render_group):
for key in ["vbo", "ibo", "vao"]:
if render_group[key] is not None:
render_group[key].release()
def set_mobjects_as_static(self, *mobjects):
# Creates buffer and array objects holding each mobjects shader data
for mob in mobjects:
self.static_mobject_to_render_group_list[id(mob)] = [
self.get_render_group(sw, single_use=False)
for sw in mob.get_shader_wrapper_list()
]
def release_static_mobjects(self):
for rg_list in self.static_mobject_to_render_group_list.values():
for render_group in rg_list:
self.release_render_group(render_group)
self.static_mobject_to_render_group_list = {}
# Shaders
def init_shaders(self):
# Initialize with the null id going to None
self.id_to_shader_program = {"": None}
def get_shader_program(self, shader_wrapper):
sid = shader_wrapper.get_program_id()
if sid not in self.id_to_shader_program:
# Create shader program for the first time, then cache
# in the id_to_shader_program dictionary
program = self.ctx.program(**shader_wrapper.get_program_code())
vert_format = moderngl.detect_format(program, shader_wrapper.vert_attributes)
self.id_to_shader_program[sid] = (program, vert_format)
return self.id_to_shader_program[sid]
def set_shader_uniforms(self, shader, shader_wrapper):
for name, path in shader_wrapper.texture_paths.items():
tid = self.get_texture_id(path)
shader[name].value = tid
for name, value in it.chain(self.perspective_uniforms.items(), shader_wrapper.uniforms.items()):
try:
if isinstance(value, np.ndarray):
value = tuple(value)
shader[name].value = value
except KeyError:
pass
def refresh_perspective_uniforms(self):
def refresh_uniforms(self) -> None:
frame = self.frame
pw, ph = self.get_pixel_shape()
fw, fh = frame.get_shape()
# TODO, this should probably be a mobject uniform, with
# the camera taking care of the conversion factor
anti_alias_width = self.anti_alias_width / (ph / fh)
# Orient light
rotation = frame.get_inverse_camera_rotation_matrix()
offset = frame.get_center()
light_pos = np.dot(
rotation, self.light_source.get_location() + offset
view_matrix = frame.get_view_matrix()
light_pos = self.light_source.get_location()
cam_pos = self.frame.get_implied_camera_location()
self.uniforms.update(
view=tuple(view_matrix.T.flatten()),
frame_scale=frame.get_scale(),
frame_rescale_factors=(
2.0 / FRAME_WIDTH,
2.0 / FRAME_HEIGHT,
frame.get_scale() / frame.get_focal_distance(),
),
pixel_size=self.get_pixel_size(),
camera_position=tuple(cam_pos),
light_position=tuple(light_pos),
)
cam_pos = self.frame.get_implied_camera_location() # TODO
self.perspective_uniforms = {
"frame_shape": frame.get_shape(),
"anti_alias_width": anti_alias_width,
"camera_offset": tuple(offset),
"camera_rotation": tuple(np.array(rotation).T.flatten()),
"camera_position": tuple(cam_pos),
"light_source_position": tuple(light_pos),
"focal_distance": frame.get_focal_distance(),
}
def init_textures(self):
self.n_textures = 0
self.path_to_texture = {}
def get_texture_id(self, path):
if path not in self.path_to_texture:
if self.n_textures == 15: # I have no clue why this is needed
self.n_textures += 1
tid = self.n_textures
self.n_textures += 1
im = Image.open(path).convert("RGBA")
texture = self.ctx.texture(
size=im.size,
components=len(im.getbands()),
data=im.tobytes(),
)
texture.use(location=tid)
self.path_to_texture[path] = (tid, texture)
return self.path_to_texture[path][0]
def release_texture(self, path):
tid_and_texture = self.path_to_texture.pop(path, None)
if tid_and_texture:
tid_and_texture[1].release()
return self
# Mostly just defined so old scenes don't break
class ThreeDCamera(Camera):
CONFIG = {
"samples": 4,
"anti_alias_width": 0,
}
def __init__(self, samples: int = 4, **kwargs):
super().__init__(samples=samples, **kwargs)

View File

@@ -0,0 +1,266 @@
from __future__ import annotations
import math
import warnings
import numpy as np
from scipy.spatial.transform import Rotation
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
from manimlib.mobject.mobject import Mobject
from manimlib.utils.space_ops import normalize
from manimlib.utils.simple_functions import clip
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import Vect3
class CameraFrame(Mobject):
def __init__(
self,
frame_shape: tuple[float, float] = FRAME_SHAPE,
center_point: Vect3 = ORIGIN,
# Field of view in the y direction
fovy: float = 45 * DEG,
euler_axes: str = "zxz",
# This keeps it ordered first in a scene
z_index=-1,
**kwargs,
):
super().__init__(z_index=z_index, **kwargs)
self.uniforms["orientation"] = Rotation.identity().as_quat()
self.uniforms["fovy"] = fovy
self.default_orientation = Rotation.identity()
self.view_matrix = np.identity(4)
self.id4x4 = np.identity(4)
self.camera_location = OUT # This will be updated by set_points
self.euler_axes = euler_axes
self.set_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP]))
self.set_width(frame_shape[0], stretch=True)
self.set_height(frame_shape[1], stretch=True)
self.move_to(center_point)
def set_orientation(self, rotation: Rotation):
self.uniforms["orientation"][:] = rotation.as_quat()
return self
def get_orientation(self):
return Rotation.from_quat(self.uniforms["orientation"])
def make_orientation_default(self):
self.default_orientation = self.get_orientation()
return self
def to_default_state(self):
self.set_shape(*FRAME_SHAPE)
self.center()
self.set_orientation(self.default_orientation)
return self
def get_euler_angles(self) -> np.ndarray:
orientation = self.get_orientation()
if np.isclose(orientation.as_quat(), [0, 0, 0, 1]).all():
return np.zeros(3)
with warnings.catch_warnings():
warnings.simplefilter('ignore', UserWarning) # Ignore UserWarnings
angles = orientation.as_euler(self.euler_axes)[::-1]
# Handle Gimble lock case
if self.euler_axes == "zxz":
if np.isclose(angles[1], 0, atol=1e-2):
angles[0] = angles[0] + angles[2]
angles[2] = 0
if np.isclose(angles[1], PI, atol=1e-2):
angles[0] = angles[0] - angles[2]
angles[2] = 0
return angles
def get_theta(self):
return self.get_euler_angles()[0]
def get_phi(self):
return self.get_euler_angles()[1]
def get_gamma(self):
return self.get_euler_angles()[2]
def get_scale(self):
return self.get_height() / FRAME_SHAPE[1]
def get_inverse_camera_rotation_matrix(self):
return self.get_orientation().as_matrix().T
def get_view_matrix(self, refresh=False):
"""
Returns a 4x4 for the affine transformation mapping a point
into the camera's internal coordinate system
"""
if self._data_has_changed:
shift = self.id4x4.copy()
rotation = self.id4x4.copy()
scale = self.get_scale()
shift[:3, 3] = -self.get_center()
rotation[:3, :3] = self.get_inverse_camera_rotation_matrix()
np.dot(rotation, shift, out=self.view_matrix)
if scale > 0:
self.view_matrix[:3, :4] /= scale
return self.view_matrix
def get_inv_view_matrix(self):
return np.linalg.inv(self.get_view_matrix())
@Mobject.affects_data
def interpolate(self, *args, **kwargs):
super().interpolate(*args, **kwargs)
@Mobject.affects_data
def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
rot = Rotation.from_rotvec(angle * normalize(axis))
self.set_orientation(rot * self.get_orientation())
return self
def set_euler_angles(
self,
theta: float | None = None,
phi: float | None = None,
gamma: float | None = None,
units: float = RADIANS
):
eulers = self.get_euler_angles() # theta, phi, gamma
for i, var in enumerate([theta, phi, gamma]):
if var is not None:
eulers[i] = var * units
if all(eulers == 0):
rot = Rotation.identity()
else:
rot = Rotation.from_euler(self.euler_axes, eulers[::-1])
self.set_orientation(rot)
return self
def increment_euler_angles(
self,
dtheta: float = 0,
dphi: float = 0,
dgamma: float = 0,
units: float = RADIANS
):
angles = self.get_euler_angles()
new_angles = angles + np.array([dtheta, dphi, dgamma]) * units
# Limit range for phi
if self.euler_axes == "zxz":
new_angles[1] = clip(new_angles[1], 0, PI)
elif self.euler_axes == "zxy":
new_angles[1] = clip(new_angles[1], -PI / 2, PI / 2)
new_rot = Rotation.from_euler(self.euler_axes, new_angles[::-1])
self.set_orientation(new_rot)
return self
def set_euler_axes(self, seq: str):
self.euler_axes = seq
def reorient(
self,
theta_degrees: float | None = None,
phi_degrees: float | None = None,
gamma_degrees: float | None = None,
center: Vect3 | tuple[float, float, float] | None = None,
height: float | None = None
):
"""
Shortcut for set_euler_angles, defaulting to taking
in angles in 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:
self.set_height(height)
return self
def set_theta(self, theta: float):
return self.set_euler_angles(theta=theta)
def set_phi(self, phi: float):
return self.set_euler_angles(phi=phi)
def set_gamma(self, gamma: float):
return self.set_euler_angles(gamma=gamma)
def increment_theta(self, dtheta: float, units=RADIANS):
self.increment_euler_angles(dtheta=dtheta, units=units)
return self
def increment_phi(self, dphi: float, units=RADIANS):
self.increment_euler_angles(dphi=dphi, units=units)
return self
def increment_gamma(self, dgamma: float, units=RADIANS):
self.increment_euler_angles(dgamma=dgamma, units=units)
return self
def add_ambient_rotation(self, angular_speed=1 * DEG):
self.add_updater(lambda m, dt: m.increment_theta(angular_speed * dt))
return self
@Mobject.affects_data
def set_focal_distance(self, focal_distance: float):
self.uniforms["fovy"] = 2 * math.atan(0.5 * self.get_height() / focal_distance)
return self
@Mobject.affects_data
def set_field_of_view(self, field_of_view: float):
self.uniforms["fovy"] = field_of_view
return self
def get_shape(self):
return (self.get_width(), self.get_height())
def get_aspect_ratio(self):
width, height = self.get_shape()
return width / height
def get_center(self) -> np.ndarray:
# Assumes first point is at the center
return self.get_points()[0]
def get_width(self) -> float:
points = self.get_points()
return points[2, 0] - points[1, 0]
def get_height(self) -> float:
points = self.get_points()
return points[4, 1] - points[3, 1]
def get_focal_distance(self) -> float:
return 0.5 * self.get_height() / math.tan(0.5 * self.uniforms["fovy"])
def get_field_of_view(self) -> float:
return self.uniforms["fovy"]
def get_implied_camera_location(self) -> np.ndarray:
if self._data_has_changed:
to_camera = self.get_inverse_camera_rotation_matrix()[2]
dist = self.get_focal_distance()
self.camera_location = self.get_center() + dist * to_camera
return self.camera_location
def to_fixed_frame_point(self, point: Vect3, relative: bool = False):
view = self.get_view_matrix()
point4d = [*point, 0 if relative else 1]
return np.dot(point4d, view.T)[:3]
def from_fixed_frame_point(self, point: Vect3, relative: bool = False):
inv_view = self.get_inv_view_matrix()
point4d = [*point, 0 if relative else 1]
return np.dot(point4d, inv_view.T)[:3]

View File

@@ -1,19 +1,54 @@
from __future__ import annotations
import argparse
import colour
import inspect
import importlib
import inspect
import os
import sys
import yaml
from contextlib import contextmanager
from screeninfo import get_monitors
from pathlib import Path
from ast import literal_eval
from addict import Dict
from manimlib.utils.config_ops import merge_dicts_recursively
from manimlib.utils.init_config import init_customization
from manimlib.logger import log
from manimlib.utils.dict_ops import merge_dicts_recursively
from typing import TYPE_CHECKING
if TYPE_CHECKING:
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():
@@ -23,7 +58,7 @@ def parse_cli():
module_location.add_argument(
"file",
nargs="?",
help="path to file holding the python code for the scene",
help="Path to file holding the python code for the scene",
)
parser.add_argument(
"scene_names",
@@ -43,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",
@@ -66,9 +101,10 @@ def parse_cli():
help="Show window in full screen",
)
parser.add_argument(
"-g", "--save_pngs",
"-p", "--presenter_mode",
action="store_true",
help="Save each frame as a png",
help="Scene will stay paused during wait calls until " + \
"space bar or right arrow is hit, like a slide show"
)
parser.add_argument(
"-i", "--gif",
@@ -80,6 +116,14 @@ def parse_cli():
action="store_true",
help="Render to a movie file with an alpha channel",
)
parser.add_argument(
"--vcodec",
help="Video codec to use with ffmpeg",
)
parser.add_argument(
"--pix_fmt",
help="Pixel format to use for the output of ffmpeg, defaults to `yuv420p`",
)
parser.add_argument(
"-q", "--quiet",
action="store_true",
@@ -101,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",
@@ -111,23 +156,23 @@ def parse_cli():
)
parser.add_argument(
"-n", "--start_at_animation_number",
help="Start rendering not from the first animation, but"
"from another, specified by its index. If you pass"
"in two comma separated values, e.g. \"3,6\", it will end"
help="Start rendering not from the first animation, but " + \
"from another, specified by its index. If you pass " + \
"in two comma separated values, e.g. \"3,6\", it will end " + \
"the rendering at the second value",
)
parser.add_argument(
"-e", "--embed", metavar="LINENO",
help="Takes a line number as an argument, and results"
"in the scene being called as if the line `self.embed()`"
"was inserted into the scene code at that line number."
"-e", "--embed",
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",
help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"",
)
parser.add_argument(
"--frame_rate",
"--fps",
help="Frame rate, as an integer",
)
parser.add_argument(
@@ -139,6 +184,17 @@ def parse_cli():
action="store_true",
help="Leave progress bars displayed in terminal",
)
parser.add_argument(
"--show_animation_progress",
action="store_true",
help="Show progress bar for each animation",
)
parser.add_argument(
"--prerun",
action="store_true",
help="Calculate total framecount, to display in a progress bar, by doing " + \
"an initial run of the scene which skips animations."
)
parser.add_argument(
"--video_dir",
help="Directory to write video",
@@ -156,228 +212,184 @@ 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
except argparse.ArgumentError as err:
log.error(str(err))
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:
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):
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_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
@contextmanager
def insert_embed_line(file_name, lineno):
with open(file_name, 'r') as fp:
lines = fp.readlines()
line = lines[lineno - 1]
n_spaces = len(line) - len(line.lstrip())
lines.insert(lineno, " " * n_spaces + "self.embed()\n")
alt_file = file_name.replace(".py", "_inserted_embed.py")
with open(alt_file, 'w') as fp:
fp.writelines(lines)
try:
yield alt_file
finally:
os.remove(alt_file)
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:
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:
config = merge_dicts_recursively(
config,
local_defaults,
)
else:
with open(__config_file__, "r") as file:
config = yaml.safe_load(file)
return config
def check_temporary_storage(config):
if config["directories"]["temporary_storage"] == "" and sys.platform == "win32":
log.warning(
"You may be using Windows platform and have not specified the path of"
" `temporary_storage`, which may cause OSError. So it is recommended"
" to specify the `temporary_storage` in the config file (.yml)"
)
def get_configuration(args):
global __config_file__
# ensure __config_file__ always exists
if args.config_file is not None:
if not os.path.exists(args.config_file):
log.error(f"Can't find {args.config_file}.")
if sys.platform == 'win32':
log.info(f"Copying default configuration file to {args.config_file}...")
os.system(f"copy default_config.yml {args.config_file}")
elif sys.platform in ["linux2", "darwin"]:
log.info(f"Copying default configuration file to {args.config_file}...")
os.system(f"cp default_config.yml {args.config_file}")
else:
log.info("Please create the configuration file manually.")
log.info("Read configuration from default_config.yml.")
else:
__config_file__ = args.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`"
)
custom_config = get_custom_config()
check_temporary_storage(custom_config)
write_file = any([args.write_file, args.open, args.finder])
def get_file_ext(args: Namespace) -> str:
if args.transparent:
file_ext = ".mov"
elif args.gif:
file_ext = ".gif"
else:
file_ext = ".mp4"
return file_ext
file_writer_config = {
"write_to_movie": not args.skip_animations and write_file,
"break_into_partial_movies": custom_config["break_into_partial_movies"],
"save_last_frame": args.skip_animations and 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": file_ext,
"mirror_module_path": custom_config["directories"]["mirror_module_path"],
"output_directory": args.video_dir or custom_config["directories"]["output"],
"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,
}
if args.embed is None:
module = get_module(args.file)
def get_animations_numbers(args: Namespace) -> tuple[int | None, int | None]:
stan = args.start_at_animation_number
if stan is None:
return (None, None)
elif "," in stan:
return tuple(map(int, stan.split(",")))
else:
with insert_embed_line(args.file, int(args.embed)) as alt_file:
module = get_module(alt_file)
config = {
"module": module,
"scene_names": args.scene_names,
"file_writer_config": file_writer_config,
"quiet": args.quiet or args.write_all,
"write_all": args.write_all,
"skip_animations": args.skip_animations,
"start_at_animation_number": args.start_at_animation_number,
"end_at_animation_number": None,
"preview": not write_file,
"leave_progress_bars": args.leave_progress_bars,
}
# Camera configuration
config["camera_config"] = get_camera_configuration(args, custom_config)
# Default to making window half the screen size
# but make it full screen if -f is passed in
monitors = get_monitors()
mon_index = custom_config["window_monitor"]
monitor = monitors[min(mon_index, len(monitors) - 1)]
window_width = monitor.width
if not (args.full_screen or custom_config["full_screen"]):
window_width //= 2
window_height = window_width * 9 // 16
config["window_config"] = {
"size": (window_width, window_height),
}
# Arguments related to skipping
stan = config["start_at_animation_number"]
if stan is not None:
if "," in stan:
start, end = stan.split(",")
config["start_at_animation_number"] = int(start)
config["end_at_animation_number"] = int(end)
else:
config["start_at_animation_number"] = int(stan)
return config
return int(stan), None
def get_camera_configuration(args, custom_config):
camera_config = {}
camera_qualities = get_custom_config()["camera_qualities"]
if args.low_quality:
quality = camera_qualities["low"]
elif args.medium_quality:
quality = camera_qualities["medium"]
elif args.hd:
quality = camera_qualities["high"]
elif args.uhd:
quality = camera_qualities["ultra_high"]
else:
quality = camera_qualities[camera_qualities["default_quality"]]
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()
rel_path = file_path.relative_to(dir_config.removed_mirror_prefix)
rel_path = Path(str(rel_path).lstrip("_"))
out_dir = Path(out_dir, rel_path).with_suffix("")
return out_dir
if args.resolution:
quality["resolution"] = args.resolution
if args.frame_rate:
quality["frame_rate"] = int(args.frame_rate)
width_str, height_str = quality["resolution"].split("x")
width = int(width_str)
height = int(height_str)
camera_config.update({
"pixel_width": width,
"pixel_height": height,
"frame_rate": quality["frame_rate"],
})
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
# Create global configuration
manim_config: Dict = initialize_manim_config()

View File

@@ -1,143 +1,147 @@
from __future__ import annotations
import numpy as np
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 = 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
DEFAULT_PIXEL_HEIGHT = 1080
DEFAULT_PIXEL_WIDTH = 1920
DEFAULT_FRAME_RATE = 30
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
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF
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
# All in seconds
DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0
DEFAULT_WAIT_TIME = 1.0
# 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_BUFF: float = manim_config.sizes.default_mobject_to_edge_buff
DEFAULT_MOBJECT_TO_MOBJECT_BUFF: float = manim_config.sizes.default_mobject_to_mobject_buff
ORIGIN = np.array((0., 0., 0.))
UP = np.array((0., 1., 0.))
DOWN = np.array((0., -1., 0.))
RIGHT = np.array((1., 0., 0.))
LEFT = np.array((-1., 0., 0.))
IN = np.array((0., 0., -1.))
OUT = np.array((0., 0., 1.))
X_AXIS = np.array((1., 0., 0.))
Y_AXIS = np.array((0., 1., 0.))
Z_AXIS = np.array((0., 0., 1.))
# Standard vectors
ORIGIN: Vect3 = np.array([0., 0., 0.])
UP: Vect3 = np.array([0., 1., 0.])
DOWN: Vect3 = np.array([0., -1., 0.])
RIGHT: Vect3 = np.array([1., 0., 0.])
LEFT: Vect3 = np.array([-1., 0., 0.])
IN: Vect3 = np.array([0., 0., -1.])
OUT: Vect3 = np.array([0., 0., 1.])
X_AXIS: Vect3 = np.array([1., 0., 0.])
Y_AXIS: Vect3 = np.array([0., 1., 0.])
Z_AXIS: Vect3 = np.array([0., 0., 1.])
NULL_POINTS = np.array([[0., 0., 0.]])
# Useful abbreviations for diagonals
UL = UP + LEFT
UR = UP + RIGHT
DL = DOWN + LEFT
DR = DOWN + RIGHT
UL: Vect3 = UP + LEFT
UR: Vect3 = UP + RIGHT
DL: Vect3 = DOWN + LEFT
DR: Vect3 = DOWN + RIGHT
TOP = FRAME_Y_RADIUS * UP
BOTTOM = FRAME_Y_RADIUS * DOWN
LEFT_SIDE = FRAME_X_RADIUS * LEFT
RIGHT_SIDE = FRAME_X_RADIUS * RIGHT
TOP: Vect3 = FRAME_Y_RADIUS * UP
BOTTOM: Vect3 = FRAME_Y_RADIUS * DOWN
LEFT_SIDE: Vect3 = FRAME_X_RADIUS * LEFT
RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT
PI = np.pi
TAU = 2 * PI
DEGREES = TAU / 360
# Angles
PI: float = np.pi
TAU: float = 2 * PI
DEG: float = TAU / 360
DEGREES = DEG # Many older animations use teh full name
# Nice to have a constant for readability
# when juxtaposed with expressions like 30 * DEGREES
RADIANS = 1
FFMPEG_BIN = "ffmpeg"
JOINT_TYPE_MAP = {
"auto": 0,
"round": 1,
"bevel": 2,
"miter": 3,
}
# when juxtaposed with expressions like 30 * DEG
RADIANS: float = 1
# Related to Text
START_X = 30
START_Y = 20
NORMAL = "NORMAL"
ITALIC = "ITALIC"
OBLIQUE = "OBLIQUE"
BOLD = "BOLD"
NORMAL: str = "NORMAL"
ITALIC: str = "ITALIC"
OBLIQUE: str = "OBLIQUE"
BOLD: str = "BOLD"
DEFAULT_STROKE_WIDTH = 4
DEFAULT_STROKE_WIDTH: float = manim_config.vmobject.default_stroke_width
# 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"
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
MANIM_COLORS: List[ManimColor] = list(manim_config.colors.values())
# Abbreviated names for the "median" colors
BLUE = BLUE_C
TEAL = TEAL_C
GREEN = GREEN_C
YELLOW = YELLOW_C
GOLD = GOLD_C
RED = RED_C
MAROON = MAROON_C
PURPLE = PURPLE_C
GREY = GREY_C
BLUE: ManimColor = BLUE_C
TEAL: ManimColor = TEAL_C
GREEN: ManimColor = GREEN_C
YELLOW: ManimColor = YELLOW_C
GOLD: ManimColor = GOLD_C
RED: ManimColor = RED_C
MAROON: ManimColor = MAROON_C
PURPLE: ManimColor = PURPLE_C
GREY: ManimColor = GREY_C
COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED]
COLORMAP_3B1B: List[ManimColor] = [BLUE_E, GREEN, YELLOW, RED]

View File

@@ -1,58 +1,175 @@
# 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 whereever 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
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: ""
tex:
executable: "latex"
template_file: "tex_template.tex"
intermediate_filetype: "dvi"
text_to_replace: "[tex_expression]"
# For ctex, use the following configuration
# executable: "xelatex -no-pdf"
# template_file: "ctex_template.tex"
# intermediate_filetype: "xdv"
universal_import_line: "from manimlib import *"
style:
font: "Consolas"
# Manim may write to and read from teh 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 coordiantes, 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
# 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
camera_qualities:
low:
resolution: "854x480"
frame_rate: 15
medium:
resolution: "1280x720"
frame_rate: 30
high:
resolution: "1920x1080"
frame_rate: 30
ultra_high:
resolution: "3840x2160"
frame_rate: 60
default_quality: "high"
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
# 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
tex:
# See tex_templates.yml
template: "default"
text:
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"
# 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,27 +1,31 @@
from __future__ import annotations
import numpy as np
from manimlib.event_handler.event_listner import EventListener
from manimlib.event_handler.event_type import EventType
from manimlib.event_handler.event_listner import EventListner
class EventDispatcher(object):
def __init__(self):
self.event_listners = {
self.event_listners: dict[
EventType, list[EventListener]
] = {
event_type: []
for event_type in EventType
}
self.mouse_point = np.array((0., 0., 0.))
self.mouse_drag_point = np.array((0., 0., 0.))
self.pressed_keys = set()
self.draggable_object_listners = []
self.pressed_keys: set[int] = set()
self.draggable_object_listners: list[EventListener] = []
def add_listner(self, event_listner):
assert(isinstance(event_listner, EventListner))
def add_listner(self, event_listner: EventListener):
assert isinstance(event_listner, EventListener)
self.event_listners[event_listner.event_type].append(event_listner)
return self
def remove_listner(self, event_listner):
assert(isinstance(event_listner, EventListner))
def remove_listner(self, event_listner: EventListener):
assert isinstance(event_listner, EventListener)
try:
while event_listner in self.event_listners[event_listner.event_type]:
self.event_listners[event_listner.event_type].remove(event_listner)
@@ -30,8 +34,7 @@ class EventDispatcher(object):
pass
return self
def dispatch(self, event_type, **event_data):
def dispatch(self, event_type: EventType, **event_data):
if event_type == EventType.MouseMotionEvent:
self.mouse_point = event_data["point"]
elif event_type == EventType.MouseDragEvent:
@@ -53,7 +56,7 @@ class EventDispatcher(object):
if event_type == EventType.MouseDragEvent:
for listner in self.draggable_object_listners:
assert(isinstance(listner, EventListner))
assert isinstance(listner, EventListener)
propagate_event = listner.callback(listner.mobject, event_data)
if propagate_event is not None and propagate_event is False:
return propagate_event
@@ -74,16 +77,16 @@ class EventDispatcher(object):
return propagate_event
def get_listners_count(self):
def get_listners_count(self) -> int:
return sum([len(value) for key, value in self.event_listners.items()])
def get_mouse_point(self):
def get_mouse_point(self) -> np.ndarray:
return self.mouse_point
def get_mouse_drag_point(self):
def get_mouse_drag_point(self) -> np.ndarray:
return self.mouse_drag_point
def is_key_pressed(self, symbol):
def is_key_pressed(self, symbol: int) -> bool:
return (symbol in self.pressed_keys)
__iadd__ = add_listner

View File

@@ -1,5 +1,21 @@
class EventListner(object):
def __init__(self, mobject, event_type, event_callback):
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
from manimlib.event_handler.event_type import EventType
from manimlib.mobject.mobject import Mobject
class EventListener(object):
def __init__(
self,
mobject: Mobject,
event_type: EventType,
event_callback: Callable[[Mobject, dict[str]]]
):
self.mobject = mobject
self.event_type = event_type
self.callback = event_callback

View File

@@ -1,15 +1,26 @@
from __future__ import annotations
import copy
import inspect
import sys
import copy
from manimlib.scene.scene import Scene
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(Scene):
class BlankScene(InteractiveScene):
def construct(self):
exec(get_custom_config()["universal_import_line"])
exec(manim_config.universal_import_line)
self.embed()
@@ -33,11 +44,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(",")
@@ -52,70 +59,59 @@ def prompt_user_for_choice(scene_classes):
sys.exit(1)
def get_scene_config(config):
return dict([
(key, config[key])
for key in [
"window_config",
"camera_config",
"file_writer_config",
"skip_animations",
"start_at_animation_number",
"end_at_animation_number",
"leave_progress_bars",
"preview",
]
])
def compute_total_frames(scene_class, scene_config):
"""
When a scene is being written to file, a copy of the scene is run with
skip_animations set to true so as to count how many frames it will require.
This allows for a total progress bar on rendering, and also allows runtime
errors to be exposed preemptively for long running scenes. The final frame
is saved by default, so that one can more quickly check that the last frame
looks as expected.
errors to be exposed preemptively for long running scenes.
"""
pre_config = copy.deepcopy(scene_config)
pre_config["file_writer_config"]["write_to_movie"] = False
pre_config["file_writer_config"]["save_last_frame"] = True
pre_config["file_writer_config"]["save_last_frame"] = False
pre_config["file_writer_config"]["quiet"] = True
pre_config["skip_animations"] = True
pre_scene = scene_class(**pre_config)
pre_scene.run()
total_time = pre_scene.time - pre_scene.skip_time
return int(total_time * scene_config["camera_config"]["frame_rate"])
return int(total_time * manim_config.camera.fps)
def get_scenes_to_render(scene_classes, scene_config, config):
if config["write_all"]:
return [sc(**scene_config) for sc in scene_classes]
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)
result = []
for scene_name in config["scene_names"]:
found = False
for scene_class in scene_classes:
if scene_class.__name__ == scene_name:
fw_config = scene_config["file_writer_config"]
if fw_config["write_to_movie"]:
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
scene = scene_class(**scene_config)
result.append(scene)
found = True
break
if not found and (scene_name != ""):
log.error(f"No scene named {scene_name} found")
if result:
return result
if len(scene_classes) == 1:
result = [scene_classes[0]]
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")
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:
result = prompt_user_for_choice(scene_classes)
return [scene_class(**scene_config) for scene_class in result]
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, 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:
@@ -128,13 +124,55 @@ 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, line_number: int):
"""
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()
# 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)
# 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(file_name: Optional[str], embed_line: Optional[int], is_reload: bool = False) -> Module:
module = ModuleLoader.get_module(file_name, is_reload)
if embed_line:
insert_embed_line_to_module(module, embed_line)
return module
def main(scene_config: Dict, run_config: Dict):
module = get_module(run_config.file_name, run_config.embed_line, run_config.is_reload)
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

@@ -1,4 +1,5 @@
import logging
from rich.logging import RichHandler
__all__ = ["log"]
@@ -10,4 +11,3 @@ logging.basicConfig(
)
log = logging.getLogger("manimgl")
log.setLevel("DEBUG")

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import numpy as np
import pathops
@@ -5,23 +7,26 @@ 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):
def _convert_vmobject_to_skia_path(vmobject: VMobject) -> pathops.Path:
path = pathops.Path()
subpaths = vmobject.get_subpaths_from_points(vmobject.get_all_points())
for subpath in subpaths:
quads = vmobject.get_bezier_tuples_from_points(subpath)
start = subpath[0]
path.moveTo(*start[:2])
for p0, p1, p2 in quads:
path.quadTo(*p1[:2], *p2[:2])
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
path.close()
for submob in vmobject.family_members_with_points():
for subpath in submob.get_subpaths():
quads = vmobject.get_bezier_tuples_from_points(subpath)
start = subpath[0]
path.moveTo(*start[:2])
for p0, p1, p2 in quads:
path.quadTo(*p1[:2], *p2[:2])
if vmobject.consider_points_equal(subpath[0], subpath[-1]):
path.close()
return path
def _convert_skia_path_to_vmobject(path, vmobject):
def _convert_skia_path_to_vmobject(
path: pathops.Path,
vmobject: VMobject
) -> VMobject:
PathVerb = pathops.PathVerb
current_path_start = np.array([0.0, 0.0, 0.0])
for path_verb, points in path:
@@ -41,11 +46,11 @@ def _convert_skia_path_to_vmobject(path, vmobject):
vmobject.add_quadratic_bezier_curve_to(*points)
else:
raise Exception(f"Unsupported: {path_verb}")
return vmobject
return vmobject.reverse_points()
class Union(VMobject):
def __init__(self, *vmobjects, **kwargs):
def __init__(self, *vmobjects: VMobject, **kwargs):
if len(vmobjects) < 2:
raise ValueError("At least 2 mobjects needed for Union.")
super().__init__(**kwargs)
@@ -59,7 +64,7 @@ class Union(VMobject):
class Difference(VMobject):
def __init__(self, subject, clip, **kwargs):
def __init__(self, subject: VMobject, clip: VMobject, **kwargs):
super().__init__(**kwargs)
outpen = pathops.Path()
pathops.difference(
@@ -71,7 +76,7 @@ class Difference(VMobject):
class Intersection(VMobject):
def __init__(self, *vmobjects, **kwargs):
def __init__(self, *vmobjects: VMobject, **kwargs):
if len(vmobjects) < 2:
raise ValueError("At least 2 mobjects needed for Intersection.")
super().__init__(**kwargs)
@@ -94,7 +99,7 @@ class Intersection(VMobject):
class Exclusion(VMobject):
def __init__(self, *vmobjects, **kwargs):
def __init__(self, *vmobjects: VMobject, **kwargs):
if len(vmobjects) < 2:
raise ValueError("At least 2 mobjects needed for Exclusion.")
super().__init__(**kwargs)

View File

@@ -1,29 +1,42 @@
from __future__ import annotations
import numpy as np
from manimlib.constants import BLUE_D
from manimlib.constants import BLUE_B
from manimlib.constants import BLUE_E
from manimlib.constants import GREY_BROWN
from manimlib.constants import WHITE
from manimlib.constants import BLUE_B, BLUE_D, BLUE_E, GREY_BROWN, WHITE
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.rate_functions import smooth
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, List, Iterable
from manimlib.typing import ManimColor, Vect3, Self
class AnimatedBoundary(VGroup):
CONFIG = {
"colors": [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN],
"max_stroke_width": 3,
"cycle_rate": 0.5,
"back_and_forth": True,
"draw_rate_func": smooth,
"fade_rate_func": smooth,
}
def __init__(self, vmobject, **kwargs):
def __init__(
self,
vmobject: VMobject,
colors: List[ManimColor] = [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN],
max_stroke_width: float = 3.0,
cycle_rate: float = 0.5,
back_and_forth: bool = True,
draw_rate_func: Callable[[float], float] = smooth,
fade_rate_func: Callable[[float], float] = smooth,
**kwargs
):
super().__init__(**kwargs)
self.vmobject = vmobject
self.boundary_copies = [
self.vmobject: VMobject = vmobject
self.colors = colors
self.max_stroke_width = max_stroke_width
self.cycle_rate = cycle_rate
self.back_and_forth = back_and_forth
self.draw_rate_func = draw_rate_func
self.fade_rate_func = fade_rate_func
self.boundary_copies: list[VMobject] = [
vmobject.copy().set_style(
stroke_width=0,
fill_opacity=0
@@ -31,12 +44,12 @@ class AnimatedBoundary(VGroup):
for x in range(2)
]
self.add(*self.boundary_copies)
self.total_time = 0
self.total_time: float = 0
self.add_updater(
lambda m, dt: self.update_boundary_copies(dt)
)
def update_boundary_copies(self, dt):
def update_boundary_copies(self, dt: float) -> Self:
# Not actual time, but something which passes at
# an altered rate to make the implementation below
# cleaner
@@ -66,8 +79,15 @@ class AnimatedBoundary(VGroup):
)
self.total_time += dt
return self
def full_family_become_partial(self, mob1, mob2, a, b):
def full_family_become_partial(
self,
mob1: VMobject,
mob2: VMobject,
a: float,
b: float
) -> Self:
family1 = mob1.family_members_with_points()
family2 = mob2.family_members_with_points()
for sm1, sm2 in zip(family1, family2):
@@ -76,22 +96,25 @@ class AnimatedBoundary(VGroup):
class TracedPath(VMobject):
CONFIG = {
"stroke_width": 2,
"stroke_color": WHITE,
"time_traced": np.inf,
"fill_opacity": 0,
"time_per_anchor": 1 / 15,
}
def __init__(self, traced_point_func, **kwargs):
def __init__(
self,
traced_point_func: Callable[[], Vect3],
time_traced: float = np.inf,
time_per_anchor: float = 1.0 / 15,
stroke_width: float | Iterable[float] = 2.0,
stroke_color: ManimColor = WHITE,
**kwargs
):
super().__init__(**kwargs)
self.traced_point_func = traced_point_func
self.time = 0
self.traced_points = []
self.time_traced = time_traced
self.time_per_anchor = time_per_anchor
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):
def update_path(self, dt: float) -> Self:
if dt == 0:
return self
point = self.traced_point_func().copy()
@@ -99,23 +122,15 @@ class TracedPath(VMobject):
if self.time_traced < np.inf:
n_relevant_points = int(self.time_traced / dt + 0.5)
# n_anchors = int(self.time_traced / self.time_per_anchor)
n_tps = len(self.traced_points)
if n_tps < n_relevant_points:
points = self.traced_points + [point] * (n_relevant_points - n_tps)
else:
points = self.traced_points[n_tps - n_relevant_points:]
# points = [
# self.traced_points[max(n_tps - int(alpha * n_relevant_points) - 1, 0)]
# for alpha in np.linspace(1, 0, n_anchors)
# ]
# Every now and then refresh the list
if n_tps > 10 * n_relevant_points:
self.traced_points = self.traced_points[-n_relevant_points:]
else:
# sparseness = max(int(self.time_per_anchor / dt), 1)
# points = self.traced_points[::sparseness]
# points[-1] = self.traced_points[-1]
points = self.traced_points
if points:
@@ -126,16 +141,25 @@ class TracedPath(VMobject):
class TracingTail(TracedPath):
CONFIG = {
"stroke_width": (0, 3),
"stroke_opacity": (0, 1),
"stroke_color": WHITE,
"time_traced": 1.0,
}
def __init__(self, mobject_or_func, **kwargs):
def __init__(
self,
mobject_or_func: Mobject | Callable[[], np.ndarray],
time_traced: float = 1.0,
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, **kwargs)
super().__init__(
func,
time_traced=time_traced,
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))

View File

@@ -1,142 +1,220 @@
import numpy as np
from __future__ import annotations
from abc import ABC, abstractmethod
import numbers
from manimlib.constants import *
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 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
from manimlib.mobject.functions import ParametricCurve
from manimlib.mobject.geometry import Arrow
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import DashedLine
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.number_line import NumberLine
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.types.dot_cloud import DotCloud
from manimlib.mobject.types.surface import ParametricSurface
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.config_ops import digest_config
from manimlib.utils.config_ops import merge_dicts_recursively
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.simple_functions import binary_search
from manimlib.utils.space_ops import angle_of_vector
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import rotate_vector
from manimlib.utils.space_ops import normalize
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, Sequence, Type, TypeVar, Optional
from manimlib.mobject.mobject import Mobject
from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier, Self
T = TypeVar("T", bound=Mobject)
EPSILON = 1e-8
DEFAULT_X_RANGE = (-8.0, 8.0, 1.0)
DEFAULT_Y_RANGE = (-4.0, 4.0, 1.0)
class CoordinateSystem():
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
"""
CONFIG = {
"dimension": 2,
"default_x_range": [-8.0, 8.0, 1.0],
"default_y_range": [-4.0, 4.0, 1.0],
"width": FRAME_WIDTH,
"height": FRAME_HEIGHT,
"num_sampled_graph_points_per_tick": 20,
}
dimension: int = 2
def __init__(self, **kwargs):
digest_config(self, kwargs)
self.x_range = np.array(self.default_x_range)
self.y_range = np.array(self.default_y_range)
def __init__(
self,
x_range: RangeSpecifier = DEFAULT_X_RANGE,
y_range: RangeSpecifier = DEFAULT_Y_RANGE,
num_sampled_graph_points_per_tick: int = 5,
):
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
def coords_to_point(self, *coords):
@abstractmethod
def coords_to_point(self, *coords: float | VectN) -> Vect3 | Vect3Array:
raise Exception("Not implemented")
def point_to_coords(self, point):
@abstractmethod
def point_to_coords(self, point: Vect3 | Vect3Array) -> tuple[float | VectN, ...]:
raise Exception("Not implemented")
def c2p(self, *coords):
def c2p(self, *coords: float) -> Vect3 | Vect3Array:
"""Abbreviation for coords_to_point"""
return self.coords_to_point(*coords)
def p2c(self, point):
def p2c(self, point: Vect3) -> tuple[float | VectN, ...]:
"""Abbreviation for point_to_coords"""
return self.point_to_coords(point)
def get_origin(self):
def get_origin(self) -> Vect3:
return self.c2p(*[0] * self.dimension)
def get_axes(self):
@abstractmethod
def get_axes(self) -> VGroup:
raise Exception("Not implemented")
def get_all_ranges(self):
@abstractmethod
def get_all_ranges(self) -> list[np.ndarray]:
raise Exception("Not implemented")
def get_axis(self, index):
def get_axis(self, index: int) -> NumberLine:
return self.get_axes()[index]
def get_x_axis(self):
def get_x_axis(self) -> NumberLine:
return self.get_axis(0)
def get_y_axis(self):
def get_y_axis(self) -> NumberLine:
return self.get_axis(1)
def get_z_axis(self):
def get_z_axis(self) -> NumberLine:
return self.get_axis(2)
def get_x_axis_label(self, label_tex, edge=RIGHT, direction=DL, **kwargs):
def get_x_axis_label(
self,
label_tex: str,
edge: Vect3 = RIGHT,
direction: Vect3 = DL,
**kwargs
) -> Tex:
return self.get_axis_label(
label_tex, self.get_x_axis(),
edge, direction, **kwargs
)
def get_y_axis_label(self, label_tex, edge=UP, direction=DR, **kwargs):
def get_y_axis_label(
self,
label_tex: str,
edge: Vect3 = UP,
direction: Vect3 = DR,
**kwargs
) -> Tex:
return self.get_axis_label(
label_tex, self.get_y_axis(),
edge, direction, **kwargs
)
def get_axis_label(self, label_tex, axis, edge, direction, buff=MED_SMALL_BUFF):
def get_axis_label(
self,
label_tex: str,
axis: Vect3,
edge: Vect3,
direction: Vect3,
buff: float = MED_SMALL_BUFF,
ensure_on_screen: bool = False
) -> Tex:
label = Tex(label_tex)
label.next_to(
axis.get_edge_center(edge), direction,
buff=buff
)
label.shift_onto_screen(buff=MED_SMALL_BUFF)
if ensure_on_screen:
label.shift_onto_screen(buff=MED_SMALL_BUFF)
return label
def get_axis_labels(self, x_label_tex="x", y_label_tex="y"):
def get_axis_labels(
self,
x_label_tex: str = "x",
y_label_tex: str = "y"
) -> VGroup:
self.axis_labels = VGroup(
self.get_x_axis_label(x_label_tex),
self.get_y_axis_label(y_label_tex),
)
return self.axis_labels
def get_line_from_axis_to_point(self, index, point,
line_func=DashedLine,
color=GREY_A,
stroke_width=2):
def get_line_from_axis_to_point(
self,
index: int,
point: Vect3,
line_func: Type[T] = DashedLine,
color: ManimColor = GREY_A,
stroke_width: float = 2
) -> T:
axis = self.get_axis(index)
line = line_func(axis.get_projection(point), point)
line.set_stroke(color, stroke_width)
return line
def get_v_line(self, point, **kwargs):
def get_v_line(self, point: Vect3, **kwargs):
return self.get_line_from_axis_to_point(0, point, **kwargs)
def get_h_line(self, point, **kwargs):
def get_h_line(self, point: Vect3, **kwargs):
return self.get_line_from_axis_to_point(1, point, **kwargs)
# Useful for graphing
def get_graph(self, function, x_range=None, **kwargs):
t_range = np.array(self.x_range, dtype=float)
if x_range is not None:
t_range[:len(x_range)] = x_range
def get_graph(
self,
function: Callable[[float], float],
x_range: Sequence[float] | None = None,
bind: bool = False,
**kwargs
) -> ParametricCurve:
x_range = x_range or self.x_range
t_range = np.ones(3)
t_range[:len(x_range)] = x_range
# For axes, the third coordinate of x_range indicates
# tick frequency. But for functions, it indicates a
# sample frequency
if x_range is None or len(x_range) < 3:
t_range[2] /= self.num_sampled_graph_points_per_tick
t_range[2] /= self.num_sampled_graph_points_per_tick
def parametric_function(t: float) -> Vect3:
return self.c2p(t, function(t))
graph = ParametricCurve(
lambda t: self.c2p(t, function(t)),
t_range=t_range,
parametric_function,
t_range=tuple(t_range),
**kwargs
)
graph.underlying_function = function
graph.x_range = x_range
if bind:
self.bind_graph_to_func(graph, function)
return graph
def get_parametric_curve(self, function, **kwargs):
def get_parametric_curve(
self,
function: Callable[[float], Vect3],
**kwargs
) -> ParametricCurve:
dim = self.dimension
graph = ParametricCurve(
lambda t: self.coords_to_point(*function(t)[:dim]),
@@ -145,7 +223,11 @@ class CoordinateSystem():
graph.underlying_function = function
return graph
def input_to_graph_point(self, x, graph):
def input_to_graph_point(
self,
x: float,
graph: ParametricCurve
) -> Vect3 | None:
if hasattr(graph, "underlying_function"):
return self.coords_to_point(x, graph.underlying_function(x))
else:
@@ -162,19 +244,50 @@ class CoordinateSystem():
else:
return None
def i2gp(self, x, graph):
def i2gp(self, x: float, graph: ParametricCurve) -> Vect3 | None:
"""
Alias for input_to_graph_point
"""
return self.input_to_graph_point(x, graph)
def get_graph_label(self,
graph,
label="f(x)",
x=None,
direction=RIGHT,
buff=MED_SMALL_BUFF,
color=None):
def bind_graph_to_func(
self,
graph: VMobject,
func: Callable[[VectN], VectN],
jagged: bool = False,
get_discontinuities: Optional[Callable[[], Vect3]] = None
) -> VMobject:
"""
Use for graphing functions which might change over time, or change with
conditions
"""
x_values = np.array([self.x_axis.p2n(p) for p in graph.get_points()])
def get_graph_points():
xs = x_values
if get_discontinuities:
ds = get_discontinuities()
ep = 1e-6
added_xs = it.chain(*((d - ep, d + ep) for d in ds))
xs[:] = sorted([*x_values, *added_xs])[:len(x_values)]
return self.c2p(xs, func(xs))
graph.add_updater(
lambda g: g.set_points_as_corners(get_graph_points())
)
if not jagged:
graph.add_updater(lambda g: g.make_smooth(approx=True))
return graph
def get_graph_label(
self,
graph: ParametricCurve,
label: str | Mobject = "f(x)",
x: float | None = None,
direction: Vect3 = RIGHT,
buff: float = MED_SMALL_BUFF,
color: ManimColor | None = None
) -> Tex | Mobject:
if isinstance(label, str):
label = Tex(label)
if color is None:
@@ -194,45 +307,71 @@ class CoordinateSystem():
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)
label.shift_onto_screen()
return label
def get_v_line_to_graph(self, x, graph, **kwargs):
def get_v_line_to_graph(self, x: float, graph: ParametricCurve, **kwargs):
return self.get_v_line(self.i2gp(x, graph), **kwargs)
def get_h_line_to_graph(self, x, graph, **kwargs):
def get_h_line_to_graph(self, x: float, graph: ParametricCurve, **kwargs):
return self.get_h_line(self.i2gp(x, graph), **kwargs)
def get_scatterplot(self,
x_values: Vect3Array,
y_values: Vect3Array,
**dot_config):
return DotCloud(self.c2p(x_values, y_values), **dot_config)
# For calculus
def angle_of_tangent(self, x, graph, dx=EPSILON):
def angle_of_tangent(
self,
x: float,
graph: ParametricCurve,
dx: float = EPSILON
) -> float:
p0 = self.input_to_graph_point(x, graph)
p1 = self.input_to_graph_point(x + dx, graph)
return angle_of_vector(p1 - p0)
def slope_of_tangent(self, x, graph, **kwargs):
def slope_of_tangent(
self,
x: float,
graph: ParametricCurve,
**kwargs
) -> float:
return np.tan(self.angle_of_tangent(x, graph, **kwargs))
def get_tangent_line(self, x, graph, length=5, line_func=Line):
def get_tangent_line(
self,
x: float,
graph: ParametricCurve,
length: float = 5,
line_func: Type[T] = Line
) -> T:
line = line_func(LEFT, RIGHT)
line.set_width(length)
line.rotate(self.angle_of_tangent(x, graph))
line.move_to(self.input_to_graph_point(x, graph))
return line
def get_riemann_rectangles(self,
graph,
x_range=None,
dx=None,
input_sample_type="left",
stroke_width=1,
stroke_color=BLACK,
fill_opacity=1,
colors=(BLUE, GREEN),
show_signed_area=True):
def get_riemann_rectangles(
self,
graph: ParametricCurve,
x_range: Sequence[float] = None,
dx: float | None = None,
input_sample_type: str = "left",
stroke_width: float = 1,
stroke_color: ManimColor = BLACK,
fill_opacity: float = 1,
colors: Iterable[ManimColor] = (BLUE, GREEN),
negative_color: ManimColor = RED,
stroke_background: bool = True,
show_signed_area: bool = True
) -> VGroup:
if x_range is None:
x_range = self.x_range[:2]
if dx is None:
@@ -241,6 +380,7 @@ class CoordinateSystem():
x_range = [*x_range, dx]
rects = []
x_range[1] = x_range[1] + dx
xs = np.arange(*x_range)
for x0, x1 in zip(xs, xs[1:]):
if input_sample_type == "left":
@@ -251,11 +391,13 @@ class CoordinateSystem():
sample = 0.5 * x0 + 0.5 * x1
else:
raise Exception("Invalid input sample type")
height = get_norm(
self.i2gp(sample, graph) - self.c2p(sample, 0)
height_vect = self.i2gp(sample, graph) - self.c2p(sample, 0)
rect = Rectangle(
width=self.x_axis.n2p(x1)[0] - self.x_axis.n2p(x0)[0],
height=get_norm(height_vect),
)
rect = Rectangle(width=x1 - x0, height=height)
rect.move_to(self.c2p(x0, 0), DL)
rect.positive = height_vect[1] > 0
rect.move_to(self.c2p(x0, 0), DL if rect.positive else UL)
rects.append(rect)
result = VGroup(*rects)
result.set_submobject_colors_by_gradient(*colors)
@@ -263,47 +405,76 @@ class CoordinateSystem():
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_opacity=fill_opacity,
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=1):
# TODO
pass
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`")
alpha_bounds = [
inverse_interpolate(*graph.x_range, x)
for x in x_range
]
sub_graph = graph.copy()
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))
sub_graph.add_line_to(sub_graph.get_start())
sub_graph.set_stroke(width=0)
sub_graph.set_fill(fill_color, fill_opacity)
return sub_graph
class Axes(VGroup, CoordinateSystem):
CONFIG = {
"axis_config": {
"include_tip": True,
"numbers_to_exclude": [0],
},
"x_axis_config": {},
"y_axis_config": {
"line_to_number_direction": LEFT,
},
"height": FRAME_HEIGHT - 2,
"width": FRAME_WIDTH - 2,
}
default_axis_config: dict = dict()
default_x_axis_config: dict = dict()
default_y_axis_config: dict = dict(line_to_number_direction=LEFT)
def __init__(self,
x_range=None,
y_range=None,
**kwargs):
CoordinateSystem.__init__(self, **kwargs)
def __init__(
self,
x_range: RangeSpecifier = DEFAULT_X_RANGE,
y_range: RangeSpecifier = DEFAULT_Y_RANGE,
axis_config: dict = dict(),
x_axis_config: dict = dict(),
y_axis_config: dict = dict(),
height: float | None = None,
width: float | None = None,
unit_size: float = 1.0,
**kwargs
):
CoordinateSystem.__init__(self, x_range, y_range, **kwargs)
kwargs.pop("num_sampled_graph_points_per_tick", None)
VGroup.__init__(self, **kwargs)
if x_range is not None:
self.x_range[:len(x_range)] = x_range
if y_range is not None:
self.y_range[:len(y_range)] = y_range
axis_config = dict(**axis_config, unit_size=unit_size)
self.x_axis = self.create_axis(
self.x_range, self.x_axis_config, self.width,
self.x_range,
axis_config=merge_dicts_recursively(
self.default_axis_config,
self.default_x_axis_config,
axis_config,
x_axis_config
),
length=width,
)
self.y_axis = self.create_axis(
self.y_range, self.y_axis_config, self.height
self.y_range,
axis_config=merge_dicts_recursively(
self.default_axis_config,
self.default_y_axis_config,
axis_config,
y_axis_config
),
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
@@ -311,118 +482,176 @@ class Axes(VGroup, CoordinateSystem):
self.add(*self.axes)
self.center()
def create_axis(self, range_terms, axis_config, length):
new_config = merge_dicts_recursively(self.axis_config, axis_config)
new_config["width"] = length
axis = NumberLine(range_terms, **new_config)
def create_axis(
self,
range_terms: RangeSpecifier,
axis_config: dict,
length: float | None
) -> NumberLine:
axis = NumberLine(range_terms, width=length, **axis_config)
axis.shift(-axis.n2p(0))
return axis
def coords_to_point(self, *coords):
def coords_to_point(self, *coords: float | VectN) -> Vect3 | Vect3Array:
origin = self.x_axis.number_to_point(0)
result = origin.copy()
for axis, coord in zip(self.get_axes(), coords):
result += (axis.number_to_point(coord) - origin)
return result
return origin + sum(
axis.number_to_point(coord) - origin
for axis, coord in zip(self.get_axes(), coords)
)
def point_to_coords(self, point):
def point_to_coords(self, point: Vect3 | Vect3Array) -> tuple[float | VectN, ...]:
return tuple([
axis.point_to_number(point)
for axis in self.get_axes()
])
def get_axes(self):
def get_axes(self) -> VGroup:
return self.axes
def get_all_ranges(self):
def get_all_ranges(self) -> list[Sequence[float]]:
return [self.x_range, self.y_range]
def add_coordinate_labels(self,
x_values=None,
y_values=None,
**kwargs):
def add_coordinate_labels(
self,
x_values: Iterable[float] | None = None,
y_values: Iterable[float] | None = None,
excluding: Iterable[float] = [0],
**kwargs
) -> VGroup:
axes = self.get_axes()
self.coordinate_labels = VGroup()
for axis, values in zip(axes, [x_values, y_values]):
labels = axis.add_numbers(values, **kwargs)
labels = axis.add_numbers(values, excluding=excluding, **kwargs)
self.coordinate_labels.add(labels)
return self.coordinate_labels
class ThreeDAxes(Axes):
CONFIG = {
"dimension": 3,
"x_range": np.array([-6.0, 6.0, 1.0]),
"y_range": np.array([-5.0, 5.0, 1.0]),
"z_range": np.array([-4.0, 4.0, 1.0]),
"z_axis_config": {},
"z_normal": DOWN,
"height": None,
"width": None,
"depth": None,
"num_axis_pieces": 20,
"gloss": 0.5,
}
dimension: int = 3
default_z_axis_config: dict = dict()
def __init__(self, x_range=None, y_range=None, z_range=None, **kwargs):
def __init__(
self,
x_range: RangeSpecifier = (-6.0, 6.0, 1.0),
y_range: RangeSpecifier = (-5.0, 5.0, 1.0),
z_range: RangeSpecifier = (-4.0, 4.0, 1.0),
z_axis_config: dict = dict(),
z_normal: Vect3 = DOWN,
depth: float | None = None,
**kwargs
):
Axes.__init__(self, x_range, y_range, **kwargs)
if z_range is not None:
self.z_range[:len(z_range)] = z_range
z_axis = self.create_axis(
self.z_range = full_range_specifier(z_range)
self.z_axis = self.create_axis(
self.z_range,
self.z_axis_config,
self.depth,
axis_config=merge_dicts_recursively(
self.default_axis_config,
self.default_z_axis_config,
kwargs.get("axis_config", {}),
z_axis_config
),
length=depth,
)
z_axis.rotate(-PI / 2, UP, about_point=ORIGIN)
z_axis.rotate(
angle_of_vector(self.z_normal), OUT,
self.z_axis.rotate(-PI / 2, UP, about_point=ORIGIN)
self.z_axis.rotate(
angle_of_vector(z_normal), OUT,
about_point=ORIGIN
)
z_axis.shift(self.x_axis.n2p(0))
self.axes.add(z_axis)
self.add(z_axis)
self.z_axis = z_axis
self.z_axis.shift(self.x_axis.n2p(0))
self.axes.add(self.z_axis)
self.add(self.z_axis)
for axis in self.axes:
axis.insert_n_curves(self.num_axis_pieces - 1)
def get_all_ranges(self):
def get_all_ranges(self) -> list[Sequence[float]]:
return [self.x_range, self.y_range, self.z_range]
def add_axis_labels(self, x_tex="x", y_tex="y", z_tex="z", font_size=24, buff=0.2):
x_label, y_label, z_label = labels = VGroup(*(
Tex(tex, font_size=font_size)
for tex in [x_tex, y_tex, z_tex]
))
z_label.rotate(PI / 2, RIGHT)
for label, axis in zip(labels, self):
label.next_to(axis, normalize(np.round(axis.get_vector()), 2), buff=buff)
axis.add(label)
self.axis_labels = labels
def get_graph(
self,
func,
color=BLUE_E,
opacity=0.9,
u_range=None,
v_range=None,
**kwargs
) -> ParametricSurface:
xu = self.x_axis.get_unit_size()
yu = self.y_axis.get_unit_size()
zu = self.z_axis.get_unit_size()
x0, y0, z0 = self.get_origin()
u_range = u_range or self.x_range[:2]
v_range = v_range or self.y_range[:2]
return ParametricSurface(
lambda u, v: [xu * u + x0, yu * v + y0, zu * func(u, v) + z0],
u_range=u_range,
v_range=v_range,
color=color,
opacity=opacity,
**kwargs
)
def get_parametric_surface(
self,
func,
color=BLUE_E,
opacity=0.9,
**kwargs
) -> ParametricSurface:
surface = ParametricSurface(func, color=color, opacity=opacity, **kwargs)
axes = [self.x_axis, self.y_axis, self.z_axis]
for dim, axis in zip(range(3), axes):
surface.stretch(axis.get_unit_size(), dim, about_point=ORIGIN)
surface.shift(self.get_origin())
return surface
class NumberPlane(Axes):
CONFIG = {
"axis_config": {
"stroke_color": WHITE,
"stroke_width": 2,
"include_ticks": False,
"include_tip": False,
"line_to_number_buff": SMALL_BUFF,
"line_to_number_direction": DL,
},
"y_axis_config": {
"line_to_number_direction": DL,
},
"background_line_style": {
"stroke_color": BLUE_D,
"stroke_width": 2,
"stroke_opacity": 1,
},
"height": None,
"width": None,
# Defaults to a faded version of line_config
"faded_line_style": None,
"faded_line_ratio": 4,
"make_smooth_after_applying_functions": True,
}
default_axis_config: dict = dict(
stroke_color=WHITE,
stroke_width=2,
include_ticks=False,
include_tip=False,
line_to_number_buff=SMALL_BUFF,
line_to_number_direction=DL,
)
default_y_axis_config: dict = dict(
line_to_number_direction=DL,
)
def __init__(self, x_range=None, y_range=None, **kwargs):
def __init__(
self,
x_range: RangeSpecifier = (-8.0, 8.0, 1.0),
y_range: RangeSpecifier = (-4.0, 4.0, 1.0),
background_line_style: dict = dict(
stroke_color=BLUE_D,
stroke_width=2,
stroke_opacity=1,
),
# Defaults to a faded version of line_config
faded_line_style: dict = dict(),
faded_line_ratio: int = 4,
make_smooth_after_applying_functions: bool = True,
**kwargs
):
super().__init__(x_range, y_range, **kwargs)
self.background_line_style = dict(background_line_style)
self.faded_line_style = dict(faded_line_style)
self.faded_line_ratio = faded_line_ratio
self.make_smooth_after_applying_functions = make_smooth_after_applying_functions
self.init_background_lines()
def init_background_lines(self):
if self.faded_line_style is None:
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
@@ -439,7 +668,7 @@ class NumberPlane(Axes):
self.background_lines,
)
def get_lines(self):
def get_lines(self) -> tuple[VGroup, VGroup]:
x_axis = self.get_x_axis()
y_axis = self.get_y_axis()
@@ -449,7 +678,11 @@ class NumberPlane(Axes):
lines2 = VGroup(*x_lines2, *y_lines2)
return lines1, lines2
def get_lines_parallel_to_axis(self, axis1, axis2):
def get_lines_parallel_to_axis(
self,
axis1: NumberLine,
axis2: NumberLine
) -> tuple[VGroup, VGroup]:
freq = axis2.x_step
ratio = self.faded_line_ratio
line = Line(axis1.get_start(), axis1.get_end())
@@ -460,6 +693,8 @@ class NumberPlane(Axes):
lines2 = VGroup()
inputs = np.arange(axis2.x_min, axis2.x_max + step, step)
for i, x in enumerate(inputs):
if abs(x) < 1e-8:
continue
new_line = line.copy()
new_line.shift(axis2.n2p(x) - axis2.n2p(0))
if i % (1 + ratio) == 0:
@@ -468,20 +703,20 @@ class NumberPlane(Axes):
lines2.add(new_line)
return lines1, lines2
def get_x_unit_size(self):
def get_x_unit_size(self) -> float:
return self.get_x_axis().get_unit_size()
def get_y_unit_size(self):
def get_y_unit_size(self) -> list:
return self.get_x_axis().get_unit_size()
def get_axes(self):
def get_axes(self) -> VGroup:
return self.axes
def get_vector(self, coords, **kwargs):
def get_vector(self, coords: Iterable[float], **kwargs) -> Arrow:
kwargs["buff"] = 0
return Arrow(self.c2p(0, 0), self.c2p(*coords), **kwargs)
def prepare_for_nonlinear_transform(self, num_inserted_curves=50):
def prepare_for_nonlinear_transform(self, num_inserted_curves: int = 50) -> Self:
for mob in self.family_members_with_points():
num_curves = mob.get_num_curves()
if num_inserted_curves > num_curves:
@@ -491,32 +726,36 @@ class NumberPlane(Axes):
class ComplexPlane(NumberPlane):
CONFIG = {
"color": BLUE,
"line_frequency": 1,
}
def number_to_point(self, number):
def number_to_point(self, number: complex | float) -> Vect3:
number = complex(number)
return self.coords_to_point(number.real, number.imag)
def n2p(self, number):
def n2p(self, number: complex | float) -> Vect3:
return self.number_to_point(number)
def point_to_number(self, point):
def point_to_number(self, point: Vect3) -> complex:
x, y = self.point_to_coords(point)
return complex(x, y)
def p2n(self, point):
def p2n(self, point: Vect3) -> complex:
return self.point_to_number(point)
def get_default_coordinate_values(self, skip_first=True):
def get_default_coordinate_values(
self,
skip_first: bool = True
) -> list[complex]:
x_numbers = self.get_x_axis().get_tick_range()[1:]
y_numbers = self.get_y_axis().get_tick_range()[1:]
y_numbers = [complex(0, y) for y in y_numbers if y != 0]
return [*x_numbers, *y_numbers]
def add_coordinate_labels(self, numbers=None, skip_first=True, **kwargs):
def add_coordinate_labels(
self,
numbers: list[complex] | None = None,
skip_first: bool = True,
font_size: int = 36,
**kwargs
) -> Self:
if numbers is None:
numbers = self.get_default_coordinate_values(skip_first)
@@ -526,14 +765,12 @@ class ComplexPlane(NumberPlane):
if abs(z.imag) > abs(z.real):
axis = self.get_y_axis()
value = z.imag
kwargs["unit"] = "i"
kwargs["unit_tex"] = "i"
else:
axis = self.get_x_axis()
value = z.real
number_mob = axis.get_number_mobject(value, **kwargs)
# For i and -i, remove the "1"
if z.imag == 1:
number_mob.remove(number_mob[0])
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(

View File

@@ -1,50 +1,56 @@
from manimlib.constants import *
from __future__ import annotations
from manimlib.constants import BLACK, GREY_E
from manimlib.constants import FRAME_HEIGHT
from manimlib.mobject.geometry import Rectangle
from manimlib.utils.config_ops import digest_config
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import ManimColor
class ScreenRectangle(Rectangle):
CONFIG = {
"aspect_ratio": 16.0 / 9.0,
"height": 4
}
def __init__(self, **kwargs):
Rectangle.__init__(self, **kwargs)
self.set_width(
self.aspect_ratio * self.get_height(),
stretch=True
def __init__(
self,
aspect_ratio: float = 16.0 / 9.0,
height: float = 4,
**kwargs
):
super().__init__(
width=aspect_ratio * height,
height=height,
**kwargs
)
class FullScreenRectangle(ScreenRectangle):
CONFIG = {
"height": FRAME_HEIGHT,
"fill_color": GREY_E,
"fill_opacity": 1,
"stroke_width": 0,
}
def __init__(
self,
height: float = FRAME_HEIGHT,
fill_color: ManimColor = GREY_E,
fill_opacity: float = 1,
stroke_width: float = 0,
**kwargs,
):
super().__init__(
height=height,
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
**kwargs
)
class FullScreenFadeRectangle(FullScreenRectangle):
CONFIG = {
"stroke_width": 0,
"fill_color": BLACK,
"fill_opacity": 0.7,
}
class PictureInPictureFrame(Rectangle):
CONFIG = {
"height": 3,
"aspect_ratio": 16.0 / 9.0
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
Rectangle.__init__(
self,
width=self.aspect_ratio * self.height,
height=self.height,
**kwargs
def __init__(
self,
stroke_width: float = 0.0,
fill_color: ManimColor = BLACK,
fill_opacity: float = 0.7,
**kwargs,
):
super().__init__(
stroke_width=stroke_width,
fill_color=fill_color,
fill_opacity=fill_opacity,
)

View File

@@ -1,32 +1,39 @@
from manimlib.constants import *
from __future__ import annotations
from isosurfaces import plot_isoline
import numpy as np
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import YELLOW
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Sequence, Tuple
from manimlib.typing import ManimColor, Vect3
class ParametricCurve(VMobject):
CONFIG = {
"t_range": [0, 1, 0.1],
"epsilon": 1e-8,
def __init__(
self,
t_func: Callable[[float], Sequence[float] | Vect3],
t_range: Tuple[float, float, float] = (0, 1, 0.1),
epsilon: float = 1e-8,
# TODO, automatically figure out discontinuities
"discontinuities": [],
"use_smoothing": True,
}
def __init__(self, t_func, t_range=None, **kwargs):
digest_config(self, kwargs)
if t_range is not None:
self.t_range[:len(t_range)] = t_range
# To be backward compatible with all the scenes specifying t_min, t_max, step_size
self.t_range = [
kwargs.get("t_min", self.t_range[0]),
kwargs.get("t_max", self.t_range[1]),
kwargs.get("step_size", self.t_range[2]),
]
discontinuities: Sequence[float] = [],
use_smoothing: bool = True,
**kwargs
):
self.t_func = t_func
VMobject.__init__(self, **kwargs)
self.t_range = t_range
self.epsilon = epsilon
self.discontinuities = discontinuities
self.use_smoothing = use_smoothing
super().__init__(**kwargs)
def get_point_from_function(self, t):
return self.t_func(t)
def get_point_from_function(self, t: float) -> Vect3:
return np.array(self.t_func(t))
def init_points(self):
t_min, t_max, step = self.t_range
@@ -41,30 +48,74 @@ class ParametricCurve(VMobject):
self.start_new_path(points[0])
self.add_points_as_corners(points[1:])
if self.use_smoothing:
self.make_approximately_smooth()
self.make_smooth(approx=True)
if not self.has_points():
self.set_points(np.array([self.t_func(t_min)]))
return self
def get_t_func(self):
return self.t_func
def get_function(self):
if hasattr(self, "underlying_function"):
return self.underlying_function
if hasattr(self, "function"):
return self.function
def get_x_range(self):
if hasattr(self, "x_range"):
return self.x_range
class FunctionGraph(ParametricCurve):
CONFIG = {
"color": YELLOW,
"x_range": [-8, 8, 0.25],
}
def __init__(self, function, x_range=None, **kwargs):
digest_config(self, kwargs)
def __init__(
self,
function: Callable[[float], float],
x_range: Tuple[float, float, float] = (-8, 8, 0.25),
color: ManimColor = YELLOW,
**kwargs
):
self.function = function
if x_range is not None:
self.x_range[:len(x_range)] = x_range
self.x_range = x_range
def parametric_function(t):
return [t, function(t), 0]
super().__init__(parametric_function, self.x_range, **kwargs)
def get_function(self):
return self.function
def get_point_from_function(self, x):
return self.t_func(x)
class ImplicitFunction(VMobject):
def __init__(
self,
func: Callable[[float, float], float],
x_range: Tuple[float, float] = (-FRAME_X_RADIUS, FRAME_X_RADIUS),
y_range: Tuple[float, float] = (-FRAME_Y_RADIUS, FRAME_Y_RADIUS),
min_depth: int = 5,
max_quads: int = 1500,
use_smoothing: bool = False,
joint_type: str = 'no_joint',
**kwargs
):
super().__init__(joint_type=joint_type, **kwargs)
p_min, p_max = (
np.array([x_range[0], y_range[0]]),
np.array([x_range[1], y_range[1]]),
)
curves = plot_isoline(
fn=lambda u: func(u[0], u[1]),
pmin=p_min,
pmax=p_max,
min_depth=min_depth,
max_quads=max_quads,
) # returns a list of lists of 2D points
curves = [
np.pad(curve, [(0, 0), (0, 1)])
for curve in curves
if curve != []
] # add z coord as 0
for curve in curves:
self.start_new_path(curve[0])
self.add_points_as_corners(curve[1:])
if use_smoothing:
self.make_smooth()

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,32 @@
from __future__ import annotations
import numpy as np
from pyglet.window import key as PygletWindowKeys
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import LEFT, RIGHT, UP, DOWN, ORIGIN
from manimlib.constants import SMALL_BUFF, MED_SMALL_BUFF, MED_LARGE_BUFF
from manimlib.constants import BLACK, GREY_A, GREY_C, RED, GREEN, BLUE, WHITE
from manimlib.mobject.mobject import Mobject, Group
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.geometry import Dot, Line, Square, Rectangle, RoundedRectangle, Circle
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.mobject.mobject import Group
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Dot
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import RoundedRectangle
from manimlib.mobject.geometry import Square
from manimlib.mobject.svg.text_mobject import Text
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.value_tracker import ValueTracker
from manimlib.utils.config_ops import digest_config
from manimlib.utils.space_ops import get_norm, get_closest_point_on_line
from manimlib.utils.color import rgb_to_color, color_to_rgba, rgb_to_hex
from manimlib.utils.color import rgb_to_hex
from manimlib.utils.space_ops import get_closest_point_on_line
from manimlib.utils.space_ops import get_norm
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
from manimlib.typing import ManimColor
# Interactive Mobjects
@@ -21,17 +35,16 @@ class MotionMobject(Mobject):
"""
You could hold and drag this object to any position
"""
def __init__(self, mobject, **kwargs):
def __init__(self, mobject: Mobject, **kwargs):
super().__init__(**kwargs)
assert(isinstance(mobject, Mobject))
assert isinstance(mobject, Mobject)
self.mobject = mobject
self.mobject.add_mouse_drag_listner(self.mob_on_mouse_drag)
# To avoid locking it as static mobject
self.mobject.add_updater(lambda mob: None)
self.add(mobject)
def mob_on_mouse_drag(self, mob, event_data):
def mob_on_mouse_drag(self, mob: Mobject, event_data: dict[str, np.ndarray]) -> bool:
mob.move_to(event_data["point"])
return False
@@ -43,15 +56,15 @@ class Button(Mobject):
The on_click method takes mobject as argument like updater
"""
def __init__(self, mobject, on_click, **kwargs):
def __init__(self, mobject: Mobject, on_click: Callable[[Mobject]], **kwargs):
super().__init__(**kwargs)
assert(isinstance(mobject, Mobject))
assert isinstance(mobject, Mobject)
self.on_click = on_click
self.mobject = mobject
self.mobject.add_mouse_press_listner(self.mob_on_mouse_press)
self.add(self.mobject)
def mob_on_mouse_press(self, mob, event_data):
def mob_on_mouse_press(self, mob: Mobject, event_data) -> bool:
self.on_click(mob)
return False
@@ -59,7 +72,7 @@ class Button(Mobject):
# Controls
class ControlMobject(ValueTracker):
def __init__(self, value, *mobjects, **kwargs):
def __init__(self, value: float, *mobjects: Mobject, **kwargs):
super().__init__(value=value, **kwargs)
self.add(*mobjects)
@@ -67,7 +80,7 @@ class ControlMobject(ValueTracker):
self.add_updater(lambda mob: None)
self.fix_in_frame()
def set_value(self, value):
def set_value(self, value: float):
self.assert_value(value)
self.set_value_anim(value)
return ValueTracker.set_value(self, value)
@@ -82,86 +95,97 @@ class ControlMobject(ValueTracker):
class EnableDisableButton(ControlMobject):
CONFIG = {
"value_type": np.dtype(bool),
"rect_kwargs": {
def __init__(
self,
value: bool = True,
value_type: np.dtype = np.dtype(bool),
rect_kwargs: dict = {
"width": 0.5,
"height": 0.5,
"fill_opacity": 1.0
},
"enable_color": GREEN,
"disable_color": RED
}
enable_color: ManimColor = GREEN,
disable_color: ManimColor = RED,
**kwargs
):
self.value = value
self.value_type = value_type
self.rect_kwargs = rect_kwargs
self.enable_color = enable_color
self.disable_color = disable_color
def __init__(self, value=True, **kwargs):
digest_config(self, kwargs)
self.box = Rectangle(**self.rect_kwargs)
super().__init__(value, self.box, **kwargs)
self.add_mouse_press_listner(self.on_mouse_press)
def assert_value(self, value):
assert(isinstance(value, bool))
def assert_value(self, value: bool) -> None:
assert isinstance(value, bool)
def set_value_anim(self, value):
def set_value_anim(self, value: bool) -> None:
if value:
self.box.set_fill(self.enable_color)
else:
self.box.set_fill(self.disable_color)
def toggle_value(self):
def toggle_value(self) -> None:
super().set_value(not self.get_value())
def on_mouse_press(self, mob, event_data):
def on_mouse_press(self, mob: Mobject, event_data) -> bool:
mob.toggle_value()
return False
class Checkbox(ControlMobject):
CONFIG = {
"value_type": np.dtype(bool),
"rect_kwargs": {
def __init__(
self,
value: bool = True,
value_type: np.dtype = np.dtype(bool),
rect_kwargs: dict = {
"width": 0.5,
"height": 0.5,
"fill_opacity": 0.0
},
"checkmark_kwargs": {
checkmark_kwargs: dict = {
"stroke_color": GREEN,
"stroke_width": 6,
},
"cross_kwargs": {
cross_kwargs: dict = {
"stroke_color": RED,
"stroke_width": 6,
},
"box_content_buff": SMALL_BUFF
}
box_content_buff: float = SMALL_BUFF,
**kwargs
):
self.value_type = value_type
self.rect_kwargs = rect_kwargs
self.checkmark_kwargs = checkmark_kwargs
self.cross_kwargs = cross_kwargs
self.box_content_buff = box_content_buff
def __init__(self, value=True, **kwargs):
digest_config(self, kwargs)
self.box = Rectangle(**self.rect_kwargs)
self.box_content = self.get_checkmark() if value else self.get_cross()
super().__init__(value, self.box, self.box_content, **kwargs)
self.add_mouse_press_listner(self.on_mouse_press)
def assert_value(self, value):
assert(isinstance(value, bool))
def assert_value(self, value: bool) -> None:
assert isinstance(value, bool)
def toggle_value(self):
def toggle_value(self) -> None:
super().set_value(not self.get_value())
def set_value_anim(self, value):
def set_value_anim(self, value: bool) -> None:
if value:
self.box_content.become(self.get_checkmark())
else:
self.box_content.become(self.get_cross())
def on_mouse_press(self, mob, event_data):
def on_mouse_press(self, mob: Mobject, event_data) -> None:
mob.toggle_value()
return False
# Helper methods
def get_checkmark(self):
def get_checkmark(self) -> VGroup:
checkmark = VGroup(
Line(UP / 2 + 2 * LEFT, DOWN + LEFT, **self.checkmark_kwargs),
Line(DOWN + LEFT, UP + RIGHT, **self.checkmark_kwargs)
@@ -173,7 +197,7 @@ class Checkbox(ControlMobject):
checkmark.move_to(self.box)
return checkmark
def get_cross(self):
def get_cross(self) -> VGroup:
cross = VGroup(
Line(UP + LEFT, DOWN + RIGHT, **self.cross_kwargs),
Line(UP + RIGHT, DOWN + LEFT, **self.cross_kwargs)
@@ -187,27 +211,33 @@ class Checkbox(ControlMobject):
class LinearNumberSlider(ControlMobject):
CONFIG = {
"value_type": np.float64,
"min_value": -10.0,
"max_value": 10.0,
"step": 1.0,
"rounded_rect_kwargs": {
def __init__(
self,
value: float = 0,
value_type: type = np.float64,
min_value: float = -10.0,
max_value: float = 10.0,
step: float = 1.0,
rounded_rect_kwargs: dict = {
"height": 0.075,
"width": 2,
"corner_radius": 0.0375
},
"circle_kwargs": {
circle_kwargs: dict = {
"radius": 0.1,
"stroke_color": GREY_A,
"fill_color": GREY_A,
"fill_opacity": 1.0
}
}
},
**kwargs
):
self.value_type = value_type
self.min_value = min_value
self.max_value = max_value
self.step = step
self.rounded_rect_kwargs = rounded_rect_kwargs
self.circle_kwargs = circle_kwargs
def __init__(self, value=0, **kwargs):
digest_config(self, kwargs)
self.bar = RoundedRectangle(**self.rounded_rect_kwargs)
self.slider = Circle(**self.circle_kwargs)
self.slider_axis = Line(
@@ -219,22 +249,22 @@ class LinearNumberSlider(ControlMobject):
self.slider.add_mouse_drag_listner(self.slider_on_mouse_drag)
super().__init__(value, self.bar, self.slider, self.slider_axis, ** kwargs)
super().__init__(value, self.bar, self.slider, self.slider_axis, **kwargs)
def assert_value(self, value):
assert(self.min_value <= value <= self.max_value)
def assert_value(self, value: float) -> None:
assert self.min_value <= value <= self.max_value
def set_value_anim(self, value):
def set_value_anim(self, value: float) -> None:
prop = (value - self.min_value) / (self.max_value - self.min_value)
self.slider.move_to(self.slider_axis.point_from_proportion(prop))
def slider_on_mouse_drag(self, mob, event_data):
def slider_on_mouse_drag(self, mob, event_data: dict[str, np.ndarray]) -> bool:
self.set_value(self.get_value_from_point(event_data["point"]))
return False
# Helper Methods
def get_value_from_point(self, point):
def get_value_from_point(self, point: np.ndarray) -> float:
start, end = self.slider_axis.get_start_and_end()
point_on_line = get_closest_point_on_line(start, end, point)
prop = get_norm(point_on_line - start) / get_norm(end - start)
@@ -245,24 +275,29 @@ class LinearNumberSlider(ControlMobject):
class ColorSliders(Group):
CONFIG = {
"sliders_kwargs": {},
"rect_kwargs": {
def __init__(
self,
sliders_kwargs: dict = {},
rect_kwargs: dict = {
"width": 2.0,
"height": 0.5,
"stroke_opacity": 1.0
},
"background_grid_kwargs": {
background_grid_kwargs: dict = {
"colors": [GREY_A, GREY_C],
"single_square_len": 0.1
},
"sliders_buff": MED_LARGE_BUFF,
"default_rgb_value": 255,
"default_a_value": 1,
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
sliders_buff: float = MED_LARGE_BUFF,
default_rgb_value: int = 255,
default_a_value: int = 1,
**kwargs
):
self.sliders_kwargs = sliders_kwargs
self.rect_kwargs = rect_kwargs
self.background_grid_kwargs = background_grid_kwargs
self.sliders_buff = sliders_buff
self.default_rgb_value = default_rgb_value
self.default_a_value = default_a_value
rgb_kwargs = {"value": self.default_rgb_value, "min_value": 0, "max_value": 255, "step": 1}
a_kwargs = {"value": self.default_a_value, "min_value": 0, "max_value": 1, "step": 0.04}
@@ -282,7 +317,7 @@ class ColorSliders(Group):
self.r_slider.slider.set_color(RED)
self.g_slider.slider.set_color(GREEN)
self.b_slider.slider.set_color(BLUE)
self.a_slider.slider.set_color_by_gradient([BLACK, WHITE])
self.a_slider.slider.set_color_by_gradient(BLACK, WHITE)
self.selected_color_box = Rectangle(**self.rect_kwargs)
self.selected_color_box.add_updater(
@@ -300,7 +335,7 @@ class ColorSliders(Group):
self.arrange(DOWN)
def get_background(self):
def get_background(self) -> VGroup:
single_square_len = self.background_grid_kwargs["single_square_len"]
colors = self.background_grid_kwargs["colors"]
width = self.rect_kwargs["width"]
@@ -316,55 +351,62 @@ class ColorSliders(Group):
grid.move_to(self.selected_color_box)
for idx, square in enumerate(grid):
assert(isinstance(square, Square))
assert isinstance(square, Square)
square.set_stroke(width=0.0, opacity=0.0)
square.set_fill(colors[idx % len(colors)], 1.0)
return grid
def set_value(self, r, g, b, a):
def set_value(self, r: float, g: float, b: float, a: float):
self.r_slider.set_value(r)
self.g_slider.set_value(g)
self.b_slider.set_value(b)
self.a_slider.set_value(a)
def get_value(self):
def get_value(self) -> np.ndarary:
r = self.r_slider.get_value() / 255
g = self.g_slider.get_value() / 255
b = self.b_slider.get_value() / 255
alpha = self.a_slider.get_value()
return color_to_rgba(rgb_to_color((r, g, b)), alpha=alpha)
return np.array((r, g, b, alpha))
def get_picked_color(self):
def get_picked_color(self) -> str:
rgba = self.get_value()
return rgb_to_hex(rgba[:3])
def get_picked_opacity(self):
def get_picked_opacity(self) -> float:
rgba = self.get_value()
return rgba[3]
class Textbox(ControlMobject):
CONFIG = {
"value_type": np.dtype(object),
"box_kwargs": {
def __init__(
self,
value: str = "",
value_type: np.dtype = np.dtype(object),
box_kwargs: dict = {
"width": 2.0,
"height": 1.0,
"fill_color": WHITE,
"fill_opacity": 1.0,
},
"text_kwargs": {
text_kwargs: dict = {
"color": BLUE
},
"text_buff": MED_SMALL_BUFF,
"isInitiallyActive": False,
"active_color": BLUE,
"deactive_color": RED,
}
text_buff: float = MED_SMALL_BUFF,
isInitiallyActive: bool = False,
active_color: ManimColor = BLUE,
deactive_color: ManimColor = RED,
**kwargs
):
self.value_type = value_type
self.box_kwargs = box_kwargs
self.text_kwargs = text_kwargs
self.text_buff = text_buff
self.isInitiallyActive = isInitiallyActive
self.active_color = active_color
self.deactive_color = deactive_color
def __init__(self, value="", **kwargs):
digest_config(self, kwargs)
self.isActive = self.isInitiallyActive
self.box = Rectangle(**self.box_kwargs)
self.box.add_mouse_press_listner(self.box_on_mouse_press)
@@ -374,10 +416,10 @@ class Textbox(ControlMobject):
self.active_anim(self.isActive)
self.add_key_press_listner(self.on_key_press)
def set_value_anim(self, value):
def set_value_anim(self, value: str) -> None:
self.update_text(value)
def update_text(self, value):
def update_text(self, value: str) -> None:
text = self.text
self.remove(text)
text.__init__(value, **self.text_kwargs)
@@ -389,18 +431,18 @@ class Textbox(ControlMobject):
text.fix_in_frame()
self.add(text)
def active_anim(self, isActive):
def active_anim(self, isActive: bool) -> None:
if isActive:
self.box.set_stroke(self.active_color)
else:
self.box.set_stroke(self.deactive_color)
def box_on_mouse_press(self, mob, event_data):
def box_on_mouse_press(self, mob, event_data) -> bool:
self.isActive = not self.isActive
self.active_anim(self.isActive)
return False
def on_key_press(self, mob, event_data):
def on_key_press(self, mob: Mobject, event_data: dict[str, int]) -> bool | None:
symbol = event_data["symbol"]
modifiers = event_data["modifiers"]
char = chr(symbol)
@@ -423,28 +465,31 @@ class Textbox(ControlMobject):
class ControlPanel(Group):
CONFIG = {
"panel_kwargs": {
def __init__(
self,
*controls: ControlMobject,
panel_kwargs: dict = {
"width": FRAME_WIDTH / 4,
"height": MED_SMALL_BUFF + FRAME_HEIGHT,
"fill_color": GREY_C,
"fill_opacity": 1.0,
"stroke_width": 0.0
},
"opener_kwargs": {
opener_kwargs: dict = {
"width": FRAME_WIDTH / 8,
"height": 0.5,
"fill_color": GREY_C,
"fill_opacity": 1.0
},
"opener_text_kwargs": {
opener_text_kwargs: dict = {
"text": "Control Panel",
"font_size": 20
}
}
def __init__(self, *controls, **kwargs):
digest_config(self, kwargs)
},
**kwargs
):
self.panel_kwargs = panel_kwargs
self.opener_kwargs = opener_kwargs
self.opener_text_kwargs = opener_text_kwargs
self.panel = Rectangle(**self.panel_kwargs)
self.panel.to_corner(UP + LEFT, buff=0)
@@ -472,7 +517,7 @@ class ControlPanel(Group):
self.move_panel_and_controls_to_panel_opener()
self.fix_in_frame()
def move_panel_and_controls_to_panel_opener(self):
def move_panel_and_controls_to_panel_opener(self) -> None:
self.panel.next_to(
self.panel_opener_rect,
direction=UP,
@@ -488,11 +533,11 @@ class ControlPanel(Group):
self.controls.set_x(controls_old_x)
def add_controls(self, *new_controls):
def add_controls(self, *new_controls: ControlMobject) -> None:
self.controls.add(*new_controls)
self.move_panel_and_controls_to_panel_opener()
def remove_controls(self, *controls_to_remove):
def remove_controls(self, *controls_to_remove: ControlMobject) -> None:
self.controls.remove(*controls_to_remove)
self.move_panel_and_controls_to_panel_opener()
@@ -510,13 +555,13 @@ class ControlPanel(Group):
self.move_panel_and_controls_to_panel_opener()
return self
def panel_opener_on_mouse_drag(self, mob, event_data):
def panel_opener_on_mouse_drag(self, mob, event_data: dict[str, np.ndarray]) -> bool:
point = event_data["point"]
self.panel_opener.match_y(Dot(point))
self.move_panel_and_controls_to_panel_opener()
return False
def panel_on_mouse_scroll(self, mob, event_data):
def panel_on_mouse_scroll(self, mob, event_data: dict[str, np.ndarray]) -> bool:
offset = event_data["offset"]
factor = 10 * offset[1]
self.controls.set_y(self.controls.get_y() + factor)

View File

@@ -1,201 +1,293 @@
import numpy as np
import itertools as it
from __future__ import annotations
from manimlib.constants import *
import numpy as np
from manimlib.constants import DOWN, LEFT, RIGHT, ORIGIN
from manimlib.constants import DEG
from manimlib.mobject.numbers import DecimalNumber
from manimlib.mobject.numbers import Integer
from manimlib.mobject.shape_matchers import BackgroundRectangle
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import TexText
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
VECTOR_LABEL_SCALE_FACTOR = 0.8
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Sequence, Union, Optional
from manimlib.typing import ManimColor, Vect3, VectNArray, Self
def matrix_to_tex_string(matrix):
matrix = np.array(matrix).astype("str")
if matrix.ndim == 1:
matrix = matrix.reshape((matrix.size, 1))
n_rows, n_cols = matrix.shape
prefix = "\\left[ \\begin{array}{%s}" % ("c" * n_cols)
suffix = "\\end{array} \\right]"
rows = [
" & ".join(row)
for row in matrix
]
return prefix + " \\\\ ".join(rows) + suffix
def matrix_to_mobject(matrix):
return Tex(matrix_to_tex_string(matrix))
def vector_coordinate_label(vector_mob, integer_labels=True,
n_dim=2, color=WHITE):
vect = np.array(vector_mob.get_end())
if integer_labels:
vect = np.round(vect).astype(int)
vect = vect[:n_dim]
vect = vect.reshape((n_dim, 1))
label = Matrix(vect, add_background_rectangles_to_entries=True)
label.scale(VECTOR_LABEL_SCALE_FACTOR)
shift_dir = np.array(vector_mob.get_end())
if shift_dir[0] >= 0: # Pointing right
shift_dir -= label.get_left() + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER * LEFT
else: # Pointing left
shift_dir -= label.get_right() + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER * RIGHT
label.shift(shift_dir)
label.set_color(color)
label.rect = BackgroundRectangle(label)
label.add_to_back(label.rect)
return label
StringMatrixType = Union[Sequence[Sequence[str]], np.ndarray[int, np.dtype[np.str_]]]
FloatMatrixType = Union[Sequence[Sequence[float]], VectNArray]
VMobjectMatrixType = Sequence[Sequence[VMobject]]
GenericMatrixType = Union[FloatMatrixType, StringMatrixType, VMobjectMatrixType]
class Matrix(VMobject):
CONFIG = {
"v_buff": 0.8,
"h_buff": 1.3,
"bracket_h_buff": 0.2,
"bracket_v_buff": 0.25,
"add_background_rectangles_to_entries": False,
"include_background_rectangle": False,
"element_to_mobject": Tex,
"element_to_mobject_config": {},
"element_alignment_corner": DOWN,
}
def __init__(self, matrix, **kwargs):
def __init__(
self,
matrix: GenericMatrixType,
v_buff: float = 0.5,
h_buff: float = 0.5,
bracket_h_buff: float = 0.2,
bracket_v_buff: float = 0.25,
height: float | None = None,
element_config: dict = dict(),
element_alignment_corner: Vect3 = DOWN,
ellipses_row: Optional[int] = None,
ellipses_col: Optional[int] = None,
):
"""
Matrix can either include numbers, tex_strings,
or mobjects
"""
VMobject.__init__(self, **kwargs)
matrix = self.matrix = np.array(matrix, ndmin=2)
mob_matrix = self.matrix_to_mob_matrix(matrix)
self.organize_mob_matrix(mob_matrix)
# self.elements = VGroup(*mob_matrix.flatten())
self.elements = VGroup(*it.chain(*mob_matrix))
self.add(self.elements)
self.add_brackets()
self.center()
self.mob_matrix = mob_matrix
if self.add_background_rectangles_to_entries:
for mob in self.elements:
mob.add_background_rectangle()
if self.include_background_rectangle:
self.add_background_rectangle()
super().__init__()
def matrix_to_mob_matrix(self, matrix):
return [
self.mob_matrix = self.create_mobject_matrix(
matrix, v_buff, h_buff, element_alignment_corner,
**element_config
)
# Create helpful groups for the elements
n_cols = len(self.mob_matrix[0])
self.elements = [elem for row in self.mob_matrix for elem in row]
self.columns = VGroup(*(
VGroup(*(row[i] for row in self.mob_matrix))
for i in range(n_cols)
))
self.rows = VGroup(*(VGroup(*row) for row in self.mob_matrix))
if height is not None:
self.rows.set_height(height - 2 * bracket_v_buff)
self.brackets = self.create_brackets(self.rows, bracket_v_buff, bracket_h_buff)
self.ellipses = []
# Add elements and brackets
self.add(*self.elements)
self.add(*self.brackets)
self.center()
# Potentially add ellipses
self.swap_entries_for_ellipses(
ellipses_row,
ellipses_col,
)
def copy(self, deep: bool = False):
result = super().copy(deep)
self_family = self.get_family()
copy_family = result.get_family()
for attr in ["elements", "ellipses"]:
setattr(result, attr, [
copy_family[self_family.index(mob)]
for mob in getattr(self, attr)
])
return result
def create_mobject_matrix(
self,
matrix: GenericMatrixType,
v_buff: float,
h_buff: float,
aligned_corner: Vect3,
**element_config
) -> VMobjectMatrixType:
"""
Creates and organizes the matrix of mobjects
"""
mob_matrix = [
[
self.element_to_mobject(item, **self.element_to_mobject_config)
for item in row
self.element_to_mobject(element, **element_config)
for element in row
]
for row in matrix
]
def organize_mob_matrix(self, matrix):
for i, row in enumerate(matrix):
max_width = max(elem.get_width() for row in mob_matrix for elem in row)
max_height = max(elem.get_height() for row in mob_matrix for elem in row)
x_step = (max_width + h_buff) * RIGHT
y_step = (max_height + v_buff) * DOWN
for i, row in enumerate(mob_matrix):
for j, elem in enumerate(row):
mob = matrix[i][j]
mob.move_to(
i * self.v_buff * DOWN + j * self.h_buff * RIGHT,
self.element_alignment_corner
)
return self
elem.move_to(i * y_step + j * x_step, aligned_corner)
return mob_matrix
def add_brackets(self):
height = self.matrix.shape[0]
bracket_pair = Tex("".join([
"\\left[",
"\\begin{array}{c}",
*height * ["\\quad \\\\"],
"\\end{array}"
"\\right]",
]))[0]
bracket_pair.set_height(
self.get_height() + 1 * self.bracket_v_buff
)
l_bracket = bracket_pair[:len(bracket_pair) // 2]
r_bracket = bracket_pair[len(bracket_pair) // 2:]
l_bracket.next_to(self, LEFT, self.bracket_h_buff)
r_bracket.next_to(self, RIGHT, self.bracket_h_buff)
self.add(l_bracket, r_bracket)
self.brackets = VGroup(l_bracket, r_bracket)
return self
def element_to_mobject(self, element, **config) -> VMobject:
if isinstance(element, VMobject):
return element
elif isinstance(element, float | complex):
return DecimalNumber(element, **config)
else:
return Tex(str(element), **config)
def get_columns(self):
return VGroup(*[
VGroup(*[row[i] for row in self.mob_matrix])
for i in range(len(self.mob_matrix[0]))
])
def create_brackets(self, rows, v_buff: float, h_buff: float) -> VGroup:
brackets = Tex("".join((
R"\left[\begin{array}{c}",
*len(rows) * [R"\quad \\"],
R"\end{array}\right]",
)))
brackets.set_height(rows.get_height() + v_buff)
l_bracket = brackets[:len(brackets) // 2]
r_bracket = brackets[len(brackets) // 2:]
l_bracket.next_to(rows, LEFT, h_buff)
r_bracket.next_to(rows, RIGHT, h_buff)
return VGroup(l_bracket, r_bracket)
def get_rows(self):
return VGroup(*[
VGroup(*row)
for row in self.mob_matrix
])
def get_column(self, index: int):
if not 0 <= index < len(self.columns):
raise IndexError(f"Index {index} out of bound for matrix with {len(self.columns)} columns")
return self.columns[index]
def set_column_colors(self, *colors):
def get_row(self, index: int):
if not 0 <= index < len(self.rows):
raise IndexError(f"Index {index} out of bound for matrix with {len(self.rows)} rows")
return self.rows[index]
def get_columns(self) -> VGroup:
return self.columns
def get_rows(self) -> VGroup:
return self.rows
def set_column_colors(self, *colors: ManimColor) -> Self:
columns = self.get_columns()
for color, column in zip(colors, columns):
column.set_color(color)
return self
def add_background_to_entries(self):
def add_background_to_entries(self) -> Self:
for mob in self.get_entries():
mob.add_background_rectangle()
return self
def get_mob_matrix(self):
def swap_entry_for_dots(self, entry, dots):
dots.move_to(entry)
entry.become(dots)
if entry in self.elements:
self.elements.remove(entry)
if entry not in self.ellipses:
self.ellipses.append(entry)
def swap_entries_for_ellipses(
self,
row_index: Optional[int] = None,
col_index: Optional[int] = None,
height_ratio: float = 0.65,
width_ratio: float = 0.4
):
rows = self.get_rows()
cols = self.get_columns()
avg_row_height = rows.get_height() / len(rows)
vdots_height = height_ratio * avg_row_height
avg_col_width = cols.get_width() / len(cols)
hdots_width = width_ratio * avg_col_width
use_vdots = row_index is not None and -len(rows) <= row_index < len(rows)
use_hdots = col_index is not None and -len(cols) <= col_index < len(cols)
if use_vdots:
for column in cols:
# Add vdots
dots = Tex(R"\vdots")
dots.set_height(vdots_height)
self.swap_entry_for_dots(column[row_index], dots)
if use_hdots:
for row in rows:
# Add hdots
dots = Tex(R"\hdots")
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 * DEG)
return self
def get_mob_matrix(self) -> VMobjectMatrixType:
return self.mob_matrix
def get_entries(self):
return self.elements
def get_entries(self) -> VGroup:
return VGroup(*self.elements)
def get_brackets(self):
return self.brackets
def get_brackets(self) -> VGroup:
return VGroup(*self.brackets)
def get_ellipses(self) -> VGroup:
return VGroup(*self.ellipses)
class DecimalMatrix(Matrix):
CONFIG = {
"element_to_mobject": DecimalNumber,
"element_to_mobject_config": {"num_decimal_places": 1}
}
def __init__(
self,
matrix: FloatMatrixType,
num_decimal_places: int = 2,
decimal_config: dict = dict(),
**config
):
self.float_matrix = matrix
super().__init__(
matrix,
element_config=dict(
num_decimal_places=num_decimal_places,
**decimal_config
),
**config
)
def element_to_mobject(self, element, **decimal_config) -> DecimalNumber:
return DecimalNumber(element, **decimal_config)
class IntegerMatrix(Matrix):
CONFIG = {
"element_to_mobject": Integer,
"element_alignment_corner": UP,
}
class IntegerMatrix(DecimalMatrix):
def __init__(
self,
matrix: FloatMatrixType,
num_decimal_places: int = 0,
decimal_config: dict = dict(),
**config
):
super().__init__(matrix, num_decimal_places, decimal_config, **config)
class TexMatrix(Matrix):
def __init__(
self,
matrix: StringMatrixType,
tex_config: dict = dict(),
**config,
):
super().__init__(
matrix,
element_config=tex_config,
**config
)
class MobjectMatrix(Matrix):
CONFIG = {
"element_to_mobject": lambda m: m,
}
def __init__(
self,
group: VGroup,
n_rows: int | None = None,
n_cols: int | None = None,
height: float = 4.0,
element_alignment_corner=ORIGIN,
**config,
):
# Have fallback defaults of n_rows and n_cols
n_mobs = len(group)
if n_rows is None:
n_rows = int(np.sqrt(n_mobs)) if n_cols is None else n_mobs // n_cols
if n_cols is None:
n_cols = n_mobs // n_rows
if len(group) < n_rows * n_cols:
raise Exception("Input to MobjectMatrix must have at least n_rows * n_cols entries")
def get_det_text(matrix, determinant=None, background_rect=False, initial_scale_factor=2):
parens = Tex("(", ")")
parens.scale(initial_scale_factor)
parens.stretch_to_fit_height(matrix.get_height())
l_paren, r_paren = parens.split()
l_paren.next_to(matrix, LEFT, buff=0.1)
r_paren.next_to(matrix, RIGHT, buff=0.1)
det = TexText("det")
det.scale(initial_scale_factor)
det.next_to(l_paren, LEFT, buff=0.1)
if background_rect:
det.add_background_rectangle()
det_text = VGroup(det, l_paren, r_paren)
if determinant is not None:
eq = Tex("=")
eq.next_to(r_paren, RIGHT, buff=0.1)
result = Tex(str(determinant))
result.next_to(eq, RIGHT, buff=0.2)
det_text.add(eq, result)
return det_text
mob_matrix = [
[group[n * n_cols + k] for k in range(n_cols)]
for n in range(n_rows)
]
config.update(
height=height,
element_alignment_corner=element_alignment_corner,
)
super().__init__(mob_matrix, **config)
def element_to_mobject(self, element: VMobject, **config) -> VMobject:
return element

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,26 @@
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
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
import numpy as np
from manimlib.animation.animation import Animation
def assert_is_mobject_method(method):
assert(inspect.ismethod(method))
assert inspect.ismethod(method)
mobject = method.__self__
assert(isinstance(mobject, Mobject))
assert isinstance(mobject, Mobject)
def always(method, *args, **kwargs):
@@ -41,27 +52,39 @@ def f_always(method, *arg_generators, **kwargs):
return mobject
def always_redraw(func, *args, **kwargs):
def always_redraw(func: Callable[..., Mobject], *args, **kwargs) -> Mobject:
mob = func(*args, **kwargs)
mob.add_updater(lambda m: mob.become(func(*args, **kwargs)))
return mob
def always_shift(mobject, direction=RIGHT, rate=0.1):
def always_shift(
mobject: Mobject,
direction: np.ndarray = RIGHT,
rate: float = 0.1
) -> Mobject:
mobject.add_updater(
lambda m, dt: m.shift(dt * rate * direction)
)
return mobject
def always_rotate(mobject, rate=20 * DEGREES, **kwargs):
def always_rotate(
mobject: Mobject,
rate: float = 20 * DEG,
**kwargs
) -> Mobject:
mobject.add_updater(
lambda m, dt: m.rotate(dt * rate, **kwargs)
)
return mobject
def turn_animation_into_updater(animation, cycle=False, **kwargs):
def turn_animation_into_updater(
animation: Animation,
cycle: bool = False,
**kwargs
) -> Mobject:
"""
Add an updater to the animation's mobject which applies
the interpolation and update functions of the animation
@@ -70,7 +93,7 @@ def turn_animation_into_updater(animation, cycle=False, **kwargs):
the updater will be popped uplon completion
"""
mobject = animation.mobject
animation.update_config(**kwargs)
animation.update_rate_info(**kwargs)
animation.suspend_mobject_updating = False
animation.begin()
animation.total_time = 0
@@ -94,7 +117,7 @@ def turn_animation_into_updater(animation, cycle=False, **kwargs):
return mobject
def cycle_animation(animation, **kwargs):
def cycle_animation(animation: Animation, **kwargs) -> Mobject:
return turn_animation_into_updater(
animation, cycle=True, **kwargs
)

View File

@@ -1,94 +1,121 @@
from manimlib.constants import *
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.mobject.numbers import DecimalNumber
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.bezier import interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.config_ops import merge_dicts_recursively
from manimlib.utils.bezier import outer_interpolate
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.simple_functions import fdiv
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterable, Optional
from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier
class NumberLine(Line):
CONFIG = {
"color": GREY_B,
"stroke_width": 2,
# List of 2 or 3 elements, x_min, x_max, step_size
"x_range": [-8, 8, 1],
def __init__(
self,
x_range: RangeSpecifier = (-8, 8, 1),
color: ManimColor = GREY_B,
stroke_width: float = 2.0,
# How big is one one unit of this number line in terms of absolute spacial distance
"unit_size": 1,
"width": None,
"include_ticks": True,
"tick_size": 0.1,
"longer_tick_multiple": 1.5,
"tick_offset": 0,
unit_size: float = 1.0,
width: Optional[float] = None,
include_ticks: bool = True,
tick_size: float = 0.1,
longer_tick_multiple: float = 1.5,
tick_offset: float = 0.0,
# Change name
"numbers_with_elongated_ticks": [],
"include_numbers": False,
"line_to_number_direction": DOWN,
"line_to_number_buff": MED_SMALL_BUFF,
"include_tip": False,
"tip_config": {
"width": 0.25,
"length": 0.25,
},
"decimal_number_config": {
"num_decimal_places": 0,
"font_size": 36,
},
"numbers_to_exclude": None
}
def __init__(self, x_range=None, **kwargs):
digest_config(self, kwargs)
if x_range is None:
x_range = self.x_range
if len(x_range) == 2:
x_range = [*x_range, 1]
x_min, x_max, x_step = x_range
# A lot of old scenes pass in x_min or x_max explicitly,
# so this is just here to keep those workin
self.x_min = kwargs.get("x_min", x_min)
self.x_max = kwargs.get("x_max", x_max)
self.x_step = kwargs.get("x_step", x_step)
super().__init__(self.x_min * RIGHT, self.x_max * RIGHT, **kwargs)
if self.width:
self.set_width(self.width)
self.unit_size = self.get_unit_size()
big_tick_spacing: Optional[float] = None,
big_tick_numbers: list[float] = [],
include_numbers: bool = False,
line_to_number_direction: Vect3 = DOWN,
line_to_number_buff: float = MED_SMALL_BUFF,
include_tip: bool = False,
tip_config: dict = dict(
width=0.25,
length=0.25,
),
decimal_number_config: dict = dict(
num_decimal_places=0,
font_size=36,
),
numbers_to_exclude: list | None = None,
**kwargs,
):
self.x_range = x_range
self.tick_size = tick_size
self.longer_tick_multiple = longer_tick_multiple
self.tick_offset = tick_offset
if big_tick_spacing is not None:
self.big_tick_numbers = np.arange(
x_range[0],
x_range[1] + big_tick_spacing,
big_tick_spacing,
)
else:
self.scale(self.unit_size)
self.big_tick_numbers = list(big_tick_numbers)
self.line_to_number_direction = line_to_number_direction
self.line_to_number_buff = line_to_number_buff
self.include_tip = include_tip
self.tip_config = dict(tip_config)
self.decimal_number_config = dict(decimal_number_config)
self.numbers_to_exclude = numbers_to_exclude
self.x_min, self.x_max = x_range[:2]
self.x_step = 1 if len(x_range) == 2 else x_range[2]
super().__init__(
self.x_min * RIGHT, self.x_max * RIGHT,
color=color,
stroke_width=stroke_width,
**kwargs
)
if width:
self.set_width(width)
else:
self.scale(unit_size)
self.center()
if self.include_tip:
if include_tip:
self.add_tip()
self.tip.set_stroke(
self.stroke_color,
self.stroke_width,
)
if self.include_ticks:
if include_ticks:
self.add_ticks()
if self.include_numbers:
if include_numbers:
self.add_numbers(excluding=self.numbers_to_exclude)
def get_tick_range(self):
def get_tick_range(self) -> np.ndarray:
if self.include_tip:
x_max = self.x_max
else:
x_max = self.x_max + self.x_step
return np.arange(self.x_min, x_max, self.x_step)
result = np.arange(self.x_min, x_max, self.x_step)
return result[result <= self.x_max]
def add_ticks(self):
def add_ticks(self) -> None:
ticks = VGroup()
for x in self.get_tick_range():
size = self.tick_size
if np.isclose(self.numbers_with_elongated_ticks, x).any():
if np.isclose(self.big_tick_numbers, x).any():
size *= self.longer_tick_multiple
ticks.add(self.get_tick(x, size))
self.add(ticks)
self.ticks = ticks
def get_tick(self, x, size=None):
def get_tick(self, x: float, size: float | None = None) -> Line:
if size is None:
size = self.tick_size
result = Line(size * DOWN, size * UP)
@@ -97,17 +124,18 @@ class NumberLine(Line):
result.match_style(self)
return result
def get_tick_marks(self):
def get_tick_marks(self) -> VGroup:
return self.ticks
def number_to_point(self, number):
alpha = float(number - self.x_min) / (self.x_max - self.x_min)
return interpolate(self.get_start(), self.get_end(), alpha)
def number_to_point(self, number: float | VectN) -> Vect3 | Vect3Array:
start = self.get_points()[0]
end = self.get_points()[-1]
alpha = (number - self.x_min) / (self.x_max - self.x_min)
return outer_interpolate(start, end, alpha)
def point_to_number(self, point):
points = self.get_points()
start = points[0]
end = points[-1]
def point_to_number(self, point: Vect3 | Vect3Array) -> float | VectN:
start = self.get_points()[0]
end = self.get_points()[-1]
vect = end - start
proportion = fdiv(
np.dot(point - start, vect),
@@ -115,30 +143,37 @@ class NumberLine(Line):
)
return interpolate(self.x_min, self.x_max, proportion)
def n2p(self, number):
def n2p(self, number: float | VectN) -> Vect3 | Vect3Array:
"""Abbreviation for number_to_point"""
return self.number_to_point(number)
def p2n(self, point):
def p2n(self, point: Vect3 | Vect3Array) -> float | VectN:
"""Abbreviation for point_to_number"""
return self.point_to_number(point)
def get_unit_size(self):
def get_unit_size(self) -> float:
return self.get_length() / (self.x_max - self.x_min)
def get_number_mobject(self, x,
direction=None,
buff=None,
**number_config):
def get_number_mobject(
self,
x: float,
direction: Vect3 | None = None,
buff: float | None = None,
unit: float = 1.0,
unit_tex: str = "",
**number_config
) -> DecimalNumber:
number_config = merge_dicts_recursively(
self.decimal_number_config, number_config
self.decimal_number_config, number_config,
)
if direction is None:
direction = self.line_to_number_direction
if buff is None:
buff = self.line_to_number_buff
if unit_tex:
number_config["unit"] = unit_tex
num_mob = DecimalNumber(x, **number_config)
num_mob = DecimalNumber(x / unit, **number_config)
num_mob.next_to(
self.number_to_point(x),
direction=direction,
@@ -147,9 +182,19 @@ 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:
center = num_mob.get_center()
num_mob.remove(num_mob[0])
num_mob.move_to(center)
return num_mob
def add_numbers(self, x_values=None, excluding=None, font_size=24, **kwargs):
def add_numbers(
self,
x_values: Iterable[float] | None = None,
excluding: Iterable[float] | None = None,
font_size: int = 24,
**kwargs
) -> VGroup:
if x_values is None:
x_values = self.get_tick_range()
@@ -169,11 +214,18 @@ class NumberLine(Line):
class UnitInterval(NumberLine):
CONFIG = {
"x_range": [0, 1, 0.1],
"unit_size": 10,
"numbers_with_elongated_ticks": [0, 1],
"decimal_number_config": {
"num_decimal_places": 1,
}
}
def __init__(
self,
x_range: RangeSpecifier = (0, 1, 0.1),
unit_size: float = 10,
big_tick_numbers: list[float] = [0, 1],
decimal_number_config: dict = dict(
num_decimal_places=1,
)
):
super().__init__(
x_range=x_range,
unit_size=unit_size,
big_tick_numbers=big_tick_numbers,
decimal_number_config=decimal_number_config,
)

View File

@@ -1,72 +1,128 @@
from manimlib.constants import *
from manimlib.mobject.svg.tex_mobject import SingleStringTex
from __future__ import annotations
from functools import lru_cache
import numpy as np
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import WHITE
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.text_mobject import Text
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.paths import straight_path
from manimlib.utils.bezier import interpolate
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import TypeVar, Callable
from manimlib.mobject.mobject import Mobject
from manimlib.typing import ManimColor, Vect3, Self
T = TypeVar("T", bound=VMobject)
string_to_mob_map = {}
@lru_cache()
def char_to_cahced_mob(char: str, **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):
CONFIG = {
"stroke_width": 0,
"fill_opacity": 1.0,
"num_decimal_places": 2,
"include_sign": False,
"group_with_commas": True,
"digit_buff_per_font_unit": 0.001,
"show_ellipsis": False,
"unit": None, # Aligned to bottom unless it starts with "^"
"include_background_rectangle": False,
"edge_to_fix": LEFT,
"font_size": 48,
}
def __init__(
self,
number: float | complex = 0,
color: ManimColor = WHITE,
stroke_width: float = 0,
fill_opacity: float = 1.0,
fill_border_width: float = 0.5,
num_decimal_places: int = 2,
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,
edge_to_fix: Vect3 = LEFT,
font_size: float = 48,
text_config: dict = dict(), # Do not pass in font_size here
**kwargs
):
self.num_decimal_places = num_decimal_places
self.include_sign = include_sign
self.group_with_commas = group_with_commas
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.edge_to_fix = edge_to_fix
self.font_size = font_size
self.text_config = dict(text_config)
super().__init__(
color=color,
stroke_width=stroke_width,
fill_opacity=fill_opacity,
fill_border_width=fill_border_width,
**kwargs
)
def __init__(self, number=0, **kwargs):
super().__init__(**kwargs)
self.set_submobjects_from_number(number)
self.init_colors()
def set_submobjects_from_number(self, number):
def set_submobjects_from_number(self, number: float | complex) -> None:
# Create the submobject list
self.number = number
self.set_submobjects([])
self.num_string = self.get_num_string(number)
num_string = self.get_num_string(number)
self.add(*map(self.string_to_mob, num_string))
# Add non-numerical bits
# Submob_templates will be a list of cached Tex and Text mobjects,
# with the intent of calling .copy or .become on them
submob_templates = list(map(self.char_to_mob, self.num_string))
if self.show_ellipsis:
dots = self.string_to_mob("...")
dots = self.char_to_mob("...")
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
self.add(dots)
submob_templates.append(dots)
if self.unit is not None:
self.unit_sign = self.string_to_mob(self.unit, SingleStringTex)
self.add(self.unit_sign)
submob_templates.append(self.char_to_mob(self.unit))
self.arrange(
buff=self.digit_buff_per_font_unit * self.get_font_size(),
aligned_edge=DOWN
)
# Set internals
font_size = self.get_font_size()
if len(submob_templates) == len(self.submobjects):
for sm, smt in zip(self.submobjects, submob_templates):
sm.become(smt)
sm.scale(font_size / smt.font_size)
else:
self.set_submobjects([
smt.copy().scale(font_size / smt.font_size)
for smt in submob_templates
])
# Handle alignment of parts that should be aligned
# to the bottom
for i, c in enumerate(num_string):
if c == "" and len(num_string) > i + 1:
digit_buff = self.digit_buff_per_font_unit * font_size
self.arrange(RIGHT, buff=digit_buff, aligned_edge=DOWN)
# Handle alignment of special characters
for i, c in enumerate(self.num_string):
if c == "" and len(self.num_string) > i + 1:
self[i].align_to(self[i + 1], UP)
self[i].shift(self[i + 1].get_height() * DOWN / 2)
elif c == ",":
self[i].shift(self[i].get_height() * DOWN / 2)
if self.unit and self.unit.startswith("^"):
self.unit_sign.align_to(self, UP)
self[-1].align_to(self, UP)
if self.include_background_rectangle:
self.add_background_rectangle()
def get_num_string(self, number):
def get_num_string(self, number: float | complex) -> str:
if isinstance(number, complex):
formatter = self.get_complex_formatter()
else:
formatter = self.get_formatter()
if self.num_decimal_places == 0 and isinstance(number, float):
number = int(number)
num_string = formatter.format(number)
rounded_num = np.round(number, self.num_decimal_places)
@@ -78,21 +134,24 @@ class DecimalNumber(VMobject):
num_string = num_string.replace("-", "")
return num_string
def init_data(self):
super().init_data()
self.data["font_size"] = np.array([self.font_size], dtype=float)
def char_to_mob(self, char: str) -> Text:
return char_to_cahced_mob(char, **self.text_config)
def get_font_size(self):
return self.data["font_size"][0]
def interpolate(
self,
mobject1: Mobject,
mobject2: Mobject,
alpha: float,
path_func: Callable[[np.ndarray, np.ndarray, float], np.ndarray] = straight_path
) -> Self:
super().interpolate(mobject1, mobject2, alpha, path_func)
if hasattr(mobject1, "font_size") and hasattr(mobject2, "font_size"):
self.font_size = interpolate(mobject1.font_size, mobject2.font_size, alpha)
def string_to_mob(self, string, mob_class=Text):
if string not in string_to_mob_map:
string_to_mob_map[string] = mob_class(string, font_size=1)
mob = string_to_mob_map[string].copy()
mob.scale(self.get_font_size())
return mob
def get_font_size(self) -> float:
return self.font_size
def get_formatter(self, **kwargs):
def get_formatter(self, **kwargs) -> str:
"""
Configuration is based first off instance attributes,
but overwritten by any kew word argument. Relevant
@@ -111,46 +170,57 @@ class DecimalNumber(VMobject):
]
])
config.update(kwargs)
ndp = config["num_decimal_places"]
return "".join([
"{",
config.get("field_name", ""),
":",
"+" if config["include_sign"] else "",
"," if config["group_with_commas"] else "",
".", str(config["num_decimal_places"]), "f",
f".{ndp}f" if ndp > 0 else "d",
"}",
])
def get_complex_formatter(self, **kwargs):
def get_complex_formatter(self, **kwargs) -> str:
return "".join([
self.get_formatter(field_name="0.real"),
self.get_formatter(field_name="0.imag", include_sign=True),
"i"
])
def set_value(self, number):
def get_tex(self):
return self.num_string
def set_value(self, number: float | complex) -> Self:
move_to_point = self.get_edge_center(self.edge_to_fix)
old_submobjects = list(self.submobjects)
style = self.family_members_with_points()[0].get_style()
self.set_submobjects_from_number(number)
self.move_to(move_to_point, self.edge_to_fix)
for sm1, sm2 in zip(self.submobjects, old_submobjects):
sm1.match_style(sm2)
self.set_style(**style)
for submob in self.get_family():
submob.uniforms.update(self.uniforms)
return self
def _handle_scale_side_effects(self, scale_factor):
self.data["font_size"] *= scale_factor
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
self.font_size *= scale_factor
return self
def get_value(self):
def get_value(self) -> float | complex:
return self.number
def increment_value(self, delta_t=1):
def increment_value(self, delta_t: float | complex = 1) -> Self:
self.set_value(self.get_value() + delta_t)
return self
class Integer(DecimalNumber):
CONFIG = {
"num_decimal_places": 0,
}
def __init__(
self,
number: int = 0,
num_decimal_places: int = 0,
**kwargs,
):
super().__init__(number, num_decimal_places=num_decimal_places, **kwargs)
def get_value(self):
def get_value(self) -> int:
return int(np.round(super().get_value()))

View File

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

View File

@@ -1,54 +1,88 @@
from manimlib.constants import *
from __future__ import annotations
from colour import Color
from manimlib.config import manim_config
from manimlib.constants import BLACK, RED, YELLOW, WHITE
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.color import Color
from manimlib.utils.customization import get_customization
from manimlib.utils.config_ops import digest_config
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Sequence
from manimlib.mobject.mobject import Mobject
from manimlib.typing import ManimColor, Self
class SurroundingRectangle(Rectangle):
CONFIG = {
"color": YELLOW,
"buff": SMALL_BUFF,
}
def __init__(
self,
mobject: Mobject,
buff: float = SMALL_BUFF,
color: ManimColor = YELLOW,
**kwargs
):
super().__init__(color=color, **kwargs)
self.buff = buff
self.surround(mobject)
if mobject.is_fixed_in_frame():
self.fix_in_frame()
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
kwargs["width"] = mobject.get_width() + 2 * self.buff
kwargs["height"] = mobject.get_height() + 2 * self.buff
Rectangle.__init__(self, **kwargs)
self.move_to(mobject)
def surround(self, mobject, buff=None) -> Self:
self.mobject = mobject
self.buff = buff if buff is not None else self.buff
super().surround(mobject, self.buff)
return self
def set_buff(self, buff) -> Self:
self.buff = buff
self.surround(self.mobject)
return self
class BackgroundRectangle(SurroundingRectangle):
CONFIG = {
"stroke_width": 0,
"stroke_opacity": 0,
"fill_opacity": 0.75,
"buff": 0
}
def __init__(self, mobject, color=None, **kwargs):
def __init__(
self,
mobject: Mobject,
color: ManimColor = None,
stroke_width: float = 0,
stroke_opacity: float = 0,
fill_opacity: float = 0.75,
buff: float = 0,
**kwargs
):
if color is None:
color = get_customization()['style']['background_color']
SurroundingRectangle.__init__(self, mobject, color=color, **kwargs)
self.original_fill_opacity = self.fill_opacity
color = manim_config.camera.background_color
super().__init__(
mobject,
color=color,
stroke_width=stroke_width,
stroke_opacity=stroke_opacity,
fill_opacity=fill_opacity,
buff=buff,
**kwargs
)
self.original_fill_opacity = fill_opacity
def pointwise_become_partial(self, mobject, a, b):
def pointwise_become_partial(self, mobject: Mobject, a: float, b: float) -> Self:
self.set_fill(opacity=b * self.original_fill_opacity)
return self
def set_style_data(self,
stroke_color=None,
stroke_width=None,
fill_color=None,
fill_opacity=None,
family=True
):
def set_style(
self,
stroke_color: ManimColor | None = None,
stroke_width: float | None = None,
fill_color: ManimColor | None = None,
fill_opacity: float | None = None,
family: bool = True
) -> Self:
# Unchangeable style, except for fill_opacity
VMobject.set_style_data(
VMobject.set_style(
self,
stroke_color=BLACK,
stroke_width=0,
@@ -57,32 +91,40 @@ class BackgroundRectangle(SurroundingRectangle):
)
return self
def get_fill_color(self):
def get_fill_color(self) -> Color:
return Color(self.color)
class Cross(VGroup):
CONFIG = {
"stroke_color": RED,
"stroke_width": [0, 6, 0],
}
def __init__(self, mobject, **kwargs):
def __init__(
self,
mobject: Mobject,
stroke_color: ManimColor = RED,
stroke_width: float | Sequence[float] = [0, 6, 0],
**kwargs
):
super().__init__(
Line(UL, DR),
Line(UR, DL),
)
self.insert_n_curves(2)
self.insert_n_curves(20)
self.replace(mobject, stretch=True)
self.set_stroke(self.stroke_color, width=self.stroke_width)
self.set_stroke(stroke_color, width=stroke_width)
class Underline(Line):
CONFIG = {
"buff": SMALL_BUFF,
}
def __init__(self, mobject, **kwargs):
def __init__(
self,
mobject: Mobject,
buff: float = SMALL_BUFF,
stroke_color=WHITE,
stroke_width: float | Sequence[float] = [0, 3, 3, 0],
stretch_factor=1.2,
**kwargs
):
super().__init__(LEFT, RIGHT, **kwargs)
self.match_width(mobject)
self.next_to(mobject, DOWN, buff=self.buff)
if not isinstance(stroke_width, (float, int)):
self.insert_n_curves(len(stroke_width) - 2)
self.set_stroke(stroke_color, stroke_width)
self.set_width(mobject.get_width() * stretch_factor)
self.next_to(mobject, DOWN, buff=buff)

View File

@@ -1,41 +1,58 @@
import numpy as np
import math
from __future__ import annotations
import math
import copy
import numpy as np
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF, SMALL_BUFF
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL
from manimlib.constants import PI
from manimlib.animation.composition import AnimationGroup
from manimlib.constants import *
from manimlib.animation.fading import FadeIn
from manimlib.animation.growing import GrowFromCenter
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import SingleStringTex
from manimlib.mobject.svg.tex_mobject import TexText
from manimlib.mobject.svg.text_mobject import Text
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config
from manimlib.utils.iterables import listify
from manimlib.utils.space_ops import get_norm
from typing import TYPE_CHECKING
class Brace(SingleStringTex):
CONFIG = {
"buff": 0.2,
"tex_string": r"\underbrace{\qquad}"
}
if TYPE_CHECKING:
from typing import Iterable
from manimlib.animation.animation import Animation
from manimlib.mobject.mobject import Mobject
from manimlib.typing import Vect3
class Brace(Tex):
def __init__(
self,
mobject: Mobject,
direction: Vect3 = DOWN,
buff: float = 0.2,
tex_string: str = R"\underbrace{\qquad}",
**kwargs
):
super().__init__(tex_string, **kwargs)
def __init__(self, mobject, direction=DOWN, **kwargs):
digest_config(self, kwargs, locals())
angle = -math.atan2(*direction[:2]) + PI
mobject.rotate(-angle, about_point=ORIGIN)
left = mobject.get_corner(DOWN + LEFT)
right = mobject.get_corner(DOWN + RIGHT)
left = mobject.get_corner(DL)
right = mobject.get_corner(DR)
target_width = right[0] - left[0]
super().__init__(self.tex_string, **kwargs)
self.tip_point_index = np.argmin(self.get_all_points()[:, 1])
self.set_initial_width(target_width)
self.shift(left - self.get_corner(UP + LEFT) + self.buff * DOWN)
self.shift(left - self.get_corner(UL) + buff * DOWN)
for mob in mobject, self:
mob.rotate(angle, about_point=ORIGIN)
def set_initial_width(self, width):
def set_initial_width(self, width: float):
width_diff = width - self.get_width()
if width_diff > 0:
for tip, rect, vect in [(self[0], self[1], RIGHT), (self[5], self[4], LEFT)]:
@@ -48,7 +65,12 @@ class Brace(SingleStringTex):
self.set_width(width, stretch=True)
return self
def put_at_tip(self, mob, use_next_to=True, **kwargs):
def put_at_tip(
self,
mob: Mobject,
use_next_to: bool = True,
**kwargs
):
if use_next_to:
mob.next_to(
self.get_tip(),
@@ -57,60 +79,69 @@ class Brace(SingleStringTex):
)
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
def get_text(self, text, **kwargs):
def get_text(self, text: str, **kwargs) -> Text:
buff = kwargs.pop("buff", SMALL_BUFF)
text_mob = Text(text, **kwargs)
self.put_at_tip(text_mob, buff=buff)
return text_mob
def get_tex(self, *tex, **kwargs):
tex_mob = Tex(*tex)
self.put_at_tip(tex_mob, **kwargs)
def get_tex(self, *tex: str, **kwargs) -> Tex:
buff = kwargs.pop("buff", SMALL_BUFF)
tex_mob = Tex(*tex, **kwargs)
self.put_at_tip(tex_mob, buff=buff)
return tex_mob
def get_tip(self):
def get_tip(self) -> np.ndarray:
# Very specific to the LaTeX representation
# of a brace, but it's the only way I can think
# of to get the tip regardless of orientation.
return self.get_all_points()[self.tip_point_index]
def get_direction(self):
def get_direction(self) -> np.ndarray:
vect = self.get_tip() - self.get_center()
return vect / get_norm(vect)
class BraceLabel(VMobject):
CONFIG = {
"label_constructor": Tex,
"label_scale": 1,
}
label_constructor: type = Tex
def __init__(self, obj, text, brace_direction=DOWN, **kwargs):
VMobject.__init__(self, **kwargs)
def __init__(
self,
obj: VMobject | list[VMobject],
text: str | Iterable[str],
brace_direction: np.ndarray = DOWN,
label_scale: float = 1.0,
label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF,
**kwargs
) -> None:
super().__init__(**kwargs)
self.brace_direction = brace_direction
self.label_scale = label_scale
self.label_buff = label_buff
if isinstance(obj, list):
obj = VMobject(*obj)
obj = VGroup(*obj)
self.brace = Brace(obj, brace_direction, **kwargs)
if isinstance(text, tuple) or isinstance(text, list):
self.label = self.label_constructor(*text, **kwargs)
else:
self.label = self.label_constructor(str(text))
if self.label_scale != 1:
self.label.scale(self.label_scale)
self.label = self.label_constructor(*listify(text), **kwargs)
self.label.scale(self.label_scale)
self.brace.put_at_tip(self.label)
self.brace.put_at_tip(self.label, buff=self.label_buff)
self.set_submobjects([self.brace, self.label])
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):
def creation_anim(
self,
label_anim: Animation = FadeIn,
brace_anim: Animation = GrowFromCenter
) -> AnimationGroup:
return AnimationGroup(brace_anim(self.brace), label_anim(self.label))
def shift_brace(self, obj, **kwargs):
def shift_brace(self, obj: VMobject | list[VMobject], **kwargs):
if isinstance(obj, list):
obj = VMobject(*obj)
self.brace = Brace(obj, self.brace_direction, **kwargs)
@@ -118,7 +149,7 @@ class BraceLabel(VMobject):
self.submobjects[0] = self.brace
return self
def change_label(self, *text, **kwargs):
def change_label(self, *text: str, **kwargs):
self.label = self.label_constructor(*text, **kwargs)
if self.label_scale != 1:
self.label.scale(self.label_scale)
@@ -127,7 +158,7 @@ class BraceLabel(VMobject):
self.submobjects[1] = self.label
return self
def change_brace_label(self, obj, *text):
def change_brace_label(self, obj: VMobject | list[VMobject], *text: str):
self.shift_brace(obj)
self.change_label(*text)
return self
@@ -142,6 +173,4 @@ class BraceLabel(VMobject):
class BraceText(BraceLabel):
CONFIG = {
"label_constructor": TexText
}
label_constructor: type = TexText

View File

@@ -1,81 +1,146 @@
from manimlib.animation.animation import Animation
from __future__ import annotations
import numpy as np
import itertools as it
import random
from manimlib.animation.composition import AnimationGroup
from manimlib.animation.rotation import Rotating
from manimlib.constants import *
from manimlib.constants import BLACK
from manimlib.constants import BLUE_A
from manimlib.constants import BLUE_B
from manimlib.constants import BLUE_C
from manimlib.constants import BLUE_D
from manimlib.constants import DOWN
from manimlib.constants import DOWN
from manimlib.constants import FRAME_WIDTH
from manimlib.constants import GREEN
from manimlib.constants import GREEN_SCREEN
from manimlib.constants import GREEN_E
from manimlib.constants import GREY
from manimlib.constants import GREY_A
from manimlib.constants import GREY_B
from manimlib.constants import GREY_E
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 ORIGIN
from manimlib.constants import OUT
from manimlib.constants import PI
from manimlib.constants import RED
from manimlib.constants import RED_E
from manimlib.constants import RIGHT
from manimlib.constants import SMALL_BUFF
from manimlib.constants import SMALL_BUFF
from manimlib.constants import UP
from manimlib.constants import UL
from manimlib.constants import UR
from manimlib.constants import DL
from manimlib.constants import DR
from manimlib.constants import WHITE
from manimlib.constants import YELLOW
from manimlib.constants import TAU
from manimlib.mobject.boolean_ops import Difference
from manimlib.mobject.boolean_ops import Union
from manimlib.mobject.geometry import Arc
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Dot
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Polygon
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import Square
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.geometry import AnnularSector
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.three_dimensions import Cube
from manimlib.mobject.svg.special_tex import TexTextFromPresetString
from manimlib.mobject.three_dimensions import Prismify
from manimlib.mobject.three_dimensions import VCube
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config
from manimlib.mobject.svg.text_mobject import Text
from manimlib.utils.bezier import interpolate
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.rate_functions import linear
from manimlib.utils.space_ops import angle_of_vector
from manimlib.utils.space_ops import complex_to_R3
from manimlib.utils.space_ops import compass_directions
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import midpoint
from manimlib.utils.space_ops import rotate_vector
from typing import TYPE_CHECKING
class Checkmark(TexText):
CONFIG = {
"color": GREEN
}
def __init__(self, **kwargs):
super().__init__("\\ding{51}")
if TYPE_CHECKING:
from typing import Tuple, Sequence, Callable
from manimlib.typing import ManimColor, Vect3
class Exmark(TexText):
CONFIG = {
"color": RED
}
class Checkmark(TexTextFromPresetString):
tex: str = R"\ding{51}"
default_color: ManimColor = GREEN
def __init__(self, **kwargs):
super().__init__("\\ding{55}")
class Exmark(TexTextFromPresetString):
tex: str = R"\ding{55}"
default_color: ManimColor = RED
class Lightbulb(SVGMobject):
CONFIG = {
"height": 1,
"stroke_color": YELLOW,
"stroke_width": 3,
"fill_color": YELLOW,
"fill_opacity": 0,
}
file_name = "lightbulb"
def __init__(self, **kwargs):
super().__init__("lightbulb", **kwargs)
def __init__(
self,
height: float = 1.0,
color: ManimColor = YELLOW,
stroke_width: float = 3.0,
fill_opacity: float = 0.0,
**kwargs
):
super().__init__(
height=height,
color=color,
stroke_width=stroke_width,
fill_opacity=fill_opacity,
**kwargs
)
self.insert_n_curves(25)
class Speedometer(VMobject):
CONFIG = {
"arc_angle": 4 * np.pi / 3,
"num_ticks": 8,
"tick_length": 0.2,
"needle_width": 0.1,
"needle_height": 0.8,
"needle_color": YELLOW,
}
def __init__(
self,
arc_angle: float = 4 * PI / 3,
num_ticks: int = 8,
tick_length: float = 0.2,
needle_width: float = 0.1,
needle_height: float = 0.8,
needle_color: ManimColor = YELLOW,
**kwargs,
):
super().__init__(**kwargs)
def init_points(self):
start_angle = np.pi / 2 + self.arc_angle / 2
end_angle = np.pi / 2 - self.arc_angle / 2
self.add(Arc(
self.arc_angle = arc_angle
self.num_ticks = num_ticks
self.tick_length = tick_length
self.needle_width = needle_width
self.needle_height = needle_height
self.needle_color = needle_color
start_angle = PI / 2 + arc_angle / 2
end_angle = PI / 2 - arc_angle / 2
self.arc = Arc(
start_angle=start_angle,
angle=-self.arc_angle
))
tick_angle_range = np.linspace(start_angle, end_angle, self.num_ticks)
)
self.add(self.arc)
tick_angle_range = np.linspace(start_angle, end_angle, num_ticks)
for index, angle in enumerate(tick_angle_range):
vect = rotate_vector(RIGHT, angle)
tick = Line((1 - self.tick_length) * vect, vect)
label = Tex(str(10 * index))
label.set_height(self.tick_length)
label.shift((1 + self.tick_length) * vect)
tick = Line((1 - tick_length) * vect, vect)
label = Integer(10 * index)
label.set_height(tick_length)
label.shift((1 + tick_length) * vect)
self.add(tick, label)
needle = Polygon(
@@ -84,8 +149,8 @@ class Speedometer(VMobject):
fill_opacity=1,
fill_color=self.needle_color
)
needle.stretch_to_fit_width(self.needle_width)
needle.stretch_to_fit_height(self.needle_height)
needle.stretch_to_fit_width(needle_width)
needle.stretch_to_fit_height(needle_height)
needle.rotate(start_angle - np.pi / 2, about_point=ORIGIN)
self.add(needle)
self.needle = needle
@@ -107,7 +172,7 @@ class Speedometer(VMobject):
)
def rotate_needle(self, angle):
self.needle.rotate(angle, about_point=self.get_center())
self.needle.rotate(angle, about_point=self.arc.get_arc_center())
return self
def move_needle_to_velocity(self, velocity):
@@ -120,66 +185,67 @@ class Speedometer(VMobject):
class Laptop(VGroup):
CONFIG = {
"width": 3,
"body_dimensions": [4, 3, 0.05],
"screen_thickness": 0.01,
"keyboard_width_to_body_width": 0.9,
"keyboard_height_to_body_height": 0.5,
"screen_width_to_screen_plate_width": 0.9,
"key_color_kwargs": {
"stroke_width": 0,
"fill_color": BLACK,
"fill_opacity": 1,
},
"fill_opacity": 1,
"stroke_width": 0,
"body_color": GREY_B,
"shaded_body_color": GREY,
"open_angle": np.pi / 4,
}
def __init__(self, **kwargs):
def __init__(
self,
width: float = 3,
body_dimensions: Tuple[float, float, float] = (4.0, 3.0, 0.05),
screen_thickness: float = 0.01,
keyboard_width_to_body_width: float = 0.9,
keyboard_height_to_body_height: float = 0.5,
screen_width_to_screen_plate_width: float = 0.9,
key_color_kwargs: dict = dict(
stroke_width=0,
fill_color=BLACK,
fill_opacity=1,
),
fill_opacity: float = 1.0,
stroke_width: float = 0.0,
body_color: ManimColor = GREY_B,
shaded_body_color: ManimColor = GREY,
open_angle: float = np.pi / 4,
**kwargs
):
super().__init__(**kwargs)
body = Cube(side_length=1)
for dim, scale_factor in enumerate(self.body_dimensions):
body = VCube(side_length=1)
for dim, scale_factor in enumerate(body_dimensions):
body.stretch(scale_factor, dim=dim)
body.set_width(self.width)
body.set_fill(self.shaded_body_color, opacity=1)
body.set_width(width)
body.set_fill(shaded_body_color, opacity=1)
body.sort(lambda p: p[2])
body[-1].set_fill(self.body_color)
body[-1].set_fill(body_color)
screen_plate = body.copy()
keyboard = VGroup(*[
VGroup(*[
Square(**self.key_color_kwargs)
Square(**key_color_kwargs)
for x in range(12 - y % 2)
]).arrange(RIGHT, buff=SMALL_BUFF)
for y in range(4)
]).arrange(DOWN, buff=MED_SMALL_BUFF)
keyboard.stretch_to_fit_width(
self.keyboard_width_to_body_width * body.get_width(),
keyboard_width_to_body_width * body.get_width(),
)
keyboard.stretch_to_fit_height(
self.keyboard_height_to_body_height * body.get_height(),
keyboard_height_to_body_height * body.get_height(),
)
keyboard.next_to(body, OUT, buff=0.1 * SMALL_BUFF)
keyboard.shift(MED_SMALL_BUFF * UP)
body.add(keyboard)
screen_plate.stretch(self.screen_thickness /
self.body_dimensions[2], dim=2)
screen_plate.stretch(screen_thickness /
body_dimensions[2], dim=2)
screen = Rectangle(
stroke_width=0,
fill_color=BLACK,
fill_opacity=1,
)
screen.replace(screen_plate, stretch=True)
screen.scale(self.screen_width_to_screen_plate_width)
screen.scale(screen_width_to_screen_plate_width)
screen.next_to(screen_plate, OUT, buff=0.1 * SMALL_BUFF)
screen_plate.add(screen)
screen_plate.next_to(body, UP, buff=0)
screen_plate.rotate(
self.open_angle, RIGHT,
open_angle, RIGHT,
about_point=screen_plate.get_bottom()
)
self.screen_plate = screen_plate
@@ -194,170 +260,182 @@ class Laptop(VGroup):
self.axis = axis
self.add(body, screen_plate, axis)
self.rotate(5 * np.pi / 12, LEFT, about_point=ORIGIN)
self.rotate(np.pi / 6, DOWN, about_point=ORIGIN)
class VideoIcon(SVGMobject):
CONFIG = {
"width": FRAME_WIDTH / 12.,
}
file_name: str = "video_icon"
def __init__(self, **kwargs):
super().__init__(file_name="video_icon", **kwargs)
self.center()
self.set_width(self.width)
self.set_stroke(color=WHITE, width=0)
self.set_fill(color=WHITE, opacity=1)
def __init__(
self,
width: float = 1.2,
color=BLUE_A,
**kwargs
):
super().__init__(color=color, **kwargs)
self.set_width(width)
class VideoSeries(VGroup):
CONFIG = {
"num_videos": 11,
"gradient_colors": [BLUE_B, BLUE_D],
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
videos = [VideoIcon() for x in range(self.num_videos)]
VGroup.__init__(self, *videos, **kwargs)
self.arrange()
self.set_width(FRAME_WIDTH - MED_LARGE_BUFF)
self.set_color_by_gradient(*self.gradient_colors)
def __init__(
self,
num_videos: int = 11,
gradient_colors: Sequence[ManimColor] = [BLUE_B, BLUE_D],
width: float = FRAME_WIDTH - MED_LARGE_BUFF,
**kwargs
):
super().__init__(
*(VideoIcon() for x in range(num_videos)),
**kwargs
)
self.arrange(RIGHT)
self.set_width(width)
self.set_color_by_gradient(*gradient_colors)
class Clock(VGroup):
CONFIG = {}
def __init__(self, **kwargs):
circle = Circle(color=WHITE)
def __init__(
self,
stroke_color: ManimColor = WHITE,
stroke_width: float = 3.0,
hour_hand_height: float = 0.3,
minute_hand_height: float = 0.6,
tick_length: float = 0.1,
**kwargs,
):
style = dict(stroke_color=stroke_color, stroke_width=stroke_width)
circle = Circle(**style)
ticks = []
for x in range(12):
alpha = x / 12.
point = complex_to_R3(
np.exp(2 * np.pi * alpha * complex(0, 1))
)
length = 0.2 if x % 3 == 0 else 0.1
ticks.append(
Line(point, (1 - length) * point)
)
self.hour_hand = Line(ORIGIN, 0.3 * UP)
self.minute_hand = Line(ORIGIN, 0.6 * UP)
# for hand in self.hour_hand, self.minute_hand:
# #Balance out where the center is
# hand.add(VectorizedPoint(-hand.get_end()))
for x, point in enumerate(compass_directions(12, UP)):
length = tick_length
if x % 3 == 0:
length *= 2
ticks.append(Line(point, (1 - length) * point, **style))
self.hour_hand = Line(ORIGIN, hour_hand_height * UP, **style)
self.minute_hand = Line(ORIGIN, minute_hand_height * UP, **style)
VGroup.__init__(
self, circle,
self.hour_hand, self.minute_hand,
super().__init__(
circle, self.hour_hand, self.minute_hand,
*ticks
)
class ClockPassesTime(Animation):
CONFIG = {
"run_time": 5,
"hours_passed": 12,
"rate_func": linear,
}
def __init__(self, clock, **kwargs):
digest_config(self, kwargs)
assert(isinstance(clock, Clock))
rot_kwargs = {
"axis": OUT,
"about_point": clock.get_center()
}
hour_radians = -self.hours_passed * 2 * np.pi / 12
self.hour_rotation = Rotating(
clock.hour_hand,
angle=hour_radians,
**rot_kwargs
class ClockPassesTime(AnimationGroup):
def __init__(
self,
clock: Clock,
run_time: float = 5.0,
hours_passed: float = 12.0,
rate_func: Callable[[float], float] = linear,
**kwargs
):
rot_kwargs = dict(
axis=OUT,
about_point=clock.get_center()
)
self.hour_rotation.begin()
self.minute_rotation = Rotating(
clock.minute_hand,
angle=12 * hour_radians,
**rot_kwargs
hour_radians = -hours_passed * 2 * PI / 12
super().__init__(
Rotating(
clock.hour_hand,
angle=hour_radians,
**rot_kwargs
),
Rotating(
clock.minute_hand,
angle=12 * hour_radians,
**rot_kwargs
),
**kwargs
)
self.minute_rotation.begin()
Animation.__init__(self, clock, **kwargs)
def interpolate_mobject(self, alpha):
for rotation in self.hour_rotation, self.minute_rotation:
rotation.interpolate_mobject(alpha)
class Bubble(SVGMobject):
CONFIG = {
"direction": LEFT,
"center_point": ORIGIN,
"content_scale_factor": 0.75,
"height": 5,
"width": 8,
"bubble_center_adjustment_factor": 1. / 8,
"file_name": None,
"fill_color": BLACK,
"fill_opacity": 0.8,
"stroke_color": WHITE,
"stroke_width": 3,
}
class Bubble(VGroup):
file_name: str = "Bubbles_speech.svg"
bubble_center_adjustment_factor = 0.125
def __init__(self, **kwargs):
digest_config(self, kwargs, locals())
if self.file_name is None:
raise Exception("Must invoke Bubble subclass")
SVGMobject.__init__(self, self.file_name, **kwargs)
self.center()
self.stretch_to_fit_height(self.height)
self.stretch_to_fit_width(self.width)
if self.direction[0] > 0:
self.flip()
self.direction_was_specified = ("direction" in kwargs)
self.content = Mobject()
self.refresh_triangulation()
def __init__(
self,
content: str | VMobject | None = None,
buff: float = 1.0,
filler_shape: Tuple[float, float] = (3.0, 2.0),
pin_point: Vect3 | None = None,
direction: Vect3 = LEFT,
add_content: bool = True,
fill_color: ManimColor = BLACK,
fill_opacity: float = 0.8,
stroke_color: ManimColor = WHITE,
stroke_width: float = 3.0,
**kwargs
):
super().__init__(**kwargs)
self.direction = direction
if content is None:
content = Rectangle(*filler_shape)
content.set_fill(opacity=0)
content.set_stroke(width=0)
elif isinstance(content, str):
content = Text(content)
self.content = content
self.body = self.get_body(content, direction, buff)
self.body.set_fill(fill_color, fill_opacity)
self.body.set_stroke(stroke_color, stroke_width)
self.add(self.body)
if add_content:
self.add(self.content)
if pin_point is not None:
self.pin_to(pin_point)
def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
body = SVGMobject(self.file_name)
if direction[0] > 0:
body.flip()
# Resize
width = content.get_width()
height = content.get_height()
target_width = width + min(buff, height)
target_height = 1.35 * (height + buff) # Magic number?
body.set_shape(target_width, target_height)
body.move_to(content)
body.shift(self.bubble_center_adjustment_factor * body.get_height() * DOWN)
return body
def get_tip(self):
# TODO, find a better way
return self.get_corner(DOWN + self.direction) - 0.6 * self.direction
return self.get_corner(DOWN + self.direction)
def get_bubble_center(self):
factor = self.bubble_center_adjustment_factor
return self.get_center() + factor * self.get_height() * UP
def move_tip_to(self, point):
mover = VGroup(self)
if self.content is not None:
mover.add(self.content)
mover.shift(point - self.get_tip())
self.shift(point - self.get_tip())
return self
def flip(self, axis=UP):
Mobject.flip(self, axis=axis)
self.refresh_unit_normal()
self.refresh_triangulation()
def flip(self, axis=UP, only_body=True, **kwargs):
super().flip(axis=axis, **kwargs)
if only_body:
# Flip in place, don't use kwargs
self.content.flip(axis=axis)
if abs(axis[1]) > 0:
self.direction = -np.array(self.direction)
return self
def pin_to(self, mobject):
def pin_to(self, mobject, auto_flip=False):
mob_center = mobject.get_center()
want_to_flip = np.sign(mob_center[0]) != np.sign(self.direction[0])
can_flip = not self.direction_was_specified
if want_to_flip and can_flip:
if want_to_flip and auto_flip:
self.flip()
boundary_point = mobject.get_bounding_box_point(UP - self.direction)
vector_from_center = 1.0 * (boundary_point - mob_center)
self.move_tip_to(mob_center + vector_from_center)
return self
def position_mobject_inside(self, mobject):
scaled_width = self.content_scale_factor * self.get_width()
if mobject.get_width() > scaled_width:
mobject.set_width(scaled_width)
mobject.shift(
self.get_bubble_center() - mobject.get_center()
)
def position_mobject_inside(self, mobject, buff=MED_LARGE_BUFF):
mobject.set_max_width(self.body.get_width() - 2 * buff)
mobject.set_max_height(self.body.get_height() / 1.5 - 2 * buff)
mobject.shift(self.get_bubble_center() - mobject.get_center())
return mobject
def add_content(self, mobject):
@@ -365,65 +443,140 @@ class Bubble(SVGMobject):
self.content = mobject
return self.content
def write(self, *text):
self.add_content(TexText(*text))
def write(self, text):
self.add_content(Text(text))
return self
def resize_to_content(self):
target_width = self.content.get_width()
target_width += max(MED_LARGE_BUFF, 2)
target_height = self.content.get_height()
target_height += 2.5 * LARGE_BUFF
tip_point = self.get_tip()
self.stretch_to_fit_width(target_width)
self.stretch_to_fit_height(target_height)
self.move_tip_to(tip_point)
self.position_mobject_inside(self.content)
def resize_to_content(self, buff=1.0): # TODO
self.body.match_points(self.get_body(
self.content, self.direction, buff
))
def clear(self):
self.add_content(VMobject())
self.remove(self.content)
return self
class SpeechBubble(Bubble):
CONFIG = {
"file_name": "Bubbles_speech.svg",
"height": 4
}
def __init__(
self,
content: str | VMobject | None = None,
buff: float = MED_SMALL_BUFF,
filler_shape: Tuple[float, float] = (2.0, 1.0),
stem_height_to_bubble_height: float = 0.5,
stem_top_x_props: Tuple[float, float] = (0.2, 0.3),
**kwargs
):
self.stem_height_to_bubble_height = stem_height_to_bubble_height
self.stem_top_x_props = stem_top_x_props
super().__init__(content, buff, filler_shape, **kwargs)
def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
rect = SurroundingRectangle(content, buff=buff)
rect.round_corners()
lp = rect.get_corner(DL)
rp = rect.get_corner(DR)
stem_height = self.stem_height_to_bubble_height * rect.get_height()
low_prop, high_prop = self.stem_top_x_props
triangle = Polygon(
interpolate(lp, rp, low_prop),
interpolate(lp, rp, high_prop),
lp + stem_height * DOWN,
)
result = Union(rect, triangle)
result.insert_n_curves(20)
if direction[0] > 0:
result.flip()
class DoubleSpeechBubble(Bubble):
CONFIG = {
"file_name": "Bubbles_double_speech.svg",
"height": 4
}
return result
class ThoughtBubble(Bubble):
CONFIG = {
"file_name": "Bubbles_thought.svg",
}
def __init__(
self,
content: str | VMobject | None = None,
buff: float = SMALL_BUFF,
filler_shape: Tuple[float, float] = (2.0, 1.0),
bulge_radius: float = 0.35,
bulge_overlap: float = 0.25,
noise_factor: float = 0.1,
circle_radii: list[float] = [0.1, 0.15, 0.2],
**kwargs
):
self.bulge_radius = bulge_radius
self.bulge_overlap = bulge_overlap
self.noise_factor = noise_factor
self.circle_radii = circle_radii
super().__init__(content, buff, filler_shape, **kwargs)
def __init__(self, **kwargs):
Bubble.__init__(self, **kwargs)
self.submobjects.sort(
key=lambda m: m.get_bottom()[1]
)
def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
rect = SurroundingRectangle(content, buff)
perimeter = rect.get_arc_length()
radius = self.bulge_radius
step = (1 - self.bulge_overlap) * (2 * radius)
nf = self.noise_factor
corners = [rect.get_corner(v) for v in [DL, UL, UR, DR]]
points = []
for c1, c2 in adjacent_pairs(corners):
n_alphas = int(get_norm(c1 - c2) / step) + 1
for alpha in np.linspace(0, 1, n_alphas):
points.append(interpolate(
c1, c2, alpha + nf * (step / n_alphas) * (random.random() - 0.5)
))
cloud = Union(rect, *(
# Add bulges
Circle(radius=radius * (1 + nf * random.random())).move_to(point)
for point in points
))
cloud.set_stroke(WHITE, 2)
circles = VGroup(Circle(radius=radius) for radius in self.circle_radii)
circ_buff = 0.25 * self.circle_radii[0]
circles.arrange(UR, buff=circ_buff)
circles[1].shift(circ_buff * DR)
circles.next_to(cloud, DOWN, 4 * circ_buff, aligned_edge=LEFT)
circles.set_stroke(WHITE, 2)
result = VGroup(*circles, cloud)
if direction[0] > 0:
result.flip()
return result
class OldSpeechBubble(Bubble):
file_name: str = "Bubbles_speech.svg"
class DoubleSpeechBubble(Bubble):
file_name: str = "Bubbles_double_speech.svg"
class OldThoughtBubble(Bubble):
file_name: str = "Bubbles_thought.svg"
def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
body = super().get_body(content, direction, buff)
body.sort(lambda p: p[1])
return body
def make_green_screen(self):
self.submobjects[-1].set_fill(GREEN_SCREEN, opacity=1)
self.body[-1].set_fill(GREEN_SCREEN, opacity=1)
return self
class VectorizedEarth(SVGMobject):
CONFIG = {
"file_name": "earth",
"height": 1.5,
"fill_color": BLACK,
}
file_name: str = "earth"
def __init__(self, **kwargs):
SVGMobject.__init__(self, **kwargs)
def __init__(
self,
height: float = 2.0,
**kwargs
):
super().__init__(height=height, **kwargs)
self.insert_n_curves(20)
circle = Circle(
stroke_width=3,
stroke_color=GREEN,
@@ -432,3 +585,188 @@ class VectorizedEarth(SVGMobject):
)
circle.replace(self)
self.add_to_back(circle)
class Piano(VGroup):
def __init__(
self,
n_white_keys = 52,
black_pattern = [0, 2, 3, 5, 6],
white_keys_per_octave = 7,
white_key_dims = (0.15, 1.0),
black_key_dims = (0.1, 0.66),
key_buff = 0.02,
white_key_color = WHITE,
black_key_color = GREY_E,
total_width = 13,
**kwargs
):
self.n_white_keys = n_white_keys
self.black_pattern = black_pattern
self.white_keys_per_octave = white_keys_per_octave
self.white_key_dims = white_key_dims
self.black_key_dims = black_key_dims
self.key_buff = key_buff
self.white_key_color = white_key_color
self.black_key_color = black_key_color
self.total_width = total_width
super().__init__(**kwargs)
self.add_white_keys()
self.add_black_keys()
self.sort_keys()
self[:-1].reverse_points()
self.set_width(self.total_width)
def add_white_keys(self):
key = Rectangle(*self.white_key_dims)
key.set_fill(self.white_key_color, 1)
key.set_stroke(width=0)
self.white_keys = key.get_grid(1, self.n_white_keys, buff=self.key_buff)
self.add(*self.white_keys)
def add_black_keys(self):
key = Rectangle(*self.black_key_dims)
key.set_fill(self.black_key_color, 1)
key.set_stroke(width=0)
self.black_keys = VGroup()
for i in range(len(self.white_keys) - 1):
if i % self.white_keys_per_octave not in self.black_pattern:
continue
wk1 = self.white_keys[i]
wk2 = self.white_keys[i + 1]
bk = key.copy()
bk.move_to(midpoint(wk1.get_top(), wk2.get_top()), UP)
big_bk = bk.copy()
big_bk.stretch((bk.get_width() + self.key_buff) / bk.get_width(), 0)
big_bk.stretch((bk.get_height() + self.key_buff) / bk.get_height(), 1)
big_bk.move_to(bk, UP)
for wk in wk1, wk2:
wk.become(Difference(wk, big_bk).match_style(wk))
self.black_keys.add(bk)
self.add(*self.black_keys)
def sort_keys(self):
self.sort(lambda p: p[0])
class Piano3D(VGroup):
def __init__(
self,
shading: Tuple[float, float, float] = (1.0, 0.2, 0.2),
stroke_width: float = 0.25,
stroke_color: ManimColor = BLACK,
key_depth: float = 0.1,
black_key_shift: float = 0.05,
piano_2d_config: dict = dict(
white_key_color=GREY_A,
key_buff=0.001
),
**kwargs
):
piano_2d = Piano(**piano_2d_config)
super().__init__(*(
Prismify(key, key_depth)
for key in piano_2d
))
self.set_stroke(stroke_color, stroke_width)
self.set_shading(*shading)
self.apply_depth_test()
# Elevate black keys
for i, key in enumerate(self):
if piano_2d[i] in piano_2d.black_keys:
key.shift(black_key_shift * OUT)
key.set_color(BLACK)
class DieFace(VGroup):
def __init__(
self,
value: int,
side_length: float = 1.0,
corner_radius: float = 0.15,
stroke_color: ManimColor = WHITE,
stroke_width: float = 2.0,
fill_color: ManimColor = GREY_E,
dot_radius: float = 0.08,
dot_color: ManimColor = WHITE,
dot_coalesce_factor: float = 0.5
):
dot = Dot(radius=dot_radius, fill_color=dot_color)
square = Square(
side_length=side_length,
stroke_color=stroke_color,
stroke_width=stroke_width,
fill_color=fill_color,
fill_opacity=1.0,
)
square.round_corners(corner_radius)
if not (1 <= value <= 6):
raise Exception("DieFace only accepts integer inputs between 1 and 6")
edge_group = [
(ORIGIN,),
(UL, DR),
(UL, ORIGIN, DR),
(UL, UR, DL, DR),
(UL, UR, ORIGIN, DL, DR),
(UL, UR, LEFT, RIGHT, DL, DR),
][value - 1]
arrangement = VGroup(*(
dot.copy().move_to(square.get_bounding_box_point(vect))
for vect in edge_group
))
arrangement.space_out_submobjects(dot_coalesce_factor)
super().__init__(square, arrangement)
self.dots = arrangement
self.value = value
self.index = value
class Dartboard(VGroup):
radius = 3
n_sectors = 20
def __init__(self, **kwargs):
super().__init__(**kwargs)
n_sectors = self.n_sectors
angle = TAU / n_sectors
segments = VGroup(*[
VGroup(*[
AnnularSector(
inner_radius=in_r,
outer_radius=out_r,
start_angle=n * angle,
angle=angle,
fill_color=color,
)
for n, color in zip(
range(n_sectors),
it.cycle(colors)
)
])
for colors, in_r, out_r in [
([GREY_B, GREY_E], 0, 1),
([GREEN_E, RED_E], 0.5, 0.55),
([GREEN_E, RED_E], 0.95, 1),
]
])
segments.rotate(-angle / 2)
bullseyes = VGroup(*[
Circle(radius=r)
for r in [0.07, 0.035]
])
bullseyes.set_fill(opacity=1)
bullseyes.set_stroke(width=0)
bullseyes[0].set_color(GREEN_E)
bullseyes[1].set_color(RED_E)
self.bullseye = bullseyes[1]
self.add(*segments, *bullseyes)
self.scale(self.radius)

View File

@@ -1,521 +0,0 @@
import itertools as it
import re
from types import MethodType
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.iterables import remove_list_redundancies
from manimlib.utils.tex_file_writing import tex_to_svg_file
from manimlib.utils.tex_file_writing import get_tex_config
from manimlib.utils.tex_file_writing import display_during_execution
SCALE_FACTOR_PER_FONT_POINT = 0.001
tex_hash_to_mob_map = {}
def _get_neighbouring_pairs(iterable):
return list(adjacent_pairs(iterable))[:-1]
class _LabelledTex(SVGMobject):
CONFIG = {
"height": None,
"path_string_config": {
"should_subdivide_sharp_curves": True,
"should_remove_null_curves": True,
},
}
@staticmethod
def color_str_to_label(color_str):
if len(color_str) == 4:
# "#RGB" => "#RRGGBB"
color_str = "#" + "".join([c * 2 for c in color_str[1:]])
return int(color_str[1:], 16) - 1
def get_mobjects_from(self, element):
result = super().get_mobjects_from(element)
for mob in result:
if not hasattr(mob, "glyph_label"):
mob.glyph_label = -1
try:
color_str = element.getAttribute("fill")
if color_str:
glyph_label = _LabelledTex.color_str_to_label(color_str)
for mob in result:
mob.glyph_label = glyph_label
except:
pass
return result
class _TexSpan(object):
def __init__(self, script_type, label):
# script_type: 0 for normal, 1 for subscript, 2 for superscript.
# Only those spans with `script_type == 0` will be colored.
self.script_type = script_type
self.label = label
self.containing_labels = []
def __repr__(self):
return "_TexSpan(" + ", ".join([
attrib_name + "=" + str(getattr(self, attrib_name))
for attrib_name in ["script_type", "label", "containing_labels"]
]) + ")"
class _TexParser(object):
def __init__(self, mtex):
self.tex_string = mtex.tex_string
strings_to_break_up = remove_list_redundancies([
*mtex.isolate, *mtex.tex_to_color_map.keys(), mtex.tex_string
])
if "" in strings_to_break_up:
strings_to_break_up.remove("")
unbreakable_commands = mtex.unbreakable_commands
self.tex_spans_dict = {}
self.current_label = 0
self.break_up_by_braces()
self.break_up_by_scripts()
self.break_up_by_additional_strings(strings_to_break_up)
self.merge_unbreakable_commands(unbreakable_commands)
self.analyse_containing_labels()
@staticmethod
def label_to_color_tuple(n):
# Get a unique color different from black,
# or the svg file will not include the color information.
rgb = n + 1
rg, b = divmod(rgb, 256)
r, g = divmod(rg, 256)
return r, g, b
@staticmethod
def contains(span_0, span_1):
return span_0[0] <= span_1[0] and span_1[1] <= span_0[1]
def add_tex_span(self, span_tuple, script_type=0, label=-1):
if script_type == 0:
# Should be additionally labelled.
label = self.current_label
self.current_label += 1
tex_span = _TexSpan(script_type, label)
self.tex_spans_dict[span_tuple] = tex_span
def break_up_by_braces(self):
tex_string = self.tex_string
span_tuples = []
left_brace_indices = []
for match_obj in re.finditer(r"(\\*)(\{|\})", tex_string):
# Braces following even numbers of backslashes are counted.
if len(match_obj.group(1)) % 2 == 1:
continue
if match_obj.group(2) == "{":
left_brace_index = match_obj.span(2)[0]
left_brace_indices.append(left_brace_index)
else:
left_brace_index = left_brace_indices.pop()
right_brace_index = match_obj.span(2)[1]
span_tuples.append((left_brace_index, right_brace_index))
if left_brace_indices:
self.raise_tex_parsing_error()
self.paired_braces_tuples = span_tuples
for span_tuple in span_tuples:
self.add_tex_span(span_tuple)
def break_up_by_scripts(self):
tex_string = self.tex_string
brace_indices_dict = dict(self.tex_spans_dict.keys())
for match_obj in re.finditer(r"((?<!\\)(_|\^)\s*)|(\s+(_|\^)\s*)", tex_string):
script_type = 1 if "_" in match_obj.group() else 2
token_begin, token_end = match_obj.span()
if token_end in brace_indices_dict:
content_span = (token_end, brace_indices_dict[token_end])
else:
content_match_obj = re.match(r"\w|\\[a-zA-Z]+", tex_string[token_end:])
if not content_match_obj:
self.raise_tex_parsing_error()
content_span = tuple([
index + token_end for index in content_match_obj.span()
])
self.add_tex_span(content_span)
label = self.tex_spans_dict[content_span].label
self.add_tex_span(
(token_begin, content_span[1]),
script_type=script_type,
label=label
)
def break_up_by_additional_strings(self, strings_to_break_up):
tex_string = self.tex_string
all_span_tuples = []
for string in strings_to_break_up:
# Only matches non-crossing strings.
for match_obj in re.finditer(re.escape(string), tex_string):
all_span_tuples.append(match_obj.span())
script_spans_dict = dict([
span_tuple[::-1]
for span_tuple, tex_span in self.tex_spans_dict.items()
if tex_span.script_type != 0
])
for span_begin, span_end in all_span_tuples:
if span_end in script_spans_dict.values():
# Deconstruct spans with subscripts & superscripts.
while span_end in script_spans_dict:
span_end = script_spans_dict[span_end]
if span_begin >= span_end:
continue
span_tuple = (span_begin, span_end)
if span_tuple not in self.tex_spans_dict:
self.add_tex_span(span_tuple)
def merge_unbreakable_commands(self, unbreakable_commands):
tex_string = self.tex_string
command_merge_spans = []
brace_indices_dict = dict(self.paired_braces_tuples)
# Braces leading by `unbreakable_commands` shouldn't be marked.
for command in unbreakable_commands:
for match_obj in re.finditer(re.escape(command), tex_string):
merge_begin_index = match_obj.span()[1]
merge_end_index = merge_begin_index
if merge_end_index not in brace_indices_dict:
continue
while merge_end_index in brace_indices_dict:
merge_end_index = brace_indices_dict[merge_end_index]
command_merge_spans.append((merge_begin_index, merge_end_index))
self.tex_spans_dict = {
span_tuple: tex_span
for span_tuple, tex_span in self.tex_spans_dict.items()
if all([
not _TexParser.contains(merge_span, span_tuple)
for merge_span in command_merge_spans
])
}
def analyse_containing_labels(self):
for span_0, tex_span_0 in self.tex_spans_dict.items():
if tex_span_0.script_type != 0:
continue
for span_1, tex_span_1 in self.tex_spans_dict.items():
if _TexParser.contains(span_1, span_0):
tex_span_1.containing_labels.append(tex_span_0.label)
def get_labelled_expression(self):
tex_string = self.tex_string
if not self.tex_spans_dict:
return tex_string
indices_with_labels = sorted([
(span_tuple[i], i, span_tuple[1 - i], tex_span.label)
for span_tuple, tex_span in self.tex_spans_dict.items()
if tex_span.script_type == 0
for i in range(2)
], key=lambda t: (t[0], -t[1], -t[2]))
# Add one more item to ensure all the substrings are joined.
indices_with_labels.append((len(tex_string), 0, 0, 0))
result = tex_string[: indices_with_labels[0][0]]
index_with_label_pairs = _get_neighbouring_pairs(indices_with_labels)
for index_with_label, next_index_with_label in index_with_label_pairs:
index, flag, _, label = index_with_label
next_index, *_ = next_index_with_label
# Adding one more pair of braces will help maintain the glyghs of tex file...
if flag == 0:
color_tuple = _TexParser.label_to_color_tuple(label)
result += "".join([
"{{",
"\\color[RGB]",
"{",
",".join(map(str, color_tuple)),
"}"
])
else:
result += "}}"
result += tex_string[index : next_index]
return result
def raise_tex_parsing_error(self):
raise ValueError(f"Failed to parse tex: \"{self.tex_string}\"")
class MTex(VMobject):
CONFIG = {
"fill_opacity": 1.0,
"stroke_width": 0,
"should_center": True,
"font_size": 48,
"height": None,
"organize_left_to_right": False,
"alignment": "\\centering",
"tex_environment": "align*",
"isolate": [],
"unbreakable_commands": ["\\begin", "\\end"],
"tex_to_color_map": {},
}
def __init__(self, tex_string, **kwargs):
super().__init__(**kwargs)
self.tex_string = MTex.modify_tex_string(tex_string)
tex_parser = _TexParser(self)
self.tex_spans_dict = tex_parser.tex_spans_dict
new_tex = tex_parser.get_labelled_expression()
full_tex = self.get_tex_file_body(new_tex)
hash_val = hash(full_tex)
if hash_val not in tex_hash_to_mob_map:
with display_during_execution(f"Writing \"{tex_string}\""):
filename = tex_to_svg_file(full_tex)
svg_mob = _LabelledTex(filename)
tex_hash_to_mob_map[hash_val] = svg_mob
self.add(*[
submob.copy()
for submob in tex_hash_to_mob_map[hash_val]
])
self.build_submobjects()
self.init_colors()
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
if self.height is None:
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
@staticmethod
def modify_tex_string(tex_string):
result = tex_string.strip("\n")
# Prevent from passing an empty string.
if not result:
result = "\\quad"
return result
def get_tex_file_body(self, new_tex):
if self.tex_environment:
new_tex = "\n".join([
f"\\begin{{{self.tex_environment}}}",
new_tex,
f"\\end{{{self.tex_environment}}}"
])
if self.alignment:
new_tex = "\n".join([self.alignment, new_tex])
tex_config = get_tex_config()
return tex_config["tex_body"].replace(
tex_config["text_to_replace"],
new_tex
)
def build_submobjects(self):
if not self.submobjects:
return
self.group_submobjects()
self.sort_scripts_in_tex_order()
self.assign_submob_tex_strings()
def group_submobjects(self):
# Simply pack together adjacent mobjects with the same label.
new_submobjects = []
def append_new_submobject(glyphs):
if glyphs:
submobject = VGroup(*glyphs)
submobject.submob_label = glyphs[0].glyph_label
new_submobjects.append(submobject)
new_glyphs = []
current_glyph_label = -1
for submob in self.submobjects:
if submob.glyph_label == current_glyph_label:
new_glyphs.append(submob)
else:
append_new_submobject(new_glyphs)
new_glyphs = [submob]
current_glyph_label = submob.glyph_label
append_new_submobject(new_glyphs)
self.set_submobjects(new_submobjects)
def sort_scripts_in_tex_order(self):
# LaTeX always puts superscripts before subscripts.
# This function sorts the submobjects of scripts in the order of tex given.
index_and_span_list = sorted([
(index, span_tuple)
for span_tuple, tex_span in self.tex_spans_dict.items()
if tex_span.script_type != 0
for index in span_tuple
])
index_and_span_pair = _get_neighbouring_pairs(index_and_span_list)
for index_and_span_0, index_and_span_1 in index_and_span_pair:
index_0, span_tuple_0 = index_and_span_0
index_1, span_tuple_1 = index_and_span_1
if index_0 != index_1:
continue
if not all([
self.tex_spans_dict[span_tuple_0].script_type == 1,
self.tex_spans_dict[span_tuple_1].script_type == 2
]):
continue
submob_slice_0 = self.slice_of_part(
self.get_part_by_span_tuples([span_tuple_0])
)
submob_slice_1 = self.slice_of_part(
self.get_part_by_span_tuples([span_tuple_1])
)
submobs = self.submobjects
self.set_submobjects([
*submobs[: submob_slice_1.start],
*submobs[submob_slice_0],
*submobs[submob_slice_1.stop : submob_slice_0.start],
*submobs[submob_slice_1],
*submobs[submob_slice_0.stop :]
])
def assign_submob_tex_strings(self):
# Not sure whether this is the best practice...
# Just a temporary hack for supporting `TransformMatchingTex`.
tex_string = self.tex_string
# Use tex strings including "_", "^".
label_dict = {}
for span_tuple, tex_span in self.tex_spans_dict.items():
if tex_span.script_type != 0:
label_dict[tex_span.label] = span_tuple
else:
if tex_span.label not in label_dict:
label_dict[tex_span.label] = span_tuple
curr_labels = [submob.submob_label for submob in self.submobjects]
prev_labels = [curr_labels[-1], *curr_labels[:-1]]
next_labels = [*curr_labels[1:], curr_labels[0]]
tex_string_spans = []
for curr_label, prev_label, next_label in zip(
curr_labels, prev_labels, next_labels
):
curr_span_tuple = label_dict[curr_label]
prev_span_tuple = label_dict[prev_label]
next_span_tuple = label_dict[next_label]
containing_labels = self.tex_spans_dict[curr_span_tuple].containing_labels
tex_string_spans.append([
prev_span_tuple[1] if prev_label in containing_labels else curr_span_tuple[0],
next_span_tuple[0] if next_label in containing_labels else curr_span_tuple[1]
])
tex_string_spans[0][0] = label_dict[curr_labels[0]][0]
tex_string_spans[-1][1] = label_dict[curr_labels[-1]][1]
for submob, tex_string_span in zip(self.submobjects, tex_string_spans):
submob.tex_string = tex_string[slice(*tex_string_span)]
# Support `get_tex()` method here.
submob.get_tex = MethodType(lambda inst: inst.tex_string, submob)
def get_part_by_span_tuples(self, span_tuples):
labels = remove_list_redundancies(list(it.chain(*[
self.tex_spans_dict[span_tuple].containing_labels
for span_tuple in span_tuples
])))
return VGroup(*filter(
lambda submob: submob.submob_label in labels,
self.submobjects
))
def find_span_components_of_custom_span(self, custom_span_tuple, partial_result=[]):
span_begin, span_end = custom_span_tuple
if span_begin == span_end:
return partial_result
next_begin_choices = sorted([
span_tuple[1]
for span_tuple in self.tex_spans_dict.keys()
if span_tuple[0] == span_begin and span_tuple[1] <= span_end
], reverse=True)
for next_begin in next_begin_choices:
result = self.find_span_components_of_custom_span(
(next_begin, span_end), [*partial_result, (span_begin, next_begin)]
)
if result is not None:
return result
return None
def get_part_by_custom_span_tuple(self, custom_span_tuple):
span_tuples = self.find_span_components_of_custom_span(custom_span_tuple)
if span_tuples is None:
tex = self.tex_string[slice(*custom_span_tuple)]
raise ValueError(f"Failed to get span of tex: \"{tex}\"")
return self.get_part_by_span_tuples(span_tuples)
def get_parts_by_tex(self, tex):
return VGroup(*[
self.get_part_by_custom_span_tuple(match_obj.span())
for match_obj in re.finditer(re.escape(tex), self.tex_string)
])
def get_part_by_tex(self, tex, index=0):
all_parts = self.get_parts_by_tex(tex)
return all_parts[index]
def set_color_by_tex(self, tex, color):
self.get_parts_by_tex(tex).set_color(color)
return self
def set_color_by_tex_to_color_map(self, tex_to_color_map):
for tex, color in list(tex_to_color_map.items()):
self.set_color_by_tex(tex, color)
return self
def indices_of_part(self, part):
indices = [
i for i, submob in enumerate(self.submobjects)
if submob in part
]
if not indices:
raise ValueError("Failed to find part in tex")
return indices
def indices_of_part_by_tex(self, tex, index=0):
part = self.get_part_by_tex(tex, index=index)
return self.indices_of_part(part)
def slice_of_part(self, part):
indices = self.indices_of_part(part)
return slice(indices[0], indices[-1] + 1)
def slice_of_part_by_tex(self, tex, index=0):
part = self.get_part_by_tex(tex, index=index)
return self.slice_of_part(part)
def index_of_part(self, part):
return self.indices_of_part(part)[0]
def index_of_part_by_tex(self, tex, index=0):
part = self.get_part_by_tex(tex, index=index)
return self.index_of_part(part)
def get_tex(self):
return self.tex_string
def get_all_isolated_substrings(self):
tex_string = self.tex_string
return remove_list_redundancies([
tex_string[slice(*span_tuple)]
for span_tuple in self.tex_spans_dict.keys()
])
def print_tex_strings_of_submobjects(self):
# For debugging
# Work with `index_labels()`
print("\n")
print(f"Submobjects of \"{self.get_tex()}\":")
for i, submob in enumerate(self.submobjects):
print(f"{i}: \"{submob.get_tex()}\"")
print("\n")
class MTexText(MTex):
CONFIG = {
"tex_environment": None,
}

View File

@@ -0,0 +1,332 @@
from __future__ import annotations
from functools import reduce
import operator as op
import re
from manimlib.constants import BLACK, WHITE
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.tex_file_writing import latex_to_svg
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterable, List, Dict
from manimlib.typing import ManimColor
SCALE_FACTOR_PER_FONT_POINT = 0.001
class SingleStringTex(SVGMobject):
height: float | None = None
def __init__(
self,
tex_string: str,
height: float | None = None,
fill_color: ManimColor = WHITE,
fill_opacity: float = 1.0,
stroke_width: float = 0,
svg_default: dict = dict(fill_color=WHITE),
path_string_config: dict = dict(),
font_size: int = 48,
alignment: str = R"\centering",
math_mode: bool = True,
organize_left_to_right: bool = False,
template: str = "",
additional_preamble: str = "",
**kwargs
):
self.tex_string = tex_string
self.svg_default = dict(svg_default)
self.path_string_config = dict(path_string_config)
self.font_size = font_size
self.alignment = alignment
self.math_mode = math_mode
self.organize_left_to_right = organize_left_to_right
self.template = template
self.additional_preamble = additional_preamble
super().__init__(
height=height,
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
path_string_config=path_string_config,
**kwargs
)
if self.height is None:
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
@property
def hash_seed(self) -> tuple:
return (
self.__class__.__name__,
self.svg_default,
self.path_string_config,
self.tex_string,
self.alignment,
self.math_mode,
self.template,
self.additional_preamble
)
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)
if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
return self.alignment + "\n" + new_tex
def get_modified_expression(self, tex_string: str) -> str:
return self.modify_special_strings(tex_string.strip())
def modify_special_strings(self, tex: str) -> str:
tex = tex.strip()
should_add_filler = reduce(op.or_, [
# Fraction line needs something to be over
tex == "\\over",
tex == "\\overline",
# Makesure sqrt has overbar
tex == "\\sqrt",
tex == "\\sqrt{",
# Need to add blank subscript or superscript
tex.endswith("_"),
tex.endswith("^"),
tex.endswith("dot"),
])
if should_add_filler:
filler = "{\\quad}"
tex += filler
should_add_double_filler = reduce(op.or_, [
tex == "\\overset",
# TODO: these can't be used since they change
# the latex draw order.
# tex == "\\frac", # you can use \\over as a alternative
# tex == "\\dfrac",
# tex == "\\binom",
])
if should_add_double_filler:
filler = "{\\quad}{\\quad}"
tex += filler
if tex == "\\substack":
tex = "\\quad"
if tex == "":
tex = "\\quad"
# To keep files from starting with a line break
if tex.startswith("\\\\"):
tex = tex.replace("\\\\", "\\quad\\\\")
tex = self.balance_braces(tex)
# Handle imbalanced \left and \right
num_lefts, num_rights = [
len([
s for s in tex.split(substr)[1:]
if s and s[0] in "(){}[]|.\\"
])
for substr in ("\\left", "\\right")
]
if num_lefts != num_rights:
tex = tex.replace("\\left", "\\big")
tex = tex.replace("\\right", "\\big")
for context in ["array"]:
begin_in = ("\\begin{%s}" % context) in tex
end_in = ("\\end{%s}" % context) in tex
if begin_in ^ end_in:
# Just turn this into a blank string,
# which means caller should leave a
# stray \\begin{...} with other symbols
tex = ""
return tex
def balance_braces(self, tex: str) -> str:
"""
Makes Tex resiliant to unmatched braces
"""
num_unclosed_brackets = 0
for i in range(len(tex)):
if i > 0 and tex[i - 1] == "\\":
# So as to not count '\{' type expressions
continue
char = tex[i]
if char == "{":
num_unclosed_brackets += 1
elif char == "}":
if num_unclosed_brackets == 0:
tex = "{" + tex
else:
num_unclosed_brackets -= 1
tex += num_unclosed_brackets * "}"
return tex
def get_tex(self) -> str:
return self.tex_string
def organize_submobjects_left_to_right(self):
self.sort(lambda p: p[0])
return self
class OldTex(SingleStringTex):
def __init__(
self,
*tex_strings: str,
arg_separator: str = "",
isolate: List[str] = [],
tex_to_color_map: Dict[str, ManimColor] = {},
**kwargs
):
self.tex_strings = self.break_up_tex_strings(
tex_strings,
substrings_to_isolate=[*isolate, *tex_to_color_map.keys()]
)
full_string = arg_separator.join(self.tex_strings)
super().__init__(full_string, **kwargs)
self.break_up_by_substrings(self.tex_strings)
self.set_color_by_tex_to_color_map(tex_to_color_map)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
def break_up_tex_strings(self, tex_strings: Iterable[str], substrings_to_isolate: List[str] = []) -> Iterable[str]:
# Separate out any strings specified in the isolate
# or tex_to_color_map lists.
if len(substrings_to_isolate) == 0:
return tex_strings
patterns = (
"({})".format(re.escape(ss))
for ss in substrings_to_isolate
)
pattern = "|".join(patterns)
pieces = []
for s in tex_strings:
if pattern:
pieces.extend(re.split(pattern, s))
else:
pieces.append(s)
return list(filter(lambda s: s, pieces))
def break_up_by_substrings(self, tex_strings: Iterable[str]):
"""
Reorganize existing submojects one layer
deeper based on the structure of tex_strings (as a list
of tex_strings)
"""
if len(list(tex_strings)) == 1:
submob = self.copy()
self.set_submobjects([submob])
return self
new_submobjects = []
curr_index = 0
for tex_string in tex_strings:
tex_string = tex_string.strip()
if len(tex_string) == 0:
continue
sub_tex_mob = SingleStringTex(tex_string, math_mode=self.math_mode)
num_submobs = len(sub_tex_mob)
if num_submobs == 0:
continue
new_index = curr_index + num_submobs
sub_tex_mob.set_submobjects(self.submobjects[curr_index:new_index])
new_submobjects.append(sub_tex_mob)
curr_index = new_index
self.set_submobjects(new_submobjects)
return self
def get_parts_by_tex(
self,
tex: str,
substring: bool = True,
case_sensitive: bool = True
) -> VGroup:
def test(tex1, tex2):
if not case_sensitive:
tex1 = tex1.lower()
tex2 = tex2.lower()
if substring:
return tex1 in tex2
else:
return tex1 == tex2
return VGroup(*filter(
lambda m: isinstance(m, SingleStringTex) and test(tex, m.get_tex()),
self.submobjects
))
def get_part_by_tex(self, tex: str, **kwargs) -> SingleStringTex | None:
all_parts = self.get_parts_by_tex(tex, **kwargs)
return all_parts[0] if all_parts else None
def set_color_by_tex(self, tex: str, color: ManimColor, **kwargs):
self.get_parts_by_tex(tex, **kwargs).set_color(color)
return self
def set_color_by_tex_to_color_map(
self,
tex_to_color_map: dict[str, ManimColor],
**kwargs
):
for tex, color in list(tex_to_color_map.items()):
self.set_color_by_tex(tex, color, **kwargs)
return self
def index_of_part(self, part: SingleStringTex, start: int = 0) -> int:
return self.submobjects.index(part, start)
def index_of_part_by_tex(self, tex: str, start: int = 0, **kwargs) -> int:
part = self.get_part_by_tex(tex, **kwargs)
return self.index_of_part(part, start)
def slice_by_tex(
self,
start_tex: str | None = None,
stop_tex: str | None = None,
**kwargs
) -> VGroup:
if start_tex is None:
start_index = 0
else:
start_index = self.index_of_part_by_tex(start_tex, **kwargs)
if stop_tex is None:
return self[start_index:]
else:
stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
return self[start_index:stop_index]
def sort_alphabetically(self) -> None:
self.submobjects.sort(key=lambda m: m.get_tex())
def set_bstroke(self, color: ManimColor = BLACK, width: float = 4):
self.set_stroke(color, width, background=True)
return self
class OldTexText(OldTex):
def __init__(
self,
*tex_strings: str,
math_mode: bool = False,
arg_separator: str = "",
**kwargs
):
super().__init__(
*tex_strings,
math_mode=math_mode,
arg_separator=arg_separator,
**kwargs
)

View File

@@ -0,0 +1,79 @@
from __future__ import annotations
from manimlib.constants import MED_SMALL_BUFF, WHITE, 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
from manimlib.mobject.geometry import Line
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.svg.tex_mobject import TexText
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import ManimColor, Vect3
class BulletedList(VGroup):
def __init__(
self,
*items: str,
buff: float = MED_LARGE_BUFF,
aligned_edge: Vect3 = LEFT,
**kwargs
):
labelled_content = [R"\item " + item for item in items]
tex_string = "\n".join([
R"\begin{itemize}",
*labelled_content,
R"\end{itemize}"
])
tex_text = TexText(tex_string, isolate=labelled_content, **kwargs)
lines = (tex_text.select_part(part) for part in labelled_content)
super().__init__(*lines)
self.arrange(DOWN, buff=buff, aligned_edge=aligned_edge)
def fade_all_but(self, index: int, opacity: float = 0.25) -> None:
for i, part in enumerate(self.submobjects):
part.set_fill(opacity=(1.0 if i == index else opacity))
class TexTextFromPresetString(TexText):
tex: str = ""
default_color: ManimColor = WHITE
def __init__(self, **kwargs):
super().__init__(
self.tex,
color=kwargs.pop("color", self.default_color),
**kwargs
)
class Title(TexText):
def __init__(
self,
*text_parts: str,
font_size: int = 72,
include_underline: bool = True,
underline_width: float = FRAME_WIDTH - 2,
# This will override underline_width
match_underline_width_to_text: bool = False,
underline_buff: float = SMALL_BUFF,
underline_style: dict = dict(stroke_width=2, stroke_color=GREY_C),
**kwargs
):
super().__init__(*text_parts, font_size=font_size, **kwargs)
self.to_edge(UP, buff=MED_SMALL_BUFF)
if include_underline:
underline = Line(LEFT, RIGHT, **underline_style)
underline.next_to(self, DOWN, buff=underline_buff)
if match_underline_width_to_text:
underline.match_width(self)
else:
underline.set_width(underline_width)
self.add(underline)
self.underline = underline

View File

@@ -0,0 +1,585 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import itertools as it
import re
from scipy.optimize import linear_sum_assignment
from scipy.spatial.distance import cdist
from manimlib.constants import WHITE
from manimlib.logger import log
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.color import color_to_hex
from manimlib.utils.color import hex_to_int
from manimlib.utils.color import int_to_hex
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
from manimlib.typing import ManimColor, Span, Selector
class StringMobject(SVGMobject, ABC):
"""
An abstract base class for `Tex` and `MarkupText`
This class aims to optimize the logic of "slicing submobjects
via substrings". This could be much clearer and more user-friendly
than slicing through numerical indices explicitly.
Users are expected to specify substrings in `isolate` parameter
if they want to do anything with their corresponding submobjects.
`isolate` parameter can be either a string, a `re.Pattern` object,
or a 2-tuple containing integers or None, or a collection of the above.
Note, substrings specified cannot *partly* overlap with each other.
Each instance of `StringMobject` may generate 2 svg files.
The additional one is generated with some color commands inserted,
so that each submobject of the original `SVGMobject` will be labelled
by the color of its paired submobject from the additional `SVGMobject`.
"""
height = None
def __init__(
self,
string: str,
fill_color: ManimColor = WHITE,
fill_border_width: float = 0.5,
stroke_color: ManimColor = WHITE,
stroke_width: float = 0,
base_color: ManimColor = WHITE,
isolate: Selector = (),
protect: Selector = (),
# When set to true, only the labelled svg is
# rendered, and its contents are used directly
# for the body of this String Mobject
use_labelled_svg: bool = False,
**kwargs
):
self.string = string
self.base_color = base_color or WHITE
self.isolate = isolate
self.protect = protect
self.use_labelled_svg = use_labelled_svg
self.parse()
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_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_svg_string_by_content(self, content: str) -> str:
return ""
def assign_labels_by_color(self, mobjects: list[VMobject]) -> None:
"""
Assuming each mobject in the list `mobjects` has a fill color
meant to represent a numerical label, this assigns those
those numerical labels to each mobject as an attribute
"""
labels_count = len(self.labelled_spans)
if labels_count == 1:
for mob in mobjects:
mob.label = 0
return
unrecognizable_colors = []
for mob in mobjects:
label = hex_to_int(color_to_hex(mob.get_fill_color()))
if label >= labels_count:
unrecognizable_colors.append(label)
label = 0
mob.label = label
if unrecognizable_colors:
log.warning(
"Unrecognizable color labels detected (%s). " + \
"The result could be unexpected.",
", ".join(
int_to_hex(color)
for color in unrecognizable_colors
)
)
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
self.assign_labels_by_color(submobs)
return submobs
# Otherwise, submobs are not colored, so generate a new list
# 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)
self.labelled_submobs = labelled_submobs
self.unlabelled_submobs = unlabelled_submobs
self.assign_labels_by_color(labelled_submobs)
self.rearrange_submobjects_by_positions(labelled_submobs, unlabelled_submobs)
for usm, lsm in zip(unlabelled_submobs, labelled_submobs):
usm.label = lsm.label
if len(unlabelled_submobs) != len(labelled_submobs):
log.warning(
"Cannot align submobjects of the labelled svg " + \
"to the original svg. Skip the labelling process."
)
for usm in unlabelled_submobs:
usm.label = 0
return unlabelled_submobs
return unlabelled_submobs
def rearrange_submobjects_by_positions(
self, labelled_submobs: list[VMobject], unlabelled_submobs: list[VMobject],
) -> None:
"""
Rearrange `labeleled_submobjects` so that each submobject
is labelled by the nearest one of `unlabelled_submobs`.
The correctness cannot be ensured, since the svg may
change significantly after inserting color commands.
"""
if len(labelled_submobs) == 0:
return
labelled_svg = VGroup(*labelled_submobs)
labelled_svg.replace(VGroup(*unlabelled_submobs))
distance_matrix = cdist(
[submob.get_center() for submob in unlabelled_submobs],
[submob.get_center() for submob in labelled_submobs]
)
_, indices = linear_sum_assignment(distance_matrix)
labelled_submobs[:] = [labelled_submobs[index] for index in indices]
# Toolkits
def find_spans_by_selector(self, selector: Selector) -> list[Span]:
def find_spans_by_single_selector(sel):
if isinstance(sel, str):
return [
match_obj.span()
for match_obj in re.finditer(re.escape(sel), self.string)
]
if isinstance(sel, re.Pattern):
return [
match_obj.span()
for match_obj in sel.finditer(self.string)
]
if isinstance(sel, tuple) and len(sel) == 2 and all(
isinstance(index, int) or index is None
for index in sel
):
l = len(self.string)
span = tuple(
default_index if index is None else
min(index, l) if index >= 0 else max(index + l, 0)
for index, default_index in zip(sel, (0, l))
)
return [span]
return None
result = find_spans_by_single_selector(selector)
if result is None:
result = []
for sel in selector:
spans = find_spans_by_single_selector(sel)
if spans is None:
raise TypeError(f"Invalid selector: '{sel}'")
result.extend(spans)
return list(filter(lambda span: span[0] <= span[1], result))
@staticmethod
def span_contains(span_0: Span, span_1: Span) -> bool:
return span_0[0] <= span_1[0] and span_0[1] >= span_1[1]
# Parsing
def parse(self) -> None:
def get_substr(span: Span) -> str:
return self.string[slice(*span)]
configured_items = self.get_configured_items()
isolated_spans = self.find_spans_by_selector(self.isolate)
protected_spans = self.find_spans_by_selector(self.protect)
command_matches = self.get_command_matches(self.string)
def get_key(category, i, flag):
def get_span_by_category(category, i):
if category == 0:
return configured_items[i][0]
if category == 1:
return isolated_spans[i]
if category == 2:
return protected_spans[i]
return command_matches[i].span()
index, paired_index = get_span_by_category(category, i)[::flag]
return (
index,
flag * (2 if index != paired_index else -1),
-paired_index,
flag * category,
flag * i
)
index_items = sorted([
(category, i, flag)
for category, item_length in enumerate((
len(configured_items),
len(isolated_spans),
len(protected_spans),
len(command_matches)
))
for i in range(item_length)
for flag in (1, -1)
], key=lambda t: get_key(*t))
inserted_items = []
labelled_items = []
overlapping_spans = []
level_mismatched_spans = []
label = 1
protect_level = 0
bracket_stack = [0]
bracket_count = 0
open_command_stack = []
open_stack = []
for category, i, flag in index_items:
if category >= 2:
protect_level += flag
if flag == 1 or category == 2:
continue
inserted_items.append((i, 0))
command_match = command_matches[i]
command_flag = self.get_command_flag(command_match)
if command_flag == 1:
bracket_count += 1
bracket_stack.append(bracket_count)
open_command_stack.append((len(inserted_items), i))
continue
if command_flag == 0:
continue
pos, i_ = open_command_stack.pop()
bracket_stack.pop()
open_command_match = command_matches[i_]
attr_dict = self.get_attr_dict_from_command_pair(
open_command_match, command_match
)
if attr_dict is None:
continue
span = (open_command_match.end(), command_match.start())
labelled_items.append((span, attr_dict))
inserted_items.insert(pos, (label, 1))
inserted_items.insert(-1, (label, -1))
label += 1
continue
if flag == 1:
open_stack.append((
len(inserted_items), category, i,
protect_level, bracket_stack.copy()
))
continue
span, attr_dict = configured_items[i] \
if category == 0 else (isolated_spans[i], {})
pos, category_, i_, protect_level_, bracket_stack_ \
= open_stack.pop()
if category_ != category or i_ != i:
overlapping_spans.append(span)
continue
if protect_level_ or protect_level:
continue
if bracket_stack_ != bracket_stack:
level_mismatched_spans.append(span)
continue
labelled_items.append((span, attr_dict))
inserted_items.insert(pos, (label, 1))
inserted_items.append((label, -1))
label += 1
labelled_items.insert(0, ((0, len(self.string)), {}))
inserted_items.insert(0, (0, 1))
inserted_items.append((0, -1))
if overlapping_spans:
log.warning(
"Partly overlapping substrings detected: %s",
", ".join(
f"'{get_substr(span)}'"
for span in overlapping_spans
)
)
if level_mismatched_spans:
log.warning(
"Cannot handle substrings: %s",
", ".join(
f"'{get_substr(span)}'"
for span in level_mismatched_spans
)
)
def reconstruct_string(
start_item: tuple[int, int],
end_item: tuple[int, int],
command_replace_func: Callable[[re.Match], str],
command_insert_func: Callable[[int, int, dict[str, str]], str]
) -> str:
def get_edge_item(i: int, flag: int) -> tuple[Span, str]:
if flag == 0:
match_obj = command_matches[i]
return (
match_obj.span(),
command_replace_func(match_obj)
)
span, attr_dict = labelled_items[i]
index = span[flag < 0]
return (
(index, index),
command_insert_func(i, flag, attr_dict)
)
items = [
get_edge_item(i, flag)
for i, flag in inserted_items[slice(
inserted_items.index(start_item),
inserted_items.index(end_item) + 1
)]
]
pieces = [
get_substr((start, end))
for start, end in zip(
[interval_end for (_, interval_end), _ in items[:-1]],
[interval_start for (interval_start, _), _ in items[1:]]
)
]
interval_pieces = [piece for _, piece in items[1:-1]]
return "".join(it.chain(*zip(pieces, (*interval_pieces, ""))))
self.labelled_spans = [span for span, _ in labelled_items]
self.reconstruct_string = reconstruct_string
def get_content(self, is_labelled: bool) -> str:
content = self.reconstruct_string(
(0, 1), (0, -1),
self.replace_for_content,
lambda label, flag, attr_dict: self.get_command_string(
attr_dict,
is_end=flag < 0,
label_hex=int_to_hex(label) if is_labelled else None
)
)
prefix, suffix = self.get_content_prefix_and_suffix(
is_labelled=is_labelled
)
return "".join((prefix, content, suffix))
@staticmethod
@abstractmethod
def get_command_matches(string: str) -> list[re.Match]:
return []
@staticmethod
@abstractmethod
def get_command_flag(match_obj: re.Match) -> int:
return 0
@staticmethod
@abstractmethod
def replace_for_content(match_obj: re.Match) -> str:
return ""
@staticmethod
@abstractmethod
def replace_for_matching(match_obj: re.Match) -> str:
return ""
@staticmethod
@abstractmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match,
) -> dict[str, str] | None:
return None
@abstractmethod
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return []
@staticmethod
@abstractmethod
def get_command_string(
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> str:
return ""
@abstractmethod
def get_content_prefix_and_suffix(
self, is_labelled: bool
) -> tuple[str, str]:
return "", ""
# Selector
def get_submob_indices_list_by_span(
self, arbitrary_span: Span
) -> list[int]:
return [
submob_index
for submob_index, label in enumerate(self.labels)
if self.span_contains(arbitrary_span, self.labelled_spans[label])
]
def get_specified_part_items(self) -> list[tuple[str, list[int]]]:
return [
(
self.string[slice(*span)],
self.get_submob_indices_list_by_span(span)
)
for span in self.labelled_spans[1:]
]
def get_specified_substrings(self) -> list[str]:
substrs = [
self.string[slice(*span)]
for span in self.labelled_spans[1:]
]
# Use dict.fromkeys to remove duplicates while retaining order
return list(dict.fromkeys(substrs).keys())
def get_group_part_items(self) -> list[tuple[str, list[int]]]:
if not self.labels:
return []
def get_neighbouring_pairs(vals):
return list(zip(vals[:-1], vals[1:]))
range_lens, group_labels = zip(*(
(len(list(grouper)), val)
for val, grouper in it.groupby(self.labels)
))
submob_indices_lists = [
list(range(*submob_range))
for submob_range in get_neighbouring_pairs(
[0, *it.accumulate(range_lens)]
)
]
labelled_spans = self.labelled_spans
start_items = [
(group_labels[0], 1),
*(
(curr_label, 1)
if self.span_contains(
labelled_spans[prev_label], labelled_spans[curr_label]
)
else (prev_label, -1)
for prev_label, curr_label in get_neighbouring_pairs(
group_labels
)
)
]
end_items = [
*(
(curr_label, -1)
if self.span_contains(
labelled_spans[next_label], labelled_spans[curr_label]
)
else (next_label, 1)
for curr_label, next_label in get_neighbouring_pairs(
group_labels
)
),
(group_labels[-1], -1)
]
group_substrs = [
re.sub(r"\s+", "", self.reconstruct_string(
start_item, end_item,
self.replace_for_matching,
lambda label, flag, attr_dict: ""
))
for start_item, end_item in zip(start_items, end_items)
]
return list(zip(group_substrs, submob_indices_lists))
def get_submob_indices_lists_by_selector(
self, selector: Selector
) -> list[list[int]]:
return list(filter(
lambda indices_list: indices_list,
[
self.get_submob_indices_list_by_span(span)
for span in self.find_spans_by_selector(selector)
]
))
def build_parts_from_indices_lists(
self, indices_lists: list[list[int]]
) -> VGroup:
return VGroup(*(
VGroup(*(
self.submobjects[submob_index]
for submob_index in indices_list
))
for indices_list in indices_lists
))
def build_groups(self) -> VGroup:
return self.build_parts_from_indices_lists([
indices_list
for _, indices_list in self.get_group_part_items()
])
def select_parts(self, selector: Selector) -> VGroup:
specified_substrings = self.get_specified_substrings()
if isinstance(selector, (str, re.Pattern)) and selector not in specified_substrings:
return self.select_unisolated_substring(selector)
indices_list = self.get_submob_indices_lists_by_selector(selector)
return self.build_parts_from_indices_lists(indices_list)
def __getitem__(self, value: int | slice | Selector) -> VMobject:
if isinstance(value, (int, slice)):
return super().__getitem__(value)
return self.select_parts(value)
def select_part(self, selector: Selector, index: int = 0) -> VMobject:
return self.select_parts(selector)[index]
def substr_to_path_count(self, substr: str) -> int:
return len(re.sub(r"\s", "", substr))
def get_symbol_substrings(self):
return list(re.sub(r"\s", "", self.string))
def select_unisolated_substring(self, pattern: str | re.Pattern) -> VGroup:
if isinstance(pattern, str):
pattern = re.compile(re.escape(pattern))
result = []
for match in re.finditer(pattern, self.string):
index = match.start()
start = self.substr_to_path_count(self.string[:index])
substr = match.group()
end = start + self.substr_to_path_count(substr)
result.append(self[start:end])
return VGroup(*result)
def set_parts_color(self, selector: Selector, color: ManimColor):
self.select_parts(selector).set_color(color)
return self
def set_parts_color_by_dict(self, color_map: dict[Selector, ManimColor]):
for selector, color in color_map.items():
self.set_parts_color(selector, color)
return self
def get_string(self) -> str:
return self.string

View File

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

View File

@@ -1,352 +1,269 @@
from functools import reduce
import operator as op
import re
from __future__ import annotations
from manimlib.constants import *
from manimlib.mobject.geometry import Line
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
import re
from pathlib import Path
from manimlib.mobject.svg.string_mobject import StringMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.config_ops import digest_config
from manimlib.utils.tex_file_writing import tex_to_svg_file
from manimlib.utils.tex_file_writing import get_tex_config
from manimlib.utils.tex_file_writing import display_during_execution
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 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, Self
SCALE_FACTOR_PER_FONT_POINT = 0.001
tex_string_to_mob_map = {}
class Tex(StringMobject):
tex_environment: str = "align*"
def __init__(
self,
*tex_strings: str,
font_size: int = 48,
alignment: str = R"\centering",
template: str = "",
additional_preamble: str = "",
tex_to_color_map: dict = dict(),
t2c: dict = dict(),
isolate: Selector = [],
use_labelled_svg: bool = True,
**kwargs
):
# Combine multi-string arg, but mark them to isolate
if len(tex_strings) > 1:
if isinstance(isolate, (str, re.Pattern, tuple)):
isolate = [isolate]
isolate = [*isolate, *tex_strings]
class SingleStringTex(VMobject):
CONFIG = {
"fill_opacity": 1.0,
"stroke_width": 0,
"should_center": True,
"font_size": 48,
"height": None,
"organize_left_to_right": False,
"alignment": "\\centering",
"math_mode": True,
}
tex_string = (" ".join(tex_strings)).strip()
def __init__(self, tex_string, **kwargs):
super().__init__(**kwargs)
assert(isinstance(tex_string, str))
# Prevent from passing an empty string.
if not tex_string.strip():
tex_string = R"\\"
self.font_size = font_size
self.tex_string = tex_string
if tex_string not in tex_string_to_mob_map:
with display_during_execution(f" Writing \"{tex_string}\""):
full_tex = self.get_tex_file_body(tex_string)
filename = tex_to_svg_file(full_tex)
svg_mob = SVGMobject(
filename,
height=None,
path_string_config={
"should_subdivide_sharp_curves": True,
"should_remove_null_curves": True,
}
)
tex_string_to_mob_map[tex_string] = svg_mob
self.add(*(
sm.copy()
for sm in tex_string_to_mob_map[tex_string]
))
self.init_colors()
self.alignment = alignment
self.template = template
self.additional_preamble = additional_preamble
self.tex_to_color_map = dict(**t2c, **tex_to_color_map)
if self.height is None:
self.scale(SCALE_FACTOR_PER_FONT_POINT * self.font_size)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
def get_tex_file_body(self, tex_string):
new_tex = self.get_modified_expression(tex_string)
if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
new_tex = self.alignment + "\n" + new_tex
tex_config = get_tex_config()
return tex_config["tex_body"].replace(
tex_config["text_to_replace"],
new_tex
super().__init__(
tex_string,
use_labelled_svg=use_labelled_svg,
isolate=isolate,
**kwargs
)
def get_modified_expression(self, tex_string):
return self.modify_special_strings(tex_string.strip())
def modify_special_strings(self, tex):
tex = tex.strip()
should_add_filler = reduce(op.or_, [
# Fraction line needs something to be over
tex == "\\over",
tex == "\\overline",
# Makesure sqrt has overbar
tex == "\\sqrt",
tex == "\\sqrt{",
# Need to add blank subscript or superscript
tex.endswith("_"),
tex.endswith("^"),
tex.endswith("dot"),
])
if should_add_filler:
filler = "{\\quad}"
tex += filler
if tex == "\\substack":
tex = "\\quad"
if tex == "":
tex = "\\quad"
# To keep files from starting with a line break
if tex.startswith("\\\\"):
tex = tex.replace("\\\\", "\\quad\\\\")
tex = self.balance_braces(tex)
# Handle imbalanced \left and \right
num_lefts, num_rights = [
len([
s for s in tex.split(substr)[1:]
if s and s[0] in "(){}[]|.\\"
])
for substr in ("\\left", "\\right")
]
if num_lefts != num_rights:
tex = tex.replace("\\left", "\\big")
tex = tex.replace("\\right", "\\big")
for context in ["array"]:
begin_in = ("\\begin{%s}" % context) in tex
end_in = ("\\end{%s}" % context) in tex
if begin_in ^ end_in:
# Just turn this into a blank string,
# which means caller should leave a
# stray \\begin{...} with other symbols
tex = ""
return tex
def balance_braces(self, tex):
"""
Makes Tex resiliant to unmatched braces
"""
num_unclosed_brackets = 0
for char in tex:
if char == "{":
num_unclosed_brackets += 1
elif char == "}":
if num_unclosed_brackets == 0:
tex = "{" + tex
else:
num_unclosed_brackets -= 1
tex += num_unclosed_brackets * "}"
return tex
def get_tex(self):
return self.tex_string
def organize_submobjects_left_to_right(self):
self.sort(lambda p: p[0])
return self
class Tex(SingleStringTex):
CONFIG = {
"arg_separator": "",
"isolate": [],
"tex_to_color_map": {},
}
def __init__(self, *tex_strings, **kwargs):
digest_config(self, kwargs)
self.tex_strings = self.break_up_tex_strings(tex_strings)
full_string = self.arg_separator.join(self.tex_strings)
super().__init__(full_string, **kwargs)
self.break_up_by_substrings()
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
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 break_up_tex_strings(self, tex_strings):
# Separate out any strings specified in the isolate
# or tex_to_color_map lists.
substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()]
if len(substrings_to_isolate) == 0:
return tex_strings
patterns = (
"({})".format(re.escape(ss))
for ss in substrings_to_isolate
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
self.font_size *= scale_factor
return self
# Parsing
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:
# Lump together adjacent brace pairs
pattern = re.compile(r"""
(?P<command>\\(?:[a-zA-Z]+|.))
|(?P<open>{+)
|(?P<close>}+)
""", flags=re.X | re.S)
result = []
open_stack = []
for match_obj in pattern.finditer(string):
if match_obj.group("open"):
open_stack.append((match_obj.span(), len(result)))
elif match_obj.group("close"):
close_start, close_end = match_obj.span()
while True:
if not open_stack:
raise ValueError("Missing '{' inserted")
(open_start, open_end), index = open_stack.pop()
n = min(open_end - open_start, close_end - close_start)
result.insert(index, pattern.fullmatch(
string, pos=open_end - n, endpos=open_end
))
result.append(pattern.fullmatch(
string, pos=close_start, endpos=close_start + n
))
close_start += n
if close_start < close_end:
continue
open_end -= n
if open_start < open_end:
open_stack.append(((open_start, open_end), index))
break
else:
result.append(match_obj)
if open_stack:
raise ValueError("Missing '}' inserted")
return result
@staticmethod
def get_command_flag(match_obj: re.Match) -> int:
if match_obj.group("open"):
return 1
if match_obj.group("close"):
return -1
return 0
@staticmethod
def replace_for_content(match_obj: re.Match) -> str:
return match_obj.group()
@staticmethod
def replace_for_matching(match_obj: re.Match) -> str:
if match_obj.group("command"):
return match_obj.group()
return ""
@staticmethod
def get_attr_dict_from_command_pair(
open_command: re.Match, close_command: re.Match
) -> dict[str, str] | None:
if len(open_command.group()) >= 2:
return {}
return None
def get_configured_items(self) -> list[tuple[Span, dict[str, str]]]:
return [
(span, {})
for selector in self.tex_to_color_map
for span in self.find_spans_by_selector(selector)
]
@staticmethod
def get_color_command(rgb_hex: str) -> str:
rgb = hex_to_int(rgb_hex)
rg, b = divmod(rgb, 256)
r, g = divmod(rg, 256)
return f"\\color[RGB]{{{r}, {g}, {b}}}"
@staticmethod
def get_command_string(
attr_dict: dict[str, str], is_end: bool, label_hex: str | None
) -> str:
if label_hex is None:
return ""
if is_end:
return "}}"
return "{{" + Tex.get_color_command(label_hex)
def get_content_prefix_and_suffix(
self, is_labelled: bool
) -> tuple[str, str]:
prefix_lines = []
suffix_lines = []
if not is_labelled:
prefix_lines.append(self.get_color_command(
color_to_hex(self.base_color)
))
if self.alignment:
prefix_lines.append(self.alignment)
if self.tex_environment:
prefix_lines.append(f"\\begin{{{self.tex_environment}}}")
suffix_lines.append(f"\\end{{{self.tex_environment}}}")
return (
"".join([line + "\n" for line in prefix_lines]),
"".join(["\n" + line for line in suffix_lines])
)
pattern = "|".join(patterns)
pieces = []
for s in tex_strings:
if pattern:
pieces.extend(re.split(pattern, s))
else:
pieces.append(s)
return list(filter(lambda s: s, pieces))
def break_up_by_substrings(self):
"""
Reorganize existing submojects one layer
deeper based on the structure of tex_strings (as a list
of tex_strings)
"""
if len(self.tex_strings) == 1:
submob = self.copy()
self.set_submobjects([submob])
return self
new_submobjects = []
curr_index = 0
config = dict(self.CONFIG)
config["alignment"] = ""
for tex_string in self.tex_strings:
tex_string = tex_string.strip()
if len(tex_string) == 0:
continue
sub_tex_mob = SingleStringTex(tex_string, **config)
num_submobs = len(sub_tex_mob)
if num_submobs == 0:
continue
new_index = curr_index + num_submobs
sub_tex_mob.set_submobjects(self[curr_index:new_index])
new_submobjects.append(sub_tex_mob)
curr_index = new_index
self.set_submobjects(new_submobjects)
return self
# Method alias
def get_parts_by_tex(self, tex, substring=True, case_sensitive=True):
def test(tex1, tex2):
if not case_sensitive:
tex1 = tex1.lower()
tex2 = tex2.lower()
if substring:
return tex1 in tex2
else:
return tex1 == tex2
def get_parts_by_tex(self, selector: Selector) -> VGroup:
return self.select_parts(selector)
return VGroup(*filter(
lambda m: isinstance(m, SingleStringTex) and test(tex, m.get_tex()),
self.submobjects
def get_part_by_tex(self, selector: Selector, index: int = 0) -> VMobject:
return self.select_part(selector, index)
def set_color_by_tex(self, selector: Selector, color: ManimColor):
return self.set_parts_color(selector, color)
def set_color_by_tex_to_color_map(
self, color_map: dict[Selector, ManimColor]
):
return self.set_parts_color_by_dict(color_map)
def get_tex(self) -> str:
return self.get_string()
# Specific to Tex
def substr_to_path_count(self, substr: str) -> int:
tex = self.get_tex()
if len(self) != num_tex_symbols(tex):
log.warning(f"Estimated size of {tex} does not match true size")
return num_tex_symbols(substr)
def get_symbol_substrings(self):
pattern = "|".join((
# Tex commands
r"\\[a-zA-Z]+",
# And most single characters, with these exceptions
r"[^\^\{\}\s\_\$\\\&]",
))
return re.findall(pattern, self.string)
def get_part_by_tex(self, tex, **kwargs):
all_parts = self.get_parts_by_tex(tex, **kwargs)
return all_parts[0] if all_parts else None
def make_number_changeable(
self,
value: float | int | str,
index: int = 0,
replace_all: bool = False,
**config,
) -> VMobject:
substr = str(value)
parts = self.select_parts(substr)
if len(parts) == 0:
log.warning(f"{value} not found in Tex.make_number_changeable call")
return VMobject()
if index > len(parts) - 1:
log.warning(f"Requested {index}th occurance of {value}, but only {len(parts)} exist")
return VMobject()
if not replace_all:
parts = [parts[index]]
def set_color_by_tex(self, tex, color, **kwargs):
self.get_parts_by_tex(tex, **kwargs).set_color(color)
return self
from manimlib.mobject.numbers import DecimalNumber
def set_color_by_tex_to_color_map(self, tex_to_color_map, **kwargs):
for tex, color in list(tex_to_color_map.items()):
self.set_color_by_tex(tex, color, **kwargs)
return self
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,
)
decimal_mob.replace(part)
decimal_mob.match_style(part)
if len(part) > 1:
self.remove(*part[1:])
self.replace_submobject(self.submobjects.index(part[0]), decimal_mob)
decimal_mobs.append(decimal_mob)
def index_of_part(self, part, start=0):
return self.submobjects.index(part, start)
# Replace substr with something that looks like a tex command. This
# is to ensure Tex.substr_to_path_count counts it correctly.
self.string = self.string.replace(substr, R"\decimalmob", 1)
def index_of_part_by_tex(self, tex, start=0, **kwargs):
part = self.get_part_by_tex(tex, **kwargs)
return self.index_of_part(part, start)
def slice_by_tex(self, start_tex=None, stop_tex=None, **kwargs):
if start_tex is None:
start_index = 0
else:
start_index = self.index_of_part_by_tex(start_tex, **kwargs)
if stop_tex is None:
return self[start_index:]
else:
stop_index = self.index_of_part_by_tex(stop_tex, start=start_index, **kwargs)
return self[start_index:stop_index]
def sort_alphabetically(self):
self.submobjects.sort(key=lambda m: m.get_tex())
def set_bstroke(self, color=BLACK, width=4):
self.set_stroke(color, width, background=True)
return self
if replace_all:
return VGroup(*decimal_mobs)
return decimal_mobs[index]
class TexText(Tex):
CONFIG = {
"math_mode": False,
"arg_separator": "",
}
class BulletedList(TexText):
CONFIG = {
"buff": MED_LARGE_BUFF,
"dot_scale_factor": 2,
"alignment": "",
}
def __init__(self, *items, **kwargs):
line_separated_items = [s + "\\\\" for s in items]
TexText.__init__(self, *line_separated_items, **kwargs)
for part in self:
dot = Tex("\\cdot").scale(self.dot_scale_factor)
dot.next_to(part[0], LEFT, SMALL_BUFF)
part.add_to_back(dot)
self.arrange(
DOWN,
aligned_edge=LEFT,
buff=self.buff
)
def fade_all_but(self, index_or_string, opacity=0.5):
arg = index_or_string
if isinstance(arg, str):
part = self.get_part_by_tex(arg)
elif isinstance(arg, int):
part = self.submobjects[arg]
else:
raise Exception("Expected int or string, got {0}".format(arg))
for other_part in self.submobjects:
if other_part is part:
other_part.set_fill(opacity=1)
else:
other_part.set_fill(opacity=opacity)
class TexFromPresetString(Tex):
CONFIG = {
# To be filled by subclasses
"tex": None,
"color": None,
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
Tex.__init__(self, self.tex, **kwargs)
self.set_color(self.color)
class Title(TexText):
CONFIG = {
"scale_factor": 1,
"include_underline": True,
"underline_width": FRAME_WIDTH - 2,
# This will override underline_width
"match_underline_width_to_text": False,
"underline_buff": MED_SMALL_BUFF,
}
def __init__(self, *text_parts, **kwargs):
TexText.__init__(self, *text_parts, **kwargs)
self.scale(self.scale_factor)
self.to_edge(UP)
if self.include_underline:
underline = Line(LEFT, RIGHT)
underline.next_to(self, DOWN, buff=self.underline_buff)
if self.match_underline_width_to_text:
underline.match_width(self)
else:
underline.set_width(self.underline_width)
self.add(underline)
self.underline = underline
tex_environment: str = ""

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,58 @@
from __future__ import annotations
import math
from manimlib.constants import *
from manimlib.mobject.types.surface import Surface
import numpy as np
from manimlib.constants import BLUE, BLUE_D, BLUE_E, GREY_A, BLACK
from manimlib.constants import IN, ORIGIN, OUT, RIGHT
from manimlib.constants import PI, TAU
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.surface import SGroup
from manimlib.mobject.types.surface import Surface
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.geometry import Square
from manimlib.mobject.geometry import Polygon
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Square
from manimlib.utils.bezier import interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.space_ops import compass_directions
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import z_to_vector
from manimlib.utils.space_ops import compass_directions
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Tuple, TypeVar
from manimlib.typing import ManimColor, Vect3, Sequence
T = TypeVar("T", bound=Mobject)
class SurfaceMesh(VGroup):
CONFIG = {
"resolution": (21, 11),
"stroke_width": 1,
"normal_nudge": 1e-2,
"depth_test": True,
"flat_stroke": False,
}
def __init__(self, uv_surface, **kwargs):
if not isinstance(uv_surface, Surface):
raise Exception("uv_surface must be of type Surface")
def __init__(
self,
uv_surface: Surface,
resolution: Tuple[int, int] = (21, 11),
stroke_width: float = 1,
stroke_color: ManimColor = GREY_A,
normal_nudge: float = 1e-2,
depth_test: bool = True,
joint_type: str = 'no_joint',
**kwargs
):
self.uv_surface = uv_surface
super().__init__(**kwargs)
self.resolution = resolution
self.normal_nudge = normal_nudge
def init_points(self):
super().__init__(
stroke_color=stroke_color,
stroke_width=stroke_width,
depth_test=depth_test,
joint_type=joint_type,
**kwargs
)
def init_points(self) -> None:
uv_surface = self.uv_surface
full_nu, full_nv = uv_surface.resolution
@@ -41,7 +63,7 @@ class SurfaceMesh(VGroup):
u_indices = np.linspace(0, full_nu - 1, part_nu)
v_indices = np.linspace(0, full_nv - 1, part_nv)
points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points()
points = uv_surface.get_points()
normals = uv_surface.get_unit_normals()
nudge = self.normal_nudge
nudged_points = points + nudge * normals
@@ -69,182 +91,286 @@ class SurfaceMesh(VGroup):
# 3D shapes
class Sphere(Surface):
CONFIG = {
"resolution": (101, 51),
"radius": 1,
"u_range": (0, TAU),
"v_range": (0, PI),
}
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
v_range: Tuple[float, float] = (0, PI),
resolution: Tuple[int, int] = (101, 51),
radius: float = 1.0,
**kwargs,
):
self.radius = radius
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs
)
def uv_func(self, u, v):
def uv_func(self, u: float, v: float) -> np.ndarray:
return self.radius * np.array([
np.cos(u) * np.sin(v),
np.sin(u) * np.sin(v),
-np.cos(v)
math.cos(u) * math.sin(v),
math.sin(u) * math.sin(v),
-math.cos(v)
])
class Torus(Surface):
CONFIG = {
"u_range": (0, TAU),
"v_range": (0, TAU),
"r1": 3,
"r2": 1,
}
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
v_range: Tuple[float, float] = (0, TAU),
r1: float = 3.0,
r2: float = 1.0,
**kwargs,
):
self.r1 = r1
self.r2 = r2
super().__init__(
u_range=u_range,
v_range=v_range,
**kwargs,
)
def uv_func(self, u, v):
def uv_func(self, u: float, v: float) -> np.ndarray:
P = np.array([math.cos(u), math.sin(u), 0])
return (self.r1 - self.r2 * math.cos(v)) * P - math.sin(v) * OUT
return (self.r1 - self.r2 * math.cos(v)) * P - self.r2 * math.sin(v) * OUT
class Cylinder(Surface):
CONFIG = {
"height": 2,
"radius": 1,
"axis": OUT,
"u_range": (0, TAU),
"v_range": (-1, 1),
"resolution": (101, 11),
}
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
v_range: Tuple[float, float] = (-1, 1),
resolution: Tuple[int, int] = (101, 11),
height: float = 2,
radius: float = 1,
axis: Vect3 = OUT,
**kwargs,
):
self.height = height
self.radius = radius
self.axis = axis
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs
)
def init_points(self):
super().init_points()
self.scale(self.radius)
self.set_depth(self.height, stretch=True)
self.apply_matrix(z_to_vector(self.axis))
return self
def uv_func(self, u, v):
return [np.cos(u), np.sin(u), v]
def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([np.cos(u), np.sin(u), v])
class 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):
CONFIG = {
"width": 0.05,
"resolution": (21, 25)
}
def __init__(self, start, end, **kwargs):
digest_config(self, kwargs)
def __init__(
self,
start: Vect3,
end: Vect3,
width: float = 0.05,
resolution: Tuple[int, int] = (21, 25),
**kwargs
):
axis = end - start
super().__init__(
height=get_norm(axis),
radius=self.width / 2,
axis=axis
radius=width / 2,
axis=axis,
resolution=resolution,
**kwargs
)
self.shift((start + end) / 2)
class Disk3D(Surface):
CONFIG = {
"radius": 1,
"u_range": (0, 1),
"v_range": (0, TAU),
"resolution": (2, 25),
}
def __init__(
self,
radius: float = 1,
u_range: Tuple[float, float] = (0, 1),
v_range: Tuple[float, float] = (0, TAU),
resolution: Tuple[int, int] = (2, 100),
**kwargs
):
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs,
)
self.scale(radius)
def init_points(self):
super().init_points()
self.scale(self.radius)
def uv_func(self, u, v):
return [
u * np.cos(v),
u * np.sin(v),
def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([
u * math.cos(v),
u * math.sin(v),
0
]
])
class Square3D(Surface):
CONFIG = {
"side_length": 2,
"u_range": (-1, 1),
"v_range": (-1, 1),
"resolution": (2, 2),
}
def __init__(
self,
side_length: float = 2.0,
u_range: Tuple[float, float] = (-1, 1),
v_range: Tuple[float, float] = (-1, 1),
resolution: Tuple[int, int] = (2, 2),
**kwargs,
):
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs
)
self.scale(side_length / 2)
def init_points(self):
super().init_points()
self.scale(self.side_length / 2)
def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([u, v, 0])
def uv_func(self, u, v):
return [u, v, 0]
def square_to_cube_faces(square: T) -> list[T]:
radius = square.get_height() / 2
square.move_to(radius * OUT)
result = [square.copy()]
result.extend([
square.copy().rotate(PI / 2, axis=vect, about_point=ORIGIN)
for vect in compass_directions(4)
])
result.append(square.copy().rotate(PI, RIGHT, about_point=ORIGIN))
return result
class Cube(SGroup):
CONFIG = {
"color": BLUE,
"opacity": 1,
"gloss": 0.5,
"square_resolution": (2, 2),
"side_length": 2,
"square_class": Square3D,
}
def init_points(self):
def __init__(
self,
color: ManimColor = BLUE,
opacity: float = 1,
shading: Tuple[float, float, float] = (0.1, 0.5, 0.1),
square_resolution: Tuple[int, int] = (2, 2),
side_length: float = 2,
**kwargs,
):
face = Square3D(
resolution=self.square_resolution,
side_length=self.side_length,
resolution=square_resolution,
side_length=side_length,
color=color,
opacity=opacity,
shading=shading,
)
self.add(*self.square_to_cube_faces(face))
@staticmethod
def square_to_cube_faces(square):
radius = square.get_height() / 2
square.move_to(radius * OUT)
result = [square]
result.extend([
square.copy().rotate(PI / 2, axis=vect, about_point=ORIGIN)
for vect in compass_directions(4)
])
result.append(square.copy().rotate(PI, RIGHT, about_point=ORIGIN))
return result
def _get_face(self):
return Square3D(resolution=self.square_resolution)
super().__init__(*square_to_cube_faces(face), **kwargs)
class VCube(VGroup):
CONFIG = {
"fill_color": BLUE_D,
"fill_opacity": 1,
"stroke_width": 0,
"gloss": 0.5,
"shadow": 0.5,
}
def __init__(self, side_length=2, **kwargs):
class Prism(Cube):
def __init__(
self,
width: float = 3.0,
height: float = 2.0,
depth: float = 1.0,
**kwargs
):
super().__init__(**kwargs)
face = Square(side_length=side_length)
face.get_triangulation()
self.add(*Cube.square_to_cube_faces(face))
self.init_colors()
self.apply_depth_test()
self.refresh_unit_normal()
for dim, value in enumerate([width, height, depth]):
self.rescale_to_fit(value, dim, stretch=True)
class Dodecahedron(VGroup):
CONFIG = {
"fill_color": BLUE_E,
"fill_opacity": 1,
"stroke_width": 1,
"reflectiveness": 0.2,
"gloss": 0.3,
"shadow": 0.2,
"depth_test": True,
}
class VGroup3D(VGroup):
def __init__(
self,
*vmobjects: VMobject,
depth_test: bool = True,
shading: Tuple[float, float, float] = (0.2, 0.2, 0.2),
joint_type: str = "no_joint",
**kwargs
):
super().__init__(*vmobjects, **kwargs)
self.set_shading(*shading)
self.set_joint_type(joint_type)
if depth_test:
self.apply_depth_test()
def init_points(self):
# Star by creating two of the pentagons, meeting
class VCube(VGroup3D):
def __init__(
self,
side_length: float = 2.0,
fill_color: ManimColor = BLUE_D,
fill_opacity: float = 1,
stroke_width: float = 0,
**kwargs
):
style = dict(
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
**kwargs
)
face = Square(side_length=side_length, **style)
super().__init__(*square_to_cube_faces(face), **style)
class VPrism(VCube):
def __init__(
self,
width: float = 3.0,
height: float = 2.0,
depth: float = 1.0,
**kwargs
):
super().__init__(**kwargs)
for dim, value in enumerate([width, height, depth]):
self.rescale_to_fit(value, dim, stretch=True)
class Dodecahedron(VGroup3D):
def __init__(
self,
fill_color: ManimColor = BLUE_E,
fill_opacity: float = 1,
stroke_color: ManimColor = BLUE_E,
stroke_width: float = 1,
shading: Tuple[float, float, float] = (0.2, 0.2, 0.2),
**kwargs,
):
style = dict(
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_color=stroke_color,
stroke_width=stroke_width,
shading=shading,
**kwargs
)
# Start by creating two of the pentagons, meeting
# back to back on the positive x-axis
phi = (1 + math.sqrt(5)) / 2
x, y, z = np.identity(3)
pentagon1 = Polygon(
[phi, 1 / phi, 0],
[1, 1, 1],
[1 / phi, 0, phi],
[1, -1, 1],
[phi, -1 / phi, 0],
np.array([phi, 1 / phi, 0]),
np.array([1, 1, 1]),
np.array([1 / phi, 0, phi]),
np.array([1, -1, 1]),
np.array([phi, -1 / phi, 0]),
**style
)
pentagon2 = pentagon1.copy().stretch(-1, 2, about_point=ORIGIN)
pentagon2.reverse_points()
@@ -252,30 +378,29 @@ class Dodecahedron(VGroup):
z_pair = x_pair.copy().apply_matrix(np.array([z, -x, -y]).T)
y_pair = x_pair.copy().apply_matrix(np.array([y, z, x]).T)
self.add(*x_pair, *y_pair, *z_pair)
for pentagon in list(self):
pentagons = [*x_pair, *y_pair, *z_pair]
for pentagon in list(pentagons):
pc = pentagon.copy()
pc.apply_function(lambda p: -p)
pc.reverse_points()
self.add(pc)
pentagons.append(pc)
# # Rotate those two pentagons by all the axis permuations to fill
# # out the dodecahedron
# Id = np.identity(3)
# for i in range(3):
# perm = [j % 3 for j in range(i, i + 3)]
# for b in [1, -1]:
# matrix = b * np.array([Id[0][perm], Id[1][perm], Id[2][perm]])
# self.add(pentagon1.copy().apply_matrix(matrix, about_point=ORIGIN))
# self.add(pentagon2.copy().apply_matrix(matrix, about_point=ORIGIN))
super().__init__(*pentagons, **style)
class Prism(Cube):
CONFIG = {
"dimensions": [3, 2, 1]
}
def init_points(self):
Cube.init_points(self)
for dim, value in enumerate(self.dimensions):
self.rescale_to_fit(value, dim, stretch=True)
class Prismify(VGroup3D):
def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):
# At the moment, this assume stright edges
vect = depth * direction
pieces = [vmobject.copy()]
points = vmobject.get_anchors()
for p1, p2 in adjacent_pairs(points):
wall = VMobject()
wall.match_style(vmobject)
wall.set_points_as_corners([p1, p2, p2 + vect, p1 + vect])
pieces.append(wall)
top = vmobject.copy()
top.shift(vect)
top.reverse_points()
pieces.append(top)
super().__init__(*pieces, **kwargs)

View File

@@ -1,11 +1,20 @@
import numpy as np
import moderngl
from __future__ import annotations
from manimlib.constants import GREY_C
from manimlib.constants import YELLOW
from manimlib.constants import ORIGIN
import moderngl
import numpy as np
from manimlib.constants import GREY_C, YELLOW
from manimlib.constants import ORIGIN, NULL_POINTS
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.point_cloud_mobject import PMobject
from manimlib.utils.iterables import resize_preserving_order
from manimlib.utils.iterables import resize_with_interpolation
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import numpy.typing as npt
from typing import Sequence, Tuple
from manimlib.typing import ManimColor, Vect3, Vect3Array, Self
DEFAULT_DOT_RADIUS = 0.05
@@ -15,41 +24,54 @@ DEFAULT_BUFF_RATIO = 0.5
class DotCloud(PMobject):
CONFIG = {
"color": GREY_C,
"opacity": 1,
"radius": DEFAULT_DOT_RADIUS,
"glow_factor": 0,
"shader_folder": "true_dot",
"render_primitive": moderngl.POINTS,
"shader_dtype": [
('point', np.float32, (3,)),
('radius', np.float32, (1,)),
('color', np.float32, (4,)),
],
}
shader_folder: str = "true_dot"
render_primitive: int = moderngl.POINTS
data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
('point', np.float32, (3,)),
('radius', np.float32, (1,)),
('rgba', np.float32, (4,)),
]
def __init__(
self,
points: Vect3Array = NULL_POINTS,
color: ManimColor = GREY_C,
opacity: float = 1.0,
radius: float = DEFAULT_DOT_RADIUS,
glow_factor: float = 0.0,
anti_alias_width: float = 2.0,
**kwargs
):
self.radius = radius
self.glow_factor = glow_factor
self.anti_alias_width = anti_alias_width
super().__init__(
color=color,
opacity=opacity,
**kwargs
)
self.set_radius(self.radius)
def __init__(self, points=None, **kwargs):
super().__init__(**kwargs)
if points is not None:
self.set_points(points)
def init_data(self):
super().init_data()
self.data["radii"] = np.zeros((1, 1))
self.set_radius(self.radius)
def init_uniforms(self):
def init_uniforms(self) -> None:
super().init_uniforms()
self.uniforms["glow_factor"] = self.glow_factor
self.uniforms["anti_alias_width"] = self.anti_alias_width
def to_grid(self, n_rows, n_cols, n_layers=1,
buff_ratio=None,
h_buff_ratio=1.0,
v_buff_ratio=1.0,
d_buff_ratio=1.0,
height=DEFAULT_GRID_HEIGHT,
):
def to_grid(
self,
n_rows: int,
n_cols: int,
n_layers: int = 1,
buff_ratio: float | None = None,
h_buff_ratio: float = 1.0,
v_buff_ratio: float = 1.0,
d_buff_ratio: float = 1.0,
height: float = DEFAULT_GRID_HEIGHT,
) -> Self:
n_points = n_rows * n_cols * n_layers
points = np.repeat(range(n_points), 3, axis=0).reshape((n_points, 3))
points[:, 0] = points[:, 0] % n_cols
@@ -74,64 +96,90 @@ class DotCloud(PMobject):
self.center()
return self
def set_radii(self, radii):
n_points = len(self.get_points())
@Mobject.affects_data
def set_radii(self, radii: npt.ArrayLike) -> Self:
n_points = self.get_num_points()
radii = np.array(radii).reshape((len(radii), 1))
self.data["radii"] = resize_preserving_order(radii, n_points)
self.data["radius"][:] = resize_with_interpolation(radii, n_points)
self.refresh_bounding_box()
return self
def get_radii(self):
return self.data["radii"]
def get_radii(self) -> np.ndarray:
return self.data["radius"]
def set_radius(self, radius):
self.data["radii"][:] = radius
@Mobject.affects_data
def set_radius(self, radius: float) -> Self:
data = self.data if self.get_num_points() > 0 else self._data_defaults
data["radius"][:] = radius
self.refresh_bounding_box()
return self
def get_radius(self):
def get_radius(self) -> float:
return self.get_radii().max()
def set_glow_factor(self, glow_factor):
self.uniforms["glow_factor"] = glow_factor
def scale_radii(self, scale_factor: float) -> Self:
self.set_radius(scale_factor * self.get_radii())
return self
def get_glow_factor(self):
def set_glow_factor(self, glow_factor: float) -> Self:
self.uniforms["glow_factor"] = glow_factor
return self
def get_glow_factor(self) -> float:
return self.uniforms["glow_factor"]
def compute_bounding_box(self):
def compute_bounding_box(self) -> Vect3Array:
bb = super().compute_bounding_box()
radius = self.get_radius()
bb[0] += np.full((3,), -radius)
bb[2] += np.full((3,), radius)
return bb
def scale(self, scale_factor, scale_radii=True, **kwargs):
def scale(
self,
scale_factor: float | npt.ArrayLike,
scale_radii: bool = True,
**kwargs
) -> Self:
super().scale(scale_factor, **kwargs)
if scale_radii:
self.set_radii(scale_factor * self.get_radii())
return self
def make_3d(self, reflectiveness=0.5, shadow=0.2):
self.set_reflectiveness(reflectiveness)
self.set_shadow(shadow)
def make_3d(
self,
reflectiveness: float = 0.5,
gloss: float = 0.1,
shadow: float = 0.2
) -> Self:
self.set_shading(reflectiveness, gloss, shadow)
self.apply_depth_test()
return self
def get_shader_data(self):
shader_data = super().get_shader_data()
self.read_data_to_shader(shader_data, "radius", "radii")
self.read_data_to_shader(shader_data, "color", "rgbas")
return shader_data
class TrueDot(DotCloud):
def __init__(self, center=ORIGIN, **kwargs):
super().__init__(points=[center], **kwargs)
def __init__(self, center: Vect3 = ORIGIN, **kwargs):
super().__init__(points=np.array([center]), **kwargs)
class GlowDot(TrueDot):
CONFIG = {
"glow_factor": 2,
"radius": DEFAULT_GLOW_DOT_RADIUS,
"color": YELLOW,
}
class GlowDots(DotCloud):
def __init__(
self,
points: Vect3Array = NULL_POINTS,
color: ManimColor = YELLOW,
radius: float = DEFAULT_GLOW_DOT_RADIUS,
glow_factor: float = 2.0,
**kwargs,
):
super().__init__(
points,
color=color,
radius=radius,
glow_factor=glow_factor,
**kwargs,
)
class GlowDot(GlowDots):
def __init__(self, center: Vect3 = ORIGIN, **kwargs):
super().__init__(points=np.array([center]), **kwargs)

View File

@@ -1,53 +1,66 @@
import numpy as np
from __future__ import annotations
import numpy as np
import moderngl
from PIL import Image
from manimlib.constants import *
from manimlib.constants import DL, DR, UL, UR
from manimlib.mobject.mobject import Mobject
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 typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Sequence, Tuple
from manimlib.typing import Vect3
class ImageMobject(Mobject):
CONFIG = {
"height": 4,
"opacity": 1,
"shader_folder": "image",
"shader_dtype": [
('point', np.float32, (3,)),
('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)),
]
}
shader_folder: str = "image"
data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
('point', np.float32, (3,)),
('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)),
]
render_primitive: int = moderngl.TRIANGLES
def __init__(self, filename, **kwargs):
self.set_image_path(get_full_raster_image_path(filename))
super().__init__(**kwargs)
def __init__(
self,
filename: str,
height: float = 4.0,
**kwargs
):
self.height = height
self.image_path = get_full_raster_image_path(filename)
self.image = Image.open(self.image_path)
super().__init__(texture_paths={"Texture": self.image_path}, **kwargs)
def set_image_path(self, path):
self.path = path
self.image = Image.open(path)
self.texture_paths = {"Texture": path}
def init_data(self) -> None:
super().init_data(length=6)
self.data["point"][:] = [UL, DL, UR, DR, UR, DL]
self.data["im_coords"][:] = [(0, 0), (0, 1), (1, 0), (1, 1), (1, 0), (0, 1)]
self.data["opacity"][:] = self.opacity
def init_data(self):
self.data = {
"points": np.array([UL, DL, UR, DR]),
"im_coords": np.array([(0, 0), (0, 1), (1, 0), (1, 1)]),
"opacity": np.array([[self.opacity]], dtype=np.float32),
}
def init_points(self):
def init_points(self) -> None:
size = self.image.size
self.set_width(2 * size[0] / size[1], stretch=True)
self.set_height(self.height)
def set_opacity(self, opacity, recurse=True):
for mob in self.get_family(recurse):
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
@Mobject.affects_data
def set_opacity(self, opacity: float, recurse: bool = True):
self.data["opacity"][:, 0] = resize_with_interpolation(
np.array(listify(opacity)),
self.get_num_points()
)
return self
def point_to_rgb(self, point):
def set_color(self, color, opacity=None, recurse=None):
return self
def point_to_rgb(self, point: Vect3) -> Vect3:
x0, y0 = self.get_corner(UL)[:2]
x1, y1 = self.get_corner(DR)[:2]
x_alpha = inverse_interpolate(x0, x1, point[0])
@@ -60,11 +73,5 @@ class ImageMobject(Mobject):
rgb = self.image.getpixel((
int((pw - 1) * x_alpha),
int((ph - 1) * y_alpha),
))
))[:3]
return np.array(rgb) / 255
def get_shader_data(self):
shader_data = super().get_shader_data()
self.read_data_to_shader(shader_data, "im_coords", "im_coords")
self.read_data_to_shader(shader_data, "opacity", "opacity")
return shader_data

View File

@@ -1,31 +1,34 @@
from manimlib.constants import *
from __future__ import annotations
import numpy as np
from manimlib.mobject.mobject import Mobject
from manimlib.utils.color import color_gradient
from manimlib.utils.color import color_to_rgba
from manimlib.utils.iterables import resize_with_interpolation
from manimlib.utils.iterables import resize_array
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
from manimlib.typing import ManimColor, Vect3, Vect3Array, Vect4Array, Self
class PMobject(Mobject):
CONFIG = {
"opacity": 1.0,
}
def resize_points(self, size, resize_func=resize_array):
# TODO
for key in self.data:
if key == "bounding_box":
continue
if len(self.data[key]) != size:
self.data[key] = resize_array(self.data[key], size)
return self
def set_points(self, points):
def set_points(self, points: Vect3Array):
if len(points) == 0:
points = np.zeros((0, 3))
super().set_points(points)
self.resize_points(len(points))
return self
def add_points(self, points, rgbas=None, color=None, opacity=None):
def add_points(
self,
points: Vect3Array,
rgbas: Vect4Array | None = None,
color: ManimColor | None = None,
opacity: float | None = None
) -> Self:
"""
points must be a Nx3 numpy array, as must rgbas if it is not None
"""
@@ -33,40 +36,44 @@ class PMobject(Mobject):
# rgbas array will have been resized with points
if color is not None:
if opacity is None:
opacity = self.data["rgbas"][-1, 3]
new_rgbas = np.repeat(
opacity = self.data["rgba"][-1, 3]
rgbas = np.repeat(
[color_to_rgba(color, opacity)],
len(points),
axis=0
)
elif rgbas is not None:
new_rgbas = rgbas
self.data["rgbas"][-len(new_rgbas):] = new_rgbas
if rgbas is not None:
self.data["rgba"][-len(rgbas):] = rgbas
return self
def set_color_by_gradient(self, *colors):
self.data["rgbas"] = np.array(list(map(
def add_point(self, point: Vect3, rgba=None, color=None, opacity=None) -> Self:
rgbas = None if rgba is None else [rgba]
self.add_points([point], rgbas, color, opacity)
return self
@Mobject.affects_data
def set_color_by_gradient(self, *colors: ManimColor) -> Self:
self.data["rgba"][:] = np.array(list(map(
color_to_rgba,
color_gradient(colors, self.get_num_points())
)))
return self
def match_colors(self, pmobject):
self.data["rgbas"][:] = resize_with_interpolation(
pmobject.data["rgbas"], self.get_num_points()
@Mobject.affects_data
def match_colors(self, pmobject: PMobject) -> Self:
self.data["rgba"][:] = resize_with_interpolation(
pmobject.data["rgba"], self.get_num_points()
)
return self
def filter_out(self, condition):
@Mobject.affects_data
def filter_out(self, condition: Callable[[np.ndarray], bool]) -> Self:
for mob in self.family_members_with_points():
to_keep = ~np.apply_along_axis(condition, 1, mob.get_points())
for key in mob.data:
if key == "bounding_box":
continue
mob.data[key] = mob.data[key][to_keep]
mob.data = mob.data[~np.apply_along_axis(condition, 1, mob.get_points())]
return self
def sort_points(self, function=lambda p: p[0]):
@Mobject.affects_data
def sort_points(self, function: Callable[[Vect3], None] = lambda p: p[0]) -> Self:
"""
function is any map from R^3 to R
"""
@@ -74,44 +81,31 @@ class PMobject(Mobject):
indices = np.argsort(
np.apply_along_axis(function, 1, mob.get_points())
)
for key in mob.data:
mob.data[key] = mob.data[key][indices]
mob.data[:] = mob.data[indices]
return self
def ingest_submobjects(self):
for key in self.data:
self.data[key] = np.vstack([
sm.data[key]
for sm in self.get_family()
])
@Mobject.affects_data
def ingest_submobjects(self) -> Self:
self.data = np.vstack([
sm.data for sm in self.get_family()
])
return self
def point_from_proportion(self, alpha):
def point_from_proportion(self, alpha: float) -> np.ndarray:
index = alpha * (self.get_num_points() - 1)
return self.get_points()[int(index)]
def pointwise_become_partial(self, pmobject, a, b):
@Mobject.affects_data
def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float) -> Self:
lower_index = int(a * pmobject.get_num_points())
upper_index = int(b * pmobject.get_num_points())
for key in self.data:
if key == "bounding_box":
continue
self.data[key] = pmobject.data[key][lower_index:upper_index].copy()
self.data = pmobject.data[lower_index:upper_index].copy()
return self
class PGroup(PMobject):
def __init__(self, *pmobs, **kwargs):
def __init__(self, *pmobs: PMobject, **kwargs):
if not all([isinstance(m, PMobject) for m in pmobs]):
raise Exception("All submobjects must be of type PMobject")
super().__init__(*pmobs, **kwargs)
class Point(PMobject):
CONFIG = {
"color": BLACK,
}
def __init__(self, location=ORIGIN, **kwargs):
super().__init__(**kwargs)
self.add_points([location])
self.add(*pmobs)

View File

@@ -1,51 +1,75 @@
import numpy as np
import moderngl
from __future__ import annotations
from manimlib.constants import *
import moderngl
import numpy as np
from manimlib.constants import GREY
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.images import get_full_raster_image_path
from manimlib.utils.iterables import listify
from manimlib.utils.iterables import resize_with_interpolation
from manimlib.utils.space_ops import normalize_along_axis
from manimlib.utils.space_ops import cross
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, Sequence, Tuple
from manimlib.camera.camera import Camera
from manimlib.typing import ManimColor, Vect3, Vect3Array, Self
class Surface(Mobject):
CONFIG = {
"u_range": (0, 1),
"v_range": (0, 1),
render_primitive: int = moderngl.TRIANGLES
shader_folder: str = "surface"
data_dtype: np.dtype = np.dtype([
('point', np.float32, (3,)),
('du_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('rgba', np.float32, (4,)),
])
pointlike_data_keys = ['point', 'du_point', 'dv_point']
def __init__(
self,
color: ManimColor = GREY,
shading: Tuple[float, float, float] = (0.3, 0.2, 0.4),
depth_test: bool = True,
u_range: Tuple[float, float] = (0.0, 1.0),
v_range: Tuple[float, float] = (0.0, 1.0),
# Resolution counts number of points sampled, which for
# each coordinate is one more than the the number of
# rows/columns of approximating squares
"resolution": (101, 101),
"color": GREY,
"opacity": 1.0,
"reflectiveness": 0.3,
"gloss": 0.1,
"shadow": 0.4,
"prefered_creation_axis": 1,
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": 1e-5,
"render_primitive": moderngl.TRIANGLES,
"depth_test": True,
"shader_folder": "surface",
"shader_dtype": [
('point', np.float32, (3,)),
('du_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('color', np.float32, (4,)),
]
}
epsilon: float = 1e-4,
**kwargs
):
self.u_range = u_range
self.v_range = v_range
self.resolution = resolution
self.prefered_creation_axis = prefered_creation_axis
self.epsilon = epsilon
def __init__(self, **kwargs):
super().__init__(**kwargs)
super().__init__(
**kwargs,
color=color,
shading=shading,
depth_test=depth_test,
)
self.compute_triangle_indices()
def uv_func(self, u, v):
def uv_func(self, u: float, v: float) -> tuple[float, float, float]:
# To be implemented in subclasses
return (u, v, 0.0)
@Mobject.affects_data
def init_points(self):
dim = self.dim
nu, nv = self.resolution
@@ -56,25 +80,35 @@ class Surface(Mobject):
# - Points generated by pure uv values
# - Those generated by values nudged by du
# - Those generated by values nudged by dv
point_lists = []
for (du, dv) in [(0, 0), (self.epsilon, 0), (0, self.epsilon)]:
uv_grid = np.array([[[u + du, v + dv] for v in v_range] for u in u_range])
point_grid = np.apply_along_axis(lambda p: self.uv_func(*p), 2, uv_grid)
point_lists.append(point_grid.reshape((nu * nv, dim)))
# Rather than tracking normal vectors, the points list will hold on to the
# infinitesimal nudged values alongside the original values. This way, one
# can perform all the manipulations they'd like to the surface, and normals
# are still easily recoverable.
self.set_points(np.vstack(point_lists))
uv_grid = np.array([[[u, v] for v in v_range] for u in u_range])
uv_plus_du = uv_grid.copy()
uv_plus_du[:, :, 0] += self.epsilon
uv_plus_dv = uv_grid.copy()
uv_plus_dv[:, :, 1] += self.epsilon
def compute_triangle_indices(self):
points, du_points, dv_points = [
np.apply_along_axis(
lambda p: self.uv_func(*p), 2, grid
).reshape((nu * nv, dim))
for grid in (uv_grid, uv_plus_du, uv_plus_dv)
]
self.set_points(points)
self.data['du_point'][:] = du_points
self.data['dv_point'][:] = dv_points
def apply_points_function(self, *args, **kwargs) -> Self:
super().apply_points_function(*args, **kwargs)
self.get_unit_normals()
return self
def compute_triangle_indices(self) -> np.ndarray:
# TODO, if there is an event which changes
# the resolution of the surface, make sure
# this is called.
nu, nv = self.resolution
if nu == 0 or nv == 0:
self.triangle_indices = np.zeros(0, dtype=int)
return
return self.triangle_indices
index_grid = np.arange(nu * nv).reshape((nu, nv))
indices = np.zeros(6 * (nu - 1) * (nv - 1), dtype=int)
indices[0::6] = index_grid[:-1, :-1].flatten() # Top left
@@ -84,25 +118,28 @@ class Surface(Mobject):
indices[4::6] = index_grid[+1:, :-1].flatten() # Bottom left
indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right
self.triangle_indices = indices
def get_triangle_indices(self):
return self.triangle_indices
def get_surface_points_and_nudged_points(self):
def get_triangle_indices(self) -> np.ndarray:
return self.triangle_indices
def get_unit_normals(self) -> Vect3Array:
points = self.get_points()
k = len(points) // 3
return points[:k], points[k:2 * k], points[2 * k:]
def get_unit_normals(self):
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
normals = np.cross(
(du_points - s_points) / self.epsilon,
(dv_points - s_points) / self.epsilon,
crosses = cross(
self.data['du_point'] - points,
self.data['dv_point'] - points,
)
return normalize_along_axis(normals, 1)
return normalize_along_axis(crosses, 1)
def pointwise_become_partial(self, smobject, a, b, axis=None):
assert(isinstance(smobject, Surface))
@Mobject.affects_data
def pointwise_become_partial(
self,
smobject: "Surface",
a: float,
b: float,
axis: int | None = None
) -> Self:
assert isinstance(smobject, Surface)
if axis is None:
axis = self.prefered_creation_axis
if a <= 0 and b >= 1:
@@ -110,17 +147,25 @@ class Surface(Mobject):
return self
nu, nv = smobject.resolution
self.set_points(np.vstack([
self.get_partial_points_array(arr.copy(), a, b, (nu, nv, 3), axis=axis)
for arr in smobject.get_surface_points_and_nudged_points()
]))
self.data['point'][:] = self.get_partial_points_array(
smobject.data['point'], a, b,
(nu, nv, 3),
axis=axis
)
return self
def get_partial_points_array(self, points, a, b, resolution, axis):
def get_partial_points_array(
self,
points: Vect3Array,
a: float,
b: float,
resolution: Sequence[int],
axis: int
) -> Vect3Array:
if len(points) == 0:
return points
nu, nv = resolution[:2]
points = points.reshape(resolution)
points = points.reshape(resolution).copy()
max_index = resolution[axis] - 1
lower_index, lower_residue = integer_interpolate(0, max_index, a)
upper_index, upper_residue = integer_interpolate(0, max_index, b)
@@ -149,45 +194,36 @@ class Surface(Mobject):
).reshape(shape)
return points.reshape((nu * nv, *resolution[2:]))
def sort_faces_back_to_front(self, vect=OUT):
@Mobject.affects_data
def sort_faces_back_to_front(self, vect: Vect3 = OUT) -> Self:
tri_is = self.triangle_indices
indices = list(range(len(tri_is) // 3))
points = self.get_points()
def index_dot(index):
return np.dot(points[tri_is[3 * index]], vect)
indices.sort(key=index_dot)
dots = (points[tri_is[::3]] * vect).sum(1)
indices = np.argsort(dots)
for k in range(3):
tri_is[k::3] = tri_is[k::3][indices]
return self
def always_sort_to_camera(self, camera):
self.add_updater(lambda m: m.sort_faces_back_to_front(
camera.get_location() - self.get_center()
))
def always_sort_to_camera(self, camera: Camera) -> Self:
def updater(surface: Surface):
vect = camera.get_location() - surface.get_center()
surface.sort_faces_back_to_front(vect)
self.add_updater(updater)
return self
# For shaders
def get_shader_data(self):
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
shader_data = self.get_resized_shader_data_array(len(s_points))
if "points" not in self.locked_data_keys:
shader_data["point"] = s_points
shader_data["du_point"] = du_points
shader_data["dv_point"] = dv_points
self.fill_in_shader_color_info(shader_data)
return shader_data
def fill_in_shader_color_info(self, shader_data):
self.read_data_to_shader(shader_data, "color", "rgbas")
return shader_data
def get_shader_vert_indices(self):
def get_shader_vert_indices(self) -> np.ndarray:
return self.get_triangle_indices()
class ParametricSurface(Surface):
def __init__(self, uv_func, u_range=(0, 1), v_range=(0, 1), **kwargs):
def __init__(
self,
uv_func: Callable[[float, float], Iterable[float]],
u_range: tuple[float, float] = (0, 1),
v_range: tuple[float, float] = (0, 1),
**kwargs
):
self.passed_uv_func = uv_func
super().__init__(u_range=u_range, v_range=v_range, **kwargs)
@@ -196,12 +232,12 @@ class ParametricSurface(Surface):
class SGroup(Surface):
CONFIG = {
"resolution": (0, 0),
}
def __init__(self, *parametric_surfaces, **kwargs):
super().__init__(uv_func=None, **kwargs)
def __init__(
self,
*parametric_surfaces: Surface,
**kwargs
):
super().__init__(resolution=(0, 0), **kwargs)
self.add(*parametric_surfaces)
def init_points(self):
@@ -209,18 +245,22 @@ class SGroup(Surface):
class TexturedSurface(Surface):
CONFIG = {
"shader_folder": "textured_surface",
"shader_dtype": [
('point', np.float32, (3,)),
('du_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)),
]
}
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,)),
('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)),
]
def __init__(self, uv_surface, image_file, dark_image_file=None, **kwargs):
def __init__(
self,
uv_surface: Surface,
image_file: str,
dark_image_file: str | None = None,
**kwargs
):
if not isinstance(uv_surface, Surface):
raise Exception("uv_surface must be of type Surface")
# Set texture information
@@ -229,27 +269,33 @@ class TexturedSurface(Surface):
self.num_textures = 1
else:
self.num_textures = 2
self.texture_paths = {
texture_paths = {
"LightTexture": get_full_raster_image_path(image_file),
"DarkTexture": get_full_raster_image_path(dark_image_file),
}
self.uv_surface = uv_surface
self.uv_func = uv_surface.uv_func
self.u_range = uv_surface.u_range
self.v_range = uv_surface.v_range
self.resolution = uv_surface.resolution
self.gloss = self.uv_surface.gloss
super().__init__(**kwargs)
def init_data(self):
super().init_data()
self.data["im_coords"] = np.zeros((0, 2))
self.data["opacity"] = np.zeros((0, 1))
self.u_range: Tuple[float, float] = uv_surface.u_range
self.v_range: Tuple[float, float] = uv_surface.v_range
self.resolution: Tuple[int, int] = uv_surface.resolution
super().__init__(
texture_paths=texture_paths,
shading=tuple(uv_surface.shading),
**kwargs
)
@Mobject.affects_data
def init_points(self):
nu, nv = self.uv_surface.resolution
self.set_points(self.uv_surface.get_points())
surf = self.uv_surface
nu, nv = surf.resolution
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['opacity'][:, 0] = surf.data["rgba"][:, 3]
self.data["im_coords"] = np.array([
[u, v]
for u in np.linspace(0, 1, nu)
@@ -260,15 +306,29 @@ class TexturedSurface(Surface):
super().init_uniforms()
self.uniforms["num_textures"] = self.num_textures
def init_colors(self):
self.data["opacity"] = np.array([self.uv_surface.data["rgbas"][:, 3]])
def set_opacity(self, opacity, recurse=True):
for mob in self.get_family(recurse):
mob.data["opacity"] = np.array([[o] for o in listify(opacity)])
@Mobject.affects_data
def set_opacity(self, opacity: float | Iterable[float]) -> Self:
op_arr = np.array(listify(opacity))
self.data["opacity"][:, 0] = resize_with_interpolation(op_arr, len(self.data))
return self
def pointwise_become_partial(self, tsmobject, a, b, axis=1):
def set_color(
self,
color: ManimColor | Iterable[ManimColor] | None,
opacity: float | Iterable[float] | None = None,
recurse: bool = True
) -> Self:
if opacity is not None:
self.set_opacity(opacity)
return self
def pointwise_become_partial(
self,
tsmobject: "TexturedSurface",
a: float,
b: float,
axis: int = 1
) -> Self:
super().pointwise_become_partial(tsmobject, a, b, axis)
im_coords = self.data["im_coords"]
im_coords[:] = tsmobject.data["im_coords"]
@@ -279,8 +339,3 @@ class TexturedSurface(Surface):
im_coords, a, b, (nu, nv, 2), axis
)
return self
def fill_in_shader_color_info(self, shader_data):
self.read_data_to_shader(shader_data, "opacity", "opacity")
self.read_data_to_shader(shader_data, "im_coords", "im_coords")
return shader_data

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,14 @@
import numpy as np
from __future__ import annotations
import numpy as np
from manimlib.mobject.mobject import Mobject
from manimlib.utils.iterables import listify
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import Self
class ValueTracker(Mobject):
"""
@@ -11,33 +17,34 @@ class ValueTracker(Mobject):
uses for its update function, and by treating it as a mobject it can
still be animated and manipulated just like anything else.
"""
CONFIG = {
"value_type": np.float64,
}
value_type: type = np.float64
def __init__(self, value=0, **kwargs):
def __init__(
self,
value: float | complex | np.ndarray = 0,
**kwargs
):
self.value = value
super().__init__(**kwargs)
def init_data(self):
super().init_data()
self.data["value"] = np.array(
def init_uniforms(self) -> None:
super().init_uniforms()
self.uniforms["value"] = np.array(
listify(self.value),
ndmin=2,
dtype=self.value_type,
)
def get_value(self):
result = self.data["value"][0, :]
def get_value(self) -> float | complex | np.ndarray:
result = self.uniforms["value"]
if len(result) == 1:
return result[0]
return result
def set_value(self, value):
self.data["value"][0, :] = value
def set_value(self, value: float | complex | np.ndarray) -> Self:
self.uniforms["value"][:] = value
return self
def increment_value(self, d_value):
def increment_value(self, d_value: float | complex) -> None:
self.set_value(self.get_value() + d_value)
@@ -48,14 +55,12 @@ class ExponentialValueTracker(ValueTracker):
behaves
"""
def get_value(self):
def get_value(self) -> float | complex:
return np.exp(ValueTracker.get_value(self))
def set_value(self, value):
def set_value(self, value: float | complex):
return ValueTracker.set_value(self, np.log(value))
class ComplexValueTracker(ValueTracker):
CONFIG = {
"value_type": np.complex128
}
value_type: type = np.complex128

View File

@@ -1,25 +1,41 @@
import numpy as np
from __future__ import annotations
import itertools as it
import random
from manimlib.constants import *
import numpy as np
from scipy.integrate import solve_ivp
from manimlib.animation.composition import AnimationGroup
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import WHITE
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 inverse_interpolate
from manimlib.utils.bezier import interpolate
from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.color import get_colormap_list
from manimlib.utils.config_ops import merge_dicts_recursively
from manimlib.utils.config_ops import digest_config
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
def get_vectorized_rgb_gradient_function(min_value, max_value, color_map):
if TYPE_CHECKING:
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
T = TypeVar("T")
#### Delete these two ###
def get_vectorized_rgb_gradient_function(
min_value: T,
max_value: T,
color_map: str
) -> Callable[[VectN], Vect3Array]:
rgbs = np.array(get_colormap_list(color_map))
def func(values):
@@ -34,15 +50,34 @@ def get_vectorized_rgb_gradient_function(min_value, max_value, color_map):
inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3))
result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas)
return result
return func
def get_rgb_gradient_function(min_value, max_value, color_map):
def get_rgb_gradient_function(
min_value: T,
max_value: T,
color_map: str
) -> Callable[[float], Vect3]:
vectorized_func = get_vectorized_rgb_gradient_function(min_value, max_value, color_map)
return lambda value: vectorized_func([value])[0]
return lambda value: vectorized_func(np.array([value]))[0]
####
def move_along_vector_field(mobject, func):
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(
mobject: Mobject,
func: Callable[[Vect3], Vect3]
) -> Mobject:
mobject.add_updater(
lambda m, dt: m.shift(
func(m.get_center()) * dt
@@ -51,7 +86,10 @@ def move_along_vector_field(mobject, func):
return mobject
def move_submobjects_along_vector_field(mobject, func):
def move_submobjects_along_vector_field(
mobject: Mobject,
func: Callable[[Vect3], Vect3]
) -> Mobject:
def apply_nudge(mob, dt):
for submob in mob:
x, y = submob.get_center()[:2]
@@ -62,155 +100,321 @@ def move_submobjects_along_vector_field(mobject, func):
return mobject
def move_points_along_vector_field(mobject, func, coordinate_system):
def move_points_along_vector_field(
mobject: Mobject,
func: Callable[[float, float], Iterable[float]],
coordinate_system: CoordinateSystem
) -> Mobject:
cs = coordinate_system
origin = cs.get_origin()
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(coordinate_system, step_multiple):
def get_sample_coords(
coordinate_system: CoordinateSystem,
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
class VectorField(VGroup):
CONFIG = {
"step_multiple": 0.5,
"magnitude_range": (0, 2),
"color_map": "3b1b_colormap",
# Takes in actual norm, spits out displayed norm
"length_func": lambda norm: 0.45 * sigmoid(norm),
"opacity": 1.0,
"vector_config": {},
}
def __init__(self, func, coordinate_system, **kwargs):
super().__init__(**kwargs)
class VectorField(VMobject):
def __init__(
self,
# 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,
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,
stroke_width: float = 3,
tip_width_ratio: float = 4,
tip_len_to_width: float = 0.01,
max_vect_len: float | None = None,
max_vect_len_to_step_size: float = 0.8,
flat_stroke: bool = False,
norm_to_opacity_func=None, # TODO, check on this
**kwargs
):
self.func = func
self.coordinate_system = coordinate_system
self.value_to_rgb = get_rgb_gradient_function(
*self.magnitude_range, self.color_map,
)
self.stroke_width = stroke_width
self.tip_width_ratio = tip_width_ratio
self.tip_len_to_width = tip_len_to_width
self.norm_to_opacity_func = norm_to_opacity_func
samples = get_sample_points_from_coordinate_system(
coordinate_system, self.step_multiple
# Search for sample_points
self.sample_coords = get_sample_coords(coordinate_system, density)
self.update_sample_points()
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.max_displayed_vect_len = max_vect_len * coordinate_system.get_x_unit_size()
# 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_opacity=stroke_opacity,
flat_stroke=flat_stroke,
**kwargs
)
self.add(*(
self.get_vector(coords)
for coords in samples
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,
width: float,
height: float,
depth: float,
x_density: float,
y_density: float,
z_density: float
) -> np.ndarray:
to_corner = np.array([width / 2, height / 2, depth / 2])
spacings = 1.0 / np.array([x_density, y_density, z_density])
to_corner = spacings * (to_corner / spacings).astype(int)
lower_corner = center - to_corner
upper_corner = center + to_corner + spacings
return cartesian_product(*(
np.arange(low, high, space)
for low, high, space in zip(lower_corner, upper_corner, spacings)
))
def get_vector(self, coords, **kwargs):
vector_config = merge_dicts_recursively(
self.vector_config,
kwargs
)
def init_base_stroke_width_array(self, n_sample_points):
arr = np.ones(8 * n_sample_points - 1)
arr[4::8] = self.tip_width_ratio
arr[5::8] = self.tip_width_ratio * 0.5
arr[6::8] = 0
arr[7::8] = 0
self.base_stroke_width_array = arr
output = np.array(self.func(*coords))
norm = get_norm(output)
if norm > 0:
output *= self.length_func(norm) / norm
def set_sample_coords(self, sample_coords: VectArray):
self.sample_coords = sample_coords
return self
origin = self.coordinate_system.get_origin()
_input = self.coordinate_system.c2p(*coords)
_output = self.coordinate_system.c2p(*output)
def set_stroke(self, color=None, width=None, opacity=None, behind=None, flat=None, recurse=True):
super().set_stroke(color, None, opacity, behind, flat, recurse)
if width is not None:
self.set_stroke_width(float(width))
return self
vect = Arrow(
origin, _output, buff=0,
**vector_config
)
vect.shift(_input - origin)
vect.set_rgba_array([[*self.value_to_rgb(norm), self.opacity]])
return vect
def set_stroke_width(self, width: float):
if self.get_num_points() > 0:
self.get_stroke_widths()[:] = width * self.base_stroke_width_array
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
# Outputs in the coordinate system
outputs = self.func(self.sample_coords)
output_norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis]
# 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(out_vect_norms / max_len)
else:
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) # Mixing units!
# Set all points
points = self.get_points()
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] = 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]
# Adjust stroke widths
width_arr = self.stroke_width * self.base_stroke_width_array
width_scalars = np.clip(drawn_norms / tip_len, 0, 1)
width_scalars = np.repeat(width_scalars, 8)[:-1]
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(output_norms, 8)[:-1]
)
self.note_changed_data()
return self
class TimeVaryingVectorField(VectorField):
def __init__(
self,
# Takes in an array of points and a float for time
time_func: Callable[[VectArray, float], VectArray],
coordinate_system: CoordinateSystem,
**kwargs
):
self.time = 0
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))
self.always.update_vectors()
def increment_time(self, dt):
self.time += dt
class StreamLines(VGroup):
CONFIG = {
"step_multiple": 0.5,
"n_repeats": 1,
"noise_factor": None,
def __init__(
self,
func: Callable[[VectArray], VectArray],
coordinate_system: CoordinateSystem,
density: float = 1.0,
n_repeats: int = 1,
noise_factor: float | None = None,
# Config for drawing lines
"dt": 0.05,
"arc_len": 3,
"max_time_steps": 200,
"n_samples_per_line": 10,
"cutoff_norm": 15,
solution_time: float = 3,
dt: float = 0.05,
arc_len: float = 3,
max_time_steps: int = 200,
n_samples_per_line: int = 10,
cutoff_norm: float = 15,
# Style info
"stroke_width": 1,
"stroke_color": WHITE,
"stroke_opacity": 1,
"color_by_magnitude": True,
"magnitude_range": (0, 2.0),
"taper_stroke_width": False,
"color_map": "3b1b_colormap",
}
def __init__(self, func, coordinate_system, **kwargs):
stroke_width: float = 1.0,
stroke_color: ManimColor = WHITE,
stroke_opacity: float = 1,
color_by_magnitude: bool = True,
magnitude_range: Tuple[float, float] = (0, 2.0),
taper_stroke_width: bool = False,
color_map: str = "3b1b_colormap",
**kwargs
):
super().__init__(**kwargs)
self.func = func
self.coordinate_system = coordinate_system
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
self.n_samples_per_line = n_samples_per_line
self.cutoff_norm = cutoff_norm
self.stroke_width = stroke_width
self.stroke_color = stroke_color
self.stroke_opacity = stroke_opacity
self.color_by_magnitude = color_by_magnitude
self.magnitude_range = magnitude_range
self.taper_stroke_width = taper_stroke_width
self.color_map = color_map
self.draw_lines()
self.init_style()
def point_func(self, point):
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):
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_approximately_smooth()
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):
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.get_x_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
])
def init_style(self):
def init_style(self) -> None:
if self.color_by_magnitude:
values_to_rgbs = get_vectorized_rgb_gradient_function(
*self.magnitude_range, self.color_map,
@@ -237,59 +441,35 @@ class StreamLines(VGroup):
class AnimatedStreamLines(VGroup):
CONFIG = {
"lag_range": 4,
"line_anim_class": VShowPassingFlash,
"line_anim_config": {
# "run_time": 4,
"rate_func": linear,
"time_width": 0.5,
},
}
def __init__(self, stream_lines, **kwargs):
def __init__(
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,
),
**kwargs
):
super().__init__(**kwargs)
self.stream_lines = stream_lines
for line in stream_lines:
line.anim = self.line_anim_class(
line.anim = VShowPassingFlash(
line,
run_time=line.virtual_time,
**self.line_anim_config,
run_time=line.virtual_time / rate_multiple,
**line_anim_config,
)
line.anim.begin()
line.time = -self.lag_range * random.random()
line.time = -lag_range * np.random.random()
self.add(line.anim.mobject)
self.add_updater(lambda m, dt: m.update(dt))
def update(self, dt):
def update(self, dt: float) -> None:
stream_lines = self.stream_lines
for line in stream_lines:
line.time += dt
adjusted_time = max(line.time, 0) % line.anim.run_time
line.anim.update(adjusted_time / line.anim.run_time)
# TODO: This class should be deleted
class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
CONFIG = {
"n_segments": 10,
"time_width": 0.1,
"remover": True
}
def __init__(self, vmobject, **kwargs):
digest_config(self, kwargs)
max_stroke_width = vmobject.get_stroke_width()
max_time_width = kwargs.pop("time_width", self.time_width)
AnimationGroup.__init__(self, *[
VShowPassingFlash(
vmobject.deepcopy().set_stroke(width=stroke_width),
time_width=time_width,
**kwargs
)
for stroke_width, time_width in zip(
np.linspace(0, max_stroke_width, self.n_segments),
np.linspace(max_time_width, 0, self.n_segments)
)
])

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

@@ -1 +0,0 @@
This folder contains a collection of various things that were built for a video at some point, but were really one-off and should be given more careful consideration before being brought into the main library. In particular, there is really no guarantee of these being fully functional.

View File

@@ -1,101 +0,0 @@
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.constants import *
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.scene.scene import Scene
class RearrangeEquation(Scene):
def construct(
self,
start_terms,
end_terms,
index_map,
path_arc=np.pi,
start_transform=None,
end_transform=None,
leave_start_terms=False,
transform_kwargs={},
):
transform_kwargs["path_func"] = path
start_mobs, end_mobs = self.get_mobs_from_terms(
start_terms, end_terms
)
if start_transform:
start_mobs = start_transform(Mobject(*start_mobs)).split()
if end_transform:
end_mobs = end_transform(Mobject(*end_mobs)).split()
unmatched_start_indices = set(range(len(start_mobs)))
unmatched_end_indices = set(range(len(end_mobs)))
unmatched_start_indices.difference_update(
[n % len(start_mobs) for n in index_map]
)
unmatched_end_indices.difference_update(
[n % len(end_mobs) for n in list(index_map.values())]
)
mobject_pairs = [
(start_mobs[a], end_mobs[b])
for a, b in index_map.items()
] + [
(Point(end_mobs[b].get_center()), end_mobs[b])
for b in unmatched_end_indices
]
if not leave_start_terms:
mobject_pairs += [
(start_mobs[a], Point(start_mobs[a].get_center()))
for a in unmatched_start_indices
]
self.add(*start_mobs)
if leave_start_terms:
self.add(Mobject(*start_mobs))
self.wait()
self.play(*[
Transform(*pair, **transform_kwargs)
for pair in mobject_pairs
])
self.wait()
def get_mobs_from_terms(self, start_terms, end_terms):
"""
Need to ensure that all image mobjects for a tex expression
stemming from the same string are point-for-point copies of one
and other. This makes transitions much smoother, and not look
like point-clouds.
"""
num_start_terms = len(start_terms)
all_mobs = np.array(
Tex(start_terms).split() + Tex(end_terms).split())
all_terms = np.array(start_terms + end_terms)
for term in set(all_terms):
matches = all_terms == term
if sum(matches) > 1:
base_mob = all_mobs[list(all_terms).index(term)]
all_mobs[matches] = [
base_mob.copy().replace(target_mob)
for target_mob in all_mobs[matches]
]
return all_mobs[:num_start_terms], all_mobs[num_start_terms:]
class FlipThroughSymbols(Animation):
CONFIG = {
"start_center": ORIGIN,
"end_center": ORIGIN,
}
def __init__(self, tex_list, **kwargs):
mobject = Tex(self.curr_tex).shift(start_center)
Animation.__init__(self, mobject, **kwargs)
def interpolate_mobject(self, alpha):
new_tex = self.tex_list[np.ceil(alpha * len(self.tex_list)) - 1]
if new_tex != self.curr_tex:
self.curr_tex = new_tex
self.mobject = Tex(new_tex).shift(self.start_center)
if not all(self.start_center == self.end_center):
self.mobject.center().shift(
(1 - alpha) * self.start_center + alpha * self.end_center
)

View File

@@ -1,188 +0,0 @@
from manimlib.constants import *
from manimlib.mobject.numbers import Integer
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.types.vectorized_mobject import VMobject, VGroup
from manimlib.scene.scene import Scene
from manimlib.utils.simple_functions import choose
DEFAULT_COUNT_NUM_OFFSET = (FRAME_X_RADIUS - 1, FRAME_Y_RADIUS - 1, 0)
DEFAULT_COUNT_RUN_TIME = 5.0
class CountingScene(Scene):
def count(self, items, item_type="mobject", *args, **kwargs):
if item_type == "mobject":
self.count_mobjects(items, *args, **kwargs)
elif item_type == "region":
self.count_regions(items, *args, **kwargs)
else:
raise Exception("Unknown item_type, should be mobject or region")
return self
def count_mobjects(
self, mobjects, mode="highlight",
color="red",
display_numbers=True,
num_offset=DEFAULT_COUNT_NUM_OFFSET,
run_time=DEFAULT_COUNT_RUN_TIME,
):
"""
Note, leaves final number mobject as "number" attribute
mode can be "highlight", "show_creation" or "show", otherwise
a warning is given and nothing is animating during the count
"""
if len(mobjects) > 50: # TODO
raise Exception("I don't know if you should be counting \
too many mobjects...")
if len(mobjects) == 0:
raise Exception("Counting mobject list of length 0")
if mode not in ["highlight", "show_creation", "show"]:
raise Warning("Unknown mode")
frame_time = run_time / len(mobjects)
if mode == "highlight":
self.add(*mobjects)
for mob, num in zip(mobjects, it.count(1)):
if display_numbers:
num_mob = Tex(str(num))
num_mob.center().shift(num_offset)
self.add(num_mob)
if mode == "highlight":
original_color = mob.color
mob.set_color(color)
self.wait(frame_time)
mob.set_color(original_color)
if mode == "show_creation":
self.play(ShowCreation(mob, run_time=frame_time))
if mode == "show":
self.add(mob)
self.wait(frame_time)
if display_numbers:
self.remove(num_mob)
if display_numbers:
self.add(num_mob)
self.number = num_mob
return self
def count_regions(self, regions,
mode="one_at_a_time",
num_offset=DEFAULT_COUNT_NUM_OFFSET,
run_time=DEFAULT_COUNT_RUN_TIME,
**unused_kwargsn):
if mode not in ["one_at_a_time", "show_all"]:
raise Warning("Unknown mode")
frame_time = run_time / (len(regions))
for region, count in zip(regions, it.count(1)):
num_mob = Tex(str(count))
num_mob.center().shift(num_offset)
self.add(num_mob)
self.set_color_region(region)
self.wait(frame_time)
if mode == "one_at_a_time":
self.reset_background()
self.remove(num_mob)
self.add(num_mob)
self.number = num_mob
return self
def combinationMobject(n, k):
return Integer(choose(n, k))
class GeneralizedPascalsTriangle(VMobject):
CONFIG = {
"nrows": 7,
"height": FRAME_HEIGHT - 1,
"width": 1.5 * FRAME_X_RADIUS,
"portion_to_fill": 0.7,
"submob_class": combinationMobject,
}
def init_points(self):
self.cell_height = float(self.height) / self.nrows
self.cell_width = float(self.width) / self.nrows
self.bottom_left = (self.cell_width * self.nrows / 2.0) * LEFT + \
(self.cell_height * self.nrows / 2.0) * DOWN
self.coords_to_mobs = {}
self.coords = [
(n, k)
for n in range(self.nrows)
for k in range(n + 1)
]
for n, k in self.coords:
center = self.coords_to_center(n, k)
num_mob = self.submob_class(n, k) # Tex(str(num))
scale_factor = min(
1,
self.portion_to_fill * self.cell_height / num_mob.get_height(),
self.portion_to_fill * self.cell_width / num_mob.get_width(),
)
num_mob.center().scale(scale_factor).shift(center)
if n not in self.coords_to_mobs:
self.coords_to_mobs[n] = {}
self.coords_to_mobs[n][k] = num_mob
self.add(*[
self.coords_to_mobs[n][k]
for n, k in self.coords
])
return self
def coords_to_center(self, n, k):
x_offset = self.cell_width * (k + self.nrows / 2.0 - n / 2.0)
y_offset = self.cell_height * (self.nrows - n)
return self.bottom_left + x_offset * RIGHT + y_offset * UP
def generate_n_choose_k_mobs(self):
self.coords_to_n_choose_k = {}
for n, k in self.coords:
nck_mob = Tex(r"{%d \choose %d}" % (n, k))
scale_factor = min(
1,
self.portion_to_fill * self.cell_height / nck_mob.get_height(),
self.portion_to_fill * self.cell_width / nck_mob.get_width(),
)
center = self.coords_to_mobs[n][k].get_center()
nck_mob.center().scale(scale_factor).shift(center)
if n not in self.coords_to_n_choose_k:
self.coords_to_n_choose_k[n] = {}
self.coords_to_n_choose_k[n][k] = nck_mob
return self
def fill_with_n_choose_k(self):
if not hasattr(self, "coords_to_n_choose_k"):
self.generate_n_choose_k_mobs()
self.set_submobjects([])
self.add(*[
self.coords_to_n_choose_k[n][k]
for n, k in self.coords
])
return self
def generate_sea_of_zeros(self):
zero = Tex("0")
self.sea_of_zeros = []
for n in range(self.nrows):
for a in range((self.nrows - n) / 2 + 1):
for k in (n + a + 1, -a - 1):
self.coords.append((n, k))
mob = zero.copy()
mob.shift(self.coords_to_center(n, k))
self.coords_to_mobs[n][k] = mob
self.add(mob)
return self
def get_lowest_row(self):
n = self.nrows - 1
lowest_row = VGroup(*[
self.coords_to_mobs[n][k]
for k in range(n + 1)
])
return lowest_row
class PascalsTriangle(GeneralizedPascalsTriangle):
CONFIG = {
"submob_class": combinationMobject,
}

View File

@@ -1,156 +0,0 @@
from manimlib.animation.animation import Animation
from manimlib.animation.movement import ComplexHomotopy
from manimlib.animation.transform import MoveToTarget
from manimlib.constants import *
from manimlib.mobject.coordinate_systems import ComplexPlane
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.scene.scene import Scene
# TODO, refactor this full scene
class ComplexTransformationScene(Scene):
CONFIG = {
"plane_config": {},
"background_fade_factor": 0.5,
"use_multicolored_plane": False,
"vert_start_color": BLUE, # TODO
"vert_end_color": BLUE,
"horiz_start_color": BLUE,
"horiz_end_color": BLUE,
"num_anchors_to_add_per_line": 50,
"post_transformation_stroke_width": None,
"default_apply_complex_function_kwargs": {
"run_time": 5,
},
"background_label_scale_val": 0.5,
"include_coordinate_labels": True,
}
def setup(self):
self.foreground_mobjects = []
self.transformable_mobjects = []
self.add_background_plane()
if self.include_coordinate_labels:
self.add_coordinate_labels()
def add_foreground_mobject(self, mobject):
self.add_foreground_mobjects(mobject)
def add_transformable_mobjects(self, *mobjects):
self.transformable_mobjects += list(mobjects)
self.add(*mobjects)
def add_foreground_mobjects(self, *mobjects):
self.foreground_mobjects += list(mobjects)
Scene.add(self, *mobjects)
def add(self, *mobjects):
Scene.add(self, *list(mobjects) + self.foreground_mobjects)
def play(self, *animations, **kwargs):
Scene.play(
self,
*list(animations) + list(map(Animation, self.foreground_mobjects)),
**kwargs
)
def add_background_plane(self):
background = ComplexPlane(**self.plane_config)
background.fade(self.background_fade_factor)
self.add(background)
self.background = background
def add_coordinate_labels(self):
self.background.add_coordinates()
self.add(self.background)
def add_transformable_plane(self, **kwargs):
self.plane = self.get_transformable_plane()
self.add(self.plane)
def get_transformable_plane(self, x_range=None, y_range=None):
"""
x_range and y_range would be tuples (min, max)
"""
plane_config = dict(self.plane_config)
shift_val = ORIGIN
if x_range is not None:
x_min, x_max = x_range
plane_config["x_radius"] = x_max - x_min
shift_val += (x_max + x_min) * RIGHT / 2.
if y_range is not None:
y_min, y_max = y_range
plane_config["y_radius"] = y_max - y_min
shift_val += (y_max + y_min) * UP / 2.
plane = ComplexPlane(**plane_config)
plane.shift(shift_val)
if self.use_multicolored_plane:
self.paint_plane(plane)
return plane
def prepare_for_transformation(self, mob):
if hasattr(mob, "prepare_for_nonlinear_transform"):
mob.prepare_for_nonlinear_transform(
self.num_anchors_to_add_per_line
)
# TODO...
def paint_plane(self, plane):
for lines in planes, plane.secondary_lines:
lines.set_color_by_gradient(
self.vert_start_color,
self.vert_end_color,
self.horiz_start_color,
self.horiz_end_color,
)
# plane.axes.set_color_by_gradient(
# self.horiz_start_color,
# self.vert_start_color
# )
def z_to_point(self, z):
return self.background.number_to_point(z)
def get_transformer(self, **kwargs):
transform_kwargs = dict(self.default_apply_complex_function_kwargs)
transform_kwargs.update(kwargs)
transformer = VGroup()
if hasattr(self, "plane"):
self.prepare_for_transformation(self.plane)
transformer.add(self.plane)
transformer.add(*self.transformable_mobjects)
return transformer, transform_kwargs
def apply_complex_function(self, func, added_anims=[], **kwargs):
transformer, transform_kwargs = self.get_transformer(**kwargs)
transformer.generate_target()
# Rescale, apply function, scale back
transformer.target.shift(-self.background.get_center_point())
transformer.target.scale(1. / self.background.unit_size)
transformer.target.apply_complex_function(func)
transformer.target.scale(self.background.unit_size)
transformer.target.shift(self.background.get_center_point())
#
for mob in transformer.target[0].family_members_with_points():
mob.make_smooth()
if self.post_transformation_stroke_width is not None:
transformer.target.set_stroke(
width=self.post_transformation_stroke_width)
self.play(
MoveToTarget(transformer, **transform_kwargs),
*added_anims
)
def apply_complex_homotopy(self, complex_homotopy, added_anims=[], **kwargs):
transformer, transform_kwargs = self.get_transformer(**kwargs)
# def homotopy(x, y, z, t):
# output = complex_homotopy(complex(x, y), t)
# rescaled_output = self.z_to_point(output)
# return (rescaled_output.real, rescaled_output.imag, z)
self.play(
ComplexHomotopy(complex_homotopy, transformer, **transform_kwargs),
*added_anims
)

View File

@@ -1,263 +0,0 @@
from manimlib.animation.creation import ShowCreation
from manimlib.animation.fading import FadeIn
from manimlib.animation.transform import MoveToTarget
from manimlib.animation.transform import Transform
from manimlib.constants import *
from manimlib.mobject.geometry import Arrow
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Dot
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.scene.scene import Scene
import itertools as it
class CountingScene(Scene):
CONFIG = {
"digit_place_colors": [YELLOW, MAROON_B, RED, GREEN, BLUE, PURPLE_D],
"counting_dot_starting_position": (FRAME_X_RADIUS - 1) * RIGHT + (FRAME_Y_RADIUS - 1) * UP,
"count_dot_starting_radius": 0.5,
"dot_configuration_height": 2,
"ones_configuration_location": UP + 2 * RIGHT,
"num_scale_factor": 2,
"num_start_location": 2 * DOWN,
}
def setup(self):
self.dots = VGroup()
self.number = 0
self.max_place = 0
self.number_mob = VGroup(Tex(str(self.number)))
self.number_mob.scale(self.num_scale_factor)
self.number_mob.shift(self.num_start_location)
self.dot_templates = []
self.dot_template_iterators = []
self.curr_configurations = []
self.arrows = VGroup()
self.add(self.number_mob)
def get_template_configuration(self, place):
# This should probably be replaced for non-base-10 counting scenes
down_right = (0.5) * RIGHT + (np.sqrt(3) / 2) * DOWN
result = []
for down_right_steps in range(5):
for left_steps in range(down_right_steps):
result.append(
down_right_steps * down_right + left_steps * LEFT
)
return reversed(result[:self.get_place_max(place)])
def get_dot_template(self, place):
# This should be replaced for non-base-10 counting scenes
dots = VGroup(*[
Dot(
point,
radius=0.25,
fill_opacity=0,
stroke_width=2,
stroke_color=WHITE,
)
for point in self.get_template_configuration(place)
])
dots.set_height(self.dot_configuration_height)
return dots
def add_configuration(self):
new_template = self.get_dot_template(len(self.dot_templates))
new_template.move_to(self.ones_configuration_location)
left_vect = (new_template.get_width() + LARGE_BUFF) * LEFT
new_template.shift(
left_vect * len(self.dot_templates)
)
self.dot_templates.append(new_template)
self.dot_template_iterators.append(
it.cycle(new_template)
)
self.curr_configurations.append(VGroup())
def count(self, max_val, run_time_per_anim=1):
for x in range(max_val):
self.increment(run_time_per_anim)
def increment(self, run_time_per_anim=1):
moving_dot = Dot(
self.counting_dot_starting_position,
radius=self.count_dot_starting_radius,
color=self.digit_place_colors[0],
)
moving_dot.generate_target()
moving_dot.set_fill(opacity=0)
kwargs = {
"run_time": run_time_per_anim
}
continue_rolling_over = True
first_move = True
place = 0
while continue_rolling_over:
added_anims = []
if first_move:
added_anims += self.get_digit_increment_animations()
first_move = False
moving_dot.target.replace(
next(self.dot_template_iterators[place])
)
self.play(MoveToTarget(moving_dot), *added_anims, **kwargs)
self.curr_configurations[place].add(moving_dot)
if len(self.curr_configurations[place].split()) == self.get_place_max(place):
full_configuration = self.curr_configurations[place]
self.curr_configurations[place] = VGroup()
place += 1
center = full_configuration.get_center_of_mass()
radius = 0.6 * max(
full_configuration.get_width(),
full_configuration.get_height(),
)
circle = Circle(
radius=radius,
stroke_width=0,
fill_color=self.digit_place_colors[place],
fill_opacity=0.5,
)
circle.move_to(center)
moving_dot = VGroup(circle, full_configuration)
moving_dot.generate_target()
moving_dot[0].set_fill(opacity=0)
else:
continue_rolling_over = False
def get_digit_increment_animations(self):
result = []
self.number += 1
is_next_digit = self.is_next_digit()
if is_next_digit:
self.max_place += 1
new_number_mob = self.get_number_mob(self.number)
new_number_mob.move_to(self.number_mob, RIGHT)
if is_next_digit:
self.add_configuration()
place = len(new_number_mob.split()) - 1
result.append(FadeIn(self.dot_templates[place]))
arrow = Arrow(
new_number_mob[place].get_top(),
self.dot_templates[place].get_bottom(),
color=self.digit_place_colors[place]
)
self.arrows.add(arrow)
result.append(ShowCreation(arrow))
result.append(Transform(
self.number_mob, new_number_mob,
lag_ratio=0.5
))
return result
def get_number_mob(self, num):
result = VGroup()
place = 0
max_place = self.max_place
while place < max_place:
digit = Tex(str(self.get_place_num(num, place)))
if place >= len(self.digit_place_colors):
self.digit_place_colors += self.digit_place_colors
digit.set_color(self.digit_place_colors[place])
digit.scale(self.num_scale_factor)
digit.next_to(result, LEFT, buff=SMALL_BUFF, aligned_edge=DOWN)
result.add(digit)
place += 1
return result
def is_next_digit(self):
return False
def get_place_num(self, num, place):
return 0
def get_place_max(self, place):
return 0
class PowerCounter(CountingScene):
def is_next_digit(self):
number = self.number
while number > 1:
if number % self.base != 0:
return False
number /= self.base
return True
def get_place_max(self, place):
return self.base
def get_place_num(self, num, place):
return (num / (self.base ** place)) % self.base
class CountInDecimal(PowerCounter):
CONFIG = {
"base": 10,
}
def construct(self):
for x in range(11):
self.increment()
for x in range(85):
self.increment(0.25)
for x in range(20):
self.increment()
class CountInTernary(PowerCounter):
CONFIG = {
"base": 3,
"dot_configuration_height": 1,
"ones_configuration_location": UP + 4 * RIGHT
}
def construct(self):
self.count(27)
# def get_template_configuration(self, place):
# return [ORIGIN, UP]
class CountInBinaryTo256(PowerCounter):
CONFIG = {
"base": 2,
"dot_configuration_height": 1,
"ones_configuration_location": UP + 5 * RIGHT
}
def construct(self):
self.count(128, 0.3)
def get_template_configuration(self, place):
return [ORIGIN, UP]
class FactorialBase(CountingScene):
CONFIG = {
"dot_configuration_height": 1,
"ones_configuration_location": UP + 4 * RIGHT
}
def construct(self):
self.count(30, 0.4)
def is_next_digit(self):
return self.number == self.factorial(self.max_place + 1)
def get_place_max(self, place):
return place + 2
def get_place_num(self, num, place):
return (num / self.factorial(place + 1)) % self.get_place_max(place)
def factorial(self, n):
if (n == 1):
return 1
else:
return n * self.factorial(n - 1)

View File

@@ -1,676 +0,0 @@
from functools import reduce
from manimlib.constants import *
# from manimlib.for_3b1b_videos.pi_creature import PiCreature
# from manimlib.for_3b1b_videos.pi_creature import Randolph
# from manimlib.for_3b1b_videos.pi_creature import get_all_pi_creature_modes
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Polygon
from manimlib.mobject.geometry import RegularPolygon
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.color import color_gradient
from manimlib.utils.config_ops import digest_config
from manimlib.utils.space_ops import center_of_mass
from manimlib.utils.space_ops import compass_directions
from manimlib.utils.space_ops import rotate_vector
from manimlib.utils.space_ops import rotation_matrix
def rotate(points, angle=np.pi, axis=OUT):
if axis is None:
return points
matrix = rotation_matrix(angle, axis)
points = np.dot(points, np.transpose(matrix))
return points
def fractalify(vmobject, order=3, *args, **kwargs):
for x in range(order):
fractalification_iteration(vmobject)
return vmobject
def fractalification_iteration(vmobject, dimension=1.05, num_inserted_anchors_range=list(range(1, 4))):
num_points = vmobject.get_num_points()
if num_points > 0:
# original_anchors = vmobject.get_anchors()
original_anchors = [
vmobject.point_from_proportion(x)
for x in np.linspace(0, 1 - 1. / num_points, num_points)
]
new_anchors = []
for p1, p2, in zip(original_anchors, original_anchors[1:]):
num_inserts = random.choice(num_inserted_anchors_range)
inserted_points = [
interpolate(p1, p2, alpha)
for alpha in np.linspace(0, 1, num_inserts + 2)[1:-1]
]
mass_scaling_factor = 1. / (num_inserts + 1)
length_scaling_factor = mass_scaling_factor**(1. / dimension)
target_length = get_norm(p1 - p2) * length_scaling_factor
curr_length = get_norm(p1 - p2) * mass_scaling_factor
# offset^2 + curr_length^2 = target_length^2
offset_len = np.sqrt(target_length**2 - curr_length**2)
unit_vect = (p1 - p2) / get_norm(p1 - p2)
offset_unit_vect = rotate_vector(unit_vect, np.pi / 2)
inserted_points = [
point + u * offset_len * offset_unit_vect
for u, point in zip(it.cycle([-1, 1]), inserted_points)
]
new_anchors += [p1] + inserted_points
new_anchors.append(original_anchors[-1])
vmobject.set_points_as_corners(new_anchors)
vmobject.set_submobjects([
fractalification_iteration(
submob, dimension, num_inserted_anchors_range)
for submob in vmobject.submobjects
])
return vmobject
class SelfSimilarFractal(VMobject):
CONFIG = {
"order": 5,
"num_subparts": 3,
"height": 4,
"colors": [RED, WHITE],
"stroke_width": 1,
"fill_opacity": 1,
}
def init_colors(self):
VMobject.init_colors(self)
self.set_color_by_gradient(*self.colors)
def init_points(self):
order_n_self = self.get_order_n_self(self.order)
if self.order == 0:
self.set_submobjects([order_n_self])
else:
self.set_submobjects(order_n_self.submobjects)
return self
def get_order_n_self(self, order):
if order == 0:
result = self.get_seed_shape()
else:
lower_order = self.get_order_n_self(order - 1)
subparts = [
lower_order.copy()
for x in range(self.num_subparts)
]
self.arrange_subparts(*subparts)
result = VGroup(*subparts)
result.set_height(self.height)
result.center()
return result
def get_seed_shape(self):
raise Exception("Not implemented")
def arrange_subparts(self, *subparts):
raise Exception("Not implemented")
class Sierpinski(SelfSimilarFractal):
def get_seed_shape(self):
return Polygon(
RIGHT, np.sqrt(3) * UP, LEFT,
)
def arrange_subparts(self, *subparts):
tri1, tri2, tri3 = subparts
tri1.move_to(tri2.get_corner(DOWN + LEFT), UP)
tri3.move_to(tri2.get_corner(DOWN + RIGHT), UP)
class DiamondFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts": 4,
"height": 4,
"colors": [GREEN_E, YELLOW],
}
def get_seed_shape(self):
return RegularPolygon(n=4)
def arrange_subparts(self, *subparts):
# VGroup(*subparts).rotate(np.pi/4)
for part, vect in zip(subparts, compass_directions(start_vect=UP + RIGHT)):
part.next_to(ORIGIN, vect, buff=0)
VGroup(*subparts).rotate(np.pi / 4, about_point=ORIGIN)
class PentagonalFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts": 5,
"colors": [MAROON_B, YELLOW, RED],
"height": 6,
}
def get_seed_shape(self):
return RegularPolygon(n=5, start_angle=np.pi / 2)
def arrange_subparts(self, *subparts):
for x, part in enumerate(subparts):
part.shift(0.95 * part.get_height() * UP)
part.rotate(2 * np.pi * x / 5, about_point=ORIGIN)
class PentagonalPiCreatureFractal(PentagonalFractal):
def init_colors(self):
SelfSimilarFractal.init_colors(self)
internal_pis = [
pi
for pi in self.get_family()
if isinstance(pi, PiCreature)
]
colors = color_gradient(self.colors, len(internal_pis))
for pi, color in zip(internal_pis, colors):
pi.init_colors()
pi.body.set_stroke(color, width=0.5)
pi.set_color(color)
def get_seed_shape(self):
return Randolph(mode="shruggie")
def arrange_subparts(self, *subparts):
for part in subparts:
part.rotate(2 * np.pi / 5, about_point=ORIGIN)
PentagonalFractal.arrange_subparts(self, *subparts)
class PiCreatureFractal(VMobject):
CONFIG = {
"order": 7,
"scale_val": 2.5,
"start_mode": "hooray",
"height": 6,
"colors": [
BLUE_D, BLUE_B, MAROON_B, MAROON_D, GREY,
YELLOW, RED, GREY_BROWN, RED, RED_E,
],
"random_seed": 0,
"stroke_width": 0,
}
def init_colors(self):
VMobject.init_colors(self)
internal_pis = [
pi
for pi in self.get_family()
if isinstance(pi, PiCreature)
]
random.seed(self.random_seed)
for pi in reversed(internal_pis):
color = random.choice(self.colors)
pi.set_color(color)
pi.set_stroke(color, width=0)
def init_points(self):
random.seed(self.random_seed)
modes = get_all_pi_creature_modes()
seed = PiCreature(mode=self.start_mode)
seed.set_height(self.height)
seed.to_edge(DOWN)
creatures = [seed]
self.add(VGroup(seed))
for x in range(self.order):
new_creatures = []
for creature in creatures:
for eye, vect in zip(creature.eyes, [LEFT, RIGHT]):
new_creature = PiCreature(
mode=random.choice(modes)
)
new_creature.set_height(
self.scale_val * eye.get_height()
)
new_creature.next_to(
eye, vect,
buff=0,
aligned_edge=DOWN
)
new_creatures.append(new_creature)
creature.look_at(random.choice(new_creatures))
self.add_to_back(VGroup(*new_creatures))
creatures = new_creatures
# def init_colors(self):
# VMobject.init_colors(self)
# self.set_color_by_gradient(*self.colors)
class WonkyHexagonFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts": 7
}
def get_seed_shape(self):
return RegularPolygon(n=6)
def arrange_subparts(self, *subparts):
for i, piece in enumerate(subparts):
piece.rotate(i * np.pi / 12, about_point=ORIGIN)
p1, p2, p3, p4, p5, p6, p7 = subparts
center_row = VGroup(p1, p4, p7)
center_row.arrange(RIGHT, buff=0)
for p in p2, p3, p5, p6:
p.set_width(p1.get_width())
p2.move_to(p1.get_top(), DOWN + LEFT)
p3.move_to(p1.get_bottom(), UP + LEFT)
p5.move_to(p4.get_top(), DOWN + LEFT)
p6.move_to(p4.get_bottom(), UP + LEFT)
class CircularFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts": 3,
"colors": [GREEN, BLUE, GREY]
}
def get_seed_shape(self):
return Circle()
def arrange_subparts(self, *subparts):
if not hasattr(self, "been_here"):
self.num_subparts = 3 + self.order
self.been_here = True
for i, part in enumerate(subparts):
theta = np.pi / self.num_subparts
part.next_to(
ORIGIN, UP,
buff=self.height / (2 * np.tan(theta))
)
part.rotate(i * 2 * np.pi / self.num_subparts, about_point=ORIGIN)
self.num_subparts -= 1
######## Space filling curves ############
class JaggedCurvePiece(VMobject):
def insert_n_curves(self, n):
if self.get_num_curves() == 0:
self.set_points(np.zeros((1, 3)))
anchors = self.get_anchors()
indices = np.linspace(
0, len(anchors) - 1, n + len(anchors)
).astype('int')
self.set_points_as_corners(anchors[indices])
class FractalCurve(VMobject):
CONFIG = {
"radius": 3,
"order": 5,
"colors": [RED, GREEN],
"num_submobjects": 20,
"monochromatic": False,
"order_to_stroke_width_map": {
3: 3,
4: 2,
5: 1,
},
}
def init_points(self):
points = self.get_anchor_points()
self.set_points_as_corners(points)
if not self.monochromatic:
alphas = np.linspace(0, 1, self.num_submobjects)
for alpha_pair in zip(alphas, alphas[1:]):
submobject = JaggedCurvePiece()
submobject.pointwise_become_partial(
self, *alpha_pair
)
self.add(submobject)
self.set_points(np.zeros((0, 3)))
def init_colors(self):
VMobject.init_colors(self)
self.set_color_by_gradient(*self.colors)
for order in sorted(self.order_to_stroke_width_map.keys()):
if self.order >= order:
self.set_stroke(width=self.order_to_stroke_width_map[order])
def get_anchor_points(self):
raise Exception("Not implemented")
class LindenmayerCurve(FractalCurve):
CONFIG = {
"axiom": "A",
"rule": {},
"scale_factor": 2,
"radius": 3,
"start_step": RIGHT,
"angle": np.pi / 2,
}
def expand_command_string(self, command):
result = ""
for letter in command:
if letter in self.rule:
result += self.rule[letter]
else:
result += letter
return result
def get_command_string(self):
result = self.axiom
for x in range(self.order):
result = self.expand_command_string(result)
return result
def get_anchor_points(self):
step = float(self.radius) * self.start_step
step /= (self.scale_factor**self.order)
curr = np.zeros(3)
result = [curr]
for letter in self.get_command_string():
if letter == "+":
step = rotate(step, self.angle)
elif letter == "-":
step = rotate(step, -self.angle)
else:
curr = curr + step
result.append(curr)
return np.array(result) - center_of_mass(result)
class SelfSimilarSpaceFillingCurve(FractalCurve):
CONFIG = {
"offsets": [],
# keys must awkwardly be in string form...
"offset_to_rotation_axis": {},
"scale_factor": 2,
"radius_scale_factor": 0.5,
}
def transform(self, points, offset):
"""
How to transform the copy of points shifted by
offset. Generally meant to be extended in subclasses
"""
copy = np.array(points)
if str(offset) in self.offset_to_rotation_axis:
copy = rotate(
copy,
axis=self.offset_to_rotation_axis[str(offset)]
)
copy /= self.scale_factor,
copy += offset * self.radius * self.radius_scale_factor
return copy
def refine_into_subparts(self, points):
transformed_copies = [
self.transform(points, offset)
for offset in self.offsets
]
return reduce(
lambda a, b: np.append(a, b, axis=0),
transformed_copies
)
def get_anchor_points(self):
points = np.zeros((1, 3))
for count in range(self.order):
points = self.refine_into_subparts(points)
return points
def generate_grid(self):
raise Exception("Not implemented")
class HilbertCurve(SelfSimilarSpaceFillingCurve):
CONFIG = {
"offsets": [
LEFT + DOWN,
LEFT + UP,
RIGHT + UP,
RIGHT + DOWN,
],
"offset_to_rotation_axis": {
str(LEFT + DOWN): RIGHT + UP,
str(RIGHT + DOWN): RIGHT + DOWN,
},
}
class HilbertCurve3D(SelfSimilarSpaceFillingCurve):
CONFIG = {
"offsets": [
RIGHT + DOWN + IN,
LEFT + DOWN + IN,
LEFT + DOWN + OUT,
RIGHT + DOWN + OUT,
RIGHT + UP + OUT,
LEFT + UP + OUT,
LEFT + UP + IN,
RIGHT + UP + IN,
],
"offset_to_rotation_axis_and_angle": {
str(RIGHT + DOWN + IN): (LEFT + UP + OUT, 2 * np.pi / 3),
str(LEFT + DOWN + IN): (RIGHT + DOWN + IN, 2 * np.pi / 3),
str(LEFT + DOWN + OUT): (RIGHT + DOWN + IN, 2 * np.pi / 3),
str(RIGHT + DOWN + OUT): (UP, np.pi),
str(RIGHT + UP + OUT): (UP, np.pi),
str(LEFT + UP + OUT): (LEFT + DOWN + OUT, 2 * np.pi / 3),
str(LEFT + UP + IN): (LEFT + DOWN + OUT, 2 * np.pi / 3),
str(RIGHT + UP + IN): (RIGHT + UP + IN, 2 * np.pi / 3),
},
}
# Rewrote transform method to include the rotation angle
def transform(self, points, offset):
copy = np.array(points)
copy = rotate(
copy,
axis=self.offset_to_rotation_axis_and_angle[str(offset)][0],
angle=self.offset_to_rotation_axis_and_angle[str(offset)][1],
)
copy /= self.scale_factor,
copy += offset * self.radius * self.radius_scale_factor
return copy
class PeanoCurve(SelfSimilarSpaceFillingCurve):
CONFIG = {
"colors": [PURPLE, TEAL],
"offsets": [
LEFT + DOWN,
LEFT,
LEFT + UP,
UP,
ORIGIN,
DOWN,
RIGHT + DOWN,
RIGHT,
RIGHT + UP,
],
"offset_to_rotation_axis": {
str(LEFT): UP,
str(UP): RIGHT,
str(ORIGIN): LEFT + UP,
str(DOWN): RIGHT,
str(RIGHT): UP,
},
"scale_factor": 3,
"radius_scale_factor": 2.0 / 3,
}
class TriangleFillingCurve(SelfSimilarSpaceFillingCurve):
CONFIG = {
"colors": [MAROON, YELLOW],
"offsets": [
LEFT / 4. + DOWN / 6.,
ORIGIN,
RIGHT / 4. + DOWN / 6.,
UP / 3.,
],
"offset_to_rotation_axis": {
str(ORIGIN): RIGHT,
str(UP / 3.): UP,
},
"scale_factor": 2,
"radius_scale_factor": 1.5,
}
# class HexagonFillingCurve(SelfSimilarSpaceFillingCurve):
# CONFIG = {
# "start_color" : WHITE,
# "end_color" : BLUE_D,
# "axis_offset_pairs" : [
# (None, 1.5*DOWN + 0.5*np.sqrt(3)*LEFT),
# (UP+np.sqrt(3)*RIGHT, 1.5*DOWN + 0.5*np.sqrt(3)*RIGHT),
# (np.sqrt(3)*UP+RIGHT, ORIGIN),
# ((UP, RIGHT), np.sqrt(3)*LEFT),
# (None, 1.5*UP + 0.5*np.sqrt(3)*LEFT),
# (None, 1.5*UP + 0.5*np.sqrt(3)*RIGHT),
# (RIGHT, np.sqrt(3)*RIGHT),
# ],
# "scale_factor" : 3,
# "radius_scale_factor" : 2/(3*np.sqrt(3)),
# }
# def refine_into_subparts(self, points):
# return SelfSimilarSpaceFillingCurve.refine_into_subparts(
# self,
# rotate(points, np.pi/6, IN)
# )
class UtahFillingCurve(SelfSimilarSpaceFillingCurve):
CONFIG = {
"colors": [WHITE, BLUE_D],
"axis_offset_pairs": [
],
"scale_factor": 3,
"radius_scale_factor": 2 / (3 * np.sqrt(3)),
}
class FlowSnake(LindenmayerCurve):
CONFIG = {
"colors": [YELLOW, GREEN],
"axiom": "A",
"rule": {
"A": "A-B--B+A++AA+B-",
"B": "+A-BB--B-A++A+B",
},
"radius": 6, # TODO, this is innaccurate
"scale_factor": np.sqrt(7),
"start_step": RIGHT,
"angle": -np.pi / 3,
}
def __init__(self, **kwargs):
LindenmayerCurve.__init__(self, **kwargs)
self.rotate(-self.order * np.pi / 9, about_point=ORIGIN)
class SierpinskiCurve(LindenmayerCurve):
CONFIG = {
"colors": [RED, WHITE],
"axiom": "B",
"rule": {
"A": "+B-A-B+",
"B": "-A+B+A-",
},
"radius": 6, # TODO, this is innaccurate
"scale_factor": 2,
"start_step": RIGHT,
"angle": -np.pi / 3,
}
class KochSnowFlake(LindenmayerCurve):
CONFIG = {
"colors": [BLUE_D, WHITE, BLUE_D],
"axiom": "A--A--A--",
"rule": {
"A": "A+A--A+A"
},
"radius": 4,
"scale_factor": 3,
"start_step": RIGHT,
"angle": np.pi / 3,
"order_to_stroke_width_map": {
3: 3,
5: 2,
6: 1,
},
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
self.scale_factor = 2 * (1 + np.cos(self.angle))
LindenmayerCurve.__init__(self, **kwargs)
class KochCurve(KochSnowFlake):
CONFIG = {
"axiom": "A--"
}
class QuadraticKoch(LindenmayerCurve):
CONFIG = {
"colors": [YELLOW, WHITE, MAROON_B],
"axiom": "A",
"rule": {
"A": "A+A-A-AA+A+A-A"
},
"radius": 4,
"scale_factor": 4,
"start_step": RIGHT,
"angle": np.pi / 2
}
class QuadraticKochIsland(QuadraticKoch):
CONFIG = {
"axiom": "A+A+A+A"
}
class StellarCurve(LindenmayerCurve):
CONFIG = {
"start_color": RED,
"end_color": BLUE_E,
"rule": {
"A": "+B-A-B+A-B+",
"B": "-A+B+A-B+A-",
},
"scale_factor": 3,
"angle": 2 * np.pi / 5,
}
class SnakeCurve(FractalCurve):
CONFIG = {
"start_color": BLUE,
"end_color": YELLOW,
}
def get_anchor_points(self):
result = []
resolution = 2**self.order
step = 2.0 * self.radius / resolution
lower_left = ORIGIN + \
LEFT * (self.radius - step / 2) + \
DOWN * (self.radius - step / 2)
for y in range(resolution):
x_range = list(range(resolution))
if y % 2 == 0:
x_range.reverse()
for x in x_range:
result.append(
lower_left + x * step * RIGHT + y * step * UP
)
return result

View File

@@ -1,566 +0,0 @@
import itertools as it
from manimlib.animation.creation import Write, DrawBorderThenFill, ShowCreation
from manimlib.animation.transform import Transform
from manimlib.animation.update import UpdateFromAlphaFunc
from manimlib.constants import *
from manimlib.mobject.functions import ParametricCurve
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import RegularPolygon
from manimlib.mobject.number_line import NumberLine
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import TexText
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VectorizedPoint
from manimlib.scene.scene import Scene
from manimlib.utils.bezier import interpolate
from manimlib.utils.color import color_gradient
from manimlib.utils.color import invert_color
from manimlib.utils.space_ops import angle_of_vector
# TODO, this class should be deprecated, with all its
# functionality moved to Axes and handled at the mobject
# level rather than the scene level
class GraphScene(Scene):
CONFIG = {
"x_min": -1,
"x_max": 10,
"x_axis_width": 9,
"x_tick_frequency": 1,
"x_leftmost_tick": None, # Change if different from x_min
"x_labeled_nums": None,
"x_axis_label": "$x$",
"y_min": -1,
"y_max": 10,
"y_axis_height": 6,
"y_tick_frequency": 1,
"y_bottom_tick": None, # Change if different from y_min
"y_labeled_nums": None,
"y_axis_label": "$y$",
"axes_color": GREY,
"graph_origin": 2.5 * DOWN + 4 * LEFT,
"exclude_zero_label": True,
"default_graph_colors": [BLUE, GREEN, YELLOW],
"default_derivative_color": GREEN,
"default_input_color": YELLOW,
"default_riemann_start_color": BLUE,
"default_riemann_end_color": GREEN,
"area_opacity": 0.8,
"num_rects": 50,
}
def setup(self):
self.default_graph_colors_cycle = it.cycle(self.default_graph_colors)
self.left_T_label = VGroup()
self.left_v_line = VGroup()
self.right_T_label = VGroup()
self.right_v_line = VGroup()
def setup_axes(self, animate=False):
# TODO, once eoc is done, refactor this to be less redundant.
x_num_range = float(self.x_max - self.x_min)
self.space_unit_to_x = self.x_axis_width / x_num_range
if self.x_labeled_nums is None:
self.x_labeled_nums = []
if self.x_leftmost_tick is None:
self.x_leftmost_tick = self.x_min
x_axis = NumberLine(
x_min=self.x_min,
x_max=self.x_max,
unit_size=self.space_unit_to_x,
tick_frequency=self.x_tick_frequency,
leftmost_tick=self.x_leftmost_tick,
numbers_with_elongated_ticks=self.x_labeled_nums,
color=self.axes_color
)
x_axis.shift(self.graph_origin - x_axis.number_to_point(0))
if len(self.x_labeled_nums) > 0:
if self.exclude_zero_label:
self.x_labeled_nums = [x for x in self.x_labeled_nums if x != 0]
x_axis.add_numbers(self.x_labeled_nums)
if self.x_axis_label:
x_label = TexText(self.x_axis_label)
x_label.next_to(
x_axis.get_tick_marks(), UP + RIGHT,
buff=SMALL_BUFF
)
x_label.shift_onto_screen()
x_axis.add(x_label)
self.x_axis_label_mob = x_label
y_num_range = float(self.y_max - self.y_min)
self.space_unit_to_y = self.y_axis_height / y_num_range
if self.y_labeled_nums is None:
self.y_labeled_nums = []
if self.y_bottom_tick is None:
self.y_bottom_tick = self.y_min
y_axis = NumberLine(
x_min=self.y_min,
x_max=self.y_max,
unit_size=self.space_unit_to_y,
tick_frequency=self.y_tick_frequency,
leftmost_tick=self.y_bottom_tick,
numbers_with_elongated_ticks=self.y_labeled_nums,
color=self.axes_color,
line_to_number_vect=LEFT,
label_direction=LEFT,
)
y_axis.shift(self.graph_origin - y_axis.number_to_point(0))
y_axis.rotate(np.pi / 2, about_point=y_axis.number_to_point(0))
if len(self.y_labeled_nums) > 0:
if self.exclude_zero_label:
self.y_labeled_nums = [y for y in self.y_labeled_nums if y != 0]
y_axis.add_numbers(self.y_labeled_nums)
if self.y_axis_label:
y_label = TexText(self.y_axis_label)
y_label.next_to(
y_axis.get_corner(UP + RIGHT), UP + RIGHT,
buff=SMALL_BUFF
)
y_label.shift_onto_screen()
y_axis.add(y_label)
self.y_axis_label_mob = y_label
if animate:
self.play(Write(VGroup(x_axis, y_axis)))
else:
self.add(x_axis, y_axis)
self.x_axis, self.y_axis = self.axes = VGroup(x_axis, y_axis)
self.default_graph_colors = it.cycle(self.default_graph_colors)
def coords_to_point(self, x, y):
assert(hasattr(self, "x_axis") and hasattr(self, "y_axis"))
result = self.x_axis.number_to_point(x)[0] * RIGHT
result += self.y_axis.number_to_point(y)[1] * UP
return result
def point_to_coords(self, point):
return (self.x_axis.point_to_number(point),
self.y_axis.point_to_number(point))
def get_graph(
self, func,
color=None,
x_min=None,
x_max=None,
**kwargs
):
if color is None:
color = next(self.default_graph_colors_cycle)
if x_min is None:
x_min = self.x_min
if x_max is None:
x_max = self.x_max
def parameterized_function(alpha):
x = interpolate(x_min, x_max, alpha)
y = func(x)
if not np.isfinite(y):
y = self.y_max
return self.coords_to_point(x, y)
graph = ParametricCurve(
parameterized_function,
color=color,
**kwargs
)
graph.underlying_function = func
return graph
def input_to_graph_point(self, x, graph):
return self.coords_to_point(x, graph.underlying_function(x))
def angle_of_tangent(self, x, graph, dx=0.01):
vect = self.input_to_graph_point(
x + dx, graph) - self.input_to_graph_point(x, graph)
return angle_of_vector(vect)
def slope_of_tangent(self, *args, **kwargs):
return np.tan(self.angle_of_tangent(*args, **kwargs))
def get_derivative_graph(self, graph, dx=0.01, **kwargs):
if "color" not in kwargs:
kwargs["color"] = self.default_derivative_color
def deriv(x):
return self.slope_of_tangent(x, graph, dx) / self.space_unit_to_y
return self.get_graph(deriv, **kwargs)
def get_graph_label(
self,
graph,
label="f(x)",
x_val=None,
direction=RIGHT,
buff=MED_SMALL_BUFF,
color=None,
):
label = Tex(label)
color = color or graph.get_color()
label.set_color(color)
if x_val is None:
# Search from right to left
for x in np.linspace(self.x_max, self.x_min, 100):
point = self.input_to_graph_point(x, graph)
if point[1] < FRAME_Y_RADIUS:
break
x_val = x
label.next_to(
self.input_to_graph_point(x_val, graph),
direction,
buff=buff
)
label.shift_onto_screen()
return label
def get_riemann_rectangles(
self,
graph,
x_min=None,
x_max=None,
dx=0.1,
input_sample_type="left",
stroke_width=1,
stroke_color=BLACK,
fill_opacity=1,
start_color=None,
end_color=None,
show_signed_area=True,
width_scale_factor=1.001
):
x_min = x_min if x_min is not None else self.x_min
x_max = x_max if x_max is not None else self.x_max
if start_color is None:
start_color = self.default_riemann_start_color
if end_color is None:
end_color = self.default_riemann_end_color
rectangles = VGroup()
x_range = np.arange(x_min, x_max, dx)
colors = color_gradient([start_color, end_color], len(x_range))
for x, color in zip(x_range, colors):
if input_sample_type == "left":
sample_input = x
elif input_sample_type == "right":
sample_input = x + dx
elif input_sample_type == "center":
sample_input = x + 0.5 * dx
else:
raise Exception("Invalid input sample type")
graph_point = self.input_to_graph_point(sample_input, graph)
points = VGroup(*list(map(VectorizedPoint, [
self.coords_to_point(x, 0),
self.coords_to_point(x + width_scale_factor * dx, 0),
graph_point
])))
rect = Rectangle()
rect.replace(points, stretch=True)
if graph_point[1] < self.graph_origin[1] and show_signed_area:
fill_color = invert_color(color)
else:
fill_color = color
rect.set_fill(fill_color, opacity=fill_opacity)
rect.set_stroke(stroke_color, width=stroke_width)
rectangles.add(rect)
return rectangles
def get_riemann_rectangles_list(
self,
graph,
n_iterations,
max_dx=0.5,
power_base=2,
stroke_width=1,
**kwargs
):
return [
self.get_riemann_rectangles(
graph=graph,
dx=float(max_dx) / (power_base**n),
stroke_width=float(stroke_width) / (power_base**n),
**kwargs
)
for n in range(n_iterations)
]
def get_area(self, graph, t_min, t_max):
numerator = max(t_max - t_min, 0.0001)
dx = float(numerator) / self.num_rects
return self.get_riemann_rectangles(
graph,
x_min=t_min,
x_max=t_max,
dx=dx,
stroke_width=0,
).set_fill(opacity=self.area_opacity)
def transform_between_riemann_rects(self, curr_rects, new_rects, **kwargs):
transform_kwargs = {
"run_time": 2,
"lag_ratio": 0.5
}
added_anims = kwargs.get("added_anims", [])
transform_kwargs.update(kwargs)
curr_rects.align_family(new_rects)
x_coords = set() # Keep track of new repetitions
for rect in curr_rects:
x = rect.get_center()[0]
if x in x_coords:
rect.set_fill(opacity=0)
else:
x_coords.add(x)
self.play(
Transform(curr_rects, new_rects, **transform_kwargs),
*added_anims
)
def get_vertical_line_to_graph(
self,
x, graph,
line_class=Line,
**line_kwargs
):
if "color" not in line_kwargs:
line_kwargs["color"] = graph.get_color()
return line_class(
self.coords_to_point(x, 0),
self.input_to_graph_point(x, graph),
**line_kwargs
)
def get_vertical_lines_to_graph(
self, graph,
x_min=None,
x_max=None,
num_lines=20,
**kwargs
):
x_min = x_min or self.x_min
x_max = x_max or self.x_max
return VGroup(*[
self.get_vertical_line_to_graph(x, graph, **kwargs)
for x in np.linspace(x_min, x_max, num_lines)
])
def get_secant_slope_group(
self,
x, graph,
dx=None,
dx_line_color=None,
df_line_color=None,
dx_label=None,
df_label=None,
include_secant_line=True,
secant_line_color=None,
secant_line_length=10,
):
"""
Resulting group is of the form VGroup(
dx_line,
df_line,
dx_label, (if applicable)
df_label, (if applicable)
secant_line, (if applicable)
)
with attributes of those names.
"""
kwargs = locals()
kwargs.pop("self")
group = VGroup()
group.kwargs = kwargs
dx = dx or float(self.x_max - self.x_min) / 10
dx_line_color = dx_line_color or self.default_input_color
df_line_color = df_line_color or graph.get_color()
p1 = self.input_to_graph_point(x, graph)
p2 = self.input_to_graph_point(x + dx, graph)
interim_point = p2[0] * RIGHT + p1[1] * UP
group.dx_line = Line(
p1, interim_point,
color=dx_line_color
)
group.df_line = Line(
interim_point, p2,
color=df_line_color
)
group.add(group.dx_line, group.df_line)
labels = VGroup()
if dx_label is not None:
group.dx_label = Tex(dx_label)
labels.add(group.dx_label)
group.add(group.dx_label)
if df_label is not None:
group.df_label = Tex(df_label)
labels.add(group.df_label)
group.add(group.df_label)
if len(labels) > 0:
max_width = 0.8 * group.dx_line.get_width()
max_height = 0.8 * group.df_line.get_height()
if labels.get_width() > max_width:
labels.set_width(max_width)
if labels.get_height() > max_height:
labels.set_height(max_height)
if dx_label is not None:
group.dx_label.next_to(
group.dx_line,
np.sign(dx) * DOWN,
buff=group.dx_label.get_height() / 2
)
group.dx_label.set_color(group.dx_line.get_color())
if df_label is not None:
group.df_label.next_to(
group.df_line,
np.sign(dx) * RIGHT,
buff=group.df_label.get_height() / 2
)
group.df_label.set_color(group.df_line.get_color())
if include_secant_line:
secant_line_color = secant_line_color or self.default_derivative_color
group.secant_line = Line(p1, p2, color=secant_line_color)
group.secant_line.scale(
secant_line_length / group.secant_line.get_length()
)
group.add(group.secant_line)
return group
def add_T_label(self, x_val, side=RIGHT, label=None, color=WHITE, animated=False, **kwargs):
triangle = RegularPolygon(n=3, start_angle=np.pi / 2)
triangle.set_height(MED_SMALL_BUFF)
triangle.move_to(self.coords_to_point(x_val, 0), UP)
triangle.set_fill(color, 1)
triangle.set_stroke(width=0)
if label is None:
T_label = Tex(self.variable_point_label, fill_color=color)
else:
T_label = Tex(label, fill_color=color)
T_label.next_to(triangle, DOWN)
v_line = self.get_vertical_line_to_graph(
x_val, self.v_graph,
color=YELLOW
)
if animated:
self.play(
DrawBorderThenFill(triangle),
ShowCreation(v_line),
Write(T_label, run_time=1),
**kwargs
)
if np.all(side == LEFT):
self.left_T_label_group = VGroup(T_label, triangle)
self.left_v_line = v_line
self.add(self.left_T_label_group, self.left_v_line)
elif np.all(side == RIGHT):
self.right_T_label_group = VGroup(T_label, triangle)
self.right_v_line = v_line
self.add(self.right_T_label_group, self.right_v_line)
def get_animation_integral_bounds_change(
self,
graph,
new_t_min,
new_t_max,
fade_close_to_origin=True,
run_time=1.0
):
curr_t_min = self.x_axis.point_to_number(self.area.get_left())
curr_t_max = self.x_axis.point_to_number(self.area.get_right())
if new_t_min is None:
new_t_min = curr_t_min
if new_t_max is None:
new_t_max = curr_t_max
group = VGroup(self.area)
group.add(self.left_v_line)
group.add(self.left_T_label_group)
group.add(self.right_v_line)
group.add(self.right_T_label_group)
def update_group(group, alpha):
area, left_v_line, left_T_label, right_v_line, right_T_label = group
t_min = interpolate(curr_t_min, new_t_min, alpha)
t_max = interpolate(curr_t_max, new_t_max, alpha)
new_area = self.get_area(graph, t_min, t_max)
new_left_v_line = self.get_vertical_line_to_graph(
t_min, graph
)
new_left_v_line.set_color(left_v_line.get_color())
left_T_label.move_to(new_left_v_line.get_bottom(), UP)
new_right_v_line = self.get_vertical_line_to_graph(
t_max, graph
)
new_right_v_line.set_color(right_v_line.get_color())
right_T_label.move_to(new_right_v_line.get_bottom(), UP)
# Fade close to 0
if fade_close_to_origin:
if len(left_T_label) > 0:
left_T_label[0].set_fill(opacity=min(1, np.abs(t_min)))
if len(right_T_label) > 0:
right_T_label[0].set_fill(opacity=min(1, np.abs(t_max)))
Transform(area, new_area).update(1)
Transform(left_v_line, new_left_v_line).update(1)
Transform(right_v_line, new_right_v_line).update(1)
return group
return UpdateFromAlphaFunc(group, update_group, run_time=run_time)
def animate_secant_slope_group_change(
self, secant_slope_group,
target_dx=None,
target_x=None,
run_time=3,
added_anims=None,
**anim_kwargs
):
if target_dx is None and target_x is None:
raise Exception(
"At least one of target_x and target_dx must not be None")
if added_anims is None:
added_anims = []
start_dx = secant_slope_group.kwargs["dx"]
start_x = secant_slope_group.kwargs["x"]
if target_dx is None:
target_dx = start_dx
if target_x is None:
target_x = start_x
def update_func(group, alpha):
dx = interpolate(start_dx, target_dx, alpha)
x = interpolate(start_x, target_x, alpha)
kwargs = dict(secant_slope_group.kwargs)
kwargs["dx"] = dx
kwargs["x"] = x
new_group = self.get_secant_slope_group(**kwargs)
group.become(new_group)
return group
self.play(
UpdateFromAlphaFunc(
secant_slope_group, update_func,
run_time=run_time,
**anim_kwargs
),
*added_anims
)
secant_slope_group.kwargs["x"] = target_x
secant_slope_group.kwargs["dx"] = target_dx

View File

@@ -1,414 +0,0 @@
from functools import reduce
import itertools as it
import operator as op
import numpy as np
from manimlib.constants import *
from manimlib.scene.scene import Scene
from manimlib.utils.rate_functions import there_and_back
from manimlib.utils.space_ops import center_of_mass
class Graph():
def __init__(self):
# List of points in R^3
# vertices = []
# List of pairs of indices of vertices
# edges = []
# List of tuples of indices of vertices. The last should
# be a cycle whose interior is the entire graph, and when
# regions are computed its complement will be taken.
# region_cycles = []
self.construct()
def construct(self):
pass
def __str__(self):
return self.__class__.__name__
class CubeGraph(Graph):
"""
5 7
12
03
4 6
"""
def construct(self):
self.vertices = [
(x, y, 0)
for r in (1, 2)
for x, y in it.product([-r, r], [-r, r])
]
self.edges = [
(0, 1),
(0, 2),
(3, 1),
(3, 2),
(4, 5),
(4, 6),
(7, 5),
(7, 6),
(0, 4),
(1, 5),
(2, 6),
(3, 7),
]
self.region_cycles = [
[0, 2, 3, 1],
[4, 0, 1, 5],
[4, 6, 2, 0],
[6, 7, 3, 2],
[7, 5, 1, 3],
[4, 6, 7, 5], # By convention, last region will be "outside"
]
class SampleGraph(Graph):
"""
4 2 3 8
0 1
7
5 6
"""
def construct(self):
self.vertices = [
(0, 0, 0),
(2, 0, 0),
(1, 2, 0),
(3, 2, 0),
(-1, 2, 0),
(-2, -2, 0),
(2, -2, 0),
(4, -1, 0),
(6, 2, 0),
]
self.edges = [
(0, 1),
(1, 2),
(1, 3),
(3, 2),
(2, 4),
(4, 0),
(2, 0),
(4, 5),
(0, 5),
(1, 5),
(5, 6),
(6, 7),
(7, 1),
(7, 8),
(8, 3),
]
self.region_cycles = [
(0, 1, 2),
(1, 3, 2),
(2, 4, 0),
(4, 5, 0),
(0, 5, 1),
(1, 5, 6, 7),
(1, 7, 8, 3),
(4, 5, 6, 7, 8, 3, 2),
]
class OctohedronGraph(Graph):
"""
3
1 0
2
4 5
"""
def construct(self):
self.vertices = [
(r * np.cos(angle), r * np.sin(angle) - 1, 0)
for r, s in [(1, 0), (3, 3)]
for angle in (np.pi / 6) * np.array([s, 4 + s, 8 + s])
]
self.edges = [
(0, 1),
(1, 2),
(2, 0),
(5, 0),
(0, 3),
(3, 5),
(3, 1),
(3, 4),
(1, 4),
(4, 2),
(4, 5),
(5, 2),
]
self.region_cycles = [
(0, 1, 2),
(0, 5, 3),
(3, 1, 0),
(3, 4, 1),
(1, 4, 2),
(2, 4, 5),
(5, 0, 2),
(3, 4, 5),
]
class CompleteGraph(Graph):
def __init__(self, num_vertices, radius=3):
self.num_vertices = num_vertices
self.radius = radius
Graph.__init__(self)
def construct(self):
self.vertices = [
(self.radius * np.cos(theta), self.radius * np.sin(theta), 0)
for x in range(self.num_vertices)
for theta in [2 * np.pi * x / self.num_vertices]
]
self.edges = it.combinations(list(range(self.num_vertices)), 2)
def __str__(self):
return Graph.__str__(self) + str(self.num_vertices)
class DiscreteGraphScene(Scene):
args_list = [
(CubeGraph(),),
(SampleGraph(),),
(OctohedronGraph(),),
]
@staticmethod
def args_to_string(*args):
return str(args[0])
def __init__(self, graph, *args, **kwargs):
# See CubeGraph() above for format of graph
self.graph = graph
Scene.__init__(self, *args, **kwargs)
def construct(self):
self._points = list(map(np.array, self.graph.vertices))
self.vertices = self.dots = [Dot(p) for p in self._points]
self.edges = self.lines = [
Line(self._points[i], self._points[j])
for i, j in self.graph.edges
]
self.add(*self.dots + self.edges)
def generate_regions(self):
regions = [
self.region_from_cycle(cycle)
for cycle in self.graph.region_cycles
]
regions[-1].complement() # Outer region painted outwardly...
self.regions = regions
def region_from_cycle(self, cycle):
point_pairs = [
[
self._points[cycle[i]],
self._points[cycle[(i + 1) % len(cycle)]]
]
for i in range(len(cycle))
]
return region_from_line_boundary(
*point_pairs, shape=self.shape
)
def draw_vertices(self, **kwargs):
self.clear()
self.play(ShowCreation(Mobject(*self.vertices), **kwargs))
def draw_edges(self):
self.play(*[
ShowCreation(edge, run_time=1.0)
for edge in self.edges
])
def accent_vertices(self, **kwargs):
self.remove(*self.vertices)
start = Mobject(*self.vertices)
end = Mobject(*[
Dot(point, radius=3 * Dot.DEFAULT_RADIUS, color="lightgreen")
for point in self._points
])
self.play(Transform(
start, end, rate_func=there_and_back,
**kwargs
))
self.remove(start)
self.add(*self.vertices)
def replace_vertices_with(self, mobject):
mobject.center()
diameter = max(mobject.get_height(), mobject.get_width())
self.play(*[
CounterclockwiseTransform(
vertex,
mobject.copy().shift(vertex.get_center())
)
for vertex in self.vertices
] + [
ApplyMethod(
edge.scale,
(edge.get_length() - diameter) / edge.get_length()
)
for edge in self.edges
])
def annotate_edges(self, mobject, fade_in=True, **kwargs):
angles = list(map(np.arctan, list(map(Line.get_slope, self.edges))))
self.edge_annotations = [
mobject.copy().rotate(angle).move_to(edge.get_center())
for angle, edge in zip(angles, self.edges)
]
if fade_in:
self.play(*[
FadeIn(ann, **kwargs)
for ann in self.edge_annotations
])
def trace_cycle(self, cycle=None, color="yellow", run_time=2.0):
if cycle is None:
cycle = self.graph.region_cycles[0]
time_per_edge = run_time / len(cycle)
next_in_cycle = it.cycle(cycle)
next(next_in_cycle) # jump one ahead
self.traced_cycle = Mobject(*[
Line(self._points[i], self._points[j]).set_color(color)
for i, j in zip(cycle, next_in_cycle)
])
self.play(
ShowCreation(self.traced_cycle),
run_time=run_time
)
def generate_spanning_tree(self, root=0, color="yellow"):
self.spanning_tree_root = 0
pairs = deepcopy(self.graph.edges)
pairs += [tuple(reversed(pair)) for pair in pairs]
self.spanning_tree_index_pairs = []
curr = root
spanned_vertices = set([curr])
to_check = set([curr])
while len(to_check) > 0:
curr = to_check.pop()
for pair in pairs:
if pair[0] == curr and pair[1] not in spanned_vertices:
self.spanning_tree_index_pairs.append(pair)
spanned_vertices.add(pair[1])
to_check.add(pair[1])
self.spanning_tree = Mobject(*[
Line(
self._points[pair[0]],
self._points[pair[1]]
).set_color(color)
for pair in self.spanning_tree_index_pairs
])
def generate_treeified_spanning_tree(self):
bottom = -FRAME_Y_RADIUS + 1
x_sep = 1
y_sep = 2
if not hasattr(self, "spanning_tree"):
self.generate_spanning_tree()
root = self.spanning_tree_root
color = self.spanning_tree.get_color()
indices = list(range(len(self._points)))
# Build dicts
parent_of = dict([
tuple(reversed(pair))
for pair in self.spanning_tree_index_pairs
])
children_of = dict([(index, []) for index in indices])
for child in parent_of:
children_of[parent_of[child]].append(child)
x_coord_of = {root: 0}
y_coord_of = {root: bottom}
# width to allocate to a given node, computed as
# the maximum number of decendents in a single generation,
# minus 1, multiplied by x_sep
width_of = {}
for index in indices:
next_generation = children_of[index]
curr_max = max(1, len(next_generation))
while next_generation != []:
next_generation = reduce(op.add, [
children_of[node]
for node in next_generation
])
curr_max = max(curr_max, len(next_generation))
width_of[index] = x_sep * (curr_max - 1)
to_process = [root]
while to_process != []:
index = to_process.pop()
if index not in y_coord_of:
y_coord_of[index] = y_sep + y_coord_of[parent_of[index]]
children = children_of[index]
left_hand = x_coord_of[index] - width_of[index] / 2.0
for child in children:
x_coord_of[child] = left_hand + width_of[child] / 2.0
left_hand += width_of[child] + x_sep
to_process += children
new_points = [
np.array([
x_coord_of[index],
y_coord_of[index],
0
])
for index in indices
]
self.treeified_spanning_tree = Mobject(*[
Line(new_points[i], new_points[j]).set_color(color)
for i, j in self.spanning_tree_index_pairs
])
def generate_dual_graph(self):
point_at_infinity = np.array([np.inf] * 3)
cycles = self.graph.region_cycles
self.dual_points = [
center_of_mass([
self._points[index]
for index in cycle
])
for cycle in cycles
]
self.dual_vertices = [
Dot(point).set_color("green")
for point in self.dual_points
]
self.dual_vertices[-1] = Circle().scale(FRAME_X_RADIUS + FRAME_Y_RADIUS)
self.dual_points[-1] = point_at_infinity
self.dual_edges = []
for pair in self.graph.edges:
dual_point_pair = []
for cycle in cycles:
if not (pair[0] in cycle and pair[1] in cycle):
continue
index1, index2 = cycle.index(pair[0]), cycle.index(pair[1])
if abs(index1 - index2) in [1, len(cycle) - 1]:
dual_point_pair.append(
self.dual_points[cycles.index(cycle)]
)
assert(len(dual_point_pair) == 2)
for i in 0, 1:
if all(dual_point_pair[i] == point_at_infinity):
new_point = np.array(dual_point_pair[1 - i])
vect = center_of_mass([
self._points[pair[0]],
self._points[pair[1]]
]) - new_point
new_point += FRAME_X_RADIUS * vect / get_norm(vect)
dual_point_pair[i] = new_point
self.dual_edges.append(
Line(*dual_point_pair).set_color()
)

View File

@@ -1,602 +0,0 @@
from traceback import *
from scipy.spatial import ConvexHull
from manimlib.animation.composition import LaggedStartMap
from manimlib.animation.fading import FadeIn
from manimlib.animation.fading import FadeOut
from manimlib.animation.transform import Transform
from manimlib.constants import *
from manimlib.mobject.geometry import AnnularSector
from manimlib.mobject.geometry import Annulus
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import VectorizedPoint
from manimlib.utils.space_ops import angle_between_vectors
from manimlib.utils.space_ops import project_along_vector
from manimlib.utils.space_ops import rotate_vector
from manimlib.utils.space_ops import z_to_vector
LIGHT_COLOR = YELLOW
SHADOW_COLOR = BLACK
SWITCH_ON_RUN_TIME = 1.5
FAST_SWITCH_ON_RUN_TIME = 0.1
NUM_LEVELS = 30
NUM_CONES = 7 # in first lighthouse scene
NUM_VISIBLE_CONES = 5 # ibidem
ARC_TIP_LENGTH = 0.2
AMBIENT_FULL = 0.8
AMBIENT_DIMMED = 0.5
SPOTLIGHT_FULL = 0.8
SPOTLIGHT_DIMMED = 0.5
LIGHTHOUSE_HEIGHT = 0.8
DEGREES = TAU / 360
def inverse_power_law(maxint, scale, cutoff, exponent):
return (lambda r: maxint * (cutoff / (r / scale + cutoff))**exponent)
def inverse_quadratic(maxint, scale, cutoff):
return inverse_power_law(maxint, scale, cutoff, 2)
class SwitchOn(LaggedStartMap):
CONFIG = {
"lag_ratio": 0.2,
"run_time": SWITCH_ON_RUN_TIME
}
def __init__(self, light, **kwargs):
if (not isinstance(light, AmbientLight) and not isinstance(light, Spotlight)):
raise Exception(
"Only AmbientLights and Spotlights can be switched on")
LaggedStartMap.__init__(
self, FadeIn, light, **kwargs
)
class SwitchOff(LaggedStartMap):
CONFIG = {
"lag_ratio": 0.2,
"run_time": SWITCH_ON_RUN_TIME
}
def __init__(self, light, **kwargs):
if (not isinstance(light, AmbientLight) and not isinstance(light, Spotlight)):
raise Exception(
"Only AmbientLights and Spotlights can be switched off")
light.set_submobjects(light.submobjects[::-1])
LaggedStartMap.__init__(self, FadeOut, light, **kwargs)
light.set_submobjects(light.submobjects[::-1])
class Lighthouse(SVGMobject):
CONFIG = {
"height": LIGHTHOUSE_HEIGHT,
"fill_color": WHITE,
"fill_opacity": 1.0,
}
def __init__(self, **kwargs):
super().__init__("lighthouse", **kwargs)
def move_to(self, point):
self.next_to(point, DOWN, buff=0)
class AmbientLight(VMobject):
# Parameters are:
# * a source point
# * an opacity function
# * a light color
# * a max opacity
# * a radius (larger than the opacity's dropoff length)
# * the number of subdivisions (levels, annuli)
CONFIG = {
"source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0),
"opacity_function": lambda r: 1.0 / (r + 1.0)**2,
"color": LIGHT_COLOR,
"max_opacity": 1.0,
"num_levels": NUM_LEVELS,
"radius": 5.0
}
def init_points(self):
# in theory, this method is only called once, right?
# so removing submobs shd not be necessary
#
# Note: Usually, yes, it is only called within Mobject.__init__,
# but there is no strong guarantee of that, and you may want certain
# update functions to regenerate points here and there.
for submob in self.submobjects:
self.remove(submob)
self.add(self.source_point)
# create annuli
self.radius = float(self.radius)
dr = self.radius / self.num_levels
for r in np.arange(0, self.radius, dr):
alpha = self.max_opacity * self.opacity_function(r)
annulus = Annulus(
inner_radius=r,
outer_radius=r + dr,
color=self.color,
fill_opacity=alpha
)
annulus.move_to(self.get_source_point())
self.add(annulus)
def move_source_to(self, point):
# old_source_point = self.get_source_point()
# self.shift(point - old_source_point)
self.move_to(point)
return self
def get_source_point(self):
return self.source_point.get_location()
def dimming(self, new_alpha):
old_alpha = self.max_opacity
self.max_opacity = new_alpha
for submob in self.submobjects:
old_submob_alpha = submob.fill_opacity
new_submob_alpha = old_submob_alpha * new_alpha / old_alpha
submob.set_fill(opacity=new_submob_alpha)
class Spotlight(VMobject):
CONFIG = {
"source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0),
"opacity_function": lambda r: 1.0 / (r / 2 + 1.0)**2,
"color": GREEN, # LIGHT_COLOR,
"max_opacity": 1.0,
"num_levels": 10,
"radius": 10.0,
"screen": None,
"camera_mob": None
}
def projection_direction(self):
# Note: This seems reasonable, though for it to work you'd
# need to be sure that any 3d scene including a spotlight
# somewhere assigns that spotlights "camera" attribute
# to be the camera associated with that scene.
if self.camera_mob is None:
return OUT
else:
[phi, theta, r] = self.camera_mob.get_center()
v = np.array([np.sin(phi) * np.cos(theta),
np.sin(phi) * np.sin(theta), np.cos(phi)])
return v # /get_norm(v)
def project(self, point):
v = self.projection_direction()
w = project_along_vector(point, v)
return w
def get_source_point(self):
return self.source_point.get_location()
def init_points(self):
self.set_submobjects([])
self.add(self.source_point)
if self.screen is not None:
# look for the screen and create annular sectors
lower_angle, upper_angle = self.viewing_angles(self.screen)
self.radius = float(self.radius)
dr = self.radius / self.num_levels
lower_ray, upper_ray = self.viewing_rays(self.screen)
for r in np.arange(0, self.radius, dr):
new_sector = self.new_sector(r, dr, lower_angle, upper_angle)
self.add(new_sector)
def new_sector(self, r, dr, lower_angle, upper_angle):
alpha = self.max_opacity * self.opacity_function(r)
annular_sector = AnnularSector(
inner_radius=r,
outer_radius=r + dr,
color=self.color,
fill_opacity=alpha,
start_angle=lower_angle,
angle=upper_angle - lower_angle
)
# rotate (not project) it into the viewing plane
rotation_matrix = z_to_vector(self.projection_direction())
annular_sector.apply_matrix(rotation_matrix)
# now rotate it inside that plane
rotated_RIGHT = np.dot(RIGHT, rotation_matrix.T)
projected_RIGHT = self.project(RIGHT)
omega = angle_between_vectors(rotated_RIGHT, projected_RIGHT)
annular_sector.rotate(omega, axis=self.projection_direction())
annular_sector.move_arc_center_to(self.get_source_point())
return annular_sector
def viewing_angle_of_point(self, point):
# as measured from the positive x-axis
v1 = self.project(RIGHT)
v2 = self.project(np.array(point) - self.get_source_point())
absolute_angle = angle_between_vectors(v1, v2)
# determine the angle's sign depending on their plane's
# choice of orientation. That choice is set by the camera
# position, i. e. projection direction
if np.dot(self.projection_direction(), np.cross(v1, v2)) > 0:
return absolute_angle
else:
return -absolute_angle
def viewing_angles(self, screen):
screen_points = screen.get_anchors()
projected_screen_points = list(map(self.project, screen_points))
viewing_angles = np.array(list(map(self.viewing_angle_of_point,
projected_screen_points)))
lower_angle = upper_angle = 0
if len(viewing_angles) != 0:
lower_angle = np.min(viewing_angles)
upper_angle = np.max(viewing_angles)
if upper_angle - lower_angle > TAU / 2:
lower_angle, upper_angle = upper_angle, lower_angle + TAU
return lower_angle, upper_angle
def viewing_rays(self, screen):
lower_angle, upper_angle = self.viewing_angles(screen)
projected_RIGHT = self.project(
RIGHT) / get_norm(self.project(RIGHT))
lower_ray = rotate_vector(
projected_RIGHT, lower_angle, axis=self.projection_direction())
upper_ray = rotate_vector(
projected_RIGHT, upper_angle, axis=self.projection_direction())
return lower_ray, upper_ray
def opening_angle(self):
l, u = self.viewing_angles(self.screen)
return u - l
def start_angle(self):
l, u = self.viewing_angles(self.screen)
return l
def stop_angle(self):
l, u = self.viewing_angles(self.screen)
return u
def move_source_to(self, point):
self.source_point.set_location(np.array(point))
# self.source_point.move_to(np.array(point))
# self.move_to(point)
self.update_sectors()
return self
def update_sectors(self):
if self.screen is None:
return
for submob in self.submobjects:
if type(submob) == AnnularSector:
lower_angle, upper_angle = self.viewing_angles(self.screen)
# dr = submob.outer_radius - submob.inner_radius
dr = self.radius / self.num_levels
new_submob = self.new_sector(
submob.inner_radius, dr, lower_angle, upper_angle
)
# submob.points = new_submob.points
# submob.set_fill(opacity = 10 * self.opacity_function(submob.outer_radius))
Transform(submob, new_submob).update(1)
def dimming(self, new_alpha):
old_alpha = self.max_opacity
self.max_opacity = new_alpha
for submob in self.submobjects:
# Note: Maybe it'd be best to have a Shadow class so that the
# type can be checked directly?
if type(submob) != AnnularSector:
# it's the shadow, don't dim it
continue
old_submob_alpha = submob.fill_opacity
new_submob_alpha = old_submob_alpha * new_alpha / old_alpha
submob.set_fill(opacity=new_submob_alpha)
def change_opacity_function(self, new_f):
self.opacity_function = new_f
dr = self.radius / self.num_levels
sectors = []
for submob in self.submobjects:
if type(submob) == AnnularSector:
sectors.append(submob)
for (r, submob) in zip(np.arange(0, self.radius, dr), sectors):
if type(submob) != AnnularSector:
# it's the shadow, don't dim it
continue
alpha = self.opacity_function(r)
submob.set_fill(opacity=alpha)
# Warning: This class is likely quite buggy.
class LightSource(VMobject):
# combines:
# a lighthouse
# an ambient light
# a spotlight
# and a shadow
CONFIG = {
"source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0),
"color": LIGHT_COLOR,
"num_levels": 10,
"radius": 10.0,
"screen": None,
"opacity_function": inverse_quadratic(1, 2, 1),
"max_opacity_ambient": AMBIENT_FULL,
"max_opacity_spotlight": SPOTLIGHT_FULL,
"camera_mob": None
}
def init_points(self):
self.add(self.source_point)
self.lighthouse = Lighthouse()
self.ambient_light = AmbientLight(
source_point=VectorizedPoint(location=self.get_source_point()),
color=self.color,
num_levels=self.num_levels,
radius=self.radius,
opacity_function=self.opacity_function,
max_opacity=self.max_opacity_ambient
)
if self.has_screen():
self.spotlight = Spotlight(
source_point=VectorizedPoint(location=self.get_source_point()),
color=self.color,
num_levels=self.num_levels,
radius=self.radius,
screen=self.screen,
opacity_function=self.opacity_function,
max_opacity=self.max_opacity_spotlight,
camera_mob=self.camera_mob
)
else:
self.spotlight = Spotlight()
self.shadow = VMobject(fill_color=SHADOW_COLOR,
fill_opacity=1.0, stroke_color=BLACK)
self.lighthouse.next_to(self.get_source_point(), DOWN, buff=0)
self.ambient_light.move_source_to(self.get_source_point())
if self.has_screen():
self.spotlight.move_source_to(self.get_source_point())
self.update_shadow()
self.add(self.ambient_light, self.spotlight,
self.lighthouse, self.shadow)
def has_screen(self):
if self.screen is None:
return False
elif self.screen.get_num_points() == 0:
return False
else:
return True
def dim_ambient(self):
self.set_max_opacity_ambient(AMBIENT_DIMMED)
def set_max_opacity_ambient(self, new_opacity):
self.max_opacity_ambient = new_opacity
self.ambient_light.dimming(new_opacity)
def dim_spotlight(self):
self.set_max_opacity_spotlight(SPOTLIGHT_DIMMED)
def set_max_opacity_spotlight(self, new_opacity):
self.max_opacity_spotlight = new_opacity
self.spotlight.dimming(new_opacity)
def set_camera_mob(self, new_cam_mob):
self.camera_mob = new_cam_mob
self.spotlight.camera_mob = new_cam_mob
def set_screen(self, new_screen):
if self.has_screen():
self.spotlight.screen = new_screen
else:
# Note: See below
index = self.submobjects.index(self.spotlight)
# camera_mob = self.spotlight.camera_mob
self.remove(self.spotlight)
self.spotlight = Spotlight(
source_point=VectorizedPoint(location=self.get_source_point()),
color=self.color,
num_levels=self.num_levels,
radius=self.radius,
screen=new_screen,
camera_mob=self.camera_mob,
opacity_function=self.opacity_function,
max_opacity=self.max_opacity_spotlight,
)
self.spotlight.move_source_to(self.get_source_point())
# Note: This line will make spotlight show up at the end
# of the submojects list, which can make it show up on
# top of the shadow. To make it show up in the
# same spot, you could try the following line,
# where "index" is what I defined above:
self.submobjects.insert(index, self.spotlight)
# self.add(self.spotlight)
# in any case
self.screen = new_screen
def move_source_to(self, point):
apoint = np.array(point)
v = apoint - self.get_source_point()
# Note: As discussed, things stand to behave better if source
# point is a submobject, so that it automatically interpolates
# during an animation, and other updates can be defined wrt
# that source point's location
self.source_point.set_location(apoint)
# self.lighthouse.next_to(apoint,DOWN,buff = 0)
# self.ambient_light.move_source_to(apoint)
self.lighthouse.shift(v)
# self.ambient_light.shift(v)
self.ambient_light.move_source_to(apoint)
if self.has_screen():
self.spotlight.move_source_to(apoint)
self.update()
return self
def change_spotlight_opacity_function(self, new_of):
self.spotlight.change_opacity_function(new_of)
def set_radius(self, new_radius):
self.radius = new_radius
self.ambient_light.radius = new_radius
self.spotlight.radius = new_radius
def update(self):
self.update_lighthouse()
self.update_ambient()
self.spotlight.update_sectors()
self.update_shadow()
def update_lighthouse(self):
self.lighthouse.move_to(self.get_source_point())
# new_lh = Lighthouse()
# new_lh.move_to(ORIGIN)
# new_lh.apply_matrix(self.rotation_matrix())
# new_lh.shift(self.get_source_point())
# self.lighthouse.submobjects = new_lh.submobjects
def update_ambient(self):
new_ambient_light = AmbientLight(
source_point=VectorizedPoint(location=ORIGIN),
color=self.color,
num_levels=self.num_levels,
radius=self.radius,
opacity_function=self.opacity_function,
max_opacity=self.max_opacity_ambient
)
new_ambient_light.apply_matrix(self.rotation_matrix())
new_ambient_light.move_source_to(self.get_source_point())
self.ambient_light.set_submobjects(new_ambient_light.submobjects)
def get_source_point(self):
return self.source_point.get_location()
def rotation_matrix(self):
if self.camera_mob is None:
return np.eye(3)
phi = self.camera_mob.get_center()[0]
theta = self.camera_mob.get_center()[1]
R1 = np.array([
[1, 0, 0],
[0, np.cos(phi), -np.sin(phi)],
[0, np.sin(phi), np.cos(phi)]
])
R2 = np.array([
[np.cos(theta + TAU / 4), -np.sin(theta + TAU / 4), 0],
[np.sin(theta + TAU / 4), np.cos(theta + TAU / 4), 0],
[0, 0, 1]
])
R = np.dot(R2, R1)
return R
def update_shadow(self):
point = self.get_source_point()
projected_screen_points = []
if not self.has_screen():
return
for point in self.screen.get_anchors():
projected_screen_points.append(self.spotlight.project(point))
projected_source = project_along_vector(
self.get_source_point(), self.spotlight.projection_direction())
projected_point_cloud_3d = np.append(
projected_screen_points,
np.reshape(projected_source, (1, 3)),
axis=0
)
# z_to_vector(self.spotlight.projection_direction())
rotation_matrix = self.rotation_matrix()
back_rotation_matrix = rotation_matrix.T # i. e. its inverse
rotated_point_cloud_3d = np.dot(
projected_point_cloud_3d, back_rotation_matrix.T)
# these points now should all have z = 0
point_cloud_2d = rotated_point_cloud_3d[:, :2]
# now we can compute the convex hull
hull_2d = ConvexHull(point_cloud_2d) # guaranteed to run ccw
hull = []
# we also need the projected source point
source_point_2d = np.dot(self.spotlight.project(
self.get_source_point()), back_rotation_matrix.T)[:2]
index = 0
for point in point_cloud_2d[hull_2d.vertices]:
if np.all(np.abs(point - source_point_2d) < 1.0e-6):
source_index = index
index += 1
continue
point_3d = np.array([point[0], point[1], 0])
hull.append(point_3d)
index += 1
hull_mobject = VMobject()
hull_mobject.set_points_as_corners(hull)
hull_mobject.apply_matrix(rotation_matrix)
anchors = hull_mobject.get_anchors()
# add two control points for the outer cone
if np.size(anchors) == 0:
self.shadow.resize_points(0)
return
ray1 = anchors[source_index - 1] - projected_source
ray1 = ray1 / get_norm(ray1) * 100
ray2 = anchors[source_index] - projected_source
ray2 = ray2 / get_norm(ray2) * 100
outpoint1 = anchors[source_index - 1] + ray1
outpoint2 = anchors[source_index] + ray2
new_anchors = anchors[:source_index]
new_anchors = np.append(new_anchors, np.array(
[outpoint1, outpoint2]), axis=0)
new_anchors = np.append(new_anchors, anchors[source_index:], axis=0)
self.shadow.set_points_as_corners(new_anchors)
# shift it closer to the camera so it is in front of the spotlight
self.shadow.mark_paths_closed = True
# Redefining what was once a ContinualAnimation class
# as a function
def ScreenTracker(light_source):
light_source.add_updater(lambda m: m.update())
return light_source

View File

@@ -1,141 +0,0 @@
import numpy as np
from manimlib.animation.creation import ShowCreation
from manimlib.animation.fading import FadeOut
from manimlib.animation.transform import ApplyMethod
from manimlib.animation.transform import Transform
from manimlib.constants import *
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Line
from manimlib.mobject.matrix import Matrix
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.scene.scene import Scene
class NumericalMatrixMultiplication(Scene):
CONFIG = {
"left_matrix": [[1, 2], [3, 4]],
"right_matrix": [[5, 6], [7, 8]],
"use_parens": True,
}
def construct(self):
left_string_matrix, right_string_matrix = [
np.array(matrix).astype("string")
for matrix in (self.left_matrix, self.right_matrix)
]
if right_string_matrix.shape[0] != left_string_matrix.shape[1]:
raise Exception("Incompatible shapes for matrix multiplication")
left = Matrix(left_string_matrix)
right = Matrix(right_string_matrix)
result = self.get_result_matrix(
left_string_matrix, right_string_matrix
)
self.organize_matrices(left, right, result)
self.animate_product(left, right, result)
def get_result_matrix(self, left, right):
(m, k), n = left.shape, right.shape[1]
mob_matrix = np.array([VGroup()]).repeat(m * n).reshape((m, n))
for a in range(m):
for b in range(n):
template = "(%s)(%s)" if self.use_parens else "%s%s"
parts = [
prefix + template % (left[a][c], right[c][b])
for c in range(k)
for prefix in ["" if c == 0 else "+"]
]
mob_matrix[a][b] = Tex(parts, next_to_buff=0.1)
return Matrix(mob_matrix)
def add_lines(self, left, right):
line_kwargs = {
"color": BLUE,
"stroke_width": 2,
}
left_rows = [
VGroup(*row) for row in left.get_mob_matrix()
]
h_lines = VGroup()
for row in left_rows[:-1]:
h_line = Line(row.get_left(), row.get_right(), **line_kwargs)
h_line.next_to(row, DOWN, buff=left.v_buff / 2.)
h_lines.add(h_line)
right_cols = [
VGroup(*col) for col in np.transpose(right.get_mob_matrix())
]
v_lines = VGroup()
for col in right_cols[:-1]:
v_line = Line(col.get_top(), col.get_bottom(), **line_kwargs)
v_line.next_to(col, RIGHT, buff=right.h_buff / 2.)
v_lines.add(v_line)
self.play(ShowCreation(h_lines))
self.play(ShowCreation(v_lines))
self.wait()
self.show_frame()
def organize_matrices(self, left, right, result):
equals = Tex("=")
everything = VGroup(left, right, equals, result)
everything.arrange()
everything.set_width(FRAME_WIDTH - 1)
self.add(everything)
def animate_product(self, left, right, result):
l_matrix = left.get_mob_matrix()
r_matrix = right.get_mob_matrix()
result_matrix = result.get_mob_matrix()
circle = Circle(
radius=l_matrix[0][0].get_height(),
color=GREEN
)
circles = VGroup(*[
entry.get_point_mobject()
for entry in (l_matrix[0][0], r_matrix[0][0])
])
(m, k), n = l_matrix.shape, r_matrix.shape[1]
for mob in result_matrix.flatten():
mob.set_color(BLACK)
lagging_anims = []
for a in range(m):
for b in range(n):
for c in range(k):
l_matrix[a][c].set_color(YELLOW)
r_matrix[c][b].set_color(YELLOW)
for c in range(k):
start_parts = VGroup(
l_matrix[a][c].copy(),
r_matrix[c][b].copy()
)
result_entry = result_matrix[a][b].split()[c]
new_circles = VGroup(*[
circle.copy().shift(part.get_center())
for part in start_parts.split()
])
self.play(Transform(circles, new_circles))
self.play(
Transform(
start_parts,
result_entry.copy().set_color(YELLOW),
path_arc=-np.pi / 2,
lag_ratio=0,
),
*lagging_anims
)
result_entry.set_color(YELLOW)
self.remove(start_parts)
lagging_anims = [
ApplyMethod(result_entry.set_color, WHITE)
]
for c in range(k):
l_matrix[a][c].set_color(WHITE)
r_matrix[c][b].set_color(WHITE)
self.play(FadeOut(circles), *lagging_anims)
self.wait()

View File

@@ -1,66 +0,0 @@
from manimlib.animation.transform import Transform
from manimlib.constants import *
from manimlib.mobject.mobject import Mobject
from manimlib.scene.scene import Scene
class ReconfigurableScene(Scene):
"""
Note, this seems to no longer work as intented.
"""
CONFIG = {
"allow_recursion": True,
}
def setup(self):
self.states = []
self.num_recursions = 0
def transition_to_alt_config(
self,
return_to_original_configuration=True,
transformation_kwargs=None,
**new_config
):
if transformation_kwargs is None:
transformation_kwargs = {}
original_state = self.get_state()
state_copy = original_state.copy()
self.states.append(state_copy)
if not self.allow_recursion:
return
alt_scene = self.__class__(
skip_animations=True,
allow_recursion=False,
**new_config
)
alt_state = alt_scene.states[len(self.states) - 1]
if return_to_original_configuration:
self.clear()
self.transition_between_states(
state_copy, alt_state,
**transformation_kwargs
)
self.transition_between_states(
state_copy, original_state,
**transformation_kwargs
)
self.clear()
self.add(*original_state)
else:
self.transition_between_states(
original_state, alt_state,
**transformation_kwargs
)
self.__dict__.update(new_config)
def get_state(self):
# Want to return a mobject that maintains the most
# structure. The way to do that is to extract only
# those that aren't inside another.
return Mobject(*self.get_top_level_mobjects())
def transition_between_states(self, start_state, target_state, **kwargs):
self.play(Transform(start_state, target_state, **kwargs))
self.wait()

View File

@@ -1,107 +0,0 @@
from copy import deepcopy
import itertools as it
from manimlib.constants import *
from manimlib.mobject.mobject import Mobject
from manimlib.utils.iterables import adjacent_pairs
# Warning: This is all now pretty deprecated, and should not be expected to work
class Region(Mobject):
CONFIG = {
"display_mode": "region"
}
def __init__(self, condition=(lambda x, y: True), **kwargs):
"""
Condition must be a function which takes in two real
arrays (representing x and y values of space respectively)
and return a boolean array. This can essentially look like
a function from R^2 to {True, False}, but & and | must be
used in place of "and" and "or"
"""
Mobject.__init__(self, **kwargs)
self.condition = condition
def _combine(self, region, op):
self.condition = lambda x, y: op(
self.condition(x, y),
region.condition(x, y)
)
def union(self, region):
self._combine(region, lambda bg1, bg2: bg1 | bg2)
return self
def intersect(self, region):
self._combine(region, lambda bg1, bg2: bg1 & bg2)
return self
def complement(self):
self.bool_grid = ~self.bool_grid
return self
class HalfPlane(Region):
def __init__(self, point_pair, upper_left=True, *args, **kwargs):
"""
point_pair of the form [(x_0, y_0,...), (x_1, y_1,...)]
Pf upper_left is True, the side of the region will be
everything on the upper left side of the line through
the point pair
"""
if not upper_left:
point_pair = list(point_pair)
point_pair.reverse()
(x0, y0), (x1, y1) = point_pair[0][:2], point_pair[1][:2]
def condition(x, y):
return (x1 - x0) * (y - y0) > (y1 - y0) * (x - x0)
Region.__init__(self, condition, *args, **kwargs)
def region_from_line_boundary(*lines, **kwargs):
reg = Region(**kwargs)
for line in lines:
reg.intersect(HalfPlane(line, **kwargs))
return reg
def region_from_polygon_vertices(*vertices, **kwargs):
return region_from_line_boundary(*adjacent_pairs(vertices), **kwargs)
def plane_partition(*lines, **kwargs):
"""
A 'line' is a pair of points [(x0, y0,...), (x1, y1,...)]
Returns the list of regions of the plane cut out by
these lines
"""
result = []
half_planes = [HalfPlane(line, **kwargs) for line in lines]
complements = [deepcopy(hp).complement() for hp in half_planes]
num_lines = len(lines)
for bool_list in it.product(*[[True, False]] * num_lines):
reg = Region(**kwargs)
for i in range(num_lines):
if bool_list[i]:
reg.intersect(half_planes[i])
else:
reg.intersect(complements[i])
if reg.bool_grid.any():
result.append(reg)
return result
def plane_partition_from_points(*points, **kwargs):
"""
Returns list of regions cut out by the complete graph
with points from the argument as vertices.
Each point comes in the form (x, y)
"""
lines = [[p1, p2] for (p1, p2) in it.combinations(points, 2)]
return plane_partition(*lines, **kwargs)

View File

@@ -0,0 +1,638 @@
from __future__ import annotations
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.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 DEG
from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import Square
from manimlib.mobject.mobject import Group
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.numbers import DecimalNumber
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.text_mobject import Text
from manimlib.mobject.types.dot_cloud import DotCloud
from manimlib.mobject.types.vectorized_mobject import VGroup
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.utils.family_ops import extract_mobject_family_members
from manimlib.utils.space_ops import get_norm
from manimlib.utils.tex_file_writing import LatexError
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import Vect3
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 = 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.
class InteractiveScene(Scene):
"""
To select mobjects on screen, hold ctrl and move the mouse to highlight a region,
or just tap ctrl to select the mobject under the cursor.
Pressing command + t will toggle between modes where you either select top level
mobjects part of the scene, or low level pieces.
Hold 'g' to grab the selection and move it around
Hold 'h' to drag it constrained in the horizontal direction
Hold 'v' to drag it constrained in the vertical direction
Hold 't' to resize selection, adding 'shift' to resize with respect to a corner
Command + 'c' copies the ids of selections to clipboard
Command + 'v' will paste either:
- The copied mobject
- A Tex mobject based on copied LaTeX
- A Text mobject based on copied Text
Command + 'z' restores selection back to its original state
Command + 's' saves the selected mobjects to file
"""
corner_dot_config = dict(
color=WHITE,
radius=0.05,
glow_factor=2.0,
)
selection_rectangle_stroke_color = WHITE
selection_rectangle_stroke_width = 1.0
palette_colors = MANIM_COLORS
selection_nudge_size = 0.05
cursor_location_config = dict(
font_size=24,
fill_color=GREY_C,
num_decimal_places=3,
)
time_label_config = dict(
font_size=24,
fill_color=GREY_C,
num_decimal_places=1,
)
crosshair_width = 0.2
crosshair_style = dict(
stroke_color=GREY_A,
stroke_width=[3, 0, 3],
)
def setup(self):
self.selection = Group()
self.selection_highlight = self.get_selection_highlight()
self.selection_rectangle = self.get_selection_rectangle()
self.crosshair = self.get_crosshair()
self.information_label = self.get_information_label()
self.color_palette = self.get_color_palette()
self.unselectables = [
self.selection,
self.selection_highlight,
self.selection_rectangle,
self.crosshair,
self.information_label,
self.camera.frame
]
self.select_top_level_mobs = True
self.regenerate_selection_search_set()
self.is_selecting = False
self.is_grabbing = False
self.add(self.selection_highlight)
def get_selection_rectangle(self):
rect = Rectangle(
stroke_color=self.selection_rectangle_stroke_color,
stroke_width=self.selection_rectangle_stroke_width,
)
rect.fix_in_frame()
rect.fixed_corner = ORIGIN
rect.add_updater(self.update_selection_rectangle)
return rect
def update_selection_rectangle(self, rect: Rectangle):
p1 = rect.fixed_corner
p2 = self.frame.to_fixed_frame_point(self.mouse_point.get_center())
rect.set_points_as_corners([
p1, np.array([p2[0], p1[1], 0]),
p2, np.array([p1[0], p2[1], 0]),
p1,
])
return rect
def get_selection_highlight(self):
result = Group()
result.tracked_mobjects = []
result.add_updater(self.update_selection_highlight)
return result
def update_selection_highlight(self, highlight: Mobject):
if set(highlight.tracked_mobjects) == set(self.selection):
return
# Otherwise, refresh contents of highlight
highlight.tracked_mobjects = list(self.selection)
highlight.set_submobjects([
self.get_highlight(mob)
for mob in self.selection
])
try:
index = min((
i for i, mob in enumerate(self.mobjects)
for sm in self.selection
if sm in mob.get_family()
))
self.mobjects.remove(highlight)
self.mobjects.insert(index - 1, highlight)
except ValueError:
pass
def get_crosshair(self):
lines = VMobject().replicate(2)
lines[0].set_points([LEFT, ORIGIN, RIGHT])
lines[1].set_points([UP, ORIGIN, DOWN])
crosshair = VGroup(*lines)
crosshair.set_width(self.crosshair_width)
crosshair.set_style(**self.crosshair_style)
crosshair.set_animating_status(True)
crosshair.fix_in_frame()
return crosshair
def get_color_palette(self):
palette = VGroup(*(
Square(fill_color=color, fill_opacity=1, side_length=1)
for color in self.palette_colors
))
palette.set_stroke(width=0)
palette.arrange(RIGHT, buff=0.5)
palette.set_width(FRAME_WIDTH - 0.5)
palette.to_edge(DOWN, buff=SMALL_BUFF)
palette.fix_in_frame()
return palette
def get_information_label(self):
loc_label = VGroup(*(
DecimalNumber(**self.cursor_location_config)
for n in range(3)
))
def update_coords(loc_label):
for mob, coord in zip(loc_label, self.mouse_point.get_location()):
mob.set_value(coord)
loc_label.arrange(RIGHT, buff=loc_label.get_height())
loc_label.to_corner(DR, buff=SMALL_BUFF)
loc_label.fix_in_frame()
return loc_label
loc_label.add_updater(update_coords)
time_label = DecimalNumber(0, **self.time_label_config)
time_label.to_corner(DL, buff=SMALL_BUFF)
time_label.fix_in_frame()
time_label.add_updater(lambda m, dt: m.increment_value(dt))
return VGroup(loc_label, time_label)
# Overrides
def get_state(self):
return SceneState(self, ignore=[
self.selection_highlight,
self.selection_rectangle,
self.crosshair,
])
def restore_state(self, scene_state: SceneState):
super().restore_state(scene_state)
self.mobjects.insert(0, self.selection_highlight)
def add(self, *mobjects: Mobject):
super().add(*mobjects)
self.regenerate_selection_search_set()
def remove(self, *mobjects: Mobject):
super().remove(*mobjects)
self.regenerate_selection_search_set()
# Related to selection
def toggle_selection_mode(self):
self.select_top_level_mobs = not self.select_top_level_mobs
self.refresh_selection_scope()
self.regenerate_selection_search_set()
def get_selection_search_set(self) -> list[Mobject]:
return self.selection_search_set
def regenerate_selection_search_set(self):
selectable = list(filter(
lambda m: m not in self.unselectables,
self.mobjects
))
if self.select_top_level_mobs:
self.selection_search_set = selectable
else:
self.selection_search_set = [
submob
for mob in selectable
for submob in mob.family_members_with_points()
]
def refresh_selection_scope(self):
curr = list(self.selection)
if self.select_top_level_mobs:
self.selection.set_submobjects([
mob
for mob in self.mobjects
if any(sm in mob.get_family() for sm in curr)
])
self.selection.refresh_bounding_box(recurse_down=True)
else:
self.selection.set_submobjects(
extract_mobject_family_members(
curr, exclude_pointless=True,
)
)
def get_corner_dots(self, mobject: Mobject) -> Mobject:
dots = DotCloud(**self.corner_dot_config)
radius = float(self.corner_dot_config["radius"])
if mobject.get_depth() < 1e-2:
vects = [DL, UL, UR, DR]
else:
vects = np.array(list(it.product(*3 * [[-1, 1]])))
dots.add_updater(lambda d: d.set_points([
mobject.get_corner(v) + v * radius
for v in vects
]))
return dots
def get_highlight(self, mobject: Mobject) -> Mobject:
if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs:
length = max([mobject.get_height(), mobject.get_width()])
result = VHighlight(
mobject,
max_stroke_addition=min([50 * length, 10]),
)
result.add_updater(lambda m: m.replace(mobject, stretch=True))
return result
elif isinstance(mobject, DotCloud):
return Mobject()
else:
return self.get_corner_dots(mobject)
def add_to_selection(self, *mobjects: Mobject):
mobs = list(filter(
lambda m: m not in self.unselectables and m not in self.selection,
mobjects
))
if len(mobs) == 0:
return
self.selection.add(*mobs)
for mob in mobs:
mob.set_animating_status(True)
def toggle_from_selection(self, *mobjects: Mobject):
for mob in mobjects:
if mob in self.selection:
self.selection.remove(mob)
mob.set_animating_status(False)
mob.refresh_bounding_box()
else:
self.add_to_selection(mob)
def clear_selection(self):
for mob in self.selection:
mob.set_animating_status(False)
mob.refresh_bounding_box()
self.selection.set_submobjects([])
def disable_interaction(self, *mobjects: Mobject):
for mob in mobjects:
for sm in mob.get_family():
self.unselectables.append(sm)
self.regenerate_selection_search_set()
def enable_interaction(self, *mobjects: Mobject):
for mob in mobjects:
for sm in mob.get_family():
if sm in self.unselectables:
self.unselectables.remove(sm)
# Functions for keyboard actions
def copy_selection(self):
names = []
shell = get_ipython()
for mob in self.selection:
name = str(id(mob))
if shell is None:
continue
for key, value in shell.user_ns.items():
if mob is value:
name = key
names.append(name)
pyperclip.copy(", ".join(names))
def paste_selection(self):
clipboard_str = pyperclip.paste()
# Try pasting a mobject
try:
ids = map(int, clipboard_str.split(","))
mobs = map(self.id_to_mobject, ids)
mob_copies = [m.copy() for m in mobs if m is not None]
self.clear_selection()
self.play(*(
FadeIn(mc, run_time=0.5, scale=1.5)
for mc in mob_copies
))
self.add_to_selection(*mob_copies)
return
except ValueError:
pass
# Otherwise, treat as tex or text
if set("\\^=+").intersection(clipboard_str): # Proxy to text for LaTeX
try:
new_mob = Tex(clipboard_str)
except LatexError:
return
else:
new_mob = Text(clipboard_str)
self.clear_selection()
self.add(new_mob)
self.add_to_selection(new_mob)
def delete_selection(self):
self.remove(*self.selection)
self.clear_selection()
def enable_selection(self):
self.is_selecting = True
self.add(self.selection_rectangle)
self.selection_rectangle.fixed_corner = self.frame.to_fixed_frame_point(
self.mouse_point.get_center()
)
def gather_new_selection(self):
self.is_selecting = False
if self.selection_rectangle in self.mobjects:
self.remove(self.selection_rectangle)
additions = []
for mob in reversed(self.get_selection_search_set()):
if self.selection_rectangle.is_touching(mob):
additions.append(mob)
if self.selection_rectangle.get_arc_length() < 1e-2:
break
self.toggle_from_selection(*additions)
def prepare_grab(self):
mp = self.mouse_point.get_center()
self.mouse_to_selection = mp - self.selection.get_center()
self.is_grabbing = True
def prepare_resizing(self, about_corner=False):
center = self.selection.get_center()
mp = self.mouse_point.get_center()
if about_corner:
self.scale_about_point = self.selection.get_corner(center - mp)
else:
self.scale_about_point = center
self.scale_ref_vect = mp - self.scale_about_point
self.scale_ref_width = self.selection.get_width()
self.scale_ref_height = self.selection.get_height()
def toggle_color_palette(self):
if len(self.selection) == 0:
return
if self.color_palette not in self.mobjects:
self.save_state()
self.add(self.color_palette)
else:
self.remove(self.color_palette)
def display_information(self, show=True):
if show:
self.add(self.information_label)
else:
self.remove(self.information_label)
def group_selection(self):
group = self.get_group(*self.selection)
self.add(group)
self.clear_selection()
self.add_to_selection(group)
def ungroup_selection(self):
pieces = []
for mob in list(self.selection):
self.remove(mob)
pieces.extend(list(mob))
self.clear_selection()
self.add(*pieces)
self.add_to_selection(*pieces)
def nudge_selection(self, vect: np.ndarray, large: bool = False):
nudge = self.selection_nudge_size
if large:
nudge *= 10
self.selection.shift(nudge * vect)
# 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 & ALL_MODIFIERS) == 0:
self.enable_selection()
if char == UNSELECT_KEY:
self.clear_selection()
elif char in GRAB_KEYS and (modifiers & ALL_MODIFIERS) == 0:
self.prepare_grab()
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 & ALL_MODIFIERS) == 0:
self.toggle_color_palette()
elif char == INFORMATION_KEY and (modifiers & ALL_MODIFIERS) == 0:
self.display_information()
elif char == "c" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.copy_selection()
elif char == "v" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.paste_selection()
elif char == "x" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.copy_selection()
self.delete_selection()
elif symbol == PygletWindowKeys.BACKSPACE:
self.delete_selection()
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 & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.group_selection()
elif char == "g" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_SHIFT)):
self.ungroup_selection()
elif char == "t" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.toggle_selection_mode()
elif char == "d" and (modifiers & PygletWindowKeys.MOD_SHIFT):
self.copy_frame_positioning()
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 & PygletWindowKeys.MOD_SHIFT),
)
# Adding crosshair
if char == CURSOR_KEY:
if self.crosshair in self.mobjects:
self.remove(self.crosshair)
else:
self.add(self.crosshair)
if char == SELECT_KEY:
self.add(self.crosshair)
# Conditions for saving state
if char in [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY, RESIZE_KEY]:
self.save_state()
def on_key_release(self, symbol: int, modifiers: int) -> None:
super().on_key_release(symbol, modifiers)
if chr(symbol) == SELECT_KEY:
self.gather_new_selection()
if chr(symbol) in GRAB_KEYS:
self.is_grabbing = False
elif chr(symbol) == INFORMATION_KEY:
self.display_information(False)
elif symbol == PygletWindowKeys.LSHIFT and self.window.is_key_pressed(ord(RESIZE_KEY)):
self.prepare_resizing(about_corner=False)
# Mouse actions
def handle_grabbing(self, point: Vect3):
diff = point - self.mouse_to_selection
if self.window.is_key_pressed(ord(GRAB_KEY)):
self.selection.move_to(diff)
elif self.window.is_key_pressed(ord(X_GRAB_KEY)):
self.selection.set_x(diff[0])
elif self.window.is_key_pressed(ord(Y_GRAB_KEY)):
self.selection.set_y(diff[1])
def handle_resizing(self, point: Vect3):
if not hasattr(self, "scale_about_point"):
return
vect = point - self.scale_about_point
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(
scalar * [self.scale_ref_width, self.scale_ref_height][i],
dim=i,
about_point=self.scale_about_point,
stretch=True,
)
else:
scalar = get_norm(vect) / get_norm(self.scale_ref_vect)
self.selection.set_width(
scalar * self.scale_ref_width,
about_point=self.scale_about_point
)
def handle_sweeping_selection(self, point: Vect3):
mob = self.point_to_mobject(
point,
search_set=self.get_selection_search_set(),
buff=SMALL_BUFF
)
if mob is not None:
self.add_to_selection(mob)
def choose_color(self, point: Vect3):
# Search through all mobject on the screen, not just the palette
to_search = [
sm
for mobject in self.mobjects
for sm in mobject.family_members_with_points()
if mobject not in self.unselectables
]
mob = self.point_to_mobject(point, to_search)
if mob is not None:
self.selection.set_color(mob.get_color())
self.remove(self.color_palette)
def on_mouse_motion(self, point: Vect3, d_point: Vect3) -> None:
super().on_mouse_motion(point, d_point)
self.crosshair.move_to(self.frame.to_fixed_frame_point(point))
if self.is_grabbing:
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(PygletWindowKeys.LSHIFT):
self.handle_sweeping_selection(point)
def on_mouse_drag(
self,
point: Vect3,
d_point: Vect3,
buttons: int,
modifiers: int
) -> None:
super().on_mouse_drag(point, d_point, buttons, modifiers)
self.crosshair.move_to(self.frame.to_fixed_frame_point(point))
def on_mouse_release(self, point: Vect3, button: int, mods: int) -> None:
super().on_mouse_release(point, button, mods)
if self.color_palette in self.mobjects:
self.choose_color(point)
else:
self.clear_selection()
# Copying code to recreate state
def copy_frame_positioning(self):
frame = self.frame
center = frame.get_center()
height = frame.get_height()
angles = frame.get_euler_angles()
call = f"reorient("
theta, phi, gamma = (angles / DEG).astype(int)
call += f"{theta}, {phi}, {gamma}"
if any(center != 0):
call += f", {tuple(np.round(center, 2))}"
if height != FRAME_HEIGHT:
call += ", {:.2f}".format(height)
call += ")"
pyperclip.copy(call)
def copy_cursor_position(self):
pyperclip.copy(str(tuple(self.mouse_point.get_center().round(2))))

View File

@@ -1,143 +0,0 @@
from manimlib.animation.animation import Animation
from manimlib.animation.transform import MoveToTarget
from manimlib.animation.transform import Transform
from manimlib.animation.update import UpdateFromFunc
from manimlib.constants import *
from manimlib.scene.scene import Scene
from manimlib.mobject.probability import SampleSpace
from manimlib.mobject.types.vectorized_mobject import VGroup
class SampleSpaceScene(Scene):
def get_sample_space(self, **config):
self.sample_space = SampleSpace(**config)
return self.sample_space
def add_sample_space(self, **config):
self.add(self.get_sample_space(**config))
def get_division_change_animations(
self, sample_space, parts, p_list,
dimension=1,
new_label_kwargs=None,
**kwargs
):
if new_label_kwargs is None:
new_label_kwargs = {}
anims = []
p_list = sample_space.complete_p_list(p_list)
space_copy = sample_space.copy()
vect = DOWN if dimension == 1 else RIGHT
parts.generate_target()
for part, p in zip(parts.target, p_list):
part.replace(space_copy, stretch=True)
part.stretch(p, dimension)
parts.target.arrange(vect, buff=0)
parts.target.move_to(space_copy)
anims.append(MoveToTarget(parts))
if hasattr(parts, "labels") and parts.labels is not None:
label_kwargs = parts.label_kwargs
label_kwargs.update(new_label_kwargs)
new_braces, new_labels = sample_space.get_subdivision_braces_and_labels(
parts.target, **label_kwargs
)
anims += [
Transform(parts.braces, new_braces),
Transform(parts.labels, new_labels),
]
return anims
def get_horizontal_division_change_animations(self, p_list, **kwargs):
assert(hasattr(self.sample_space, "horizontal_parts"))
return self.get_division_change_animations(
self.sample_space, self.sample_space.horizontal_parts, p_list,
dimension=1,
**kwargs
)
def get_vertical_division_change_animations(self, p_list, **kwargs):
assert(hasattr(self.sample_space, "vertical_parts"))
return self.get_division_change_animations(
self.sample_space, self.sample_space.vertical_parts, p_list,
dimension=0,
**kwargs
)
def get_conditional_change_anims(
self, sub_sample_space_index, value, post_rects=None,
**kwargs
):
parts = self.sample_space.horizontal_parts
sub_sample_space = parts[sub_sample_space_index]
anims = self.get_division_change_animations(
sub_sample_space, sub_sample_space.vertical_parts, value,
dimension=0,
**kwargs
)
if post_rects is not None:
anims += self.get_posterior_rectangle_change_anims(post_rects)
return anims
def get_top_conditional_change_anims(self, *args, **kwargs):
return self.get_conditional_change_anims(0, *args, **kwargs)
def get_bottom_conditional_change_anims(self, *args, **kwargs):
return self.get_conditional_change_anims(1, *args, **kwargs)
def get_prior_rectangles(self):
return VGroup(*[
self.sample_space.horizontal_parts[i].vertical_parts[0]
for i in range(2)
])
def get_posterior_rectangles(self, buff=MED_LARGE_BUFF):
prior_rects = self.get_prior_rectangles()
areas = [
rect.get_width() * rect.get_height()
for rect in prior_rects
]
total_area = sum(areas)
total_height = prior_rects.get_height()
post_rects = prior_rects.copy()
for rect, area in zip(post_rects, areas):
rect.stretch_to_fit_height(total_height * area / total_area)
rect.stretch_to_fit_width(
area / rect.get_height()
)
post_rects.arrange(DOWN, buff=0)
post_rects.next_to(
self.sample_space, RIGHT, buff
)
return post_rects
def get_posterior_rectangle_braces_and_labels(
self, post_rects, labels, direction=RIGHT, **kwargs
):
return self.sample_space.get_subdivision_braces_and_labels(
post_rects, labels, direction, **kwargs
)
def update_posterior_braces(self, post_rects):
braces = post_rects.braces
labels = post_rects.labels
for rect, brace, label in zip(post_rects, braces, labels):
brace.stretch_to_fit_height(rect.get_height())
brace.next_to(rect, RIGHT, SMALL_BUFF)
label.next_to(brace, RIGHT, SMALL_BUFF)
def get_posterior_rectangle_change_anims(self, post_rects):
def update_rects(rects):
new_rects = self.get_posterior_rectangles()
Transform(rects, new_rects).update(1)
if hasattr(rects, "braces"):
self.update_posterior_braces(rects)
return rects
anims = [UpdateFromFunc(post_rects, update_rects)]
if hasattr(post_rects, "braces"):
anims += list(map(Animation, [
post_rects.labels, post_rects.braces
]))
return anims

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
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 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.
"""
# 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 IPython autoreload for automatic reloading of modules."""
self.shell.magic("load_ext autoreload")
self.shell.magic("autoreload all")
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

@@ -1,81 +1,110 @@
import numpy as np
from pydub import AudioSegment
from __future__ import annotations
import os
import platform
import shutil
import subprocess as sp
import os
import sys
import platform
from tqdm import tqdm as ProgressDisplay
from manimlib.constants import FFMPEG_BIN
from manimlib.utils.config_ops import digest_config
from manimlib.utils.file_ops import guarantee_existence
from manimlib.utils.file_ops import add_extension_if_not_present
from manimlib.utils.file_ops import get_sorted_integer_files
from manimlib.utils.sounds import get_full_sound_file_path
import numpy as np
from pydub import AudioSegment
from tqdm.auto import tqdm as ProgressDisplay
from pathlib import Path
from manimlib.logger import log
from manimlib.mobject.mobject import Mobject
from manimlib.utils.file_ops import guarantee_existence
from manimlib.utils.sounds import get_full_sound_file_path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from PIL.Image import Image
from manimlib.camera.camera import Camera
from manimlib.scene.scene import Scene
class SceneFileWriter(object):
CONFIG = {
"write_to_movie": False,
"break_into_partial_movies": False,
# TODO, save_pngs is doing nothing
"save_pngs": False,
"png_mode": "RGBA",
"save_last_frame": False,
"movie_file_extension": ".mp4",
# Should the path of output files mirror the directory
# structure of the module holding the scene?
"mirror_module_path": False,
# What python file is generating this scene
"input_file_path": "",
def __init__(
self,
scene: Scene,
write_to_movie: bool = False,
subdivide_output: bool = False,
png_mode: str = "RGBA",
save_last_frame: bool = False,
movie_file_extension: str = ".mp4",
# Where should this be written
"output_directory": None,
"file_name": None,
"open_file_upon_completion": False,
"show_file_location_upon_completion": False,
"quiet": False,
"total_frames": 0,
"progress_description_len": 60,
}
output_directory: str = ".",
file_name: str | None = None,
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,
gamma: float = 1.0,
):
self.scene: Scene = scene
self.write_to_movie = write_to_movie
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.output_directory = output_directory
self.file_name = file_name
self.open_file_upon_completion = open_file_upon_completion
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
self.gamma = gamma
# State during file writing
self.writing_process: sp.Popen | None = None
self.progress_display: ProgressDisplay | None = None
self.ended_with_interrupt: bool = False
def __init__(self, scene, **kwargs):
digest_config(self, kwargs)
self.scene = scene
self.writing_process = None
self.has_progress_display = False
self.init_output_directories()
self.init_audio()
# Output directories and files
def init_output_directories(self):
out_dir = self.output_directory
if self.mirror_module_path:
module_dir = self.get_default_module_directory()
out_dir = os.path.join(out_dir, module_dir)
scene_name = self.file_name or self.get_default_scene_name()
def init_output_directories(self) -> None:
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:
movie_dir = guarantee_existence(os.path.join(out_dir, "videos"))
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,
))
self.movie_file_path = self.init_movie_file_path()
if self.subdivide_output:
self.partial_movie_directory = self.init_partial_movie_directory()
def get_default_module_directory(self):
path, _ = os.path.splitext(self.input_file_path)
if path.startswith("_"):
path = path[1:]
return path
def init_image_file_path(self) -> Path:
return self.get_output_file_rootname().with_suffix(".png")
def get_default_scene_name(self):
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_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
@@ -85,40 +114,30 @@ class SceneFileWriter(object):
name += f"_{eaan}"
return name
def get_resolution_directory(self):
pixel_height = self.scene.camera.pixel_height
frame_rate = self.scene.camera.frame_rate
return "{}p{}".format(
pixel_height, frame_rate
)
# Directory getters
def get_image_file_path(self):
def get_image_file_path(self) -> str:
return self.image_file_path
def get_next_partial_movie_path(self):
result = os.path.join(
self.partial_movie_directory,
"{:05}{}".format(
self.scene.num_plays,
self.movie_file_extension,
)
)
return result
def get_next_partial_movie_path(self) -> str:
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):
def get_movie_file_path(self) -> str:
return self.movie_file_path
# Sound
def init_audio(self):
self.includes_sound = False
def init_audio(self) -> None:
self.includes_sound: bool = False
def create_audio_segment(self):
def create_audio_segment(self) -> None:
self.audio_segment = AudioSegment.silent()
def add_audio_segment(self, new_segment,
time=None,
gain_to_background=None):
def add_audio_segment(
self,
new_segment: AudioSegment,
time: float | None = None,
gain_to_background: float | None = None
) -> None:
if not self.includes_sound:
self.includes_sound = True
self.create_audio_segment()
@@ -142,161 +161,146 @@ class SceneFileWriter(object):
gain_during_overlay=gain_to_background,
)
def add_sound(self, sound_file, time=None, gain=None, **kwargs):
def add_sound(
self,
sound_file: str,
time: float | None = None,
gain: float | None = None,
gain_to_background: float | None = None
) -> None:
file_path = get_full_sound_file_path(sound_file)
new_segment = AudioSegment.from_file(file_path)
if gain:
new_segment = new_segment.apply_gain(gain)
self.add_audio_segment(new_segment, time, **kwargs)
self.add_audio_segment(new_segment, time, gain_to_background)
# Writers
def begin(self):
if not self.break_into_partial_movies and self.write_to_movie:
def begin(self) -> None:
if not self.subdivide_output and self.write_to_movie:
self.open_movie_pipe(self.get_movie_file_path())
def begin_animation(self):
if self.break_into_partial_movies and self.write_to_movie:
def begin_animation(self) -> None:
if self.subdivide_output and self.write_to_movie:
self.open_movie_pipe(self.get_next_partial_movie_path())
def end_animation(self):
if self.break_into_partial_movies and self.write_to_movie:
def end_animation(self) -> None:
if self.subdivide_output and self.write_to_movie:
self.close_movie_pipe()
def finish(self):
if self.write_to_movie:
if self.break_into_partial_movies:
self.combine_movie_files()
else:
self.close_movie_pipe()
def finish(self) -> None:
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())
if self.save_last_frame:
self.scene.update_frame(ignore_skipping=True)
self.scene.update_frame(force_draw=True)
self.save_final_image(self.scene.get_image())
if self.should_open_file():
self.open_file()
def open_movie_pipe(self, file_path):
def open_movie_pipe(self, file_path: str) -> None:
stem, ext = os.path.splitext(file_path)
self.final_file_path = file_path
self.temp_file_path = stem + "_temp" + ext
fps = self.scene.camera.frame_rate
fps = self.scene.camera.fps
width, height = self.scene.camera.get_pixel_shape()
vf_arg = 'vflip'
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
'-pix_fmt', 'rgba',
'-r', str(fps), # frames per second
'-i', '-', # The input comes from a pipe
'-vf', 'vflip',
'-an', # Tells FFMPEG not to expect any audio
'-vf', vf_arg,
'-an', # Tells ffmpeg not to expect any audio
'-loglevel', 'error',
]
if self.movie_file_extension == ".mov":
# This is if the background of the exported
# video should be transparent.
command += [
'-vcodec', 'qtrle',
]
elif self.movie_file_extension == ".gif":
command += []
else:
command += [
'-vcodec', 'libx264',
'-pix_fmt', 'yuv420p',
]
if self.video_codec:
command += ['-vcodec', self.video_codec]
if self.pixel_format:
command += ['-pix_fmt', self.pixel_format]
command += [self.temp_file_path]
self.writing_process = sp.Popen(command, stdin=sp.PIPE)
if self.total_frames > 0:
if not self.quiet:
self.progress_display = ProgressDisplay(
range(self.total_frames),
# bar_format="{l_bar}{bar}|{n_fmt}/{total_fmt}",
leave=False,
ascii=True if platform.system() == 'Windows' else None,
dynamic_ncols=True,
)
self.has_progress_display = True
self.set_progress_display_description()
def use_fast_encoding(self):
self.video_codec = "libx264rgb"
self.pixel_format = "rgb32"
def get_insert_file_path(self, index: int) -> Path:
movie_path = Path(self.get_movie_file_path())
scene_name = movie_path.stem
insert_dir = Path(movie_path.parent, "inserts")
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
self.write_to_movie = True
self.init_output_directories()
index = 0
while (insert_path := self.get_insert_file_path(index)).exists():
index += 1
self.inserted_file_path = insert_path
self.open_movie_pipe(self.inserted_file_path)
def end_insert(self):
self.close_movie_pipe()
self.write_to_movie = False
self.print_file_ready_message(self.inserted_file_path)
def has_progress_display(self):
return self.progress_display is not None
def set_progress_display_description(self, file: str = "", sub_desc: str = "") -> None:
if self.progress_display is None:
return
def set_progress_display_subdescription(self, sub_desc):
desc_len = self.progress_description_len
file = os.path.split(self.get_movie_file_path())[1]
full_desc = f"Rendering {file} ({sub_desc})"
if not file:
file = os.path.split(self.get_movie_file_path())[1]
full_desc = f"{file} {sub_desc}"
if len(full_desc) > desc_len:
full_desc = full_desc[:desc_len - 4] + "...)"
full_desc = full_desc[:desc_len - 3] + "..."
else:
full_desc += " " * (desc_len - len(full_desc))
self.progress_display.set_description(full_desc)
def write_frame(self, camera):
def write_frame(self, camera: Camera) -> None:
if self.write_to_movie:
raw_bytes = camera.get_raw_fbo_data()
self.writing_process.stdin.write(raw_bytes)
if self.has_progress_display:
if self.progress_display is not None:
self.progress_display.update()
def close_movie_pipe(self):
def close_movie_pipe(self) -> None:
self.writing_process.stdin.close()
self.writing_process.wait()
self.writing_process.terminate()
if self.has_progress_display:
if self.progress_display is not None:
self.progress_display.close()
shutil.move(self.temp_file_path, self.final_file_path)
def combine_movie_files(self):
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
if not self.ended_with_interrupt:
shutil.move(self.temp_file_path, self.final_file_path)
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
self.movie_file_path = self.temp_file_path
# 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):
def add_sound_to_video(self) -> None:
movie_file_path = self.get_movie_file_path()
stem, ext = os.path.splitext(movie_file_path)
sound_file_path = stem + ".wav"
@@ -308,7 +312,7 @@ class SceneFileWriter(object):
)
temp_file_path = stem + "_temp" + ext
commands = [
"ffmpeg",
self.ffmpeg_bin,
"-i", movie_file_path,
"-i", sound_file_path,
'-y', # overwrite output file if it exists
@@ -327,22 +331,22 @@ class SceneFileWriter(object):
shutil.move(temp_file_path, movie_file_path)
os.remove(sound_file_path)
def save_final_image(self, image):
def save_final_image(self, image: Image) -> None:
file_path = self.get_image_file_path()
image.save(file_path)
self.print_file_ready_message(file_path)
def print_file_ready_message(self, file_path):
def print_file_ready_message(self, file_path: str) -> None:
if not self.quiet:
log.info(f"File ready at {file_path}")
def should_open_file(self):
def should_open_file(self) -> bool:
return any([
self.show_file_location_upon_completion,
self.open_file_upon_completion,
])
def open_file(self):
def open_file(self) -> None:
if self.quiet:
curr_stdout = sys.stdout
sys.stdout = open(os.devnull, "w")

View File

@@ -1,24 +0,0 @@
from manimlib.scene.scene import Scene
class ThreeDScene(Scene):
CONFIG = {
"camera_config": {
"samples": 4,
}
}
def begin_ambient_camera_rotation(self, rate=0.02):
pass # TODO
def stop_ambient_camera_rotation(self):
pass # TODO
def move_camera(self,
phi=None,
theta=None,
distance=None,
gamma=None,
frame_center=None,
**kwargs):
pass # TODO

View File

@@ -1,512 +0,0 @@
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.animation.creation import ShowCreation
from manimlib.animation.creation import Write
from manimlib.animation.fading import FadeOut
from manimlib.animation.growing import GrowArrow
from manimlib.animation.transform import ApplyFunction
from manimlib.animation.transform import ApplyPointwiseFunction
from manimlib.animation.transform import Transform
from manimlib.constants import *
from manimlib.mobject.coordinate_systems import Axes
from manimlib.mobject.coordinate_systems import NumberPlane
from manimlib.mobject.geometry import Arrow
from manimlib.mobject.geometry import Dot
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import Vector
from manimlib.mobject.matrix import Matrix
from manimlib.mobject.matrix import VECTOR_LABEL_SCALE_FACTOR
from manimlib.mobject.matrix import vector_coordinate_label
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.tex_mobject import TexText
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.scene.scene import Scene
from manimlib.utils.rate_functions import rush_from
from manimlib.utils.rate_functions import rush_into
from manimlib.utils.space_ops import angle_of_vector
from manimlib.utils.space_ops import get_norm
X_COLOR = GREEN_C
Y_COLOR = RED_C
Z_COLOR = BLUE_D
# TODO: Much of this scene type seems dependent on the coordinate system chosen.
# That is, being centered at the origin with grid units corresponding to the
# arbitrary space units. Change it!
#
# Also, methods I would have thought of as getters, like coords_to_vector, are
# actually doing a lot of animating.
class VectorScene(Scene):
CONFIG = {
"basis_vector_stroke_width": 6
}
def add_plane(self, animate=False, **kwargs):
plane = NumberPlane(**kwargs)
if animate:
self.play(ShowCreation(plane, lag_ratio=0.5))
self.add(plane)
return plane
def add_axes(self, animate=False, color=WHITE, **kwargs):
axes = Axes(color=color, tick_frequency=1)
if animate:
self.play(ShowCreation(axes))
self.add(axes)
return axes
def lock_in_faded_grid(self, dimness=0.7, axes_dimness=0.5):
plane = self.add_plane()
axes = plane.get_axes()
plane.fade(dimness)
axes.set_color(WHITE)
axes.fade(axes_dimness)
self.add(axes)
self.freeze_background()
def get_vector(self, numerical_vector, **kwargs):
return Arrow(
self.plane.coords_to_point(0, 0),
self.plane.coords_to_point(*numerical_vector[:2]),
buff=0,
**kwargs
)
def add_vector(self, vector, color=YELLOW, animate=True, **kwargs):
if not isinstance(vector, Arrow):
vector = Vector(vector, color=color, **kwargs)
if animate:
self.play(GrowArrow(vector))
self.add(vector)
return vector
def write_vector_coordinates(self, vector, **kwargs):
coords = vector_coordinate_label(vector, **kwargs)
self.play(Write(coords))
return coords
def get_basis_vectors(self, i_hat_color=X_COLOR, j_hat_color=Y_COLOR):
return VGroup(*[
Vector(
vect,
color=color,
stroke_width=self.basis_vector_stroke_width
)
for vect, color in [
([1, 0], i_hat_color),
([0, 1], j_hat_color)
]
])
def get_basis_vector_labels(self, **kwargs):
i_hat, j_hat = self.get_basis_vectors()
return VGroup(*[
self.get_vector_label(
vect, label, color=color,
label_scale_factor=1,
**kwargs
)
for vect, label, color in [
(i_hat, "\\hat{\\imath}", X_COLOR),
(j_hat, "\\hat{\\jmath}", Y_COLOR),
]
])
def get_vector_label(self, vector, label,
at_tip=False,
direction="left",
rotate=False,
color=None,
label_scale_factor=VECTOR_LABEL_SCALE_FACTOR):
if not isinstance(label, Tex):
if len(label) == 1:
label = "\\vec{\\textbf{%s}}" % label
label = Tex(label)
if color is None:
color = vector.get_color()
label.set_color(color)
label.scale(label_scale_factor)
label.add_background_rectangle()
if at_tip:
vect = vector.get_vector()
vect /= get_norm(vect)
label.next_to(vector.get_end(), vect, buff=SMALL_BUFF)
else:
angle = vector.get_angle()
if not rotate:
label.rotate(-angle, about_point=ORIGIN)
if direction == "left":
label.shift(-label.get_bottom() + 0.1 * UP)
else:
label.shift(-label.get_top() + 0.1 * DOWN)
label.rotate(angle, about_point=ORIGIN)
label.shift((vector.get_end() - vector.get_start()) / 2)
return label
def label_vector(self, vector, label, animate=True, **kwargs):
label = self.get_vector_label(vector, label, **kwargs)
if animate:
self.play(Write(label, run_time=1))
self.add(label)
return label
def position_x_coordinate(self, x_coord, x_line, vector):
x_coord.next_to(x_line, -np.sign(vector[1]) * UP)
x_coord.set_color(X_COLOR)
return x_coord
def position_y_coordinate(self, y_coord, y_line, vector):
y_coord.next_to(y_line, np.sign(vector[0]) * RIGHT)
y_coord.set_color(Y_COLOR)
return y_coord
def coords_to_vector(self, vector, coords_start=2 * RIGHT + 2 * UP, clean_up=True):
starting_mobjects = list(self.mobjects)
array = Matrix(vector)
array.shift(coords_start)
arrow = Vector(vector)
x_line = Line(ORIGIN, vector[0] * RIGHT)
y_line = Line(x_line.get_end(), arrow.get_end())
x_line.set_color(X_COLOR)
y_line.set_color(Y_COLOR)
x_coord, y_coord = array.get_mob_matrix().flatten()
self.play(Write(array, run_time=1))
self.wait()
self.play(ApplyFunction(
lambda x: self.position_x_coordinate(x, x_line, vector),
x_coord
))
self.play(ShowCreation(x_line))
self.play(
ApplyFunction(
lambda y: self.position_y_coordinate(y, y_line, vector),
y_coord
),
FadeOut(array.get_brackets())
)
y_coord, brackets = self.get_mobjects_from_last_animation()
self.play(ShowCreation(y_line))
self.play(ShowCreation(arrow))
self.wait()
if clean_up:
self.clear()
self.add(*starting_mobjects)
def vector_to_coords(self, vector, integer_labels=True, clean_up=True):
starting_mobjects = list(self.mobjects)
show_creation = False
if isinstance(vector, Arrow):
arrow = vector
vector = arrow.get_end()[:2]
else:
arrow = Vector(vector)
show_creation = True
array = vector_coordinate_label(arrow, integer_labels=integer_labels)
x_line = Line(ORIGIN, vector[0] * RIGHT)
y_line = Line(x_line.get_end(), arrow.get_end())
x_line.set_color(X_COLOR)
y_line.set_color(Y_COLOR)
x_coord, y_coord = array.get_mob_matrix().flatten()
x_coord_start = self.position_x_coordinate(
x_coord.copy(), x_line, vector
)
y_coord_start = self.position_y_coordinate(
y_coord.copy(), y_line, vector
)
brackets = array.get_brackets()
if show_creation:
self.play(ShowCreation(arrow))
self.play(
ShowCreation(x_line),
Write(x_coord_start),
run_time=1
)
self.play(
ShowCreation(y_line),
Write(y_coord_start),
run_time=1
)
self.wait()
self.play(
Transform(x_coord_start, x_coord, lag_ratio=0),
Transform(y_coord_start, y_coord, lag_ratio=0),
Write(brackets, run_time=1),
)
self.wait()
self.remove(x_coord_start, y_coord_start, brackets)
self.add(array)
if clean_up:
self.clear()
self.add(*starting_mobjects)
return array, x_line, y_line
def show_ghost_movement(self, vector):
if isinstance(vector, Arrow):
vector = vector.get_end() - vector.get_start()
elif len(vector) == 2:
vector = np.append(np.array(vector), 0.0)
x_max = int(FRAME_X_RADIUS + abs(vector[0]))
y_max = int(FRAME_Y_RADIUS + abs(vector[1]))
dots = VMobject(*[
Dot(x * RIGHT + y * UP)
for x in range(-x_max, x_max)
for y in range(-y_max, y_max)
])
dots.set_fill(BLACK, opacity=0)
dots_halfway = dots.copy().shift(vector / 2).set_fill(WHITE, 1)
dots_end = dots.copy().shift(vector)
self.play(Transform(
dots, dots_halfway, rate_func=rush_into
))
self.play(Transform(
dots, dots_end, rate_func=rush_from
))
self.remove(dots)
class LinearTransformationScene(VectorScene):
CONFIG = {
"include_background_plane": True,
"include_foreground_plane": True,
"foreground_plane_kwargs": {
"x_max": FRAME_WIDTH / 2,
"x_min": -FRAME_WIDTH / 2,
"y_max": FRAME_WIDTH / 2,
"y_min": -FRAME_WIDTH / 2,
"faded_line_ratio": 0
},
"background_plane_kwargs": {
"color": GREY,
"axis_config": {
"stroke_color": GREY_B,
},
"axis_config": {
"color": GREY,
},
"background_line_style": {
"stroke_color": GREY,
"stroke_width": 1,
},
},
"show_coordinates": False,
"show_basis_vectors": True,
"basis_vector_stroke_width": 6,
"i_hat_color": X_COLOR,
"j_hat_color": Y_COLOR,
"leave_ghost_vectors": False,
"t_matrix": [[3, 0], [1, 2]],
}
def setup(self):
# The has_already_setup attr is to not break all the old Scenes
if hasattr(self, "has_already_setup"):
return
self.has_already_setup = True
self.background_mobjects = []
self.foreground_mobjects = []
self.transformable_mobjects = []
self.moving_vectors = []
self.transformable_labels = []
self.moving_mobjects = []
self.t_matrix = np.array(self.t_matrix)
self.background_plane = NumberPlane(
**self.background_plane_kwargs
)
if self.show_coordinates:
self.background_plane.add_coordinates()
if self.include_background_plane:
self.add_background_mobject(self.background_plane)
if self.include_foreground_plane:
self.plane = NumberPlane(**self.foreground_plane_kwargs)
self.add_transformable_mobject(self.plane)
if self.show_basis_vectors:
self.basis_vectors = self.get_basis_vectors(
i_hat_color=self.i_hat_color,
j_hat_color=self.j_hat_color,
)
self.moving_vectors += list(self.basis_vectors)
self.i_hat, self.j_hat = self.basis_vectors
self.add(self.basis_vectors)
def add_special_mobjects(self, mob_list, *mobs_to_add):
for mobject in mobs_to_add:
if mobject not in mob_list:
mob_list.append(mobject)
self.add(mobject)
def add_background_mobject(self, *mobjects):
self.add_special_mobjects(self.background_mobjects, *mobjects)
# TODO, this conflicts with Scene.add_fore
def add_foreground_mobject(self, *mobjects):
self.add_special_mobjects(self.foreground_mobjects, *mobjects)
def add_transformable_mobject(self, *mobjects):
self.add_special_mobjects(self.transformable_mobjects, *mobjects)
def add_moving_mobject(self, mobject, target_mobject=None):
mobject.target = target_mobject
self.add_special_mobjects(self.moving_mobjects, mobject)
def get_unit_square(self, color=YELLOW, opacity=0.3, stroke_width=3):
square = self.square = Rectangle(
color=color,
width=self.plane.get_x_unit_size(),
height=self.plane.get_y_unit_size(),
stroke_color=color,
stroke_width=stroke_width,
fill_color=color,
fill_opacity=opacity
)
square.move_to(self.plane.coords_to_point(0, 0), DL)
return square
def add_unit_square(self, animate=False, **kwargs):
square = self.get_unit_square(**kwargs)
if animate:
self.play(
DrawBorderThenFill(square),
Animation(Group(*self.moving_vectors))
)
self.add_transformable_mobject(square)
self.bring_to_front(*self.moving_vectors)
self.square = square
return self
def add_vector(self, vector, color=YELLOW, **kwargs):
vector = VectorScene.add_vector(
self, vector, color=color, **kwargs
)
self.moving_vectors.append(vector)
return vector
def write_vector_coordinates(self, vector, **kwargs):
coords = VectorScene.write_vector_coordinates(self, vector, **kwargs)
self.add_foreground_mobject(coords)
return coords
def add_transformable_label(
self, vector, label,
transformation_name="L",
new_label=None,
**kwargs):
label_mob = self.label_vector(vector, label, **kwargs)
if new_label:
label_mob.target_text = new_label
else:
label_mob.target_text = "%s(%s)" % (
transformation_name,
label_mob.get_tex()
)
label_mob.vector = vector
label_mob.kwargs = kwargs
if "animate" in label_mob.kwargs:
label_mob.kwargs.pop("animate")
self.transformable_labels.append(label_mob)
return label_mob
def add_title(self, title, scale_factor=1.5, animate=False):
if not isinstance(title, Mobject):
title = TexText(title).scale(scale_factor)
title.to_edge(UP)
title.add_background_rectangle()
if animate:
self.play(Write(title))
self.add_foreground_mobject(title)
self.title = title
return self
def get_matrix_transformation(self, matrix):
return self.get_transposed_matrix_transformation(np.array(matrix).T)
def get_transposed_matrix_transformation(self, transposed_matrix):
transposed_matrix = np.array(transposed_matrix)
if transposed_matrix.shape == (2, 2):
new_matrix = np.identity(3)
new_matrix[:2, :2] = transposed_matrix
transposed_matrix = new_matrix
elif transposed_matrix.shape != (3, 3):
raise Exception("Matrix has bad dimensions")
return lambda point: np.dot(point, transposed_matrix)
def get_piece_movement(self, pieces):
start = VGroup(*pieces)
target = VGroup(*[mob.target for mob in pieces])
if self.leave_ghost_vectors:
self.add(start.copy().fade(0.7))
return Transform(start, target, lag_ratio=0)
def get_moving_mobject_movement(self, func):
for m in self.moving_mobjects:
if m.target is None:
m.target = m.copy()
target_point = func(m.get_center())
m.target.move_to(target_point)
return self.get_piece_movement(self.moving_mobjects)
def get_vector_movement(self, func):
for v in self.moving_vectors:
v.target = Vector(func(v.get_end()), color=v.get_color())
norm = get_norm(v.target.get_end())
if norm < 0.1:
v.target.get_tip().scale(norm)
return self.get_piece_movement(self.moving_vectors)
def get_transformable_label_movement(self):
for l in self.transformable_labels:
l.target = self.get_vector_label(
l.vector.target, l.target_text, **l.kwargs
)
return self.get_piece_movement(self.transformable_labels)
def apply_matrix(self, matrix, **kwargs):
self.apply_transposed_matrix(np.array(matrix).T, **kwargs)
def apply_inverse(self, matrix, **kwargs):
self.apply_matrix(np.linalg.inv(matrix), **kwargs)
def apply_transposed_matrix(self, transposed_matrix, **kwargs):
func = self.get_transposed_matrix_transformation(transposed_matrix)
if "path_arc" not in kwargs:
net_rotation = np.mean([
angle_of_vector(func(RIGHT)),
angle_of_vector(func(UP)) - np.pi / 2
])
kwargs["path_arc"] = net_rotation
self.apply_function(func, **kwargs)
def apply_inverse_transpose(self, t_matrix, **kwargs):
t_inv = np.linalg.inv(np.array(t_matrix).T).T
self.apply_transposed_matrix(t_inv, **kwargs)
def apply_nonlinear_transformation(self, function, **kwargs):
self.plane.prepare_for_nonlinear_transform()
self.apply_function(function, **kwargs)
def apply_function(self, function, added_anims=[], **kwargs):
if "run_time" not in kwargs:
kwargs["run_time"] = 3
anims = [
ApplyPointwiseFunction(function, t_mob)
for t_mob in self.transformable_mobjects
] + [
self.get_vector_movement(function),
self.get_transformable_label_movement(),
self.get_moving_mobject_movement(function),
] + [
Animation(f_mob)
for f_mob in self.foreground_mobjects
] + added_anims
self.play(*anims, **kwargs)

View File

@@ -1,11 +1,28 @@
from __future__ import annotations
import copy
import os
import re
import OpenGL.GL as gl
import moderngl
import numpy as np
import copy
from functools import lru_cache
from manimlib.utils.directories import get_shader_dir
from manimlib.utils.file_ops import find_file
from manimlib.config import parse_cli
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
from manimlib.utils.shaders import set_program_uniform
from typing import TYPE_CHECKING
if TYPE_CHECKING:
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
@@ -15,151 +32,449 @@ from manimlib.utils.file_ops import find_file
class ShaderWrapper(object):
def __init__(self,
vert_data=None,
vert_indices=None,
shader_folder=None,
uniforms=None, # A dictionary mapping names of uniform variables
texture_paths=None, # A dictionary mapping names to filepaths for textures.
depth_test=False,
render_primitive=moderngl.TRIANGLE_STRIP,
):
def __init__(
self,
ctx: moderngl.context.Context,
vert_data: np.ndarray,
shader_folder: Optional[str] = None,
mobject_uniforms: Optional[UniformDict] = None, # A dictionary mapping names of uniform variables
texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures.
depth_test: bool = False,
render_primitive: int = moderngl.TRIANGLE_STRIP,
code_replacements: dict[str, str] = dict(),
):
self.ctx = ctx
self.vert_data = vert_data
self.vert_indices = vert_indices
self.vert_attributes = vert_data.dtype.names
self.shader_folder = shader_folder
self.uniforms = uniforms or dict()
self.texture_paths = texture_paths or dict()
self.depth_test = depth_test
self.render_primitive = str(render_primitive)
self.render_primitive = render_primitive
self.texture_paths = texture_paths or dict()
self.program_uniform_mirror: UniformDict = dict()
self.bind_to_mobject_uniforms(mobject_uniforms or dict())
self.init_program_code()
for old, new in code_replacements.items():
self.replace_code(old, new)
self.init_program()
self.init_textures()
self.init_vertex_objects()
self.refresh_id()
def copy(self):
result = copy.copy(self)
result.vert_data = np.array(self.vert_data)
if result.vert_indices is not None:
result.vert_indices = np.array(self.vert_indices)
if self.uniforms:
result.uniforms = dict(self.uniforms)
if self.texture_paths:
result.texture_paths = dict(self.texture_paths)
return result
def __deepcopy__(self, memo):
# Don't allow deepcopies, e.g. if the mobject with this ShaderWrapper as an
# attribute gets copies. Returning None means the parent object with this ShaderWrapper
# as an attribute should smoothly handle this case.
return None
def is_valid(self):
return all([
self.vert_data is not None,
self.program_code["vertex_shader"] is not None,
self.program_code["fragment_shader"] is not None,
])
def get_id(self):
return self.id
def get_program_id(self):
return self.program_id
def create_id(self):
# A unique id for a shader
return "|".join(map(str, [
self.program_id,
self.uniforms,
self.texture_paths,
self.depth_test,
self.render_primitive,
]))
def refresh_id(self):
self.program_id = self.create_program_id()
self.id = self.create_id()
def create_program_id(self):
return hash("".join((
self.program_code[f"{name}_shader"] or ""
for name in ("vertex", "geometry", "fragment")
)))
def init_program_code(self):
def get_code(name):
def init_program_code(self) -> None:
def get_code(name: str) -> str | None:
return get_shader_code_from_file(
os.path.join(self.shader_folder, f"{name}.glsl")
)
self.program_code = {
self.program_code: dict[str, str | None] = {
"vertex_shader": get_code("vert"),
"geometry_shader": get_code("geom"),
"fragment_shader": get_code("frag"),
}
def get_program_code(self):
return self.program_code
def init_program(self):
if not self.shader_folder:
self.program = None
self.vert_format = None
self.programs = []
return
self.program = get_shader_program(self.ctx, **self.program_code)
self.vert_format = moderngl.detect_format(self.program, self.vert_attributes)
self.programs = [self.program]
def replace_code(self, old, new):
def init_textures(self):
self.texture_names_to_ids = dict()
self.textures = []
for name, path in self.texture_paths.items():
self.add_texture(name, image_path_to_texture(path, self.ctx))
def init_vertex_objects(self):
self.vbo = None
self.vaos = []
def add_texture(self, name: str, texture: moderngl.Texture):
max_units = self.ctx.info['GL_MAX_TEXTURE_IMAGE_UNITS']
if len(self.textures) >= max_units:
raise ValueError(f"Unable to use more than {max_units} textures for a program")
# The position in the list determines its id
self.texture_names_to_ids[name] = len(self.textures)
self.textures.append(texture)
def bind_to_mobject_uniforms(self, mobject_uniforms: UniformDict):
self.mobject_uniforms = mobject_uniforms
def get_id(self) -> int:
return self.id
def refresh_id(self) -> None:
self.id = hash("".join(map(str, [
"".join(map(str, self.program_code.values())),
self.mobject_uniforms,
self.depth_test,
self.render_primitive,
self.texture_paths,
])))
def replace_code(self, old: str, new: str) -> None:
code_map = self.program_code
for (name, code) in code_map.items():
for name in code_map:
if code_map[name] is None:
continue
code_map[name] = re.sub(old, new, code_map[name])
self.init_program()
self.refresh_id()
def combine_with(self, *shader_wrappers):
# Assume they are of the same type
if len(shader_wrappers) == 0:
return
if self.vert_indices is not None:
num_verts = len(self.vert_data)
indices_list = [self.vert_indices]
data_list = [self.vert_data]
for sw in shader_wrappers:
indices_list.append(sw.vert_indices + num_verts)
data_list.append(sw.vert_data)
num_verts += len(sw.vert_data)
self.vert_indices = np.hstack(indices_list)
self.vert_data = np.hstack(data_list)
# Changing context
def use_clip_plane(self):
if "clip_plane" not in self.mobject_uniforms:
return False
return any(self.mobject_uniforms["clip_plane"])
def set_ctx_depth_test(self, enable: bool = True) -> None:
if enable:
self.ctx.enable(moderngl.DEPTH_TEST)
else:
self.vert_data = np.hstack([self.vert_data, *[sw.vert_data for sw in shader_wrappers]])
return self
self.ctx.disable(moderngl.DEPTH_TEST)
def set_ctx_clip_plane(self, enable: bool = True) -> None:
if enable:
gl.glEnable(gl.GL_CLIP_DISTANCE0)
# Adding data
def read_in(self, data_list: Iterable[np.ndarray]):
total_len = sum(map(len, data_list))
if total_len == 0:
if self.vbo is not None:
self.vbo.clear()
return
# If possible, read concatenated data into existing list
if len(self.vert_data) != total_len:
self.vert_data = np.concatenate(data_list)
else:
np.concatenate(data_list, out=self.vert_data)
# Either create new vbo, or read data into it
total_size = self.vert_data.itemsize * total_len
if self.vbo is not None and self.vbo.size != total_size:
self.release() # This sets vbo to be None
if self.vbo is None:
self.vbo = self.ctx.buffer(self.vert_data)
self.generate_vaos()
else:
self.vbo.write(self.vert_data)
def generate_vaos(self):
# Vertex array object
self.vaos = [
self.ctx.vertex_array(
program=program,
content=[(self.vbo, self.vert_format, *self.vert_attributes)],
mode=self.render_primitive,
)
for program in self.programs
]
# Related to data and rendering
def pre_render(self):
self.set_ctx_depth_test(self.depth_test)
self.set_ctx_clip_plane(self.use_clip_plane())
for tid, texture in enumerate(self.textures):
texture.use(tid)
def render(self):
for vao in self.vaos:
vao.render()
def update_program_uniforms(self, camera_uniforms: UniformDict):
for program in self.programs:
if program is None:
continue
for uniforms in [self.mobject_uniforms, camera_uniforms, self.texture_names_to_ids]:
for name, value in uniforms.items():
set_program_uniform(program, name, value)
def release(self):
for obj in (self.vbo, *self.vaos):
if obj is not None:
obj.release()
self.init_vertex_objects()
def release_textures(self):
for texture in self.textures:
texture.release()
del texture
self.textures = []
self.texture_names_to_ids = dict()
# For caching
filename_to_code_map = {}
def get_shader_code_from_file(filename):
if not filename:
return None
if filename in filename_to_code_map:
return filename_to_code_map[filename]
try:
filepath = find_file(
filename,
directories=[get_shader_dir(), "/"],
extensions=[],
class VShaderWrapper(ShaderWrapper):
def __init__(
self,
ctx: moderngl.context.Context,
vert_data: np.ndarray,
shader_folder: Optional[str] = None,
mobject_uniforms: Optional[UniformDict] = None, # A dictionary mapping names of uniform variables
texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures.
depth_test: bool = False,
render_primitive: int = moderngl.TRIANGLES,
code_replacements: dict[str, str] = dict(),
stroke_behind: bool = False,
):
self.stroke_behind = stroke_behind
super().__init__(
ctx=ctx,
vert_data=vert_data,
shader_folder=shader_folder,
mobject_uniforms=mobject_uniforms,
texture_paths=texture_paths,
depth_test=depth_test,
render_primitive=render_primitive,
code_replacements=code_replacements,
)
except IOError:
return None
self.fill_canvas = VShaderWrapper.get_fill_canvas(self.ctx)
self.add_texture('Texture', self.fill_canvas[0].color_attachments[0])
self.add_texture('DepthTexture', self.fill_canvas[2].color_attachments[0])
with open(filepath, "r") as f:
result = f.read()
def init_program_code(self) -> None:
self.program_code = {
f"{vtype}_{name}": get_shader_code_from_file(
os.path.join("quadratic_bezier", f"{vtype}", f"{name}.glsl")
)
for vtype in ["stroke", "fill", "depth"]
for name in ["vert", "geom", "frag"]
}
# To share functionality between shaders, some functions are read in
# from other files an inserted into the relevant strings before
# passing to ctx.program for compiling
# Replace "#INSERT " lines with relevant code
insertions = re.findall(r"^#INSERT .*\.glsl$", result, flags=re.MULTILINE)
for line in insertions:
inserted_code = get_shader_code_from_file(
os.path.join("inserts", line.replace("#INSERT ", ""))
def init_program(self):
self.stroke_program = get_shader_program(
self.ctx,
vertex_shader=self.program_code["stroke_vert"],
geometry_shader=self.program_code["stroke_geom"],
fragment_shader=self.program_code["stroke_frag"],
)
result = result.replace(line, inserted_code)
filename_to_code_map[filename] = result
return result
self.fill_program = get_shader_program(
self.ctx,
vertex_shader=self.program_code["fill_vert"],
geometry_shader=self.program_code["fill_geom"],
fragment_shader=self.program_code["fill_frag"],
)
self.fill_border_program = get_shader_program(
self.ctx,
vertex_shader=self.program_code["stroke_vert"],
geometry_shader=self.program_code["stroke_geom"],
fragment_shader=self.program_code["stroke_frag"].replace(
"// MODIFY FRAG COLOR",
"frag_color.a *= 0.95; frag_color.rgb *= frag_color.a;",
)
)
self.fill_depth_program = get_shader_program(
self.ctx,
vertex_shader=self.program_code["depth_vert"],
geometry_shader=self.program_code["depth_geom"],
fragment_shader=self.program_code["depth_frag"],
)
self.programs = [self.stroke_program, self.fill_program, self.fill_border_program, self.fill_depth_program]
# Full vert format looks like this (total of 4x23 = 92 bytes):
# point 3
# stroke_rgba 4
# stroke_width 1
# joint_angle 1
# fill_rgba 4
# base_normal 3
# fill_border_width 1
self.stroke_vert_format = '3f 4f 1f 1f 16x 3f 4x'
self.stroke_vert_attributes = ['point', 'stroke_rgba', 'stroke_width', 'joint_angle', 'unit_normal']
def get_colormap_code(rgb_list):
data = ",".join(
"vec3({}, {}, {})".format(*rgb)
for rgb in rgb_list
)
return f"vec3[{len(rgb_list)}]({data})"
self.fill_vert_format = '3f 24x 4f 3f 4x'
self.fill_vert_attributes = ['point', 'fill_rgba', 'base_normal']
self.fill_border_vert_format = '3f 20x 1f 4f 3f 1f'
self.fill_border_vert_attributes = ['point', 'joint_angle', 'stroke_rgba', 'unit_normal', 'stroke_width']
self.fill_depth_vert_format = '3f 40x 3f 4x'
self.fill_depth_vert_attributes = ['point', 'base_normal']
def init_vertex_objects(self):
self.vbo = None
self.stroke_vao = None
self.fill_vao = None
self.fill_border_vao = None
self.vaos = []
def generate_vaos(self):
self.stroke_vao = self.ctx.vertex_array(
program=self.stroke_program,
content=[(self.vbo, self.stroke_vert_format, *self.stroke_vert_attributes)],
mode=self.render_primitive,
)
self.fill_vao = self.ctx.vertex_array(
program=self.fill_program,
content=[(self.vbo, self.fill_vert_format, *self.fill_vert_attributes)],
mode=self.render_primitive,
)
self.fill_border_vao = self.ctx.vertex_array(
program=self.fill_border_program,
content=[(self.vbo, self.fill_border_vert_format, *self.fill_border_vert_attributes)],
mode=self.render_primitive,
)
self.fill_depth_vao = self.ctx.vertex_array(
program=self.fill_depth_program,
content=[(self.vbo, self.fill_depth_vert_format, *self.fill_depth_vert_attributes)],
mode=self.render_primitive,
)
self.vaos = [self.stroke_vao, self.fill_vao, self.fill_border_vao, self.fill_depth_vao]
def set_backstroke(self, value: bool = True):
self.stroke_behind = value
def refresh_id(self):
super().refresh_id()
self.id = hash(str(self.id) + str(self.stroke_behind))
# Rendering
def render_stroke(self):
if self.stroke_vao is None:
return
self.stroke_vao.render()
def render_fill(self):
if self.fill_vao is None:
return
original_fbo = self.ctx.fbo
fill_tx_fbo, fill_tx_vao, depth_tx_fbo = self.fill_canvas
# Render to a separate texture, due to strange alpha compositing
# for the blended winding calculation
fill_tx_fbo.clear()
fill_tx_fbo.use()
# Be sure not to apply depth test while rendering fill
# but set it back to where it was after
apply_depth_test = bool(gl.glGetBooleanv(gl.GL_DEPTH_TEST))
self.ctx.disable(moderngl.DEPTH_TEST)
# With this blend function, the effect of blending alpha a with
# -a / (1 - a) cancels out, so we can cancel positively and negatively
# oriented triangles
gl.glBlendFuncSeparate(
gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA,
gl.GL_ONE_MINUS_DST_ALPHA, gl.GL_ONE
)
self.fill_vao.render()
if apply_depth_test:
self.ctx.enable(moderngl.DEPTH_TEST)
depth_tx_fbo.clear(1.0)
depth_tx_fbo.use()
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE)
gl.glBlendEquation(gl.GL_MIN)
self.fill_depth_vao.render()
# Now add border, just taking the max alpha
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE)
gl.glBlendEquation(gl.GL_MAX)
self.fill_border_vao.render()
# Take the texture we were just drawing to, and render it to
# the main scene. Account for how alphas have been premultiplied
original_fbo.use()
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
gl.glBlendEquation(gl.GL_FUNC_ADD)
fill_tx_vao.render()
# Return to original blending state
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
# Static method returning one shared value across all VShaderWrappers
@lru_cache
@staticmethod
def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Framebuffer]:
"""
Because VMobjects with fill are rendered in a funny way, using
alpha blending to effectively compute the winding number around
each pixel, they need to be rendered to a separate texture, which
is then composited onto the ordinary frame buffer.
This returns a texture, loaded into a frame buffer, and a vao
which can display that texture as a simple quad onto a screen,
along with the rgb value which is meant to be discarded.
"""
size = manim_config.camera.resolution
double_size = (2 * size[0], 2 * size[1])
# Important to make sure dtype is floating point (not fixed point)
# so that alpha values can be negative and are not clipped
fill_texture = ctx.texture(size=double_size, components=4, dtype='f2')
# Use another one to keep track of depth
depth_texture = ctx.texture(size=size, components=1, dtype='f4')
fill_texture_fbo = ctx.framebuffer(fill_texture)
depth_texture_fbo = ctx.framebuffer(depth_texture)
simple_vert = '''
#version 330
in vec2 texcoord;
out vec2 uv;
void main() {
gl_Position = vec4((2.0 * texcoord - 1.0), 0.0, 1.0);
uv = texcoord;
}
'''
alpha_adjust_frag = '''
#version 330
uniform sampler2D Texture;
uniform sampler2D DepthTexture;
in vec2 uv;
out vec4 color;
void main() {
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;
gl_FragDepth = texture(DepthTexture, uv)[0];
}
'''
fill_program = ctx.program(
vertex_shader=simple_vert,
fragment_shader=alpha_adjust_frag,
)
verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
simple_vbo = ctx.buffer(verts.astype('f4').tobytes())
fill_texture_vao = ctx.simple_vertex_array(
fill_program, simple_vbo, 'texcoord',
mode=moderngl.TRIANGLE_STRIP
)
return (fill_texture_fbo, fill_texture_vao, depth_texture_fbo)
def render(self):
if self.stroke_behind:
self.render_stroke()
self.render_fill()
else:
self.render_fill()
self.render_stroke()

View File

@@ -1,7 +1,5 @@
#version 330
#INSERT camera_uniform_declarations.glsl
uniform sampler2D Texture;
in vec3 point;
@@ -12,11 +10,10 @@ out vec2 v_im_coords;
out float v_opacity;
// Analog of import for manim only
#INSERT get_gl_Position.glsl
#INSERT position_point_into_frame.glsl
#INSERT emit_gl_Position.glsl
void main(){
v_im_coords = im_coords;
v_opacity = opacity;
gl_Position = get_gl_Position(position_point_into_frame(point));
emit_gl_Position(point);
}

View File

@@ -4,4 +4,4 @@ There seems to be no analog to #include in C++ for OpenGL shaders. While there
with the code from one of the files in this folder.
The functions in this file often include reference to uniforms which are assumed to be part of the surrounding context into which they are inserted.
The functions in this file may include declarations of uniforms, so one should not re-declare those in the surrounding context.

View File

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

View File

@@ -0,0 +1,21 @@
uniform float is_fixed_in_frame;
uniform mat4 view;
uniform float focal_distance;
uniform vec3 frame_rescale_factors;
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
result = mix(view * result, result, is_fixed_in_frame);
// Essentially a projection matrix
result.xyz *= frame_rescale_factors;
result.w = 1.0 - result.z;
// Flip and scale to prevent premature clipping
result.z *= -0.1;
gl_Position = result;
if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){
gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane);
}
}

View File

@@ -1,3 +1,7 @@
uniform vec3 light_position;
uniform vec3 camera_position;
uniform vec3 shading;
vec3 float_to_color(float value, float min_val, float max_val, vec3[9] colormap_data){
float alpha = clamp((value - min_val) / (max_val - min_val), 0.0, 1.0);
int disc_alpha = min(int(alpha * 8), 7);
@@ -9,30 +13,22 @@ vec3 float_to_color(float value, float min_val, float max_val, vec3[9] colormap_
}
vec4 add_light(vec4 color,
vec3 point,
vec3 unit_normal,
vec3 light_coords,
vec3 cam_coords,
float reflectiveness,
float gloss,
float shadow){
if(reflectiveness == 0.0 && gloss == 0.0 && shadow == 0.0) return color;
vec4 add_light(vec4 color, vec3 point, vec3 unit_normal){
if(shading == vec3(0.0)) return color;
float reflectiveness = shading.x;
float gloss = shading.y;
float shadow = shading.z;
vec4 result = color;
// Assume everything has already been rotated such that camera is in the z-direction
// cam_coords = vec3(0, 0, focal_distance);
vec3 to_camera = normalize(cam_coords - point);
vec3 to_light = normalize(light_coords - point);
// Note, this effectively treats surfaces as two-sided
// if(dot(to_camera, unit_normal) < 0) unit_normal *= -1;
vec3 to_camera = normalize(camera_position - point);
vec3 to_light = normalize(light_position - point);
float light_to_normal = dot(to_light, unit_normal);
// When unit normal points towards light, brighten
float bright_factor = max(light_to_normal, 0) * reflectiveness;
// For glossy surface, add extra shine if light beam go towards camera
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
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));
bright_factor += shine;
@@ -40,29 +36,18 @@ vec4 add_light(vec4 color,
result.rgb = mix(result.rgb, vec3(1.0), bright_factor);
if (light_to_normal < 0){
// Darken
result.rgb = mix(result.rgb, vec3(0.0), -light_to_normal * shadow);
result.rgb = mix(
result.rgb,
vec3(0.0),
max(-light_to_normal, 0) * shadow
);
}
// float darkening = mix(1, max(light_to_normal, 0), shadow);
// return vec4(
// darkening * mix(color.rgb, vec3(1.0), shine),
// color.a
// );
return result;
}
vec4 finalize_color(vec4 color,
vec3 point,
vec3 unit_normal,
vec3 light_coords,
vec3 cam_coords,
float reflectiveness,
float gloss,
float shadow){
vec4 finalize_color(vec4 color, vec3 point, vec3 unit_normal){
///// INSERT COLOR FUNCTION HERE /////
// The line above may be replaced by arbitrary code snippets, as per
// the method Mobject.set_color_by_code
return add_light(
color, point, unit_normal, light_coords, cam_coords,
reflectiveness, gloss, shadow
);
return add_light(color, point, unit_normal);
}

View File

@@ -1,31 +0,0 @@
// Assumes the following uniforms exist in the surrounding context:
// uniform vec2 frame_shape;
// uniform float focal_distance;
// uniform float is_fixed_in_frame;
const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0);
float perspective_scale_factor(float z, float focal_distance){
return max(0.0, focal_distance / (focal_distance - z));
}
vec4 get_gl_Position(vec3 point){
vec4 result = vec4(point, 1.0);
if(!bool(is_fixed_in_frame)){
result.x *= 2.0 / frame_shape.x;
result.y *= 2.0 / frame_shape.y;
float psf = perspective_scale_factor(result.z, focal_distance);
if (psf > 0){
result.xy *= psf;
// TODO, what's the better way to do this?
// This is to keep vertices too far out of frame from getting cut.
result.z *= 0.01;
}
} else{
result.x *= 2.0 / DEFAULT_FRAME_SHAPE.x;
result.y *= 2.0 / DEFAULT_FRAME_SHAPE.y;
}
result.z *= -1;
return result;
}

View File

@@ -1,16 +0,0 @@
// Assumes the following uniforms exist in the surrounding context:
// uniform vec3 camera_offset;
// uniform mat3 camera_rotation;
vec3 get_rotated_surface_unit_normal_vector(vec3 point, vec3 du_point, vec3 dv_point){
vec3 cp = cross(
(du_point - point),
(dv_point - point)
);
if(length(cp) == 0){
// Instead choose a normal to just dv_point - point in the direction of point
vec3 v2 = dv_point - point;
cp = cross(cross(v2, point), v2);
}
return normalize(rotate_point_into_frame(cp));
}

View File

@@ -1,22 +1,20 @@
vec3 get_unit_normal(in vec3[3] points){
vec3 get_unit_normal(vec3 p0, vec3 p1, vec3 p2){
float tol = 1e-6;
vec3 v1 = normalize(points[1] - points[0]);
vec3 v2 = normalize(points[2] - points[0]);
vec3 v1 = normalize(p1 - p0);
vec3 v2 = normalize(p2 - p0);
vec3 cp = cross(v1, v2);
float cp_norm = length(cp);
if(cp_norm < tol){
// Three points form a line, so find a normal vector
// to that line in the plane shared with the z-axis
vec3 k_hat = vec3(0.0, 0.0, 1.0);
vec3 new_cp = cross(cross(v2, k_hat), v2);
float new_cp_norm = length(new_cp);
if(new_cp_norm < tol){
// We only come here if all three points line up
// on the z-axis.
return vec3(0.0, -1.0, 0.0);
// return k_hat;
}
return new_cp / new_cp_norm;
}
return cp / cp_norm;
if(cp_norm > tol) return cp / cp_norm;
// Otherwise, three pionts form a line, so find
// a normal vector to that line in the plane shared
// with the z-axis
vec3 comb = v1 + v2;
cp = cross(cross(comb, vec3(0.0, 0.0, 1.0)), comb);
cp_norm = length(cp);
if(cp_norm > tol) return cp / cp_norm;
// Otherwise, the points line up with the z-axis.
return vec3(0.0, -1.0, 0.0);
}

View File

@@ -0,0 +1,104 @@
vec2 xs_on_clean_parabola(vec3 b0, vec3 b1, vec3 b2){
/*
Given three control points for a quadratic bezier,
this returns the two values (x0, x2) such that the
section of the parabola y = x^2 between those values
is isometric to the given quadratic bezier.
Adapated from https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
*/
vec3 dd = 2 * b1 - b0 - b2;
float u0 = dot(b1 - b0, dd);
float u2 = dot(b2 - b1, dd);
float cp = length(cross(b2 - b0, dd));
return vec2(u0 / cp, u2 / cp);
}
mat4 map_triangles(vec3 src0, vec3 src1, vec3 src2, vec3 dst0, vec3 dst1, vec3 dst2){
/*
Return an affine transform which maps the triangle (src0, src1, src2)
onto the triangle (dst0, dst1, dst2)
*/
mat4 src_mat = mat4(
src0, 1.0,
src1, 1.0,
src2, 1.0,
vec4(1.0)
);
mat4 dst_mat = mat4(
dst0, 1.0,
dst1, 1.0,
dst2, 1.0,
vec4(1.0)
);
return dst_mat * inverse(src_mat);
}
mat4 rotation(vec3 axis, float cos_angle){
float c = cos_angle;
float s = sqrt(1 - c * c); // Sine of the angle
float oc = 1.0 - c;
float ax = axis.x;
float ay = axis.y;
float az = axis.z;
return mat4(
oc * ax * ax + c, oc * ax * ay + az * s, oc * az * ax - ay * s, 0.0,
oc * ax * ay - az * s, oc * ay * ay + c, oc * ay * az + ax * s, 0.0,
oc * az * ax + ay * s, oc * ay * az - ax * s, oc * az * az + c, 0.0,
0.0, 0.0, 0.0, 1.0
);
}
mat4 map_onto_x_axis(vec3 src0, vec3 src1){
mat4 shift = mat4(1.0);
shift[3].xyz = -src0;
// Find rotation matrix between unit vectors in each direction
vec3 vect = normalize(src1 - src0);
// No rotation needed
if(vect.x > 1 - 1e-6) return shift;
// Equivalent to cross(vect, vec3(1, 0, 0))
vec3 axis = normalize(vec3(0.0, vect.z, -vect.y));
mat4 rotate = rotation(axis, vect.x);
return rotate * shift;
}
mat4 get_xyz_to_uv(
vec3 b0, vec3 b1, vec3 b2,
float threshold,
out bool exceeds_threshold
){
/*
Populates the matrix `result` with an affine transformation which maps a set of
quadratic bezier controls points into a new coordinate system such that the bezier
curve coincides with y = x^2.
If the x-range under this part of the curve exceeds `threshold`, this returns false
and populates result a matrix mapping b0 and b2 onto the x-axis
*/
vec2 xs = xs_on_clean_parabola(b0, b1, b2);
float x0 = xs[0];
float x1 = 0.5 * (xs[0] + xs[1]);
float x2 = xs[1];
// Portions of the parabola y = x^2 where abs(x) exceeds
// this value are treated as straight lines.
exceeds_threshold = (min(x0, x2) > threshold || max(x0, x2) < -threshold);
if(exceeds_threshold){
return map_onto_x_axis(b0, b2);
}
// This triangle on the xy plane should be isometric
// to (b0, b1, b2), and it should define a quadratic
// bezier segment aligned with y = x^2
vec3 dst0 = vec3(x0, x0 * x0, 0.0);
vec3 dst1 = vec3(x1, x0 * x2, 0.0);
vec3 dst2 = vec3(x2, x2 * x2, 0.0);
return map_triangles(b0, b1, b2, dst0, dst1, dst2);
}

View File

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

Some files were not shown because too many files have changed in this diff Show More