diff --git a/manim/mobject/geometry/arc.py b/manim/mobject/geometry/arc.py index 32b1133a6b..3ec04ce878 100644 --- a/manim/mobject/geometry/arc.py +++ b/manim/mobject/geometry/arc.py @@ -807,10 +807,11 @@ def __init__( point: Point3DLike = ORIGIN, radius: float = DEFAULT_DOT_RADIUS, stroke_width: float = 0, - fill_opacity: float = 1.0, + fill_opacity: float | None = None, color: ParsableManimColor = WHITE, **kwargs: Any, ) -> None: + self.submobjects = [] super().__init__( arc_center=point, radius=radius, diff --git a/manim/mobject/geometry/shape_matchers.py b/manim/mobject/geometry/shape_matchers.py index ebcbc70d45..109210078e 100644 --- a/manim/mobject/geometry/shape_matchers.py +++ b/manim/mobject/geometry/shape_matchers.py @@ -127,7 +127,7 @@ def __init__( buff=buff, **kwargs, ) - self.original_fill_opacity: float = self.fill_opacity + self.original_fill_opacity: float = self.get_fill_opacities() def pointwise_become_partial(self, mobject: Mobject, a: Any, b: float) -> Self: self.set_fill(opacity=b * self.original_fill_opacity) diff --git a/manim/mobject/graphing/probability.py b/manim/mobject/graphing/probability.py index 3da8a839c5..6324a1a056 100644 --- a/manim/mobject/graphing/probability.py +++ b/manim/mobject/graphing/probability.py @@ -227,6 +227,7 @@ class BarChart(Axes): bar_names A sequence of names for each bar. Does not have to match the length of ``values``. y_range + Takes the form (min, max, stepsize) The y_axis range of values. If ``None``, the range will be calculated based on the min/max of ``values`` and the step will be calculated based on ``y_length``. x_length diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 615d6b90e7..a299d570ce 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -106,7 +106,7 @@ def __init_subclass__(cls, **kwargs: Any) -> None: def __init__( self, - color: ParsableManimColor | list[ParsableManimColor] = WHITE, + color: ParsableManimColor | list[ParsableManimColor] | None = WHITE, name: str | None = None, dim: int = 3, target: Mobject | None = None, @@ -451,7 +451,9 @@ def __deepcopy__(self, clone_from_id: dict[int, Mobject]) -> Self: return result def __repr__(self) -> str: - return str(self.name) + if hasattr(self, "name"): + return str(self.name) + return str(self.__class__.__name__) def reset_points(self) -> Self: """Sets :attr:`points` to be an empty array.""" diff --git a/manim/mobject/text/numbers.py b/manim/mobject/text/numbers.py index cd2372c543..fd3d32eeea 100644 --- a/manim/mobject/text/numbers.py +++ b/manim/mobject/text/numbers.py @@ -111,7 +111,7 @@ def __init__( self.include_background_rectangle = include_background_rectangle self.edge_to_fix = edge_to_fix self._font_size = font_size - self.fill_opacity = fill_opacity + self.set_fill_opacity(fill_opacity) self.initial_config = kwargs.copy() self.initial_config.update( @@ -132,7 +132,6 @@ def __init__( ) self._set_submobjects_from_number(number) - self.init_colors() @property def font_size(self) -> float: diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 768091ea65..aa90f1c396 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -14,7 +14,7 @@ import itertools as it import sys from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, cast import numpy as np from PIL.Image import Image @@ -39,6 +39,7 @@ ) from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor from manim.utils.iterables import ( + listify, make_even, resize_array, stretch_array_to_length, @@ -108,13 +109,14 @@ class VMobject(Mobject): def __init__( self, + color: ParsableManimColor | list[ParsableManimColor] | None = None, fill_color: ParsableManimColor | None = None, - fill_opacity: float = 0.0, + fill_opacity: float | None = None, stroke_color: ParsableManimColor | None = None, - stroke_opacity: float = 1.0, + stroke_opacity: float | None = None, stroke_width: float = DEFAULT_STROKE_WIDTH, background_stroke_color: ParsableManimColor | None = BLACK, - background_stroke_opacity: float = 1.0, + background_stroke_opacity: float | None = None, background_stroke_width: float = 0, sheen_factor: float = 0.0, joint_type: LineJointType | None = None, @@ -130,14 +132,7 @@ def __init__( cap_style: CapStyleType = CapStyleType.AUTO, **kwargs: Any, ): - self.fill_opacity = fill_opacity - self.stroke_opacity = stroke_opacity self.stroke_width = stroke_width - if background_stroke_color is not None: - self.background_stroke_color: ManimColor = ManimColor( - background_stroke_color - ) - 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 = ( @@ -159,17 +154,35 @@ def __init__( 0, 1, n_points_per_cubic_curve ) self.cap_style: CapStyleType = cap_style - super().__init__(**kwargs) + + # TODO: Refactor color initialization + # This must be after init + self.submobjects: list[VMobject] + # if fill_color is not None or stroke_color is not None: + # color = None + + if background_stroke_color is not None: + self.background_stroke_color: ManimColor | list[ManimColor] = ( + ManimColor.parse(background_stroke_color) + ) + if background_stroke_opacity is not None: + self.background_stroke_color = self.background_stroke_color.opacity( + background_stroke_opacity + ) + + print("Before", color, fill_color, stroke_color) + super().__init__(color=color, **kwargs) + print("After", self.color, self.fill_color, self.stroke_color) - # 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 fill_opacity is not None: + self.fill_color = self.fill_color.opacity(fill_opacity) if stroke_color is not None: self.stroke_color = ManimColor.parse(stroke_color) + if stroke_opacity is not None: + self.stroke_color = self.stroke_color.opacity(stroke_opacity) def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self: return self._assert_valid_submobjects_internal(submobjects, VMobject) @@ -191,21 +204,9 @@ def get_mobject_type_class() -> type[VMobject]: # Colors def init_colors(self, propagate_colors: bool = True) -> Self: - self.set_fill( - color=self.fill_color, - opacity=self.fill_opacity, - family=propagate_colors, - ) - self.set_stroke( - color=self.stroke_color, - width=self.stroke_width, - opacity=self.stroke_opacity, - 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( @@ -221,9 +222,7 @@ def init_colors(self, propagate_colors: bool = True) -> Self: return self def generate_rgbas_array( - self, - color: ParsableManimColor | Iterable[ManimColor] | None, - opacity: float | Iterable[float], + self, color: ParsableManimColor | Iterable[ManimColor] | None ) -> FloatRGBA: """ First arg can be either a color, or a tuple/list of colors. @@ -233,17 +232,10 @@ def generate_rgbas_array( 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) + ManimColor(c) if (c is not None) else BLACK + for c in cast(tuple[ParsableManimColor, ...], tuplify(color)) ] - rgbas: FloatRGBA_Array = np.array( - [ - c.to_rgba_with_alpha(o) - for c, o in zip(*make_even(colors, opacities), strict=True) - ], - ) + rgbas: FloatRGBA_Array = np.array([c.to_rgba() for c in colors]) sheen_factor = self.get_sheen_factor() if sheen_factor != 0 and len(rgbas) == 1: @@ -257,9 +249,10 @@ def update_rgbas_array( self, array_name: str, color: ParsableManimColor | Iterable[ManimColor] | None = None, - opacity: float | None = None, ) -> Self: - rgbas = self.generate_rgbas_array(color, opacity) + if color is None: + return self + rgbas = self.generate_rgbas_array(color) if not hasattr(self, array_name): setattr(self, array_name, rgbas) return self @@ -273,15 +266,14 @@ def update_rgbas_array( 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] + curr_rgbas[:, :4] = rgbas[:, :4] + print(array_name, curr_rgbas) + setattr(self, array_name, curr_rgbas) return self def set_fill( self, - color: ParsableManimColor | None = None, + color: ParsableManimColor | list[ParsableManimColor] | None = None, opacity: float | None = None, family: bool = True, ) -> Self: @@ -320,23 +312,33 @@ def construct(self): -------- :meth:`~.VMobject.set_style` """ + new_color: list[ManimColor] = listify(self.get_fill_color()) + if color is not None: + new_color: list[ManimColor] = listify(ManimColor.parse(color)) + if opacity is not None: + new_color = [c.opacity(opacity) for c in new_color] + if family: for submobject in self.submobjects: submobject.set_fill(color, opacity, family) - self.update_rgbas_array("fill_rgbas", color, opacity) + self.update_rgbas_array("fill_rgbas", new_color) self.fill_rgbas: FloatRGBA_Array - if opacity is not None: - self.fill_opacity = opacity return self def set_stroke( self, - color: ParsableManimColor = None, + color: ParsableManimColor | list[ParsableManimColor] | None = None, width: float | None = None, opacity: float | None = None, background=False, family: bool = True, ) -> Self: + new_color: list[ManimColor] = listify(self.get_fill_color()) + if color is not None: + new_color: list[ManimColor] = listify(ManimColor.parse(color)) + if opacity is not None: + new_color = [c.opacity(opacity) for c in new_color] + if family: for submobject in self.submobjects: submobject.set_stroke(color, width, opacity, background, family) @@ -348,16 +350,14 @@ def set_stroke( array_name = "stroke_rgbas" width_name = "stroke_width" opacity_name = "stroke_opacity" - self.update_rgbas_array(array_name, color, opacity) + + self.update_rgbas_array(array_name, new_color) 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.background_stroke_color = ManimColor.parse(color) return self def set_cap_style(self, cap_style: CapStyleType) -> Self: @@ -587,6 +587,10 @@ def get_fill_opacity(self) -> ManimFloat: """ return self.get_fill_opacities()[0] + def set_fill_opacity(self, opacity: float): + self.fill_color = [x.opacity(opacity) for x in tuplify(self.fill_color)] + return self + # TODO: Does this just do a copy? # TODO: I have the feeling that this function should not return None, does that have any usage ? def get_fill_colors(self) -> list[ManimColor | None]: diff --git a/manim/utils/color/core.py b/manim/utils/color/core.py index 88eaca23b4..e671802330 100644 --- a/manim/utils/color/core.py +++ b/manim/utils/color/core.py @@ -166,7 +166,9 @@ def __init__( alpha: float = 1.0, ) -> None: if value is None: - self._internal_value = np.array((0, 0, 0, alpha), dtype=ManimColorDType) + self._internal_value = np.array( + (1.0, 1.0, 1.0, alpha), dtype=ManimColorDType + ) elif isinstance(value, ManimColor): # logger.info( # "ManimColor was passed another ManimColor. This is probably not what " @@ -629,6 +631,39 @@ def invert(self, with_alpha: bool = False) -> Self: new[-1] = alpha return self._construct_from_space(new) + @overload + def opacity(self, opacity: float) -> ManimColor: + """Returns a new ManimColor with the same color and the given opacity + + Parameters + ---------- + opacity : float + The opacity for the new ManimColor + + Returns + ------- + ManimColor + The new ManimColor object with changed opacity + """ + + @overload + def opacity(self, opacity: None) -> float: + """Returns the opacity of the current ManimColor in a range from zero to one + + Returns + ------- + float + The opacity of the ManimColor + """ + + def opacity(self, opacity=None): + """Returns a new ManimColor with the same color and a new opacity or changes the opacity""" + if opacity is None: + return self._internal_value[3] + tmp = self._internal_value.copy() + tmp[3] = opacity + return ManimColor.parse(tmp) + def interpolate(self, other: Self, alpha: float) -> Self: """Interpolate between the current and the given :class:`ManimColor`, and return the result. @@ -744,24 +779,6 @@ def contrasting( return dark return self._from_internal(BLACK._internal_value) - def opacity(self, opacity: float) -> Self: - """Create a new :class:`ManimColor` with the given opacity and the same color - values as before. - - Parameters - ---------- - opacity - The new opacity value to be used. - - Returns - ------- - ManimColor - The new :class:`ManimColor` with the same color values and the new opacity. - """ - tmp = self._internal_space.copy() - tmp[-1] = opacity - return self._construct_from_space(tmp) - def into(self, class_type: type[ManimColorT]) -> ManimColorT: """Convert the current color into a different colorspace given by ``class_type``, without changing the :attr:`_internal_value`. @@ -965,7 +982,7 @@ def gradient( raise NotImplementedError def __repr__(self) -> str: - return f"{self.__class__.__name__}('{self.to_hex()}')" + return f"{self.__class__.__name__}('{self.to_hex(True)}')" def __str__(self) -> str: return f"{self.to_hex()}"