diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index e2b43833..a525e5e1 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -42,57 +42,67 @@ from manimlib.shader_wrapper import ShaderWrapper from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Callable, Iterable, Sequence - + from typing import Callable, Iterable, Sequence, Tuple import numpy.typing as npt - from manimlib.constants import ManimColor + + from manimlib.constants import ManimColor, np_vector class VMobject(Mobject): - CONFIG = { - "fill_color": None, - "fill_opacity": 0.0, - "stroke_color": None, - "stroke_opacity": 1.0, - "stroke_width": DEFAULT_STROKE_WIDTH, - "draw_stroke_behind_fill": False, - # Indicates that it will not be displayed, but - # that it should count in parent mobject's path - "pre_function_handle_to_anchor_scale_factor": 0.01, - "make_smooth_after_applying_functions": False, - "background_image_file": None, - # This is within a pixel - # TODO, do we care about accounting for - # varying zoom levels? - "tolerance_for_point_equality": 1e-8, - "n_points_per_curve": 3, - "long_lines": False, - # For shaders - "stroke_shader_folder": "quadratic_bezier_stroke", - "fill_shader_folder": "quadratic_bezier_fill", - # Could also be "bevel", "miter", "round" - "joint_type": "auto", - "flat_stroke": False, - "render_primitive": moderngl.TRIANGLES, - "fill_dtype": [ - ('point', np.float32, (3,)), - ('unit_normal', np.float32, (3,)), - ('color', np.float32, (4,)), - ('vert_index', np.float32, (1,)), - ], - "stroke_dtype": [ - ("point", np.float32, (3,)), - ("prev_point", np.float32, (3,)), - ("next_point", np.float32, (3,)), - ('unit_normal', np.float32, (3,)), - ("stroke_width", np.float32, (1,)), - ("color", np.float32, (4,)), - ] - } + n_points_per_curve: int = 3 + stroke_shader_folder: str = "quadratic_bezier_stroke" + fill_shader_folder: str = "quadratic_bezier_fill" + fill_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [ + ('point', np.float32, (3,)), + ('unit_normal', np.float32, (3,)), + ('color', np.float32, (4,)), + ('vert_index', np.float32, (1,)), + ] + stroke_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [ + ("point", np.float32, (3,)), + ("prev_point", np.float32, (3,)), + ("next_point", np.float32, (3,)), + ('unit_normal', np.float32, (3,)), + ("stroke_width", np.float32, (1,)), + ("color", np.float32, (4,)), + ] + render_primitive: int = moderngl.TRIANGLES + + pre_function_handle_to_anchor_scale_factor: float = 0.01 + make_smooth_after_applying_functions: bool = False + # TODO, do we care about accounting for varying zoom levels? + tolerance_for_point_equality: float = 1e-8 + + def __init__( + self, + color: ManimColor = None, # If set, this will override stroke_color and fill_color + fill_color: ManimColor = WHITE, + fill_opacity: float = 0.0, + stroke_color: ManimColor = GREY_C, + stroke_opacity: float = 1.0, + stroke_width: float = DEFAULT_STROKE_WIDTH, + draw_stroke_behind_fill: bool = False, + background_image_file: str | None = None, + long_lines: bool = False, + # Could also be "bevel", "miter", "round" + joint_type: str = "auto", + flat_stroke: bool = False, + **kwargs + ): + self.fill_color = color or fill_color + self.fill_opacity = fill_opacity + self.stroke_color = color or stroke_color + self.stroke_opacity = stroke_opacity + self.stroke_width = stroke_width + self.draw_stroke_behind_fill = draw_stroke_behind_fill + self.background_image_file = background_image_file + self.long_lines = long_lines + self.joint_type = joint_type + self.flat_stroke = flat_stroke - def __init__(self, **kwargs): self.needs_new_triangulation = True self.triangulation = np.zeros(0, dtype='i4') + super().__init__(**kwargs) def get_group_class(self): @@ -108,32 +118,40 @@ class VMobject(Mobject): "unit_normal": np.zeros((1, 3)) }) + # These are here just to make type checkers happy def get_family(self, recurse: bool = True) -> list[VMobject]: return super().get_family(recurse) + def family_members_with_points(self) -> list[VMobject]: + return super().family_members_with_points() + def replicate(self, n: int) -> VGroup: return super().replicate(n) + def get_grid(self, *args, **kwargs) -> VGroup: + return super().get_grid(*args, **kwargs) + # Colors def init_colors(self): self.set_fill( - color=self.fill_color or self.color, + color=self.fill_color, opacity=self.fill_opacity, ) self.set_stroke( - color=self.stroke_color or self.color, + color=self.stroke_color, width=self.stroke_width, opacity=self.stroke_opacity, background=self.draw_stroke_behind_fill, ) self.set_gloss(self.gloss) self.set_flat_stroke(self.flat_stroke) + self.color = self.get_color() return self def set_rgba_array( self, - rgba_array: npt.ArrayLike, - name: str = None, + rgba_array: np_vector, + name: str | None = None, recurse: bool = False ): if name is None: @@ -147,7 +165,7 @@ class VMobject(Mobject): def set_fill( self, - color: ManimColor | Iterable[ManimColor] | None = None, + color: ManimColor | Iterable[ManimColor] = None, opacity: float | Iterable[float] | None = None, recurse: bool = True ): @@ -156,7 +174,7 @@ class VMobject(Mobject): def set_stroke( self, - color: ManimColor | Iterable[ManimColor] | None = None, + color: ManimColor | Iterable[ManimColor] = None, width: float | Iterable[float] | None = None, opacity: float | Iterable[float] | None = None, background: bool | None = None, @@ -196,10 +214,10 @@ class VMobject(Mobject): self, fill_color: ManimColor | Iterable[ManimColor] | None = None, fill_opacity: float | Iterable[float] | None = None, - fill_rgba: npt.ArrayLike | None = None, + fill_rgba: np_vector | None = None, stroke_color: ManimColor | Iterable[ManimColor] | None = None, stroke_opacity: float | Iterable[float] | None = None, - stroke_rgba: npt.ArrayLike | None = None, + stroke_rgba: np_vector | None = None, stroke_width: float | Iterable[float] | None = None, stroke_background: bool = True, reflectiveness: float | None = None, @@ -218,7 +236,7 @@ class VMobject(Mobject): ) if stroke_rgba is not None: - mob.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba)) + mob.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(stroke_rgba)) mob.set_stroke( width=stroke_width, background=stroke_background, @@ -305,7 +323,7 @@ class VMobject(Mobject): for rgba in self.data['fill_rgba'] ] - def get_fill_opacities(self) -> np.ndarray: + def get_fill_opacities(self) -> np_vector: return self.data['fill_rgba'][:, 3] def get_stroke_colors(self) -> list[str]: @@ -314,10 +332,10 @@ class VMobject(Mobject): for rgba in self.data['stroke_rgba'] ] - def get_stroke_opacities(self) -> np.ndarray: + def get_stroke_opacities(self) -> np_vector: return self.data['stroke_rgba'][:, 3] - def get_stroke_widths(self) -> np.ndarray: + def get_stroke_widths(self) -> np_vector: return self.data['stroke_width'][:, 0] # TODO, it's weird for these to return the first of various lists @@ -339,7 +357,7 @@ class VMobject(Mobject): def get_stroke_color(self) -> str: return self.get_stroke_colors()[0] - def get_stroke_width(self) -> float | np.ndarray: + def get_stroke_width(self) -> float | np_vector: return self.get_stroke_widths()[0] def get_stroke_opacity(self) -> float: @@ -380,9 +398,9 @@ class VMobject(Mobject): # Points def set_anchors_and_handles( self, - anchors1: np.ndarray, - handles: np.ndarray, - anchors2: np.ndarray + anchors1: np_vector, + handles: np_vector, + anchors2: np_vector ): assert(len(anchors1) == len(handles) == len(anchors2)) nppc = self.n_points_per_curve @@ -393,26 +411,26 @@ class VMobject(Mobject): self.set_points(new_points) return self - def start_new_path(self, point: np.ndarray): + def start_new_path(self, point: np_vector): assert(self.get_num_points() % self.n_points_per_curve == 0) self.append_points([point]) return self def add_cubic_bezier_curve( self, - anchor1: npt.ArrayLike, - handle1: npt.ArrayLike, - handle2: npt.ArrayLike, - anchor2: npt.ArrayLike + anchor1: np_vector, + handle1: np_vector, + handle2: np_vector, + anchor2: np_vector ): new_points = get_quadratic_approximation_of_cubic(anchor1, handle1, handle2, anchor2) self.append_points(new_points) def add_cubic_bezier_curve_to( self, - handle1: npt.ArrayLike, - handle2: npt.ArrayLike, - anchor: npt.ArrayLike + handle1: np_vector, + handle2: np_vector, + anchor: np_vector ): """ Add cubic bezier curve to the path. @@ -426,14 +444,14 @@ class VMobject(Mobject): else: self.append_points(quadratic_approx) - def add_quadratic_bezier_curve_to(self, handle: np.ndarray, anchor: np.ndarray): + def add_quadratic_bezier_curve_to(self, handle: np_vector, anchor: np_vector): self.throw_error_if_no_points() if self.has_new_path_started(): self.append_points([handle, anchor]) else: self.append_points([self.get_last_point(), handle, anchor]) - def add_line_to(self, point: np.ndarray): + def add_line_to(self, point: np_vector): end = self.get_points()[-1] alphas = np.linspace(0, 1, self.n_points_per_curve) if self.long_lines: @@ -455,7 +473,7 @@ class VMobject(Mobject): self.append_points(points) return self - def add_smooth_curve_to(self, point: np.ndarray): + def add_smooth_curve_to(self, point: np_vector): if self.has_new_path_started(): self.add_line_to(point) else: @@ -464,7 +482,7 @@ class VMobject(Mobject): self.add_quadratic_bezier_curve_to(new_handle, point) return self - def add_smooth_cubic_curve_to(self, handle: np.ndarray, point: np.ndarray): + def add_smooth_cubic_curve_to(self, handle: np_vector, point: np_vector): self.throw_error_if_no_points() if self.get_num_points() == 1: new_handle = self.get_points()[-1] @@ -475,10 +493,10 @@ class VMobject(Mobject): def has_new_path_started(self) -> bool: return self.get_num_points() % self.n_points_per_curve == 1 - def get_last_point(self) -> np.ndarray: + def get_last_point(self) -> np_vector: return self.get_points()[-1] - def get_reflection_of_last_handle(self) -> np.ndarray: + def get_reflection_of_last_handle(self) -> np_vector: points = self.get_points() return 2 * points[-1] - points[-2] @@ -513,12 +531,12 @@ class VMobject(Mobject): vmob.set_points(np.vstack(new_points)) return self - def add_points_as_corners(self, points: Iterable[np.ndarray]): + def add_points_as_corners(self, points: Iterable[np_vector]): for point in points: self.add_line_to(point) return points - def set_points_as_corners(self, points: Iterable[np.ndarray]): + def set_points_as_corners(self, points: Iterable[np_vector]): nppc = self.n_points_per_curve points = np.array(points) self.set_anchors_and_handles(*[ @@ -529,7 +547,7 @@ class VMobject(Mobject): def set_points_smoothly( self, - points: Iterable[np.ndarray], + points: Iterable[np_vector], true_smooth: bool = False ): self.set_points_as_corners(points) @@ -584,7 +602,7 @@ class VMobject(Mobject): self.change_anchor_mode("jagged") return self - def add_subpath(self, points: Iterable[np.ndarray]): + def add_subpath(self, points: Iterable[np_vector]): assert(len(points) % self.n_points_per_curve == 0) self.append_points(points) return self @@ -600,11 +618,11 @@ class VMobject(Mobject): return self # - def consider_points_equals(self, p0: np.ndarray, p1: np.ndarray) -> bool: + def consider_points_equals(self, p0: np_vector, p1: np_vector) -> bool: return get_norm(p1 - p0) < self.tolerance_for_point_equality # Information about the curve - def get_bezier_tuples_from_points(self, points: Sequence[np.ndarray]): + def get_bezier_tuples_from_points(self, points: Sequence[np_vector]): nppc = self.n_points_per_curve remainder = len(points) % nppc points = points[:len(points) - remainder] @@ -618,8 +636,8 @@ class VMobject(Mobject): def get_subpaths_from_points( self, - points: Sequence[np.ndarray] - ) -> list[Sequence[np.ndarray]]: + points: Sequence[np_vector] + ) -> list[Sequence[np_vector]]: nppc = self.n_points_per_curve diffs = points[nppc - 1:-1:nppc] - points[nppc::nppc] splits = (diffs * diffs).sum(1) > self.tolerance_for_point_equality @@ -636,28 +654,28 @@ class VMobject(Mobject): if (i2 - i1) >= nppc ] - def get_subpaths(self) -> list[Sequence[np.ndarray]]: + def get_subpaths(self) -> list[Sequence[np_vector]]: return self.get_subpaths_from_points(self.get_points()) - def get_nth_curve_points(self, n: int) -> np.ndarray: + def get_nth_curve_points(self, n: int) -> np_vector: assert(n < self.get_num_curves()) nppc = self.n_points_per_curve return self.get_points()[nppc * n:nppc * (n + 1)] - def get_nth_curve_function(self, n: int) -> Callable[[float], np.ndarray]: + def get_nth_curve_function(self, n: int) -> Callable[[float], np_vector]: return bezier(self.get_nth_curve_points(n)) def get_num_curves(self) -> int: return self.get_num_points() // self.n_points_per_curve - def quick_point_from_proportion(self, alpha: float) -> np.ndarray: + def quick_point_from_proportion(self, alpha: float) -> np_vector: # Assumes all curves have the same length, so is inaccurate num_curves = self.get_num_curves() n, residue = integer_interpolate(0, num_curves, alpha) curve_func = self.get_nth_curve_function(n) return curve_func(residue) - def point_from_proportion(self, alpha: float) -> np.ndarray: + def point_from_proportion(self, alpha: float) -> np_vector: if alpha <= 0: return self.get_start() elif alpha >= 1: @@ -679,7 +697,7 @@ class VMobject(Mobject): residue = inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha) return self.get_nth_curve_function(i - 1)(residue) - def get_anchors_and_handles(self) -> list[np.ndarray]: + def get_anchors_and_handles(self) -> list[np_vector]: """ returns anchors1, handles, anchors2, where (anchors1[i], handles[i], anchors2[i]) @@ -693,14 +711,14 @@ class VMobject(Mobject): for i in range(nppc) ] - def get_start_anchors(self) -> np.ndarray: + def get_start_anchors(self) -> np_vector: return self.get_points()[0::self.n_points_per_curve] - def get_end_anchors(self) -> np.ndarray: + def get_end_anchors(self) -> np_vector: nppc = self.n_points_per_curve return self.get_points()[nppc - 1::nppc] - def get_anchors(self) -> np.ndarray: + def get_anchors(self) -> np_vector: points = self.get_points() if len(points) == 1: return points @@ -709,7 +727,7 @@ class VMobject(Mobject): self.get_end_anchors(), )))) - def get_points_without_null_curves(self, atol: float = 1e-9) -> np.ndarray: + def get_points_without_null_curves(self, atol: float = 1e-9) -> np_vector: nppc = self.n_points_per_curve points = self.get_points() distinct_curves = reduce(op.or_, [ @@ -729,7 +747,7 @@ class VMobject(Mobject): norms = np.array([get_norm(d) for d in diffs]) return norms.sum() - def get_area_vector(self) -> np.ndarray: + def get_area_vector(self) -> np_vector: # Returns a vector whose length is the area bound by # the polygon formed by the anchor points, pointing # in a direction perpendicular to the polygon according @@ -749,7 +767,7 @@ class VMobject(Mobject): sum((p0[:, 0] + p1[:, 0]) * (p1[:, 1] - p0[:, 1])), # Add up (x1 + x2)*(y2 - y1) ]) - def get_unit_normal(self, recompute: bool = False) -> np.ndarray: + def get_unit_normal(self, recompute: bool = False) -> np_vector: if not recompute: return self.data["unit_normal"][0] @@ -834,7 +852,7 @@ class VMobject(Mobject): mob.set_points(new_points) return self - def insert_n_curves_to_point_list(self, n: int, points: np.ndarray): + def insert_n_curves_to_point_list(self, n: int, points: np_vector): nppc = self.n_points_per_curve if len(points) == 1: return np.repeat(points, nppc * n, 0) @@ -936,7 +954,7 @@ class VMobject(Mobject): mob.needs_new_triangulation = True return self - def get_triangulation(self, normal_vector: np.ndarray | None = None): + def get_triangulation(self, normal_vector: np_vector | None = None): # Figure out how to triangulate the interior to know # how to send the points as to the vertex shader. # First triangles come directly from the points @@ -1005,7 +1023,7 @@ class VMobject(Mobject): return wrapper @triggers_refreshed_triangulation - def set_points(self, points: npt.ArrayLike): + def set_points(self, points: np_vector): super().set_points(points) return self @@ -1018,7 +1036,7 @@ class VMobject(Mobject): @triggers_refreshed_triangulation def apply_function( self, - function: Callable[[np.ndarray], np.ndarray], + function: Callable[[np_vector], np_vector], make_smooth: bool = False, **kwargs ): @@ -1027,7 +1045,7 @@ class VMobject(Mobject): self.make_approximately_smooth() return self - def flip(self, axis: np.ndarray = UP, **kwargs): + def flip(self, axis: np_vector = UP, **kwargs): super().flip(axis, **kwargs) self.refresh_unit_normal() self.refresh_triangulation() @@ -1154,17 +1172,22 @@ class VGroup(VMobject): class VectorizedPoint(Point, VMobject): - CONFIG = { - "color": BLACK, - "fill_opacity": 0, - "stroke_width": 0, - "artificial_width": 0.01, - "artificial_height": 0.01, - } - - def __init__(self, location: np.ndarray = ORIGIN, **kwargs): - Point.__init__(self, **kwargs) - VMobject.__init__(self, **kwargs) + def __init__( + self, + location: np.ndarray = ORIGIN, + color: ManimColor = BLACK, + fill_opacity: float = 0.0, + stroke_width: float = 0.0, + **kwargs + ): + Point.__init__(self, location, **kwargs) + VMobject.__init__( + self, + color=color, + fill_opacity=fill_opacity, + stroke_width=stroke_width, + **kwargs + ) self.set_points(np.array([location])) @@ -1179,23 +1202,22 @@ class CurvesAsSubmobjects(VGroup): class DashedVMobject(VMobject): - CONFIG = { - "num_dashes": 15, - "positive_space_ratio": 0.5, - "color": WHITE - } - - def __init__(self, vmobject: VMobject, **kwargs): + def __init__( + self, + vmobject: VMobject, + num_dashes: int = 15, + positive_space_ratio: float = 0.5, + **kwargs + ): super().__init__(**kwargs) - num_dashes = self.num_dashes - ps_ratio = self.positive_space_ratio + if num_dashes > 0: # End points of the unit interval for division alphas = np.linspace(0, 1, num_dashes + 1) # This determines the length of each "dash" full_d_alpha = (1.0 / num_dashes) - partial_d_alpha = full_d_alpha * ps_ratio + partial_d_alpha = full_d_alpha * positive_space_ratio # Rescale so that the last point of vmobject will # be the end of the last dash @@ -1215,7 +1237,7 @@ class VHighlight(VGroup): self, vmobject: VMobject, n_layers: int = 5, - color_bounds: tuple[ManimColor] = (GREY_C, GREY_E), + color_bounds: Tuple[ManimColor] = (GREY_C, GREY_E), max_stroke_addition: float = 5.0, ): outline = vmobject.replicate(n_layers)