diff --git a/CHANGELOG.md b/CHANGELOG.md index 072f92175e..2fe96b8968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed `VERTICAL_BREAKPOINTS` doesn't work https://github.com/Textualize/textual/pull/5785 - Fixed `Button` allowing text selection https://github.com/Textualize/textual/pull/5770 +### Added + +- Added `Color.hsv` property and `Color.from_hsv` class method https://github.com/Textualize/textual/pull/5803 + ## [3.2.0] - 2025-05-02 ### Fixed diff --git a/src/textual/color.py b/src/textual/color.py index 302fac9b42..08d7b43864 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -31,7 +31,7 @@ from __future__ import annotations import re -from colorsys import hls_to_rgb, rgb_to_hls +from colorsys import hls_to_rgb, hsv_to_rgb, rgb_to_hls, rgb_to_hsv from functools import lru_cache from operator import itemgetter from typing import Callable, NamedTuple @@ -82,7 +82,7 @@ class HSV(NamedTuple): s: float """Saturation in range 0 to 1.""" v: float - """Value un range 0 to 1.""" + """Value in range 0 to 1.""" class Lab(NamedTuple): @@ -212,6 +212,21 @@ def from_hsl(cls, h: float, s: float, l: float) -> Color: r, g, b = hls_to_rgb(h, l, s) return cls(int(r * 255 + 0.5), int(g * 255 + 0.5), int(b * 255 + 0.5)) + @classmethod + def from_hsv(cls, h: float, s: float, v: float) -> Color: + """Create a color from HSV components. + + Args: + h: Hue. + s: Saturation. + v: Value. + + Returns: + A new color. + """ + r, g, b = hsv_to_rgb(h, s, v) + return cls(int(r * 255 + 0.5), int(g * 255 + 0.5), int(b * 255 + 0.5)) + @property def inverse(self) -> Color: """The inverse of this color. @@ -286,6 +301,19 @@ def hsl(self) -> HSL: h, l, s = rgb_to_hls(r, g, b) return HSL(h, s, l) + @property + def hsv(self) -> HSV: + """This color in HSV format. + + HSV color is an alternative way of representing a color, which can be used in certain color calculations. + + Returns: + Color encoded in HSV format. + """ + r, g, b = self.normalized + h, s, v = rgb_to_hsv(r, g, b) + return HSV(h, s, v) + @property def brightness(self) -> float: """The human perceptual brightness. diff --git a/tests/test_color.py b/tests/test_color.py index 5c7450853a..e504321c69 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -52,6 +52,17 @@ def test_hsl(): assert red.hsl.css == "hsl(356,81.8%,43.1%)" +def test_hsv(): + red = Color(200, 20, 32) + print(red.hsv) + assert red.hsv == pytest.approx( + (0.9888888888888889, 0.8999999999999999, 0.7843137254901961) + ) + assert Color.from_hsv( + 0.9888888888888889, 0.8999999999999999, 0.7843137254901961 + ).normalized == pytest.approx(red.normalized, rel=1e-5) + + def test_color_brightness(): assert Color(255, 255, 255).brightness == 1 assert Color(0, 0, 0).brightness == 0