mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
color docs
This commit is contained in:
@@ -5,13 +5,13 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## [1.1.15] - 2022-01-31
|
## [0.1.15] - 2022-01-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added Windows Driver
|
- Added Windows Driver
|
||||||
|
|
||||||
## [1.1.14] - 2022-01-09
|
## [0.1.14] - 2022-01-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -552,32 +552,67 @@ class Color(NamedTuple):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_rich_color(cls, rich_color: RichColor) -> Color:
|
def from_rich_color(cls, rich_color: RichColor) -> Color:
|
||||||
"""Create color from Rich's color class."""
|
"""Create a new color from Rich's Color class.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rich_color (RichColor): An instance of rich.color.Color.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Color: A new Color.
|
||||||
|
"""
|
||||||
r, g, b = rich_color.get_truecolor()
|
r, g, b = rich_color.get_truecolor()
|
||||||
return cls(r, g, b)
|
return cls(r, g, b)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_hls(cls, h: float, l: float, s: float) -> Color:
|
def from_hls(cls, h: float, l: float, s: float) -> Color:
|
||||||
|
"""Create a color from HLS components.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
h (float): Hue.
|
||||||
|
l (float): Lightness.
|
||||||
|
s (float): Saturation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Color: A new color.
|
||||||
|
"""
|
||||||
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
r, g, b = colorsys.hls_to_rgb(h, l, s)
|
||||||
return cls(int(r * 255), int(g * 255), int(b * 255))
|
return cls(int(r * 255), int(g * 255), int(b * 255))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_hsv(cls, h: float, s: float, v: float) -> Color:
|
def from_hsv(cls, h: float, s: float, v: float) -> Color:
|
||||||
|
"""Create a color from HSV components.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
h (float): Hue
|
||||||
|
s (float): Saturation
|
||||||
|
v (float): Value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Color: A new Color.
|
||||||
|
"""
|
||||||
r, g, b = colorsys.hsv_to_rgb(h, s, v)
|
r, g, b = colorsys.hsv_to_rgb(h, s, v)
|
||||||
return cls(int(r * 255), int(g * 255), int(b * 255))
|
return cls(int(r * 255), int(g * 255), int(b * 255))
|
||||||
|
|
||||||
def __rich__(self) -> Text:
|
def __rich__(self) -> Text:
|
||||||
|
"""A Rich method to show the color."""
|
||||||
r, g, b, _ = self
|
r, g, b, _ = self
|
||||||
return Text(
|
return Text(
|
||||||
" " * 10,
|
f" {self!r} ",
|
||||||
style=Style.from_color(RichColor.default(), RichColor.from_rgb(r, g, b)),
|
style=Style.from_color(
|
||||||
|
self.get_contrast_text().rich_color, RichColor.from_rgb(r, g, b)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def saturate(self) -> Color:
|
def saturate(self) -> Color:
|
||||||
|
"""Get a color with all components saturated to maximum and minimum values."""
|
||||||
r, g, b, a = self
|
r, g, b, a = self
|
||||||
|
_clamp = clamp
|
||||||
color = Color(
|
color = Color(
|
||||||
clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), clamp(a, 0.0, 1.0)
|
_clamp(r, 0, 255),
|
||||||
|
_clamp(g, 0, 255),
|
||||||
|
_clamp(b, 0, 255),
|
||||||
|
_clamp(a, 0.0, 1.0),
|
||||||
)
|
)
|
||||||
return color
|
return color
|
||||||
|
|
||||||
@@ -595,19 +630,21 @@ class Color(NamedTuple):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def hls(self) -> HLS:
|
def hls(self) -> HLS:
|
||||||
|
"""Get the color as HLS."""
|
||||||
r, g, b = self.normalized
|
r, g, b = self.normalized
|
||||||
hls = colorsys.rgb_to_hls(r, g, b)
|
hls = colorsys.rgb_to_hls(r, g, b)
|
||||||
return HLS(*hls)
|
return HLS(*hls)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hsv(self) -> HSV:
|
def hsv(self) -> HSV:
|
||||||
|
"""Get the color as HSV."""
|
||||||
r, g, b = self.normalized
|
r, g, b = self.normalized
|
||||||
hsv = colorsys.rgb_to_hsv(r, g, b)
|
hsv = colorsys.rgb_to_hsv(r, g, b)
|
||||||
return HSV(*hsv)
|
return HSV(*hsv)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_transparent(self) -> bool:
|
def is_transparent(self) -> bool:
|
||||||
"""Check if the color is transparent."""
|
"""Check if the color is transparent (alpha == 0)."""
|
||||||
return self.a == 0
|
return self.a == 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -630,7 +667,7 @@ class Color(NamedTuple):
|
|||||||
"""The color in CSS rgb or rgba form.
|
"""The color in CSS rgb or rgba form.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: A CSS color, e.g. "rgb(10,20,30)" or "(rgb(50,70,80,0.5)"
|
str: A CSS style color, e.g. "rgb(10,20,30)" or "rgb(50,70,80,0.5)"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
r, g, b, a = self
|
r, g, b, a = self
|
||||||
@@ -644,9 +681,18 @@ class Color(NamedTuple):
|
|||||||
yield "a", a
|
yield "a", a
|
||||||
|
|
||||||
def with_alpha(self, alpha: float) -> Color:
|
def with_alpha(self, alpha: float) -> Color:
|
||||||
|
"""Create a new color with the given alpha.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
alpha (float): New value for alpha.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Color: A new color.
|
||||||
|
"""
|
||||||
r, g, b, a = self
|
r, g, b, a = self
|
||||||
return Color(r, g, b, alpha)
|
return Color(r, g, b, alpha)
|
||||||
|
|
||||||
|
@lru_cache(maxsize=2048)
|
||||||
def blend(self, destination: Color, factor: float) -> Color:
|
def blend(self, destination: Color, factor: float) -> Color:
|
||||||
"""Generate a new color between two colors.
|
"""Generate a new color between two colors.
|
||||||
|
|
||||||
@@ -727,39 +773,52 @@ class Color(NamedTuple):
|
|||||||
return color
|
return color
|
||||||
|
|
||||||
def darken(self, amount: float) -> Color:
|
def darken(self, amount: float) -> Color:
|
||||||
|
"""Darken the color by a given amount.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
amount (float): Value between 0-1 to reduce luminance by.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Color: New color.
|
||||||
|
"""
|
||||||
h, l, s = self.hls
|
h, l, s = self.hls
|
||||||
color = Color.from_hls(h, l - amount, s)
|
color = Color.from_hls(h, l - amount, s)
|
||||||
return color.saturate
|
return color.saturate
|
||||||
|
|
||||||
def lighten(self, amount: float) -> Color:
|
def lighten(self, amount: float) -> Color:
|
||||||
|
"""Lighten the color by a given amount.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
amount (float): Value between 0-1 to increase luminance by.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Color: New color.
|
||||||
|
"""
|
||||||
return self.darken(-amount).saturate
|
return self.darken(-amount).saturate
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self) -> float:
|
def brightness(self) -> float:
|
||||||
|
"""Get the human perceptual brightness."""
|
||||||
r, g, b = self.normalized
|
r, g, b = self.normalized
|
||||||
brightness = (299 * r + 587 * g + 114 * b) / 1000
|
brightness = (299 * r + 587 * g + 114 * b) / 1000
|
||||||
return brightness
|
return brightness
|
||||||
|
|
||||||
def calculate_contrast(self, color: Color) -> float:
|
|
||||||
return abs(self.brightness - color.brightness)
|
|
||||||
# brightness = (299 * R + 587 * G + 114 * B) / 1000
|
|
||||||
|
|
||||||
# l1 = self.hls.l
|
|
||||||
# l2 = color.hls.l
|
|
||||||
# l1, l2 = sorted([l1, l2])
|
|
||||||
# return (l1 + 0.05) / (l2 + 0.05)
|
|
||||||
|
|
||||||
def get_contrast_text(self, alpha=0.95) -> Color:
|
def get_contrast_text(self, alpha=0.95) -> Color:
|
||||||
|
"""Get a light or dark color that best contrasts this color, for use with text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
alpha (float, optional): An alpha value to adjust the pure white / black by.
|
||||||
|
Defaults to 0.95.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Color: A new color, either an off-white or off-black
|
||||||
|
"""
|
||||||
white = self.blend(Color(255, 255, 255), alpha)
|
white = self.blend(Color(255, 255, 255), alpha)
|
||||||
black = self.blend(Color(0, 0, 0), alpha)
|
black = self.blend(Color(0, 0, 0), alpha)
|
||||||
|
brightness = self.brightness
|
||||||
white_contrast = self.calculate_contrast(white)
|
white_contrast = abs(brightness - white.brightness)
|
||||||
black_contrast = self.calculate_contrast(black)
|
black_contrast = abs(brightness - black.brightness)
|
||||||
|
return white if white_contrast > black_contrast else black
|
||||||
if white_contrast > black_contrast:
|
|
||||||
return white
|
|
||||||
else:
|
|
||||||
return black
|
|
||||||
|
|
||||||
|
|
||||||
class ColorPair(NamedTuple):
|
class ColorPair(NamedTuple):
|
||||||
@@ -782,22 +841,20 @@ class ColorPair(NamedTuple):
|
|||||||
"""Get a Rich style, foreground adjusted for transparency."""
|
"""Get a Rich style, foreground adjusted for transparency."""
|
||||||
r, g, b, a = self.foreground
|
r, g, b, a = self.foreground
|
||||||
if a == 0:
|
if a == 0:
|
||||||
return Style(
|
return Style.from_color(
|
||||||
color=self.background.rich_color,
|
self.background.rich_color, self.background.rich_color
|
||||||
bgcolor=self.background.rich_color,
|
|
||||||
)
|
)
|
||||||
elif a == 1:
|
elif a == 1:
|
||||||
return Style(
|
return Style.from_color(
|
||||||
color=self.foreground.rich_color,
|
self.foreground.rich_color, self.background.rich_color
|
||||||
bgcolor=self.background.rich_color,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
r2, g2, b2, _ = self.background
|
r2, g2, b2, _ = self.background
|
||||||
return Style(
|
return Style.from_color(
|
||||||
color=RichColor.from_rgb(
|
RichColor.from_rgb(
|
||||||
r + (r2 - r) * a, g + (g2 - g) * a, b + (b2 - b) * a
|
r + (r2 - r) * a, g + (g2 - g) * a, b + (b2 - b) * a
|
||||||
),
|
),
|
||||||
bgcolor=self.background.rich_color,
|
self.background.rich_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ def generate_light(
|
|||||||
secondary: Color | None = None,
|
secondary: Color | None = None,
|
||||||
warning: Color | None = None,
|
warning: Color | None = None,
|
||||||
error: Color | None = None,
|
error: Color | None = None,
|
||||||
|
success: Color | None = None,
|
||||||
accent1: Color | None = None,
|
accent1: Color | None = None,
|
||||||
accent2: Color | None = None,
|
accent2: Color | None = None,
|
||||||
accent3: Color | None = None,
|
accent3: Color | None = None,
|
||||||
@@ -33,6 +34,7 @@ def generate_light(
|
|||||||
surface: Color | None = None,
|
surface: Color | None = None,
|
||||||
luminosity_spread: float = 0.2,
|
luminosity_spread: float = 0.2,
|
||||||
text_alpha: float = 0.98,
|
text_alpha: float = 0.98,
|
||||||
|
dark: bool = False,
|
||||||
) -> tuple[dict[str, Color], dict[str, Color]]:
|
) -> tuple[dict[str, Color], dict[str, Color]]:
|
||||||
|
|
||||||
if secondary is None:
|
if secondary is None:
|
||||||
@@ -44,6 +46,9 @@ def generate_light(
|
|||||||
if error is None:
|
if error is None:
|
||||||
error = secondary
|
error = secondary
|
||||||
|
|
||||||
|
if success is None:
|
||||||
|
success = secondary
|
||||||
|
|
||||||
if accent1 is None:
|
if accent1 is None:
|
||||||
accent1 = primary
|
accent1 = primary
|
||||||
|
|
||||||
@@ -54,19 +59,16 @@ def generate_light(
|
|||||||
accent3 = accent2
|
accent3 = accent2
|
||||||
|
|
||||||
if background is None:
|
if background is None:
|
||||||
background = Color(245, 245, 245)
|
background = Color(0, 0, 0) if dark else Color(245, 245, 245)
|
||||||
|
|
||||||
if surface is None:
|
if surface is None:
|
||||||
surface = Color(229, 229, 229)
|
surface = Color.parse("#121212") if dark else Color(229, 229, 229)
|
||||||
|
|
||||||
backgrounds: dict[str, Color] = {"background": background, "surface": surface}
|
backgrounds: dict[str, Color] = {"background": background, "surface": surface}
|
||||||
foregrounds: dict[str, Color] = {
|
foregrounds: dict[str, Color] = {}
|
||||||
"on-background": background.get_contrast_text(text_alpha),
|
|
||||||
"on-surface": surface.get_contrast_text(text_alpha),
|
|
||||||
}
|
|
||||||
|
|
||||||
def luminosity_range() -> Iterable[tuple[str, float]]:
|
def luminosity_range(spread) -> Iterable[tuple[str, float]]:
|
||||||
luminosity_step = luminosity_spread / 2
|
luminosity_step = spread / 2
|
||||||
for n in range(-2, +3):
|
for n in range(-2, +3):
|
||||||
if n < 0:
|
if n < 0:
|
||||||
label = "-darken"
|
label = "-darken"
|
||||||
@@ -78,27 +80,37 @@ def generate_light(
|
|||||||
yield (f"{label}{abs(n) if n else ''}"), n * luminosity_step
|
yield (f"{label}{abs(n) if n else ''}"), n * luminosity_step
|
||||||
|
|
||||||
COLORS = [
|
COLORS = [
|
||||||
("background", background),
|
|
||||||
("surface", surface),
|
|
||||||
("primary", primary),
|
("primary", primary),
|
||||||
("secondary", secondary),
|
("secondary", secondary),
|
||||||
|
("background", background),
|
||||||
|
("surface", surface),
|
||||||
("warning", warning),
|
("warning", warning),
|
||||||
("error", error),
|
("error", error),
|
||||||
|
("success", success),
|
||||||
("accent1", accent1),
|
("accent1", accent1),
|
||||||
("accent2", accent2),
|
("accent2", accent2),
|
||||||
("accent3", accent3),
|
("accent3", accent3),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DARK_SHADES = {"primary", "secondary"}
|
||||||
|
|
||||||
for name, color in COLORS:
|
for name, color in COLORS:
|
||||||
for shade_name, luminosity_delta in luminosity_range():
|
is_dark_shade = name in DARK_SHADES
|
||||||
|
spread = luminosity_spread / 2 if is_dark_shade else luminosity_spread
|
||||||
shade_color = color.lighten(luminosity_delta)
|
for shade_name, luminosity_delta in luminosity_range(spread):
|
||||||
backgrounds[f"{name}{shade_name}"] = shade_color
|
if dark and is_dark_shade:
|
||||||
|
dark_background = background.blend(color, 8 / 100)
|
||||||
|
shade_color = dark_background.blend(
|
||||||
|
Color(255, 255, 255), spread + luminosity_delta
|
||||||
|
)
|
||||||
|
backgrounds[f"{name}{shade_name}"] = shade_color
|
||||||
|
else:
|
||||||
|
shade_color = color.lighten(luminosity_delta)
|
||||||
|
backgrounds[f"{name}{shade_name}"] = shade_color
|
||||||
for fade in range(3):
|
for fade in range(3):
|
||||||
text_color = shade_color.get_contrast_text(text_alpha)
|
text_color = shade_color.get_contrast_text(text_alpha)
|
||||||
if fade > 0:
|
if fade > 0:
|
||||||
text_color = text_color.blend(shade_color, fade * 0.2)
|
text_color = text_color.blend(shade_color, fade * 0.20 + 0.25)
|
||||||
on_name = f"on-{name}{shade_name}-fade{fade}"
|
on_name = f"on-{name}{shade_name}-fade{fade}"
|
||||||
else:
|
else:
|
||||||
on_name = f"on-{name}{shade_name}"
|
on_name = f"on-{name}{shade_name}"
|
||||||
@@ -119,22 +131,31 @@ if __name__ == "__main__":
|
|||||||
backgrounds, foregrounds = generate_light(
|
backgrounds, foregrounds = generate_light(
|
||||||
primary=Color.parse("#4caf50"),
|
primary=Color.parse("#4caf50"),
|
||||||
secondary=Color.parse("#ffa000"),
|
secondary=Color.parse("#ffa000"),
|
||||||
|
warning=Color.parse("#FDD835"),
|
||||||
error=Color.parse("#ff5722"),
|
error=Color.parse("#ff5722"),
|
||||||
|
success=Color.parse("#558B2F"),
|
||||||
|
accent1=Color.parse("#1976D2"),
|
||||||
|
dark=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(foregrounds)
|
|
||||||
|
|
||||||
for name, background in backgrounds.items():
|
for name, background in backgrounds.items():
|
||||||
|
|
||||||
|
foreground = foregrounds[f"on-{name}"]
|
||||||
|
text = Text(f"{background.hex} ", style=f"{foreground.hex} on {background.hex}")
|
||||||
for fade in range(3):
|
for fade in range(3):
|
||||||
if fade:
|
if fade:
|
||||||
foreground = foregrounds[f"on-{name}-fade{fade}"]
|
foreground = foregrounds[f"on-{name}-fade{fade}"]
|
||||||
else:
|
else:
|
||||||
foreground = foregrounds[f"on-{name}"]
|
foreground = foregrounds[f"on-{name}"]
|
||||||
|
|
||||||
console.print(
|
text.append(
|
||||||
Padding(f"{background.hex} - {name}", 0),
|
f"{name} ",
|
||||||
justify="left",
|
|
||||||
style=f"{foreground.hex} on {background.hex}",
|
style=f"{foreground.hex} on {background.hex}",
|
||||||
highlight=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
Padding(text, 1),
|
||||||
|
justify="left",
|
||||||
|
style=f"on {background.hex}",
|
||||||
|
highlight=False,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user