From 5a3f6dd841b9d8e0b5562e3e313e392d37eedcc8 Mon Sep 17 00:00:00 2001 From: Theo Barollet Date: Mon, 28 Apr 2025 16:32:31 +0200 Subject: [PATCH 1/6] moved color initialization out of mobject and there is now a default color for fill and stroke --- manim/mobject/mobject.py | 10 ---------- manim/mobject/types/vectorized_mobject.py | 24 +++++++---------------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 1399845c26..9d088c422a 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -96,7 +96,6 @@ def __init_subclass__(cls, **kwargs) -> None: def __init__( self, - color: ParsableManimColor | list[ParsableManimColor] = WHITE, name: str | None = None, dim: int = 3, target=None, @@ -110,11 +109,9 @@ def __init__( self.submobjects = [] self.updaters: list[Updater] = [] self.updating_suspended = False - self.color = ManimColor.parse(color) self.reset_points() self.generate_points() - self.init_colors() def _assert_valid_submobjects(self, submobjects: Iterable[Mobject]) -> Self: """Check that all submobjects are actually instances of @@ -408,13 +405,6 @@ def reset_points(self) -> None: """Sets :attr:`points` to be an empty array.""" self.points = np.zeros((0, self.dim)) - def init_colors(self) -> object: - """Initializes the colors. - - Gets called upon creation. This is an empty method that can be implemented by - subclasses. - """ - def generate_points(self) -> object: """Initializes :attr:`points` and therefore the shape. diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index c540b17db2..b1272c4595 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -117,9 +117,9 @@ class VMobject(Mobject): def __init__( self, - fill_color: ParsableManimColor | None = None, + fill_color: ParsableManimColor = BLACK, fill_opacity: float = 0.0, - stroke_color: ParsableManimColor | None = None, + stroke_color: ParsableManimColor = WHITE, stroke_opacity: float = 1.0, stroke_width: float = DEFAULT_STROKE_WIDTH, background_stroke_color: ParsableManimColor | None = BLACK, @@ -142,9 +142,9 @@ def __init__( self.stroke_width = stroke_width if background_stroke_color is not None: self.background_stroke_color: ManimColor = ManimColor( - background_stroke_color + background_stroke_color, + alpha=background_stroke_opacity ) - self.background_stroke_opacity: float = background_stroke_opacity self.background_stroke_width: float = background_stroke_width self.sheen_factor: float = sheen_factor self.joint_type: LineJointType = ( @@ -169,19 +169,9 @@ def __init__( super().__init__(**kwargs) self.submobjects: list[VMobject] - # TODO: Find where color overwrites are happening and remove the color doubling - # if "color" in kwargs: - # fill_color = kwargs["color"] - # stroke_color = kwargs["color"] - if fill_color is not None: - self.fill_color = ManimColor.parse(fill_color) - if stroke_color is not None: - self.stroke_color = ManimColor.parse(stroke_color) - - if fill_opacity is not None: - self.fill_color = self.fill_color.set_opacity(fill_opacity) - if stroke_opacity is not None: - self.stroke_color = self.stroke_color.set_opacity(stroke_opacity) + self.fill_color = ManimColor.parse(fill_color, alpha=fill_opacity) + self.stroke_color = ManimColor.parse(stroke_color, alpha=stroke_opacity) + self.init_colors() def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self: return self._assert_valid_submobjects_internal(submobjects, VMobject) From 06d48c1cd4b33e9cde16c990935b923e8d5b1fa6 Mon Sep 17 00:00:00 2001 From: Theo Barollet Date: Mon, 28 Apr 2025 17:55:55 +0200 Subject: [PATCH 2/6] color attributes are now always a list of colors --- manim/mobject/mobject.py | 79 --------- manim/mobject/types/vectorized_mobject.py | 195 ++++++---------------- manim/utils/color/core.py | 67 +++++++- 3 files changed, 110 insertions(+), 231 deletions(-) diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 9d088c422a..1eb32116ed 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -28,10 +28,7 @@ from manim.utils.color import ( BLACK, WHITE, - YELLOW_C, - ManimColor, ParsableManimColor, - color_gradient, interpolate_color, ) from manim.utils.exceptions import MultiAnimationOverrideException @@ -1833,34 +1830,6 @@ def add_background_rectangle_to_family_members_with_points(self, **kwargs) -> Se # Color functions - def set_color( - self, color: ParsableManimColor = YELLOW_C, family: bool = True - ) -> Self: - """Condition is function which takes in one arguments, (x, y, z). - Here it just recurses to submobjects, but in subclasses this - should be further implemented based on the the inner workings - of color - """ - if family: - for submob in self.submobjects: - submob.set_color(color, family=family) - - self.color = ManimColor.parse(color) - return self - - def set_color_by_gradient(self, *colors: ParsableManimColor) -> Self: - """ - Parameters - ---------- - colors - The colors to use for the gradient. Use like `set_color_by_gradient(RED, BLUE, GREEN)`. - - self.color = ManimColor.parse(color) - return self - """ - self.set_submobject_colors_by_gradient(*colors) - return self - def set_colors_by_radial_gradient( self, center: Point3D | None = None, @@ -1876,19 +1845,6 @@ def set_colors_by_radial_gradient( ) return self - def set_submobject_colors_by_gradient(self, *colors: Iterable[ParsableManimColor]): - if len(colors) == 0: - raise ValueError("Need at least one color") - elif len(colors) == 1: - return self.set_color(*colors) - - mobs = self.family_members_with_points() - new_colors = color_gradient(colors, len(mobs)) - - for mob, color in zip(mobs, new_colors): - mob.set_color(color, family=False) - return self - def set_submobject_colors_by_radial_gradient( self, center: Point3D | None = None, @@ -1907,41 +1863,6 @@ def set_submobject_colors_by_radial_gradient( return self - def to_original_color(self) -> Self: - self.set_color(self.color) - return self - - def fade_to( - self, color: ParsableManimColor, alpha: float, family: bool = True - ) -> Self: - if self.get_num_points() > 0: - new_color = interpolate_color(self.get_color(), color, alpha) - self.set_color(new_color, family=False) - if family: - for submob in self.submobjects: - submob.fade_to(color, alpha) - return self - - def fade(self, darkness: float = 0.5, family: bool = True) -> Self: - if family: - for submob in self.submobjects: - submob.fade(darkness, family) - return self - - def get_color(self) -> ManimColor: - """Returns the color of the :class:`~.Mobject` - - Examples - -------- - :: - - >>> from manim import Square, RED - >>> Square(color=RED).get_color() == RED - True - - """ - return self.color - ## def save_state(self) -> Self: diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index b1272c4595..aa5dfd62fa 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -66,7 +66,6 @@ QuadraticBezierPoints, RGBA_Array_Float, Vector3D, - Zeros, ) # TODO @@ -117,13 +116,14 @@ class VMobject(Mobject): def __init__( self, - fill_color: ParsableManimColor = BLACK, - fill_opacity: float = 0.0, - stroke_color: ParsableManimColor = WHITE, - stroke_opacity: float = 1.0, + fill_color: ParsableManimColor | Iterable[ParsableManimColor] = BLACK, + fill_opacity: float | Iterable[float] = 0.0, + stroke_color: ParsableManimColor | Iterable[ParsableManimColor] = WHITE, + stroke_opacity: float | Iterable[float] = 1.0, stroke_width: float = DEFAULT_STROKE_WIDTH, - background_stroke_color: ParsableManimColor | None = BLACK, - background_stroke_opacity: float = 1.0, + background_stroke_color: ParsableManimColor + | Iterable[ParsableManimColor] = BLACK, + background_stroke_opacity: float | Iterable[float] = 1.0, background_stroke_width: float = 0, sheen_factor: float = 0.0, joint_type: LineJointType | None = None, @@ -139,18 +139,18 @@ def __init__( cap_style: CapStyleType = CapStyleType.AUTO, **kwargs: Any, ): + self.sheen_factor: float = sheen_factor + self.sheen_direction: Vector3D = sheen_direction + self.stroke_width = stroke_width - if background_stroke_color is not None: - self.background_stroke_color: ManimColor = ManimColor( - background_stroke_color, - alpha=background_stroke_opacity - ) + self.background_strokes = ManimColorArray( + background_stroke_color, background_stroke_opacity, sheen_factor + ) self.background_stroke_width: float = background_stroke_width - self.sheen_factor: float = sheen_factor + self.joint_type: LineJointType = ( LineJointType.AUTO if joint_type is None else joint_type ) - self.sheen_direction: Vector3D = sheen_direction self.close_new_points: bool = close_new_points self.pre_function_handle_to_anchor_scale_factor: float = ( pre_function_handle_to_anchor_scale_factor @@ -169,9 +169,8 @@ def __init__( super().__init__(**kwargs) self.submobjects: list[VMobject] - self.fill_color = ManimColor.parse(fill_color, alpha=fill_opacity) - self.stroke_color = ManimColor.parse(stroke_color, alpha=stroke_opacity) - self.init_colors() + self.fills = ManimColorArray(fill_color, fill_opacity, sheen_factor) + self.strokes = ManimColorArray(stroke_color, stroke_opacity, sheen_factor) def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self: return self._assert_valid_submobjects_internal(submobjects, VMobject) @@ -188,93 +187,10 @@ def get_group_class(self) -> type[VGroup]: def get_mobject_type_class() -> type[VMobject]: return VMobject - # Colors - def init_colors(self, propagate_colors: bool = True) -> Self: - self.set_fill( - color=self.fill_color, - family=propagate_colors, - ) - self.set_stroke( - color=self.stroke_color, - width=self.stroke_width, - family=propagate_colors, - ) - self.set_background_stroke( - color=self.background_stroke_color, - width=self.background_stroke_width, - opacity=self.background_stroke_opacity, - family=propagate_colors, - ) - self.set_sheen( - factor=self.sheen_factor, - direction=self.sheen_direction, - family=propagate_colors, - ) - - if not propagate_colors: - for submobject in self.submobjects: - submobject.init_colors(propagate_colors=False) - - return self - - def generate_rgbas_array( - self, color: ManimColor | list[ManimColor], opacity: float | Iterable[float] - ) -> RGBA_Array_Float: - """ - First arg can be either a color, or a tuple/list of colors. - Likewise, opacity can either be a float, or a tuple of floats. - If self.sheen_factor is not zero, and only - one color was passed in, a second slightly light color - will automatically be added for the gradient - """ - colors: list[ManimColor] = [ - ManimColor(c) if (c is not None) else BLACK for c in tuplify(color) - ] - opacities: list[float] = [ - o if (o is not None) else 0.0 for o in tuplify(opacity) - ] - rgbas: npt.NDArray[RGBA_Array_Float] = np.array( - [c.to_rgba_with_alpha(o) for c, o in zip(*make_even(colors, opacities))], - ) - - sheen_factor = self.get_sheen_factor() - if sheen_factor != 0 and len(rgbas) == 1: - light_rgbas = np.array(rgbas) - light_rgbas[:, :3] += sheen_factor - np.clip(light_rgbas, 0, 1, out=light_rgbas) - rgbas = np.append(rgbas, light_rgbas, axis=0) - return rgbas - - def update_rgbas_array( - self, - array_name: str, - color: ManimColor | None = None, - opacity: float | None = None, - ) -> Self: - rgbas = self.generate_rgbas_array(color, opacity) - if not hasattr(self, array_name): - setattr(self, array_name, rgbas) - return self - # Match up current rgbas array with the newly calculated - # one. 99% of the time they'll be the same. - curr_rgbas = getattr(self, array_name) - if len(curr_rgbas) < len(rgbas): - curr_rgbas = stretch_array_to_length(curr_rgbas, len(rgbas)) - setattr(self, array_name, curr_rgbas) - elif len(rgbas) < len(curr_rgbas): - rgbas = stretch_array_to_length(rgbas, len(curr_rgbas)) - # Only update rgb if color was not None, and only - # update alpha channel if opacity was passed in - if color is not None: - curr_rgbas[:, :3] = rgbas[:, :3] - if opacity is not None: - curr_rgbas[:, 3] = rgbas[:, 3] - return self - def set_fill( self, - color: ParsableManimColor | None = None, - opacity: float | None = None, + color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, + opacity: float | Iterable[float] | None = None, family: bool = True, ) -> Self: """Set the fill color and fill opacity of a :class:`VMobject`. @@ -316,17 +232,14 @@ def construct(self): for submobject in self.submobjects: submobject.set_fill(color, opacity, family) - if color is not None: - self.fill_color = ManimColor.parse(color) - if opacity is not None: - self.fill_color = [c.opacity(opacity) for c in self.fill_color] + self.fills.update(color, opacity, self.sheen_factor) return self def set_stroke( self, - color: ParsableManimColor = None, + color: ParsableManimColor | Iterable[ParsableManimColor] = None, width: float | None = None, - opacity: float | None = None, + opacity: float | Iterable[float] | None = None, background=False, family: bool = True, ) -> Self: @@ -334,23 +247,13 @@ def set_stroke( for submobject in self.submobjects: submobject.set_stroke(color, width, opacity, background, family) if background: - array_name = "background_stroke_rgbas" - width_name = "background_stroke_width" - opacity_name = "background_stroke_opacity" + self.background_strokes.update(color, opacity, self.sheen_factor) + if width is not None: + self.background_stroke_width = width else: - array_name = "stroke_rgbas" - width_name = "stroke_width" - opacity_name = "stroke_opacity" - self.update_rgbas_array(array_name, color, opacity) - if width is not None: - setattr(self, width_name, width) - if opacity is not None: - setattr(self, opacity_name, opacity) - if color is not None and background: - if isinstance(color, (list, tuple)): - self.background_stroke_color = ManimColor.parse(color) - else: - self.background_stroke_color = ManimColor(color) + self.strokess.update(color, opacity, self.sheen_factor) + if width is not None: + self.stroke_width = width return self def set_cap_style(self, cap_style: CapStyleType) -> Self: @@ -388,14 +291,16 @@ def set_background_stroke(self, **kwargs) -> Self: def set_style( self, - fill_color: ParsableManimColor | None = None, - fill_opacity: float | None = None, - stroke_color: ParsableManimColor | None = None, + fill_color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, + fill_opacity: float | Iterable[float] | None = None, + stroke_color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, stroke_width: float | None = None, - stroke_opacity: float | None = None, - background_stroke_color: ParsableManimColor | None = None, + stroke_opacity: float | Iterable[float] | None = None, + background_stroke_color: ParsableManimColor + | Iterable[ParsableManimColor] + | None = None, background_stroke_width: float | None = None, - background_stroke_opacity: float | None = None, + background_stroke_opacity: float | Iterable[float] | None = None, sheen_factor: float | None = None, sheen_direction: Vector3D | None = None, background_image: Image | str | None = None, @@ -463,12 +368,18 @@ def match_style(self, vmobject: VMobject, family: bool = True) -> Self: sm1.match_style(sm2) return self - def set_color(self, color: ParsableManimColor, family: bool = True) -> Self: + def set_color( + self, + color: ParsableManimColor | Iterable[ParsableManimColor], + family: bool = True, + ) -> Self: self.set_fill(color, family=family) self.set_stroke(color, family=family) return self - def set_opacity(self, opacity: float, family: bool = True) -> Self: + def set_opacity( + self, opacity: float | Iterable[float], family: bool = True + ) -> Self: self.set_fill(opacity=opacity, family=family) self.set_stroke(opacity=opacity, family=family) self.set_stroke(opacity=opacity, family=family, background=True) @@ -543,11 +454,8 @@ def fade(self, darkness: float = 0.5, family: bool = True) -> Self: super().fade(darkness, family) return self - def get_fill_rgbas(self) -> RGBA_Array_Float | Zeros: - try: - return self.fill_rgbas - except AttributeError: - return np.zeros((1, 4)) + def get_fill_rgbas(self) -> RGBA_Array_Float: + return self.fills.rgbas def get_fill_color(self) -> ManimColor: """ @@ -576,17 +484,8 @@ def get_fill_colors(self) -> list[ManimColor | None]: def get_fill_opacities(self) -> npt.NDArray[ManimFloat]: return self.get_fill_rgbas()[:, 3] - def get_stroke_rgbas(self, background: bool = False) -> RGBA_Array_float | Zeros: - try: - if background: - self.background_stroke_rgbas: RGBA_Array_Float - rgbas = self.background_stroke_rgbas - else: - self.stroke_rgbas: RGBA_Array_Float - rgbas = self.stroke_rgbas - return rgbas - except AttributeError: - return np.zeros((1, 4)) + def get_stroke_rgbas(self, background: bool = False) -> RGBA_Array_float: + return self.background_strokes.rgbas if background else self.strokes.rgbas def get_stroke_color(self, background: bool = False) -> ManimColor | None: return self.get_stroke_colors(background)[0] diff --git a/manim/utils/color/core.py b/manim/utils/color/core.py index 4642922ad1..33cd985695 100644 --- a/manim/utils/color/core.py +++ b/manim/utils/color/core.py @@ -48,7 +48,7 @@ # logger = _config.logger import random import re -from collections.abc import Sequence +from collections.abc import Iterable, Sequence from typing import Any, TypeVar, Union, overload import numpy as np @@ -74,6 +74,12 @@ RGBA_Tuple_Float, RGBA_Tuple_Int, ) +from manim.utils.color.manim_colors import BLACK +from manim.utils.iterables import ( + make_even, + stretch_array_to_length, + tuplify, +) from ...utils.space_ops import normalize @@ -626,8 +632,6 @@ def darker(self, blend: float = 0.2) -> Self: -------- :meth:`lighter` """ - from manim.utils.color.manim_colors import BLACK - alpha = self._internal_space[3] black = self._from_internal(BLACK._internal_value) return self.interpolate(black, blend).opacity(alpha) @@ -687,7 +691,7 @@ def contrasting( ManimColor The contrasting ManimColor """ - from manim.utils.color.manim_colors import BLACK, WHITE + from manim.utils.color.manim_colors import WHITE luminance, _, _ = colorsys.rgb_to_yiq(*self.to_rgb()) if luminance < threshold: @@ -1033,6 +1037,61 @@ def __xor__(self, other: Self) -> Self: ) +class ManimColorArray: + """ + First arg can be either a color, or a tuple/list of colors. + Likewise, opacity can either be a float, or a tuple of floats. + If sheen_factor is not zero, and only + one color was passed in, a second slightly light color + will automatically be added for the gradient + """ + + def __init__( + self, + color: ManimColor | Iterable[ManimColor], + opacity: float | Iterable[float], + sheen_factor: float = 0.0, + ): + colors: list[ManimColor] = [ + ManimColor(c) if (c is not None) else BLACK for c in tuplify(color) + ] + opacities: list[float] = [ + o if (o is not None) else 0.0 for o in tuplify(opacity) + ] + self.rgbas: npt.NDArray[RGBA_Array_Float] = np.array( + [c.to_rgba_with_alpha(o) for c, o in zip(*make_even(colors, opacities))], + ) + + if sheen_factor != 0 and len(self.rgbas) == 1: + light_rgbas = np.array(self.rgbas) + light_rgbas[:, :3] += sheen_factor + np.clip(light_rgbas, 0, 1, out=light_rgbas) + self.rgbas = np.append(self.rgbas, light_rgbas, axis=0) + + def update( + self, + array_name: str, + color: ManimColor | Iterable[ManimColor] | None = None, + opacity: float | Iterable[float] | None = None, + sheen_factor: float = 0.0, + ): + rgbas = ManimColorArray(color, opacity, sheen_factor) + # Match up current rgbas array with the newly calculated + # one. 99% of the time they'll be the same. + curr_rgbas = self.rgbas + if len(curr_rgbas) < len(rgbas): + curr_rgbas = stretch_array_to_length(curr_rgbas, len(rgbas)) + self.rgbas = curr_rgbas + elif len(rgbas) < len(curr_rgbas): + rgbas = stretch_array_to_length(rgbas, len(curr_rgbas)) + # Only update rgb if color was not None, and only + # update alpha channel if opacity was passed in + if color is not None: + curr_rgbas[:, :3] = rgbas[:, :3] + if opacity is not None: + curr_rgbas[:, 3] = rgbas[:, 3] + + RGBA = ManimColor """RGBA Color Space""" From 0a179faddd8483a3966eaf77d180b3f502d4d788 Mon Sep 17 00:00:00 2001 From: Theo Barollet Date: Mon, 28 Apr 2025 17:58:03 +0200 Subject: [PATCH 3/6] removed color properties in vmobject because of family and opacity arguments in setter --- manim/mobject/types/vectorized_mobject.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index aa5dfd62fa..df2ff707c9 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -464,8 +464,6 @@ def get_fill_color(self) -> ManimColor: """ return self.get_fill_colors()[0] - fill_color = property(get_fill_color, set_fill) - def get_fill_opacity(self) -> ManimFloat: """ If there are multiple opacities, this returns the @@ -490,8 +488,6 @@ def get_stroke_rgbas(self, background: bool = False) -> RGBA_Array_float: def get_stroke_color(self, background: bool = False) -> ManimColor | None: return self.get_stroke_colors(background)[0] - stroke_color = property(get_stroke_color, set_stroke) - def get_stroke_width(self, background: bool = False) -> float: if background: self.background_stroke_width: float @@ -519,8 +515,6 @@ def get_color(self) -> ManimColor: return self.get_stroke_color() return self.get_fill_color() - color = property(get_color, set_color) - def set_sheen_direction(self, direction: Vector3D, family: bool = True) -> Self: """Sets the direction of the applied sheen. From cec8f6b394dd75e32209f0179d47a113615f9df3 Mon Sep 17 00:00:00 2001 From: Theo Barollet Date: Thu, 1 May 2025 08:59:48 +0200 Subject: [PATCH 4/6] fixed vmobjects tests --- manim/mobject/types/vectorized_mobject.py | 6 +++++- manim/utils/color/core.py | 12 +++++++----- .../mobject/types/vectorized_mobject/test_stroke.py | 10 +++++----- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index df2ff707c9..d9bad523f9 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -39,6 +39,7 @@ proportions_along_bezier_curve_for_point, ) from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor +from manim.utils.color.core import ManimColorArray from manim.utils.deprecation import deprecated from manim.utils.iterables import ( make_even, @@ -166,6 +167,9 @@ def __init__( 0, 1, n_points_per_cubic_curve ) self.cap_style: CapStyleType = cap_style + if "color" in kwargs: + stroke_color = kwargs["color"] + del kwargs["color"] super().__init__(**kwargs) self.submobjects: list[VMobject] @@ -251,7 +255,7 @@ def set_stroke( if width is not None: self.background_stroke_width = width else: - self.strokess.update(color, opacity, self.sheen_factor) + self.strokes.update(color, opacity, self.sheen_factor) if width is not None: self.stroke_width = width return self diff --git a/manim/utils/color/core.py b/manim/utils/color/core.py index 33cd985695..2ae023146b 100644 --- a/manim/utils/color/core.py +++ b/manim/utils/color/core.py @@ -74,7 +74,6 @@ RGBA_Tuple_Float, RGBA_Tuple_Int, ) -from manim.utils.color.manim_colors import BLACK from manim.utils.iterables import ( make_even, stretch_array_to_length, @@ -632,6 +631,8 @@ def darker(self, blend: float = 0.2) -> Self: -------- :meth:`lighter` """ + from manim.utils.color.manim_colors import BLACK + alpha = self._internal_space[3] black = self._from_internal(BLACK._internal_value) return self.interpolate(black, blend).opacity(alpha) @@ -691,7 +692,7 @@ def contrasting( ManimColor The contrasting ManimColor """ - from manim.utils.color.manim_colors import WHITE + from manim.utils.color.manim_colors import BLACK, WHITE luminance, _, _ = colorsys.rgb_to_yiq(*self.to_rgb()) if luminance < threshold: @@ -1048,10 +1049,12 @@ class ManimColorArray: def __init__( self, - color: ManimColor | Iterable[ManimColor], + color: ParsableManimColor | Iterable[ParsableManimColor], opacity: float | Iterable[float], sheen_factor: float = 0.0, ): + from manim.utils.color.manim_colors import BLACK + colors: list[ManimColor] = [ ManimColor(c) if (c is not None) else BLACK for c in tuplify(color) ] @@ -1070,12 +1073,11 @@ def __init__( def update( self, - array_name: str, color: ManimColor | Iterable[ManimColor] | None = None, opacity: float | Iterable[float] | None = None, sheen_factor: float = 0.0, ): - rgbas = ManimColorArray(color, opacity, sheen_factor) + rgbas = ManimColorArray(color, opacity, sheen_factor).rgbas # Match up current rgbas array with the newly calculated # one. 99% of the time they'll be the same. curr_rgbas = self.rgbas diff --git a/tests/module/mobject/types/vectorized_mobject/test_stroke.py b/tests/module/mobject/types/vectorized_mobject/test_stroke.py index 25c09cd294..2bd8752703 100644 --- a/tests/module/mobject/types/vectorized_mobject/test_stroke.py +++ b/tests/module/mobject/types/vectorized_mobject/test_stroke.py @@ -7,7 +7,7 @@ def test_stroke_props_in_ctor(): m = VMobject(stroke_color=C.ORANGE, stroke_width=10) - assert m.stroke_color.to_hex() == C.ORANGE.to_hex() + assert m.get_stroke_color().to_hex() == C.ORANGE.to_hex() assert m.stroke_width == 10 @@ -15,16 +15,16 @@ def test_set_stroke(): m = VMobject() m.set_stroke(color=C.ORANGE, width=2, opacity=0.8) assert m.stroke_width == 2 - assert m.stroke_opacity == 0.8 - assert m.stroke_color.to_hex() == C.ORANGE.to_hex() + assert m.get_stroke_opacity() == 0.8 + assert m.get_stroke_color().to_hex() == C.ORANGE.to_hex() def test_set_background_stroke(): m = VMobject() m.set_stroke(color=C.ORANGE, width=2, opacity=0.8, background=True) assert m.background_stroke_width == 2 - assert m.background_stroke_opacity == 0.8 - assert m.background_stroke_color.to_hex() == C.ORANGE.to_hex() + assert m.get_stroke_opacity(background=True) == 0.8 + assert m.get_stroke_color(background=True).to_hex() == C.ORANGE.to_hex() def test_streamline_attributes_for_single_color(): From aa143c148404657dcc1c71ee64544b604da33739 Mon Sep 17 00:00:00 2001 From: Theo Barollet Date: Thu, 1 May 2025 10:12:26 +0200 Subject: [PATCH 5/6] applying modifications to openglmobjects --- manim/mobject/opengl/opengl_mobject.py | 71 ----------------- .../opengl/opengl_vectorized_mobject.py | 76 +++++++------------ manim/mobject/types/vectorized_mobject.py | 20 ++--- 3 files changed, 38 insertions(+), 129 deletions(-) diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index fae0e89114..17a8cfcca4 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -116,11 +116,6 @@ class MobjectStatus: # TODO: add this to the **kwargs of all mobjects that use OpenGLMobject class MobjectKwargs(TypedDict, total=False): - color: ParsableManimColor | Sequence[ParsableManimColor] | None - opacity: float - reflectiveness: float - shadow: float - gloss: float is_fixed_in_frame: bool is_fixed_orientation: bool depth_test: bool @@ -149,22 +144,12 @@ class OpenGLMobject: # TypedDict above so that autocomplete works for users def __init__( self, - color: ParsableManimColor | Sequence[ParsableManimColor] | None = WHITE, - opacity: float = 1.0, - reflectiveness: float = 0.0, - shadow: float = 0.0, - gloss: float = 0.0, is_fixed_in_frame: bool = False, is_fixed_orientation: bool = False, depth_test: bool = True, name: str | None = None, **kwargs: Any, # just dump ): - self.color = color - self.opacity = opacity - self.reflectiveness = reflectiveness - self.shadow = shadow - self.gloss = gloss self.is_fixed_in_frame = is_fixed_in_frame self.is_fixed_orientation = is_fixed_orientation self.fixed_orientation_center = (0.0, 0.0, 0.0) @@ -191,7 +176,6 @@ def __init__( self.init_updaters() self.init_event_listeners() self.init_points() - self.color = ManimColor.parse(color) self.init_colors() @classmethod @@ -273,13 +257,6 @@ def construct(self): else: cls.__init__ = cls._original__init__ - def init_colors(self): - """Initializes the colors. - - Gets called upon creation - """ - self.set_color(self.color, self.opacity) - def init_points(self) -> object: """Initializes :attr:`points` and therefore the shape. @@ -2223,54 +2200,6 @@ def put_start_and_end_on(self, start: np.ndarray, end: np.ndarray): self.shift(start - self.get_start()) return self - # Color functions - - def set_color(self, color: ParsableManimColor | None, opacity=None, recurse=True): - # Recurse to submobjects differently from how set_rgba_array - # in case they implement set_color differently - if color is not None: - self.color: ManimColor = ManimColor.parse(color) - if opacity is not None: - self.color.opacity(opacity) - if recurse: - for submob in self.submobjects: - submob.set_color(color, recurse=True) - return self - - def set_opacity(self, opacity, recurse=True): - # self.set_rgba_array(color=None, opacity=opacity, recurse=False) - if recurse: - for submob in self.submobjects: - submob.set_opacity(opacity, recurse=True) - return self - - def get_color(self) -> ManimColor: - return self.color - - def get_opacity(self): - return self.color.opacity() - - def set_color_by_gradient(self, *colors: ParsableManimColor): - self.set_submobject_colors_by_gradient(*colors) - return self - - def set_submobject_colors_by_gradient(self, *colors): - if len(colors) == 0: - raise Exception("Need at least one color") - elif len(colors) == 1: - return self.set_color(*colors) - - # mobs = self.family_members_with_points() - mobs = self.submobjects - new_colors = color_gradient(colors, len(mobs)) - - for mob, color in zip(mobs, new_colors): - mob.set_color(color) - return self - - def fade(self, darkness=0.5, recurse=True): - self.set_opacity(1.0 - darkness, recurse=recurse) - # Background rectangle def add_background_rectangle( diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index 6d10ee3d25..4d8d7a1227 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -67,12 +67,14 @@ class VMobjectKwargs(MobjectKwargs, total=False): stroke_opacity: float | None stroke_width: float draw_stroke_behind_fill: bool + reflectiveness: float + shadow: float + gloss: float background_image_file: str | None long_lines: bool joint_type: LineJointType flat_stroke: bool shade_in_3d: bool - checkerboard_colors: bool # TODO: remove class OpenGLVMobject(OpenGLMobject): @@ -87,23 +89,28 @@ class OpenGLVMobject(OpenGLMobject): # so users can get autocomplete def __init__( self, - fill_color: ParsableManimColor | Sequence[ParsableManimColor] | None = None, - fill_opacity: float | None = None, - stroke_color: ParsableManimColor | Sequence[ParsableManimColor] | None = None, - stroke_opacity: float | None = None, + fill_color: ParsableManimColor | Sequence[ParsableManimColor] = BLACK, + fill_opacity: float | Sequence[float] = 0.0, + stroke_color: ParsableManimColor | Sequence[ParsableManimColor] = WHITE, + stroke_opacity: float | Sequence[float] = 1.0, stroke_width: float = DEFAULT_STROKE_WIDTH, draw_stroke_behind_fill: bool = False, + reflectiveness: float = 0.0, + shadow: float = 0.0, + gloss: float = 0.0, background_image_file: str | None = None, long_lines: bool = False, joint_type: LineJointType = LineJointType.AUTO, flat_stroke: bool = False, shade_in_3d: bool = False, # TODO: Can be ignored for now but we should think about using some sort of shader to introduce lighting after deferred rendering has completed - checkerboard_colors: bool = False, # ignore, **kwargs: Unpack[MobjectKwargs], ): self.stroke_width = listify(stroke_width) self.draw_stroke_behind_fill = draw_stroke_behind_fill self.background_image_file = background_image_file + self.reflectiveness = reflectiveness + self.shadow = shadow + self.gloss = gloss self.long_lines = long_lines self.joint_type = joint_type self.flat_stroke = flat_stroke @@ -112,12 +119,9 @@ def __init__( self.triangulation = np.zeros(0, dtype="i4") super().__init__(**kwargs) - if fill_color is None: - fill_color = self.color - if stroke_color is None: - stroke_color = self.color - self.set_fill(color=fill_color, opacity=fill_opacity) - self.set_stroke(color=stroke_color, width=stroke_width, opacity=stroke_opacity) + + self.fills = ManimColorArray(fill_color, fill_opacity) + self.strokes = ManimColorArray(stroke_color, stroke_opacity) # self.refresh_unit_normal() @@ -151,26 +155,10 @@ def add(self, *vmobjects: OpenGLVMobject) -> Self: # type: ignore return super().add(*vmobjects) # Colors - def init_colors(self) -> Self: - # self.set_fill( - # color=self.fill_color or self.color, - # opacity=self.fill_opacity, - # ) - # self.set_stroke( - # color=self.stroke_color or self.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_fill( self, color: ParsableManimColor | Sequence[ParsableManimColor] | None = None, - opacity: float | None = None, + opacity: float | Sequence[float] | None = None, recurse: bool = True, ) -> Self: """Set the fill color and fill opacity of a :class:`OpenGLVMobject`. @@ -211,40 +199,32 @@ def construct(self): if recurse: for submob in self.submobjects: submob.set_fill(color, opacity, recurse=True) - if color is not None: - self.fill_color: list[ManimColor] = listify(ManimColor.parse(color)) - if opacity is not None: - self.fill_color = [c.opacity(opacity) for c in self.fill_color] + self.fills.update(color, opacity) return self def set_stroke( self, - color=None, - width=None, - opacity=None, - background=None, - recurse=True, + color: ParsableManimColor | Sequence[ParsableManimColor] | None = None, + width: float | None = None, + opacity: float | Sequence[float] | None = None, + background: bool = False, + recurse: bool = True, ): for mob in self.get_family(recurse): - if color is not None: - mob.stroke_color = listify(ManimColor.parse(color)) - if opacity is not None: - mob.stroke_color = [c.opacity(opacity) for c in mob.stroke_color] + self.strokes.update(color, opacity) if width is not None: mob.stroke_width = listify(width) - if background is not None: - mob.draw_stroke_behind_fill = background + mob.draw_stroke_behind_fill = background return self def set_backstroke( self, color: ManimColor | Iterable[ManimColor] | None = None, width: float | Iterable[float] = 3, - background: bool = True, ) -> Self: - self.set_stroke(color, width, background=background) + self.set_stroke(color, width, background=True) return self def set_style( @@ -319,13 +299,13 @@ def fade(self, darkness=0.5, recurse=True) -> Self: # Todo im not quite sure why we are doing this def get_fill_colors(self): - return self.fill_color + return self.fills.rgbas def get_fill_opacities(self) -> np.ndarray: return [c.to_rgba()[3] for c in self.fill_color] def get_stroke_colors(self): - return self.stroke_color + return self.strokes.rgbas def get_stroke_opacities(self) -> np.ndarray: return [c.to_rgba()[3] for c in self.stroke_color] diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index d9bad523f9..996098e6c4 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -117,14 +117,14 @@ class VMobject(Mobject): def __init__( self, - fill_color: ParsableManimColor | Iterable[ParsableManimColor] = BLACK, - fill_opacity: float | Iterable[float] = 0.0, - stroke_color: ParsableManimColor | Iterable[ParsableManimColor] = WHITE, - stroke_opacity: float | Iterable[float] = 1.0, + fill_color: ParsableManimColor | Sequence[ParsableManimColor] = BLACK, + fill_opacity: float | Sequence[float] = 0.0, + stroke_color: ParsableManimColor | Sequence[ParsableManimColor] = WHITE, + stroke_opacity: float | Sequence[float] = 1.0, stroke_width: float = DEFAULT_STROKE_WIDTH, background_stroke_color: ParsableManimColor - | Iterable[ParsableManimColor] = BLACK, - background_stroke_opacity: float | Iterable[float] = 1.0, + | Sequence[ParsableManimColor] = BLACK, + background_stroke_opacity: float | Sequence[float] = 1.0, background_stroke_width: float = 0, sheen_factor: float = 0.0, joint_type: LineJointType | None = None, @@ -193,8 +193,8 @@ def get_mobject_type_class() -> type[VMobject]: def set_fill( self, - color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, - opacity: float | Iterable[float] | None = None, + color: ParsableManimColor | Sequence[ParsableManimColor] | None = None, + opacity: float | Sequence[float] | None = None, family: bool = True, ) -> Self: """Set the fill color and fill opacity of a :class:`VMobject`. @@ -241,9 +241,9 @@ def construct(self): def set_stroke( self, - color: ParsableManimColor | Iterable[ParsableManimColor] = None, + color: ParsableManimColor | Sequence[ParsableManimColor] = None, width: float | None = None, - opacity: float | Iterable[float] | None = None, + opacity: float | Sequence[float] | None = None, background=False, family: bool = True, ) -> Self: From e718527bab603b7c93228669f34d7ff859e6e0be Mon Sep 17 00:00:00 2001 From: Theo Barollet Date: Thu, 1 May 2025 10:20:06 +0200 Subject: [PATCH 6/6] fixed vectorized objects tests --- manim/mobject/opengl/opengl_mobject.py | 1 - manim/mobject/opengl/opengl_vectorized_mobject.py | 2 +- manim/mobject/text/numbers.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 17a8cfcca4..adfc7ae6dd 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -176,7 +176,6 @@ def __init__( self.init_updaters() self.init_event_listeners() self.init_points() - self.init_colors() @classmethod def __init_subclass__(cls, **kwargs): diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index 4d8d7a1227..462a92380e 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -26,7 +26,7 @@ proportions_along_bezier_curve_for_point, ) from manim.utils.color import * -from manim.utils.color.core import ParsableManimColor +from manim.utils.color.core import ManimColorArray, ParsableManimColor from manim.utils.deprecation import deprecated from manim.utils.iterables import ( listify, diff --git a/manim/mobject/text/numbers.py b/manim/mobject/text/numbers.py index 9e8547d7c0..c0460c3a9f 100644 --- a/manim/mobject/text/numbers.py +++ b/manim/mobject/text/numbers.py @@ -133,7 +133,6 @@ def __init__( ) self._set_submobjects_from_number(number) - self.init_colors() @property def font_size(self): @@ -302,7 +301,6 @@ def set_value(self, number: float): # not needed with opengl renderer mob.points[:] = 0 - self.init_colors() return self def get_value(self):