diff --git a/manimlib/config.py b/manimlib/config.py index ef9a3afc..e0a24036 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -174,6 +174,7 @@ def parse_cli(): parser.add_argument( "--fps", help="Frame rate, as an integer", + type=int, ) parser.add_argument( "-c", "--color", diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index c8352532..c51bf4cc 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -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 diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 7cd027d1..687f96f8 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -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 diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index f98b0a0e..7f27f6a9 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -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 diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index e39d6774..739644ca 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -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:]) diff --git a/requirements.txt b/requirements.txt index 38a547fb..9b87a300 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.cfg b/setup.cfg index 5ee19995..3e9acf0e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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