mirror of
https://github.com/3b1b/manim.git
synced 2026-01-11 23:48:12 -05:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5298385ed | ||
|
|
41613db7ec | ||
|
|
c48c4b6a9f | ||
|
|
cc5fbe17b9 | ||
|
|
98faf7ed55 | ||
|
|
d330e1db7f | ||
|
|
e81dfab0e6 | ||
|
|
fd2a6a69e5 | ||
|
|
6fb1845f4a | ||
|
|
7787730743 | ||
|
|
a5a73cb2da | ||
|
|
bd8d2fbc99 | ||
|
|
53bc83d94a | ||
|
|
8b9ae95703 | ||
|
|
c667136060 | ||
|
|
66e8b04507 | ||
|
|
c7ef8404b7 | ||
|
|
f4737828f6 | ||
|
|
be7d93cf40 | ||
|
|
dbfe7ac75d | ||
|
|
7a61a13691 | ||
|
|
3e307926fd | ||
|
|
2ddec95ce5 | ||
|
|
db421e3981 | ||
|
|
7a7bf83f11 | ||
|
|
24eefef5bf | ||
|
|
96d44bd560 | ||
|
|
39fbb677dc | ||
|
|
c13d2a946b | ||
|
|
0c69ab6a32 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -151,3 +151,5 @@ dmypy.json
|
||||
# For manim
|
||||
/videos
|
||||
/custom_config.yml
|
||||
test.py
|
||||
CLAUDE.md
|
||||
|
||||
11
README.md
11
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 *"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
"}",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -344,6 +344,8 @@ class ClockPassesTime(AnimationGroup):
|
||||
angle=12 * hour_radians,
|
||||
**rot_kwargs
|
||||
),
|
||||
group=clock,
|
||||
run_time=run_time,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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__(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user