## /utils/component/color/color.py
from typing import Any
__all__ = [
'Color',
]
[документация]
class Color:
"""
Универсальный класс для работы с цветом.
Инкапсулирует парсинг из любых форматов (HEX, RGB, RGBA, float)
и лениво преобразует его в нужные рантайм-представления.
"""
__slots__ = ('_r', '_g', '_b', '_a')
def __init__(self, *args: Any) -> None:
"""
Инициализирует объект цвета. Принимает аргументы в различных форматах.
Примеры вызовов:
Color("#FF5733") # HEX строка
Color("complex", 0.5) # HEX без решетки + прозрачность float
Color(255, 87, 51) # Изолированные целые числа RGB
Color(255, 87, 51, 128) # RGBA с альфа-каналом
Color((0.1, 0.5, 1.0)) # Нормализованный кортеж float (0.0 - 1.0)
"""
self._r: int = 0
self._g: int = 0
self._b: int = 0
self._a: int = 255 # По умолчанию полностью непрозрачный
self._parse_args(args)
def _parse_args(self, args: tuple[Any, ...]) -> None:
"""
Внутренняя функция-хелпер для разбора аргументов конструктора.
:param args: Список аргументов.
:raises ValueError: Если параметры не заданы или
если их количество не соответствует ни одному варианту инициализации.
:raises TypeError: Если переданный тип данных не поддерживется.
"""
if not args:
raise ValueError("Color value is needed for initialization.")
# Случай 1: Передан один аргумент (строка HEX или кортеж/список чисел)
if len(args) == 1:
val = args[0]
if isinstance(val, str):
self._parse_hex(val)
elif isinstance(val, tuple | list):
self._parse_sequence(val)
elif isinstance(val, Color):
self._r, self._g, self._b, self._a = val.rgba
else:
raise TypeError(f"Unsupported data type for color: {type(val).__name__}")
# Случай 2: Передана строка HEX + явная альфа-компонента (например, Color("FF5733", 128))
elif len(args) == 2 and isinstance(args[0], str):
self._parse_hex(args[0])
self._a = self._clamp_int(args[1])
# Случай 3: Переданы изолированные компоненты чисел (RGB или RGBA)
elif len(args) in (3, 4):
self._parse_sequence(args)
else:
raise ValueError("Incorrect number of arguments for initialization of Color.")
def _parse_hex(self, hex_str: str) -> None:
"""
Парсит строковое HEX представление.
:param hex_str: Строка хеша цвета.
:raises ValueError: Если хеш-строка не соответствует формату.
"""
clean = hex_str.lstrip('#').strip()
# Поддержка коротких HEX (например, 'FFF' -> 'FFFFFF')
if len(clean) in (3, 4):
clean = "".join(c * 2 for c in clean)
if len(clean) == 6:
self._r = int(clean[0:2], 16)
self._g = int(clean[2:4], 16)
self._b = int(clean[4:6], 16)
self._a = 255
elif len(clean) == 8:
self._r = int(clean[0:2], 16)
self._g = int(clean[2:4], 16)
self._b = int(clean[4:6], 16)
self._a = int(clean[6:8], 16)
else:
raise ValueError(f"Incorrect length of color HEX-string: '{hex_str}'")
def _parse_sequence(self, seq: tuple[Any, ...] | list[Any]) -> None:
"""
Парсит последовательность чисел (целые 0-255 или float 0.0-1.0).
:param seq: Последовательность чисел.
:raises ValueError: Если для последовательности цветов переданно неверное количиство аргументов.
"""
if len(seq) not in (3, 4):
raise ValueError("Sequence for a color should contain strictly either 3 or 4 elements.")
# Определяем, в каком формате переданы числа.
# Если хотя бы одно число является float меньше 1.0, считаем весь массив нормализованным
is_float = any(isinstance(x, float) and x <= 1.0 for x in seq)
if is_float:
self._r = round(float(seq[0]) * 255)
self._g = round(float(seq[1]) * 255)
self._b = round(float(seq[2]) * 255)
if len(seq) == 4:
self._a = round(float(seq[3]) * 255)
else:
self._r = self._clamp_int(seq[0])
self._g = self._clamp_int(seq[1])
self._b = self._clamp_int(seq[2])
if len(seq) == 4:
self._a = self._clamp_int(seq[3])
@staticmethod
def _clamp_int(val: Any) -> int:
"""
Ограничивает число в безопасных границах байта [0-255].
:param val: Число.
:raises TypeError: Если отдельный байт цвета задан не числом.
"""
try:
ival = int(val)
return max(0, min(255, ival))
except (ValueError, TypeError):
raise TypeError(f"Color component should be a number, received: {val!r}")
@property
def rgb(self) -> tuple[int, int, int]:
"""
Возвращает классический кортеж целых чисел (R, G, B).
:return: Цвет в формате RGB.
"""
return self._r, self._g, self._b
@property
def rgba(self) -> tuple[int, int, int, int]:
"""
Возвращает полный кортеж целых чисел с альфа-каналом (R, G, B, A).
:return: Цвет в формате RGBA.
"""
return self._r, self._g, self._b, self._a
@property
def hex(self) -> str:
"""
Возвращает стандартную шестнадцатеричную строку (например, '#FF5733').
:return: Цвет в формате HEX.
"""
return f"#{self._r:02X}{self._g:02X}{self._b:02X}"
@property
def hex_alpha(self) -> str:
"""
Возвращает HEX строку, включающую альфа-компоненту (например, '#FF573380').
:return: Цвет в формате HEXA.
"""
return f"#{self._r:02X}{self._g:02X}{self._b:02X}{self._a:02X}"
@property
def normalized(self) -> tuple[float, float, float, float]:
"""
Возвращает нормализованные float-координаты от 0.0 до 1.0.
:return: Нормализованный цвет в формате RGBA.
"""
return self._r / 255.0, self._g / 255.0, self._b / 255.0, self._a / 255.0
def __str__(self) -> str:
"""
По умолчанию строковое приведение отдает готовый HEX для UI-виджетов.
:return: Строковое представление цвета в виде hex-строки.
"""
return self.hex
def __repr__(self) -> str:
"""
Строковое представление цвета.
:return: Представление цвета в виде строки.
"""
return f"Color(R={self._r}, G={self._g}, B={self._b}, A={self._a})"
def __eq__(self, other: Any) -> bool:
"""
Два цвета равны, если полностью совпадают их RGBA компоненты.
:param other: Объект для сравнения.
:return: Результат сравнения.
"""
if isinstance(other, Color):
return self.rgba == other.rgba
if isinstance(other, tuple | list) and len(other) in (3, 4):
# Позволяет сравнивать объект Color напрямую с кортежами чисел
try:
check_color = Color(other)
return self.rgba == check_color.rgba
except Exception:
return False
if isinstance(other, str):
try:
check_color = Color(other)
return self.rgba == check_color.rgba
except Exception:
return False
return False