Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add darker, lighter and contrasting methods to ManimColor #3992

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions manim/utils/color/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@
new[-1] = alpha
return self._construct_from_space(new)

def interpolate(self, other: ManimColor, alpha: float) -> Self:
def interpolate(self, other: Self, alpha: float) -> Self:
"""Interpolates between the current and the given ManimColor an returns the interpolated color

Parameters
Expand All @@ -606,6 +606,99 @@
self._internal_space * (1 - alpha) + other._internal_space * alpha
)

def darker(self, blend: float = 0.2) -> Self:
"""Returns a new color that is darker than the current color, i.e.
interpolated with black. The opacity is unchanged.

Parameters
----------
blend : float, optional
The blend ratio for the interpolation, from 0 (the current color
unchanged) to 1 (pure black). By default 0.2 which results in a
slightly darker color

Returns
-------
ManimColor
The darker ManimColor

See Also
--------
:meth:`lighter`
"""
from manim.utils.color.manim_colors import BLACK
Dismissed Show dismissed Hide dismissed

alpha = self._internal_space[3]
black = self._from_internal(BLACK._internal_value)
return self.interpolate(black, blend).opacity(alpha)

def lighter(self, blend: float = 0.2) -> Self:
"""Returns a new color that is lighter than the current color, i.e.
interpolated with white. The opacity is unchanged.

Parameters
----------
blend : float, optional
The blend ratio for the interpolation, from 0 (the current color
unchanged) to 1 (pure white). By default 0.2 which results in a
slightly lighter color

Returns
-------
ManimColor
The lighter ManimColor

See Also
--------
:meth:`darker`
"""
from manim.utils.color.manim_colors import WHITE
Dismissed Show dismissed Hide dismissed

alpha = self._internal_space[3]
white = self._from_internal(WHITE._internal_value)
return self.interpolate(white, blend).opacity(alpha)

def contrasting(
self,
threshold: float = 0.5,
light: Self | None = None,
dark: Self | None = None,
) -> Self:
"""Returns one of two colors, light or dark (by default white or black),
that contrasts with the current color (depending on its luminance).
This is typically used to set text in a contrasting color that ensures
it is readable against a background of the current color.

Parameters
----------
threshold : float, optional
The luminance threshold that dictates whether the current color is
considered light or dark (and thus whether to return the dark or
light color, respectively), by default 0.5
light : ManimColor, optional
The light color to return if the current color is considered dark,
by default pure white
dark : ManimColor, optional
The dark color to return if the current color is considered light,
by default pure black

Returns
-------
ManimColor
The contrasting ManimColor
"""
from manim.utils.color.manim_colors import BLACK, WHITE
Dismissed Show dismissed Hide dismissed

luminance, _, _ = colorsys.rgb_to_yiq(*self.to_rgb())
if luminance < threshold:
if light is not None:
return light
return self._from_internal(WHITE._internal_value)
else:
if dark is not None:
return dark
return self._from_internal(BLACK._internal_value)

def opacity(self, opacity: float) -> Self:
"""Creates a new ManimColor with the given opacity and the same color value as before

Expand Down Expand Up @@ -1282,7 +1375,7 @@


def interpolate_color(
color1: ManimColorT, color2: ManimColor, alpha: float
color1: ManimColorT, color2: ManimColorT, alpha: float
) -> ManimColorT:
"""Standalone function to interpolate two ManimColors and get the result refer to :meth:`interpolate` in :class:`ManimColor`

Expand Down
12 changes: 5 additions & 7 deletions manim/utils/color/manim_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,22 @@ def subnames(name):
for line, char in zip(color_groups[0], "abcde"):
color_groups.add(Text(char).scale(0.6).next_to(line, LEFT, buff=0.2))

def named_lines_group(length, colors, names, text_colors, align_to_block):
def named_lines_group(length, color_names, labels, align_to_block):
colors = [getattr(Colors, color.upper()) for color in color_names]
lines = VGroup(
*[
Line(
ORIGIN,
RIGHT * length,
stroke_width=55,
color=getattr(Colors, color.upper()),
color=color,
)
for color in colors
]
).arrange_submobjects(buff=0.6, direction=DOWN)

for line, name, color in zip(lines, names, text_colors):
line.add(Text(name, color=color).scale(0.6).move_to(line))
for line, name, color in zip(lines, labels, colors):
line.add(Text(name, color=color.contrasting()).scale(0.6).move_to(line))
lines.next_to(color_groups, DOWN, buff=0.5).align_to(
color_groups[align_to_block], LEFT
)
Expand All @@ -79,7 +80,6 @@ def named_lines_group(length, colors, names, text_colors, align_to_block):
3.2,
other_colors,
other_colors,
[BLACK] * 4 + [WHITE] * 2,
0,
)

Expand All @@ -95,7 +95,6 @@ def named_lines_group(length, colors, names, text_colors, align_to_block):
"darker_gray / gray_e",
"black",
],
[BLACK] * 3 + [WHITE] * 4,
2,
)

Expand All @@ -109,7 +108,6 @@ def named_lines_group(length, colors, names, text_colors, align_to_block):
3.2,
pure_colors,
pure_colors,
[BLACK, BLACK, WHITE],
6,
)

Expand Down
29 changes: 29 additions & 0 deletions tests/module/utils/test_manim_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,32 @@ def test_hsv_init() -> None:

def test_into_HSV() -> None:
nt.assert_equal(RED.into(HSV).into(ManimColor), RED)


def test_contrasting() -> None:
nt.assert_equal(BLACK.contrasting(), WHITE)
nt.assert_equal(WHITE.contrasting(), BLACK)
nt.assert_equal(RED.contrasting(0.1), BLACK)
nt.assert_equal(RED.contrasting(0.9), WHITE)
nt.assert_equal(BLACK.contrasting(dark=GREEN, light=RED), RED)
nt.assert_equal(WHITE.contrasting(dark=GREEN, light=RED), GREEN)


def test_lighter() -> None:
c = RED.opacity(0.42)
cl = c.lighter(0.2)
nt.assert_array_equal(
cl._internal_value[:3],
0.8 * c._internal_value[:3] + 0.2 * WHITE._internal_value[:3],
)
nt.assert_equal(cl[-1], c[-1])


def test_darker() -> None:
c = RED.opacity(0.42)
cd = c.darker(0.2)
nt.assert_array_equal(
cd._internal_value[:3],
0.8 * c._internal_value[:3] + 0.2 * BLACK._internal_value[:3],
)
nt.assert_equal(cd[-1], c[-1])
Loading