30 Commits

Author SHA1 Message Date
Grant Sanderson
e5298385ed Video work (#2402)
* Bug fix for TransformMatchingStrings with incompatible lengths

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

* Add option hide_zero_components_on_complex to DecimalNumber

* Validate syntax before reloading

* Add remembered stroke_config to TracedPath

* Add CLAUDE.md to gitignore

* Move pre-calculated traced points to TracingTail

* Fix interplay between time_span and alpha in Animation

* Clearer init for points in TracingTail

* Fix CoordinateSystem.get_area_under_graph

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

* Add put_start_on and put_end_on

* Add Slider

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

* Put interp_by_hsl option in various color interpretation functions

* Swap priority of matched_keys vs key_map is TransformMatchingStrings

* Have z-index apply recursively

* Set self.svg_string property for SVGMobject

* Fix num_decimal_places config in Tex.make_number_changeable

* Add Surface. color_by_uv_function

* Add VMobject. set_color_by_proportion

* Add \mathcal to tex_to_symbol_count
2025-10-14 07:15:39 -07:00
Grant Sanderson
41613db7ec Remove stray prints 2025-06-14 10:47:52 -05:00
Grant Sanderson
c48c4b6a9f Merge branch 'master' of github.com:3b1b/manim 2025-06-10 10:59:56 -05:00
Grant Sanderson
cc5fbe17b9 Fix bug with mirroring file prefixes 2025-06-10 10:59:47 -05:00
Eivar Morales
98faf7ed55 added information for M1 Users (#2044)
* added information for M1 Users

* Update README.md

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

---------

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

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

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

* Added default values to yaml file.

* added set_background_color() function to Scene class

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

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

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

* Support `mobject.animate.` autocompletion in IPython

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

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

* Fix typos in GLSL comments

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

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

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

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

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

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

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

* Allow for specifying true normals and orientation of Sphere

* Change miter threshold on stroke shader

* Add get_start_and_end to DashedLine

* Add min_total_width option to DecimalNumber

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

Note, this feels suboptimal

* Add LineBrace

* Update font_size adjustment in Tex

* Add scale_factor parameter to BulletedList.fade_all_but

* Minor import tweaks

* Add play_sound

* Small if -> elif update

* Always use Group for FadeTransform

* Use time_spanned_alpha in ChangingDecimal

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

* Fix clock animation

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

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

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

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

* Allow for specifying true normals and orientation of Sphere

* Change miter threshold on stroke shader

* Add get_start_and_end to DashedLine

* Add min_total_width option to DecimalNumber

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

Note, this feels suboptimal

* Add LineBrace

* Update font_size adjustment in Tex

* Add scale_factor parameter to BulletedList.fade_all_but

* Minor import tweaks

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

* performance improved in `set_color_by_rgba_func`

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

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

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

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

* Allow for specifying true normals and orientation of Sphere

* Change miter threshold on stroke shader

* Add get_start_and_end to DashedLine

* Add min_total_width option to DecimalNumber

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

Note, this feels suboptimal

* Add LineBrace

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

* resolving a minor bug in string_mobject

* removing bug of string validation

* Update manimlib/default_config.yml

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

* Update manimlib/extract_scene.py

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

* update search scene names

---------

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

* Directly print traceback

Since the shell.showtraceback is giving some issues

* Make InteracrtiveSceneEmbed into a class

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

* Move remaining checkpoint_paste logic into scene_embed.py

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

* Change null key to be the empty string

* Ensure temporary svg paths for Text are deleted

* Remove unused dict_ops.py functions

* Remove break_into_partial_movies from file_writer configuration

* Rewrite guarantee_existence using Path

* Clean up SceneFileWriter

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

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

* Add --subdivide CLI arg

* Remove add_extension_if_not_present

* Remove get_sorted_integer_files

* Have find_file return Path

* Minor clean up

* Clean up num_tex_symbols

* Fix find_file

* Minor cleanup for extract_scene.py

* Add preview_frame_while_skipping option to scene config

* Use shell.showtraceback function

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

* Replace DEGREES -> DEG

* Add arg to clear the cache

* Separate out full_tex_to_svg from tex_to_svg

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

* Bug fix in handling BlankScene

* Make checkpoint_states an instance variable of CheckpointManager

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

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

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

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

* Remove input_file_path arg from SceneFileWriter

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

* Simplify get_output_directory

* Swap order of preamble and additional preamble

* Minor stylistic tweak

* Have UnitInterval pass on kwargs to NumberLine

* Add simple get_dist function

* Have TracedPath always update to the stroke configuration passed in

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

* Always call Mobject.update upon adding an updater

* Add Surface.uv_to_point

* Make sure Surface.set_opacity takes in a recurse option

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

* Update comments
2024-12-13 13:23:50 -08:00
syhner
c13d2a946b fix typos (#2270) 2024-12-13 11:05:48 -08:00
Grant Sanderson
0c69ab6a32 Update version number 2024-12-12 20:54:37 -06:00
54 changed files with 1081 additions and 248 deletions

2
.gitignore vendored
View File

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

View File

@@ -24,7 +24,7 @@ Note, there are two versions of manim. This repository began as a personal proj
Manim runs on Python 3.7 or higher.
System requirements are [FFmpeg](https://ffmpeg.org/), [OpenGL](https://www.opengl.org/) and [LaTeX](https://www.latex-project.org) (optional, if you want to use LaTeX).
For Linux, [Pango](https://pango.gnome.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
For Linux, [Pango](https://pango.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
### Directly
@@ -69,13 +69,18 @@ manim-render example_scenes.py OpeningManimExample
```sh
brew install ffmpeg mactex
```
2. If you are using an ARM-based processor, install Cairo.
```sh
arch -arm64 brew install pkg-config cairo
```
2. Install latest version of manim using these command.
3. Install latest version of manim using these command.
```sh
git clone https://github.com/3b1b/manim.git
cd manim
pip install -e .
manimgl example_scenes.py OpeningManimExample
manimgl example_scenes.py OpeningManimExample (make sure to add manimgl to path first.)
```
## Anaconda Install

View File

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

View File

@@ -326,7 +326,7 @@ class UpdatersExample(Scene):
)
self.wait()
# In general, you can alway call Mobject.add_updater, and pass in
# In general, you can always call Mobject.add_updater, and pass in
# a function that you want to be called on every frame. The function
# should take in either one argument, the mobject, or two arguments,
# the mobject and the amount of time since the last frame.
@@ -534,7 +534,7 @@ class TexAndNumbersExample(Scene):
rate_func=there_and_back,
)
# By default, tex.make_number_changeable replaces the first occurance
# By default, tex.make_number_changeable replaces the first occurrence
# of the number,but by passing replace_all=True it replaces all and
# returns a group of the results
exponents = tex.make_number_changeable("2", replace_all=True)

View File

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

View File

@@ -43,7 +43,7 @@ class AnimationGroup(Animation):
mobs = remove_list_redundancies([a.mobject for a in self.animations])
if group is not None:
self.group = group
if group_type is not None:
elif group_type is not None:
self.group = group_type(*mobs)
elif all(isinstance(anim.mobject, VMobject) for anim in animations):
self.group = VGroup(*mobs)

View File

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

View File

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

View File

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

View File

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

View File

@@ -174,6 +174,7 @@ def parse_cli():
parser.add_argument(
"--fps",
help="Frame rate, as an integer",
type=int,
)
parser.add_argument(
"-c", "--color",
@@ -256,7 +257,7 @@ def update_camera_config(config: Dict, args: Namespace):
if args.color:
try:
camera_config.background_color = colour.Color(args.color)
except Exception:
except Exception as err:
log.error("Please use a valid color")
log.error(err)
sys.exit(2)
@@ -385,8 +386,11 @@ def get_output_directory(args: Namespace, config: Dict) -> str:
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("_"))
if str(file_path).startswith(dir_config.removed_mirror_prefix):
rel_path = file_path.relative_to(dir_config.removed_mirror_prefix)
rel_path = Path(str(rel_path).lstrip("_"))
else:
rel_path = file_path.stem
out_dir = Path(out_dir, rel_path).with_suffix("")
return out_dir

View File

@@ -62,7 +62,7 @@ RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT
PI: float = np.pi
TAU: float = 2 * PI
DEG: float = TAU / 360
DEGREES = DEG # Many older animations use teh full name
DEGREES = DEG # Many older animations use the full name
# Nice to have a constant for readability
# when juxtaposed with expressions like 30 * DEG
RADIANS: float = 1
@@ -130,6 +130,9 @@ PINK: ManimColor = manim_config.colors.pink
LIGHT_PINK: ManimColor = manim_config.colors.light_pink
GREEN_SCREEN: ManimColor = manim_config.colors.green_screen
ORANGE: ManimColor = manim_config.colors.orange
PURE_RED: ManimColor = manim_config.colors.pure_red
PURE_GREEN: ManimColor = manim_config.colors.pure_green
PURE_BLUE: ManimColor = manim_config.colors.pure_blue
MANIM_COLORS: List[ManimColor] = list(manim_config.colors.values())
@@ -145,3 +148,12 @@ PURPLE: ManimColor = PURPLE_C
GREY: ManimColor = GREY_C
COLORMAP_3B1B: List[ManimColor] = [BLUE_E, GREEN, YELLOW, RED]
# Default mobject colors should be configurable just like background color
# DEFAULT_MOBJECT_COLOR is mainly for text, tex, line, etc... mobjects. Default is WHITE
# DEFAULT_LIGHT_COLOR is mainly for things like axes, arrows, annulus and other lightly colored mobjects. Default is GREY_B
DEFAULT_MOBJECT_COLOR: ManimColor = manim_config.mobject.default_mobject_color or WHITE
DEFAULT_LIGHT_COLOR: ManimColor = manim_config.mobject.default_light_color or GREY_B
DEFAULT_VMOBJECT_STROKE_COLOR : ManimColor = manim_config.vmobject.default_stroke_color or GREY_A
DEFAULT_VMOBJECT_FILL_COLOR : ManimColor = manim_config.vmobject.default_fill_color or GREY_C

View File

@@ -5,15 +5,15 @@
# 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
# Alternatively, you can create it wherever you like, and on running
# manim, pass in `--config_file /path/to/custom/config/file.yml`
directories:
# Set this to true if you want the path to video files
# to match the directory structure of the path to the
# sourcecode generating that video
# source code generating that video
mirror_module_path: False
# Manim may write to and read from teh file system, e.g.
# Manim may write to and read from the file system, e.g.
# to render videos and to look for svg/png assets. This
# will specify where those assets live, with a base directory,
# and various subdirectory names within it
@@ -44,7 +44,7 @@ window:
# 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
# position: (500, 500) # Specific position, in pixel coordinates, for upper right corner
# size: (1920, 1080) # Specific size, in pixels
camera:
resolution: (1920, 1080)
@@ -71,10 +71,16 @@ scene:
default_wait_time: 1.0
vmobject:
default_stroke_width: 4.0
default_stroke_color: "#DDDDDD" # Default is GREY_A
default_fill_color: "#888888" # Default is GREY_C
mobject:
default_mobject_color: "#FFFFFF" # Default is WHITE
default_light_color: "#BBBBBB" # Default is GREY_B
tex:
# See tex_templates.yml
template: "default"
text:
# font: "Cambria Math"
font: "Consolas"
alignment: "LEFT"
embed:
@@ -101,19 +107,19 @@ sizes:
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'
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"
@@ -169,6 +175,9 @@ colors:
light_pink: "#DC75CD"
green_screen: "#00FF00"
orange: "#FF862F"
pure_red: "#FF0000"
pure_green: "#00FF00"
pure_blue: "#0000FF"
# Can be DEBUG / INFO / WARNING / ERROR / CRITICAL
log_level: "INFO"
universal_import_line: "from manimlib import *"

View File

@@ -12,6 +12,7 @@ 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
@@ -142,7 +143,7 @@ def get_indent(code_lines: list[str], line_number: int) -> str:
return n_spaces * " "
def insert_embed_line_to_module(module: Module, line_number: int):
def insert_embed_line_to_module(module: Module, run_config: Dict) -> None:
"""
This is hacky, but convenient. When user includes the argument "-e", it will try
to recreate a file that inserts the line `self.embed()` into the end of the scene's
@@ -150,27 +151,42 @@ def insert_embed_line_to_module(module: Module, line_number: int):
the last line in the sourcefile which includes that string.
"""
lines = inspect.getsource(module).splitlines()
line_number = run_config.embed_line
# Add the relevant embed line to the code
indent = get_indent(lines, line_number)
lines.insert(line_number, indent + "self.embed()")
new_code = "\n".join(lines)
# When the user executes the `-e <line_number>` command
# without specifying scene_names, the nearest class name above
# `<line_number>` will be automatically used as 'scene_names'.
if not run_config.scene_names:
classes = list(filter(lambda line: line.startswith("class"), lines[:line_number]))
if classes:
from re import search
scene_name = search(r"(\w+)\(", classes[-1])
run_config.update(scene_names=[scene_name.group(1)])
else:
log.error(f"No 'class' found above {line_number}!")
# Execute the code, which presumably redefines the user's
# scene to include this embed line, within the relevant module.
code_object = compile(new_code, module.__name__, 'exec')
exec(code_object, module.__dict__)
def get_module(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)
def get_module(run_config: Dict) -> Module:
module = ModuleLoader.get_module(run_config.file_name, run_config.is_reload)
if run_config.embed_line:
insert_embed_line_to_module(module, run_config)
return module
def main(scene_config: Dict, run_config: Dict):
module = get_module(run_config.file_name, run_config.embed_line, run_config.is_reload)
module = get_module(run_config)
all_scene_classes = get_scene_classes(module)
scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config)
if len(scenes) == 0:

View File

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

View File

@@ -6,7 +6,7 @@ import numbers
import numpy as np
import itertools as it
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, RED, DEFAULT_MOBJECT_COLOR
from manimlib.constants import DEG, PI
from manimlib.constants import DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
@@ -412,15 +412,19 @@ class CoordinateSystem(ABC):
rect.set_fill(negative_color)
return result
def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=0.5):
if not hasattr(graph, "x_range"):
raise Exception("Argument `graph` must have attribute `x_range`")
def get_area_under_graph(self, graph, x_range=None, fill_color=BLUE, fill_opacity=0.5):
if x_range is None:
x_range = [
self.x_axis.p2n(graph.get_start()),
self.x_axis.p2n(graph.get_end()),
]
alpha_bounds = [
inverse_interpolate(*graph.x_range, x)
inverse_interpolate(*graph.x_range[:2], x)
for x in x_range
]
sub_graph = graph.copy()
sub_graph.clear_updaters()
sub_graph.pointwise_become_partial(graph, *alpha_bounds)
sub_graph.add_line_to(self.c2p(x_range[1], 0))
sub_graph.add_line_to(self.c2p(x_range[0], 0))
@@ -617,7 +621,7 @@ class ThreeDAxes(Axes):
class NumberPlane(Axes):
default_axis_config: dict = dict(
stroke_color=WHITE,
stroke_color=DEFAULT_MOBJECT_COLOR,
stroke_width=2,
include_ticks=False,
include_tip=False,
@@ -638,7 +642,10 @@ class NumberPlane(Axes):
stroke_opacity=1,
),
# Defaults to a faded version of line_config
faded_line_style: dict = dict(),
faded_line_style: dict = dict(
stroke_width=1,
stroke_opacity=0.25,
),
faded_line_ratio: int = 4,
make_smooth_after_applying_functions: bool = True,
**kwargs
@@ -651,14 +658,8 @@ class NumberPlane(Axes):
self.init_background_lines()
def init_background_lines(self) -> None:
if not self.faded_line_style:
style = dict(self.background_line_style)
# For anything numerical, like stroke_width
# and stroke_opacity, chop it in half
for key in style:
if isinstance(style[key], numbers.Number):
style[key] *= 0.5
self.faded_line_style = style
if "stroke_color" not in self.faded_line_style:
self.faded_line_style["stroke_color"] = self.background_line_style["stroke_color"]
self.background_lines, self.faded_lines = self.get_lines()
self.background_lines.set_style(**self.background_line_style)
@@ -726,11 +727,10 @@ class NumberPlane(Axes):
class ComplexPlane(NumberPlane):
def number_to_point(self, number: complex | float) -> Vect3:
number = complex(number)
return self.coords_to_point(number.real, number.imag)
def number_to_point(self, number: complex | float | np.array) -> Vect3:
return self.coords_to_point(np.real(number), np.imag(number))
def n2p(self, number: complex | float) -> Vect3:
def n2p(self, number: complex | float | np.array) -> Vect3:
return self.number_to_point(number)
def point_to_number(self, point: Vect3) -> complex:
@@ -770,13 +770,6 @@ class ComplexPlane(NumberPlane):
axis = self.get_x_axis()
value = z.real
number_mob = axis.get_number_mobject(value, font_size=font_size, **kwargs)
# For -i, remove the "1"
if z.imag == -1:
number_mob.remove(number_mob[1])
number_mob[0].next_to(
number_mob[1], LEFT,
buff=number_mob[0].get_width() / 4
)
self.coordinate_labels.add(number_mob)
self.add(self.coordinate_labels)
return self

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ from manimlib.constants import DOWN, IN, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import MED_SMALL_BUFF
from manimlib.constants import TAU
from manimlib.constants import WHITE
from manimlib.constants import DEFAULT_MOBJECT_COLOR
from manimlib.event_handler import EVENT_DISPATCHER
from manimlib.event_handler.event_listner import EventListener
from manimlib.event_handler.event_type import EventType
@@ -52,7 +52,7 @@ SubmobjectType = TypeVar('SubmobjectType', bound='Mobject')
if TYPE_CHECKING:
from typing import Callable, Iterator, Union, Tuple, Optional, Any
import numpy.typing as npt
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, UniformDict, Self
from manimlib.typing import ManimColor, Vect3, Vect4Array, Vect3Array, UniformDict, Self
from moderngl.context import Context
T = TypeVar('T')
@@ -78,7 +78,7 @@ class Mobject(object):
def __init__(
self,
color: ManimColor = WHITE,
color: ManimColor = DEFAULT_MOBJECT_COLOR,
opacity: float = 1.0,
shading: Tuple[float, float, float] = (0.0, 0.0, 0.0),
# For shaders
@@ -160,10 +160,10 @@ class Mobject(object):
return self
@property
def animate(self) -> _AnimationBuilder:
def animate(self) -> _AnimationBuilder | Self:
"""
Methods called with Mobject.animate.method() can be passed
into a Scene.play call, as if you were calling
into a Scene.play call, as if you were calling
ApplyMethod(mobject.method)
Borrowed from https://github.com/ManimCommunity/manim/
@@ -287,10 +287,7 @@ class Mobject(object):
about_point = self.get_bounding_box_point(about_edge)
for mob in self.get_family():
arrs = []
if mob.has_points():
for key in mob.pointlike_data_keys:
arrs.append(mob.data[key])
arrs = [mob.data[key] for key in mob.pointlike_data_keys if mob.has_points()]
if works_on_bounding_box:
arrs.append(mob.get_bounding_box())
@@ -307,12 +304,15 @@ class Mobject(object):
parent.refresh_bounding_box()
return self
# Others related to points
@affects_data
def match_points(self, mobject: Mobject) -> Self:
self.set_points(mobject.get_points())
self.resize_points(len(mobject.data), resize_func=resize_preserving_order)
for key in self.pointlike_data_keys:
self.data[key][:] = mobject.data[key]
return self
# Others related to points
def get_points(self) -> Vect3Array:
return self.data["point"]
@@ -842,6 +842,7 @@ class Mobject(object):
if call:
self.update(dt=0)
self.refresh_has_updater_status()
self.update()
return self
def insert_updater(self, update_func: Updater, index=0):
@@ -1231,8 +1232,9 @@ class Mobject(object):
def set_z(self, z: float, direction: Vect3 = ORIGIN) -> Self:
return self.set_coord(z, 2, direction)
def set_z_index(self, z_index: int) -> Self:
self.z_index = z_index
def set_z_index(self, z_index: int, recurse=True) -> Self:
for mob in self.get_family(recurse):
mob.z_index = z_index
return self
def space_out_submobjects(self, factor: float = 1.5, **kwargs) -> Self:
@@ -1283,6 +1285,14 @@ class Mobject(object):
self.scale((length + buff) / length)
return self
def put_start_on(self, point: Vect3) -> Self:
self.shift(point - self.get_start())
return self
def put_end_on(self, point: Vect3) -> Self:
self.shift(point - self.get_end())
return self
def put_start_and_end_on(self, start: Vect3, end: Vect3) -> Self:
curr_start, curr_end = self.get_start_and_end()
curr_vect = curr_end - curr_start
@@ -1319,20 +1329,19 @@ class Mobject(object):
def set_color_by_rgba_func(
self,
func: Callable[[Vect3], Vect4],
func: Callable[[Vect3Array], Vect4Array],
recurse: bool = True
) -> Self:
"""
Func should take in a point in R3 and output an rgba value
"""
for mob in self.get_family(recurse):
rgba_array = [func(point) for point in mob.get_points()]
mob.set_rgba_array(rgba_array)
mob.set_rgba_array(func(mob.get_points()))
return self
def set_color_by_rgb_func(
self,
func: Callable[[Vect3], Vect3],
func: Callable[[Vect3Array], Vect3Array],
opacity: float = 1,
recurse: bool = True
) -> Self:
@@ -1340,8 +1349,9 @@ class Mobject(object):
Func should take in a point in R3 and output an rgb value
"""
for mob in self.get_family(recurse):
rgba_array = [[*func(point), opacity] for point in mob.get_points()]
mob.set_rgba_array(rgba_array)
points = mob.get_points()
opacity = np.ones((points.shape[0], 1)) * opacity
mob.set_rgba_array(np.hstack((func(points), opacity)))
return self
@affects_family_data
@@ -1360,7 +1370,7 @@ class Mobject(object):
rgbs = resize_with_interpolation(rgbs, len(data))
data[name][:, :3] = rgbs
if opacity is not None:
if not isinstance(opacity, (float, int)):
if not isinstance(opacity, (float, int, np.floating)):
opacity = resize_with_interpolation(np.array(opacity), len(data))
data[name][:, 3] = opacity
return self
@@ -2255,6 +2265,18 @@ class _AnimationBuilder:
def __call__(self, **kwargs):
return self.set_anim_args(**kwargs)
def __dir__(self) -> list[str]:
"""
Extend attribute list of _AnimationBuilder object to include mobject attributes
for better autocompletion in the IPython terminal when using interactive mode.
"""
methods = super().__dir__()
mobject_methods = [
attr for attr in dir(self.mobject)
if not attr.startswith('_')
]
return sorted(set(methods+mobject_methods))
def set_anim_args(self, **kwargs):
'''
You can change the args of :class:`~manimlib.animation.transform.Transform`, such as

View File

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

View File

@@ -4,7 +4,7 @@ from functools import lru_cache
import numpy as np
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import WHITE
from manimlib.constants import DEFAULT_MOBJECT_COLOR
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.text_mobject import Text
from manimlib.mobject.types.vectorized_mobject import VMobject
@@ -35,17 +35,19 @@ class DecimalNumber(VMobject):
def __init__(
self,
number: float | complex = 0,
color: ManimColor = WHITE,
color: ManimColor = DEFAULT_MOBJECT_COLOR,
stroke_width: float = 0,
fill_opacity: float = 1.0,
fill_border_width: float = 0.5,
num_decimal_places: int = 2,
min_total_width: Optional[int] = 0,
include_sign: bool = False,
group_with_commas: bool = True,
digit_buff_per_font_unit: float = 0.001,
show_ellipsis: bool = False,
unit: str | None = None, # Aligned to bottom unless it starts with "^"
include_background_rectangle: bool = False,
hide_zero_components_on_complex: bool = True,
edge_to_fix: Vect3 = LEFT,
font_size: float = 48,
text_config: dict = dict(), # Do not pass in font_size here
@@ -54,10 +56,12 @@ class DecimalNumber(VMobject):
self.num_decimal_places = num_decimal_places
self.include_sign = include_sign
self.group_with_commas = group_with_commas
self.min_total_width = min_total_width
self.digit_buff_per_font_unit = digit_buff_per_font_unit
self.show_ellipsis = show_ellipsis
self.unit = unit
self.include_background_rectangle = include_background_rectangle
self.hide_zero_components_on_complex = hide_zero_components_on_complex
self.edge_to_fix = edge_to_fix
self.font_size = font_size
self.text_config = dict(text_config)
@@ -118,7 +122,14 @@ class DecimalNumber(VMobject):
def get_num_string(self, number: float | complex) -> str:
if isinstance(number, complex):
formatter = self.get_complex_formatter()
if self.hide_zero_components_on_complex and number.imag == 0:
number = number.real
formatter = self.get_formatter()
elif self.hide_zero_components_on_complex and number.real == 0:
number = number.imag
formatter = self.get_formatter() + "i"
else:
formatter = self.get_complex_formatter()
else:
formatter = self.get_formatter()
if self.num_decimal_places == 0 and isinstance(number, float):
@@ -167,6 +178,7 @@ class DecimalNumber(VMobject):
"include_sign",
"group_with_commas",
"num_decimal_places",
"min_total_width",
]
])
config.update(kwargs)
@@ -176,6 +188,7 @@ class DecimalNumber(VMobject):
config.get("field_name", ""),
":",
"+" if config["include_sign"] else "",
"0" + str(config.get("min_total_width", "")) if config.get("min_total_width") else "",
"," if config["group_with_commas"] else "",
f".{ndp}f" if ndp > 0 else "d",
"}",

View File

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

View File

@@ -3,7 +3,7 @@ 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 BLACK, RED, YELLOW, DEFAULT_MOBJECT_COLOR
from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR
from manimlib.constants import SMALL_BUFF
from manimlib.mobject.geometry import Line
@@ -79,7 +79,8 @@ class BackgroundRectangle(SurroundingRectangle):
stroke_width: float | None = None,
fill_color: ManimColor | None = None,
fill_opacity: float | None = None,
family: bool = True
family: bool = True,
**kwargs
) -> Self:
# Unchangeable style, except for fill_opacity
VMobject.set_style(
@@ -117,7 +118,7 @@ class Underline(Line):
self,
mobject: Mobject,
buff: float = SMALL_BUFF,
stroke_color=WHITE,
stroke_color=DEFAULT_MOBJECT_COLOR,
stroke_width: float | Sequence[float] = [0, 3, 3, 0],
stretch_factor=1.2,
**kwargs

View File

@@ -6,7 +6,7 @@ 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 DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL, UP
from manimlib.constants import PI
from manimlib.animation.composition import AnimationGroup
from manimlib.animation.fading import FadeIn
@@ -174,3 +174,12 @@ class BraceLabel(VMobject):
class BraceText(BraceLabel):
label_constructor: type = TexText
class LineBrace(Brace):
def __init__(self, line: Line, direction=UP, **kwargs):
angle = line.get_angle()
line.rotate(-angle)
super().__init__(line, direction, **kwargs)
line.rotate(angle)
self.rotate(angle, about_point=line.get_center())

View File

@@ -344,6 +344,8 @@ class ClockPassesTime(AnimationGroup):
angle=12 * hour_radians,
**rot_kwargs
),
group=clock,
run_time=run_time,
**kwargs
)

View File

@@ -4,7 +4,7 @@ from functools import reduce
import operator as op
import re
from manimlib.constants import BLACK, WHITE
from manimlib.constants import BLACK, DEFAULT_MOBJECT_COLOR
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.tex_file_writing import latex_to_svg
@@ -26,10 +26,10 @@ class SingleStringTex(SVGMobject):
self,
tex_string: str,
height: float | None = None,
fill_color: ManimColor = WHITE,
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
fill_opacity: float = 1.0,
stroke_width: float = 0,
svg_default: dict = dict(fill_color=WHITE),
svg_default: dict = dict(fill_color=DEFAULT_MOBJECT_COLOR),
path_string_config: dict = dict(),
font_size: int = 48,
alignment: str = R"\centering",

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from manimlib.constants import MED_SMALL_BUFF, WHITE, GREY_C
from manimlib.constants import MED_SMALL_BUFF, DEFAULT_MOBJECT_COLOR, GREY_C
from manimlib.constants import DOWN, LEFT, RIGHT, UP
from manimlib.constants import FRAME_WIDTH
from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF
@@ -22,7 +22,7 @@ class BulletedList(VGroup):
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}",
@@ -36,14 +36,17 @@ class BulletedList(VGroup):
self.arrange(DOWN, buff=buff, aligned_edge=aligned_edge)
def fade_all_but(self, index: int, opacity: float = 0.25) -> None:
def fade_all_but(self, index: int, opacity: float = 0.25, scale_factor=0.7) -> None:
max_dot_height = max([item[0].get_height() for item in self.submobjects])
for i, part in enumerate(self.submobjects):
trg_dot_height = (1.0 if i == index else scale_factor) * max_dot_height
part.set_fill(opacity=(1.0 if i == index else opacity))
part.scale(trg_dot_height / part[0].get_height(), about_edge=LEFT)
class TexTextFromPresetString(TexText):
tex: str = ""
default_color: ManimColor = WHITE
default_color: ManimColor = DEFAULT_MOBJECT_COLOR
def __init__(self, **kwargs):
super().__init__(

View File

@@ -6,7 +6,7 @@ import re
from scipy.optimize import linear_sum_assignment
from scipy.spatial.distance import cdist
from manimlib.constants import WHITE
from manimlib.constants import DEFAULT_MOBJECT_COLOR
from manimlib.logger import log
from manimlib.mobject.svg.svg_mobject import SVGMobject
from manimlib.mobject.types.vectorized_mobject import VMobject
@@ -46,11 +46,11 @@ class StringMobject(SVGMobject, ABC):
def __init__(
self,
string: str,
fill_color: ManimColor = WHITE,
fill_color: ManimColor = DEFAULT_MOBJECT_COLOR,
fill_border_width: float = 0.5,
stroke_color: ManimColor = WHITE,
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
stroke_width: float = 0,
base_color: ManimColor = WHITE,
base_color: ManimColor = DEFAULT_MOBJECT_COLOR,
isolate: Selector = (),
protect: Selector = (),
# When set to true, only the labelled svg is
@@ -60,7 +60,7 @@ class StringMobject(SVGMobject, ABC):
**kwargs
):
self.string = string
self.base_color = base_color or WHITE
self.base_color = base_color or DEFAULT_MOBJECT_COLOR
self.isolate = isolate
self.protect = protect
self.use_labelled_svg = use_labelled_svg
@@ -122,8 +122,8 @@ class StringMobject(SVGMobject, ABC):
# of submobject which are and use those for labels
unlabelled_submobs = submobs
labelled_content = self.get_content(is_labelled=True)
labelled_file = self.get_file_path_by_content(labelled_content)
labelled_submobs = super().mobjects_from_file(labelled_file)
labelled_file = self.get_svg_string_by_content(labelled_content)
labelled_submobs = super().mobjects_from_svg_string(labelled_file)
self.labelled_submobs = labelled_submobs
self.unlabelled_submobs = unlabelled_submobs

View File

@@ -8,6 +8,7 @@ import io
from pathlib import Path
from manimlib.constants import RIGHT
from manimlib.constants import TAU
from manimlib.logger import log
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Line
@@ -16,8 +17,10 @@ from manimlib.mobject.geometry import Polyline
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import RoundedRectangle
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import quadratic_bezier_points_for_arc
from manimlib.utils.images import get_full_vector_image_path
from manimlib.utils.iterables import hash_obj
from manimlib.utils.space_ops import rotation_about_z
from typing import TYPE_CHECKING
if TYPE_CHECKING:
@@ -70,7 +73,7 @@ class SVGMobject(VMobject):
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)
self.svg_string = self.file_name_to_svg_string(self.file_name)
else:
raise Exception("Must specify either a file_name or svg_string SVGMobject")
@@ -300,8 +303,9 @@ class VMobjectFromSVGPath(VMobject):
path_obj: se.Path,
**kwargs
):
# Get rid of arcs
path_obj.approximate_arcs_with_quads()
# caches (transform.inverse(), rot, shift)
self.transform_cache: tuple[se.Matrix, np.ndarray, np.ndarray] | None = None
self.path_obj = path_obj
super().__init__(**kwargs)
@@ -328,13 +332,55 @@ class VMobjectFromSVGPath(VMobject):
}
for segment in self.path_obj:
segment_class = segment.__class__
func, attr_names = segment_class_to_func_map[segment_class]
points = [
_convert_point_to_3d(*segment.__getattribute__(attr_name))
for attr_name in attr_names
]
func(*points)
if segment_class is se.Arc:
self.handle_arc(segment)
else:
func, attr_names = segment_class_to_func_map[segment_class]
points = [
_convert_point_to_3d(*segment.__getattribute__(attr_name))
for attr_name in attr_names
]
func(*points)
# Get rid of the side effect of trailing "Z M" commands.
if self.has_new_path_started():
self.resize_points(self.get_num_points() - 2)
def handle_arc(self, arc: se.Arc) -> None:
if self.transform_cache is not None:
transform, rot, shift = self.transform_cache
else:
# The transform obtained in this way considers the combined effect
# of all parent group transforms in the SVG.
# Therefore, the arc can be transformed inversely using this transform
# to correctly compute the arc path before transforming it back.
transform = se.Matrix(self.path_obj.values.get('transform', ''))
rot = np.array([
[transform.a, transform.c],
[transform.b, transform.d]
])
shift = np.array([transform.e, transform.f, 0])
transform.inverse()
self.transform_cache = (transform, rot, shift)
# Apply inverse transformation to the arc so that its path can be correctly computed
arc *= transform
# The value of n_components is chosen based on the implementation of VMobject.arc_to
n_components = int(np.ceil(8 * abs(arc.sweep) / TAU))
# Obtain the required angular segments on the unit circle
arc_points = quadratic_bezier_points_for_arc(arc.sweep, n_components)
arc_points @= np.array(rotation_about_z(arc.get_start_t())).T
# Transform to an ellipse, considering rotation and translating the ellipse center
arc_points[:, 0] *= arc.rx
arc_points[:, 1] *= arc.ry
arc_points @= np.array(rotation_about_z(arc.get_rotation().as_radians)).T
arc_points += [*arc.center, 0]
# Transform back
arc_points[:, :2] @= rot.T
arc_points += shift
self.append_points(arc_points[1:])

View File

@@ -18,7 +18,7 @@ if TYPE_CHECKING:
from manimlib.typing import ManimColor, Span, Selector, Self
SCALE_FACTOR_PER_FONT_POINT = 0.001
TEX_MOB_SCALE_FACTOR = 0.001
class Tex(StringMobject):
@@ -49,7 +49,6 @@ class Tex(StringMobject):
if not tex_string.strip():
tex_string = R"\\"
self.font_size = font_size
self.tex_string = tex_string
self.alignment = alignment
self.template = template
@@ -64,13 +63,16 @@ class Tex(StringMobject):
)
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
self.scale(SCALE_FACTOR_PER_FONT_POINT * font_size)
self.scale(TEX_MOB_SCALE_FACTOR * font_size)
self.font_size = font_size # Important for this to go after the scale call
def get_svg_string_by_content(self, content: str) -> str:
return latex_to_svg(content, self.template, self.additional_preamble, short_tex=self.tex_string)
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
self.font_size *= scale_factor
if hasattr(self, "font_size"):
self.font_size *= scale_factor
return self
# Parsing
@@ -240,15 +242,10 @@ class Tex(StringMobject):
decimal_mobs = []
for part in parts:
if "." in substr:
num_decimal_places = len(substr.split(".")[1])
else:
num_decimal_places = 0
decimal_mob = DecimalNumber(
float(value),
num_decimal_places=num_decimal_places,
**config,
)
if "num_decimal_places" not in config:
ndp = len(substr.split(".")[1]) if "." in substr else 0
config["num_decimal_places"] = ndp
decimal_mob = DecimalNumber(float(value), **config)
decimal_mob.replace(part)
decimal_mob.match_style(part)
if len(part) > 1:

View File

@@ -176,9 +176,6 @@ class MarkupText(StringMobject):
self.disable_ligatures = disable_ligatures
self.isolate = isolate
if not isinstance(self, Text):
self.validate_markup_string(text)
super().__init__(text, height=height, **kwargs)
if self.t2g:

View File

@@ -97,20 +97,27 @@ class Sphere(Surface):
v_range: Tuple[float, float] = (0, PI),
resolution: Tuple[int, int] = (101, 51),
radius: float = 1.0,
true_normals: bool = True,
clockwise=False,
**kwargs,
):
self.radius = radius
self.clockwise = clockwise
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs
)
# Add bespoke normal specification to avoid issue at poles
if true_normals:
self.data['d_normal_point'] = self.data['point'] * ((radius + self.normal_nudge) / radius)
def uv_func(self, u: float, v: float) -> np.ndarray:
sign = -1 if self.clockwise else +1
return self.radius * np.array([
math.cos(u) * math.sin(v),
math.sin(u) * math.sin(v),
math.cos(sign * u) * math.sin(v),
math.sin(sign * u) * math.sin(v),
-math.cos(v)
])

View File

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

View File

@@ -5,6 +5,7 @@ from functools import wraps
import numpy as np
from manimlib.constants import GREY_A, GREY_C, GREY_E
from manimlib.constants import DEFAULT_VMOBJECT_FILL_COLOR, DEFAULT_VMOBJECT_STROKE_COLOR
from manimlib.constants import BLACK
from manimlib.constants import DEFAULT_STROKE_WIDTH
from manimlib.constants import DEG
@@ -53,9 +54,6 @@ if TYPE_CHECKING:
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Self
from moderngl.context import Context
DEFAULT_STROKE_COLOR = GREY_A
DEFAULT_FILL_COLOR = GREY_C
class VMobject(Mobject):
data_dtype: np.dtype = np.dtype([
@@ -99,9 +97,9 @@ class VMobject(Mobject):
fill_border_width: float = 0.0,
**kwargs
):
self.fill_color = fill_color or color or DEFAULT_FILL_COLOR
self.fill_color = fill_color or color or DEFAULT_VMOBJECT_FILL_COLOR
self.fill_opacity = fill_opacity
self.stroke_color = stroke_color or color or DEFAULT_STROKE_COLOR
self.stroke_color = stroke_color or color or DEFAULT_VMOBJECT_STROKE_COLOR
self.stroke_opacity = stroke_opacity
self.stroke_width = stroke_width
self.stroke_behind = stroke_behind
@@ -185,7 +183,7 @@ class VMobject(Mobject):
if width is not None:
for mob in self.get_family(recurse):
data = mob.data if mob.get_num_points() > 0 else mob._data_defaults
if isinstance(width, (float, int)):
if isinstance(width, (float, int, np.floating)):
data['stroke_width'][:, 0] = width
else:
data['stroke_width'][:, 0] = resize_with_interpolation(
@@ -305,6 +303,11 @@ class VMobject(Mobject):
self.set_stroke(opacity=opacity, recurse=recurse)
return self
def set_color_by_proportion(self, prop_to_color: Callable[[float], Color]) -> Self:
colors = list(map(prop_to_color, np.linspace(0, 1, self.get_num_points())))
self.set_stroke(color=colors)
return self
def set_anti_alias_width(self, anti_alias_width: float, recurse: bool = True) -> Self:
self.set_uniform(recurse, anti_alias_width=anti_alias_width)
return self

View File

@@ -6,7 +6,7 @@ import numpy as np
from scipy.integrate import solve_ivp
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import WHITE
from manimlib.constants import DEFAULT_MOBJECT_COLOR
from manimlib.animation.indication import VShowPassingFlash
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
@@ -145,6 +145,7 @@ class VectorField(VMobject):
func: Callable[[VectArray], VectArray],
# Typically a set of Axes or NumberPlane
coordinate_system: CoordinateSystem,
sample_coords: Optional[VectArray] = None,
density: float = 2.0,
magnitude_range: Optional[Tuple[float, float]] = None,
color: Optional[ManimColor] = None,
@@ -168,14 +169,17 @@ class VectorField(VMobject):
self.norm_to_opacity_func = norm_to_opacity_func
# Search for sample_points
self.sample_coords = get_sample_coords(coordinate_system, density)
if sample_coords is not None:
self.sample_coords = sample_coords
else:
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()
self.max_displayed_vect_len = max_vect_len * coordinate_system.x_axis.get_unit_size()
# Prepare the color map
if magnitude_range is None:
@@ -347,7 +351,7 @@ class StreamLines(VGroup):
cutoff_norm: float = 15,
# Style info
stroke_width: float = 1.0,
stroke_color: ManimColor = WHITE,
stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR,
stroke_opacity: float = 1,
color_by_magnitude: bool = True,
magnitude_range: Tuple[float, float] = (0, 2.0),
@@ -386,7 +390,6 @@ class StreamLines(VGroup):
def draw_lines(self) -> None:
lines = []
origin = self.coordinate_system.get_origin()
# Todo, it feels like coordinate system should just have
# the ODE solver built into it, no?
@@ -406,7 +409,7 @@ class StreamLines(VGroup):
noise_factor = self.noise_factor
if noise_factor is None:
noise_factor = (cs.get_x_unit_size() / self.density) * 0.5
noise_factor = (cs.x_axis.get_unit_size() / self.density) * 0.5
return np.array([
coords + noise_factor * np.random.random(coords.shape)
@@ -422,7 +425,7 @@ class StreamLines(VGroup):
cs = self.coordinate_system
for line in self.submobjects:
norms = [
get_norm(self.func(*cs.p2c(point)))
get_norm(self.func(cs.p2c(point)))
for point in line.get_points()
]
rgbs = values_to_rgbs(norms)
@@ -467,7 +470,7 @@ class AnimatedStreamLines(VGroup):
self.add_updater(lambda m, dt: m.update(dt))
def update(self, dt: float) -> None:
def update(self, dt: float = 0) -> None:
stream_lines = self.stream_lines
for line in stream_lines:
line.time += dt

View File

@@ -245,6 +245,10 @@ class InteractiveScene(Scene):
super().remove(*mobjects)
self.regenerate_selection_search_set()
def remove_all_except(self, *mobjects_to_keep : Mobject):
super().remove_all_except(*mobjects_to_keep)
self.regenerate_selection_search_set()
# Related to selection
def toggle_selection_mode(self):

View File

@@ -8,10 +8,9 @@ from functools import wraps
from contextlib import contextmanager
from contextlib import ExitStack
from pyglet.window import key as PygletWindowKeys
import numpy as np
from tqdm.auto import tqdm as ProgressDisplay
from pyglet.window import key as PygletWindowKeys
from manimlib.animation.animation import prepare_animation
from manimlib.camera.camera import Camera
@@ -33,6 +32,8 @@ from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.family_ops import extract_mobject_family_members
from manimlib.utils.family_ops import recursive_mobject_remove
from manimlib.utils.iterables import batch_by_property
from manimlib.utils.sounds import play_sound
from manimlib.utils.color import color_to_rgba
from manimlib.window import Window
from typing import TYPE_CHECKING
@@ -380,6 +381,11 @@ class Scene(object):
new_mobjects, _ = recursive_mobject_remove(self.mobjects, to_remove)
self.mobjects = new_mobjects
@affects_mobject_list
def remove_all_except(self, *mobjects_to_keep : Mobject):
self.clear()
self.add(*mobjects_to_keep)
def bring_to_front(self, *mobjects: Mobject):
self.add(*mobjects)
return self
@@ -867,6 +873,11 @@ class Scene(object):
return
self.window.focus()
def set_background_color(self, background_color, background_opacity=1) -> None:
self.camera.background_rgba = list(color_to_rgba(
background_color, background_opacity
))
class SceneState():
def __init__(self, scene: Scene, ignore: list[Mobject] | None = None):

View File

@@ -109,6 +109,31 @@ class InteractiveSceneEmbed:
self.shell.set_custom_exc((Exception,), custom_exc)
def validate_syntax(self, file_path: str) -> bool:
"""
Validates the syntax of a Python file without executing it.
Returns True if syntax is valid, False otherwise.
Prints syntax errors to the console if found.
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
source_code = f.read()
# Use compile() to check for syntax errors without executing
compile(source_code, file_path, 'exec')
return True
except SyntaxError as e:
print(f"\nSyntax Error in {file_path}:")
print(f" Line {e.lineno}: {e.text.strip() if e.text else ''}")
print(f" {' ' * (e.offset - 1 if e.offset else 0)}^")
print(f" {e.msg}")
return False
except Exception as e:
print(f"\nError reading {file_path}: {e}")
return False
def reload_scene(self, embed_line: int | None = None) -> None:
"""
Reloads the scene just like the `manimgl` command would do with the
@@ -132,6 +157,14 @@ class InteractiveSceneEmbed:
`set_custom_exc` method, we cannot break out of the IPython shell by
this means.
"""
# Get the current file path for syntax validation
current_file = self.shell.user_module.__file__
# Validate syntax before attempting reload
if not self.validate_syntax(current_file):
print("[ERROR] Reload cancelled due to syntax errors. Fix the errors and try again.")
return
# Update the global run configuration.
run_config = manim_config.run
run_config.is_reload = True
@@ -142,9 +175,12 @@ class InteractiveSceneEmbed:
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")
"""Enables reload the shell's module before all calls"""
def pre_cell_func(*args, **kwargs):
new_mod = ModuleLoader.get_module(self.shell.user_module.__file__, is_during_reload=True)
self.shell.user_ns.update(vars(new_mod))
self.shell.events.register("pre_run_cell", pre_cell_func)
def checkpoint_paste(
self,

View File

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

View File

@@ -27,7 +27,7 @@ vec4 add_light(vec4 color, vec3 point, vec3 unit_normal){
float light_to_normal = dot(to_light, unit_normal);
// When unit normal points towards light, brighten
float bright_factor = max(light_to_normal, 0) * reflectiveness;
// For glossy surface, add extra shine if light beam go towards camera
// For glossy surface, add extra shine if light beam goes towards camera
vec3 light_reflection = reflect(-to_light, unit_normal);
float light_to_cam = dot(light_reflection, to_camera);
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));

View File

@@ -33,7 +33,7 @@ const float COS_THRESHOLD = 0.999;
// Used to determine how many lines to break the curve into
const float POLYLINE_FACTOR = 100;
const int MAX_STEPS = 32;
const float MITER_COS_ANGLE_THRESHOLD = -0.9;
const float MITER_COS_ANGLE_THRESHOLD = -0.8;
#INSERT emit_gl_Position.glsl
#INSERT finalize_color.glsl

View File

@@ -1,8 +1,7 @@
#version 330
in vec3 point;
in vec3 du_point;
in vec3 dv_point;
in vec3 d_normal_point;
in vec4 rgba;
out vec4 v_color;
@@ -15,10 +14,6 @@ const float EPSILON = 1e-10;
void main(){
emit_gl_Position(point);
vec3 du = (du_point - point);
vec3 dv = (dv_point - point);
vec3 normal = cross(du, dv);
float mag = length(normal);
vec3 unit_normal = (mag < EPSILON) ? vec3(0, 0, sign(point.z)) : normal / mag;
vec3 unit_normal = normalize(d_normal_point - point);
v_color = finalize_color(rgba, point, unit_normal);
}

View File

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

View File

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

View File

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

View File

@@ -7,9 +7,9 @@ if TYPE_CHECKING:
import re
try:
from typing import Self
except ImportError:
from typing_extensions import Self
except ImportError:
from typing import Self
# Abbreviations for a common types
ManimColor = Union[str, Color, None]

View File

@@ -78,19 +78,25 @@ def int_to_hex(rgb_int: int) -> str:
def color_gradient(
reference_colors: Iterable[ManimColor],
length_of_output: int
length_of_output: int,
interp_by_hsl: bool = False,
) -> list[Color]:
if length_of_output == 0:
return []
rgbs = list(map(color_to_rgb, reference_colors))
alphas = np.linspace(0, (len(rgbs) - 1), length_of_output)
n_ref_colors = len(reference_colors)
alphas = np.linspace(0, (n_ref_colors - 1), length_of_output)
floors = alphas.astype('int')
alphas_mod1 = alphas % 1
# End edge case
alphas_mod1[-1] = 1
floors[-1] = len(rgbs) - 2
floors[-1] = n_ref_colors - 2
return [
rgb_to_color(np.sqrt(interpolate(rgbs[i]**2, rgbs[i + 1]**2, alpha)))
interpolate_color(
reference_colors[i],
reference_colors[i + 1],
alpha,
interp_by_hsl=interp_by_hsl,
)
for i, alpha in zip(floors, alphas_mod1)
]
@@ -98,10 +104,16 @@ def color_gradient(
def interpolate_color(
color1: ManimColor,
color2: ManimColor,
alpha: float
alpha: float,
interp_by_hsl: bool = False,
) -> Color:
rgb = np.sqrt(interpolate(color_to_rgb(color1)**2, color_to_rgb(color2)**2, alpha))
return rgb_to_color(rgb)
if interp_by_hsl:
hsl1 = np.array(Color(color1).get_hsl())
hsl2 = np.array(Color(color2).get_hsl())
return Color(hsl=interpolate(hsl1, hsl2, alpha))
else:
rgb = np.sqrt(interpolate(color_to_rgb(color1)**2, color_to_rgb(color2)**2, alpha))
return rgb_to_color(rgb)
def interpolate_color_by_hsl(
@@ -109,9 +121,7 @@ def interpolate_color_by_hsl(
color2: ManimColor,
alpha: float
) -> Color:
hsl1 = np.array(Color(color1).get_hsl())
hsl2 = np.array(Color(color2).get_hsl())
return Color(hsl=interpolate(hsl1, hsl2, alpha))
return interpolate_color(color1, color2, alpha, interp_by_hsl=True)
def average_color(*colors: ManimColor) -> Color:

View File

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

View File

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

View File

@@ -11,7 +11,8 @@ def num_tex_symbols(tex: str) -> int:
tex = remove_tex_environments(tex)
commands_pattern = r"""
(?P<sqrt>\\sqrt\[[0-9]+\])| # Special sqrt with number
(?P<cmd>\\[a-zA-Z!,-/:;<>]+) # Regular commands
(?P<escaped_brace>\\[{}])| # Escaped braces
(?P<cmd>\\[a-zA-Z!,-/:;<>]+) # Regular commands
"""
total = 0
pos = 0
@@ -21,6 +22,8 @@ def num_tex_symbols(tex: str) -> int:
if match.group("sqrt"):
total += len(match.group()) - 5
elif match.group("escaped_brace"):
total += 1 # Count escaped brace as one symbol
else:
total += TEX_TO_SYMBOL_COUNT.get(match.group(), 1)
pos = match.end()

View File

@@ -22,10 +22,7 @@ def get_tex_template_config(template_name: str) -> dict[str, str]:
with open(template_path, encoding="utf-8") as tex_templates_file:
templates_dict = yaml.safe_load(tex_templates_file)
if name not in templates_dict:
log.warning(
"Cannot recognize template '%s', falling back to 'default'.",
name
)
log.warning(f"Cannot recognize template {name}, falling back to 'default'.")
name = "default"
return templates_dict[name]
@@ -108,7 +105,7 @@ def full_tex_to_svg(full_tex: str, compiler: str = "latex", message: str = ""):
process = subprocess.run(
[
compiler,
"-no-pdf",
*(['-no-pdf'] if compiler == "xelatex" else []),
"-interaction=batchmode",
"-halt-on-error",
f"-output-directory={temp_dir}",

View File

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

View File

@@ -1,5 +1,6 @@
addict
appdirs
audioop-lts; python_version>='3.13'
colour
diskcache
ipython>=8.18.0
@@ -20,6 +21,7 @@ pyyaml
rich
scipy
screeninfo
setuptools
skia-pathops
svgelements>=1.8.1
sympy

View File

@@ -1,6 +1,6 @@
[metadata]
name = manimgl
version = 1.7.1
version = 1.7.2
author = Grant Sanderson
author_email= grant@3blue1brown.com
description = Animation engine for explanatory math videos
@@ -31,6 +31,7 @@ include_package_data = True
install_requires =
addict
appdirs
audioop-lts; python_version >= "3.13"
colour
diskcache
ipython>=8.18.0
@@ -51,6 +52,7 @@ install_requires =
rich
scipy
screeninfo
setuptools
skia-pathops
svgelements>=1.8.1
sympy