Merge branch 'master' of github.com:3b1b/manim into video-work

This commit is contained in:
Grant Sanderson 2025-03-20 13:59:21 -05:00
commit da0605a5f3
7 changed files with 75 additions and 30 deletions

View file

@ -174,6 +174,7 @@ def parse_cli():
parser.add_argument(
"--fps",
help="Frame rate, as an integer",
type=int,
)
parser.add_argument(
"-c", "--color",

View file

@ -770,13 +770,6 @@ class ComplexPlane(NumberPlane):
axis = self.get_x_axis()
value = z.real
number_mob = axis.get_number_mobject(value, font_size=font_size, **kwargs)
# For -i, remove the "1"
if z.imag == -1:
number_mob.remove(number_mob[1])
number_mob[0].next_to(
number_mob[1], LEFT,
buff=number_mob[0].get_width() / 4
)
self.coordinate_labels.add(number_mob)
self.add(self.coordinate_labels)
return self

View file

@ -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')
@ -163,7 +163,7 @@ class Mobject(object):
def animate(self) -> _AnimationBuilder:
"""
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())
@ -1323,20 +1320,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:
@ -1344,8 +1340,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

View file

@ -164,7 +164,7 @@ class NumberLine(Line):
**number_config
) -> DecimalNumber:
number_config = merge_dicts_recursively(
self.decimal_number_config, number_config,
number_config, self.decimal_number_config,
)
if direction is None:
direction = self.line_to_number_direction
@ -182,9 +182,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

View file

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

View file

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

View file

@ -31,6 +31,7 @@ include_package_data = True
install_requires =
addict
appdirs
audioop-lts
colour
diskcache
ipython>=8.18.0
@ -51,6 +52,7 @@ install_requires =
rich
scipy
screeninfo
setuptools
skia-pathops
svgelements>=1.8.1
sympy