colors.py

5.0 kB · python · 166 lines

1import numpy as np2from typing import List, Optional, Tuple3from .errors import MrlyError4from .palette import PALETTE5from .state import state67class Color:89    def __init__(10        self,11        r: int = 0,12        g: int = 0,13        b: int = 0,14        a: int = 25515    ):16        self.r = r17        self.g = g18        self.b = b19        self.a = a2021    def __repr__(self) -> str:22        return self.to_string()2324    def __str__(self) -> str:25        return self.to_string()2627    def __eq__(self, other: "Color") -> bool:28        return self.r == other.r and self.g == other.g and self.b == other.b and self.a == other.a2930    def __hash__(self) -> int:31        return hash((self.r, self.g, self.b, self.a))3233    def to_string(self) -> str:34        if self.a == 255:35            return f"rgb({self.r},{self.g},{self.b})"36        return f"rgba({self.r},{self.g},{self.b},{self.a})"3738    def to_dict(self):39        return [self.r, self.g, self.b, self.a]4041    @classmethod42    def from_dict(cls, data):43        r, g, b, a = data44        return cls(r, g, b, a)4546    @staticmethod47    def rgba_to_hex(r: int, g: int, b: int, a: int) -> str:48        if a == 255:49            return f"#{r:02x}{g:02x}{b:02x}"50        return f"#{r:02x}{g:02x}{b:02x}{a:02x}"5152    def to_hex(self) -> str:53        return Color.rgba_to_hex(self.r, self.g, self.b, self.a)5455    @classmethod56    def from_hex(cls, hex_code: str, alpha: int = 255) -> "Color":57        hex_code = hex_code.strip("#")58        if len(hex_code) == 8:59            r = int(hex_code[0:2], 16)60            g = int(hex_code[2:4], 16)61            b = int(hex_code[4:6], 16)62            a = int(hex_code[6:8], 16)63            return cls(r, g, b, a)64        elif len(hex_code) == 6:65            r = int(hex_code[0:2], 16)66            g = int(hex_code[2:4], 16)67            b = int(hex_code[4:6], 16)68            return cls(r, g, b, alpha)69        else:70            raise MrlyError("Hex code must be in format #RRGGBB or #RRGGBBAA")7172    def to_rgb(self) -> Tuple[int, int, int]:73        return (self.r, self.g, self.b)7475    @classmethod76    def from_rgb(cls, r: int, g: int, b: int, alpha: int = 255) -> "Color":77        return cls(r, g, b, alpha)7879    def to_rgba(self) -> Tuple[int, int, int, int]:80        return (self.r, self.g, self.b, self.a)8182    @classmethod83    def from_rgba(cls, r: int, g: int, b: int, a: int) -> "Color":84        return cls(r, g, b, a)8586    def alpha(self, level: int) -> "Color":87        if not 0 <= level <= 255:88            raise MrlyError(f"Level must be between 0 and 255, got {level}")89        return Color(self.r, self.g, self.b, level)9091    def invert(self) -> "Color":92        r, g, b, a = 255 - self.r, 255 - self.g, 255 - self.b, self.a93        return Color(r, g, b, a)9495    def lightness(self, level: int) -> "Color":96        if not 0 <= level <= 100:97            raise MrlyError(f"Level must be between 0 and 100, got {level}")98        if level == 50:99            r, g, b = self.r, self.g, self.b100        elif level < 50:101            factor = level / 50102            r = int(self.r * factor)103            g = int(self.g * factor)104            b = int(self.b * factor)105        else:106            factor = (level - 50) / 50107            r = int(self.r + (255 - self.r) * factor)108            g = int(self.g + (255 - self.g) * factor)109            b = int(self.b + (255 - self.b) * factor)110        return Color(r, g, b, self.a)111112    @classmethod113    def random(cls, alpha: bool = False) -> "Color":114        return cls(115            r=state.rng.randint(0, 256),116            g=state.rng.randint(0, 256),117            b=state.rng.randint(0, 256),118            a=state.rng.randint(0, 256) if alpha else 255,119        )120121def mix(color1: Color, color2: Color, ratio: float = 0.5) -> Color:122    if not 0.0 <= ratio <= 1.0:123        raise MrlyError(f"Ratio must be between 0.0 and 1.0, got {ratio}")124    r = int(color1.r + (color2.r - color1.r) * ratio)125    g = int(color1.g + (color2.g - color1.g) * ratio)126    b = int(color1.b + (color2.b - color1.b) * ratio)127    a = int(color1.a + (color2.a - color1.a) * ratio)128    return Color(r, g, b, a)129130def gradient(colors: List[Color], steps: int) -> List[Color]:131    if not colors:132        raise MrlyError("Cannot create a gradient from an empty list of colors.")133    if steps < 1:134        raise MrlyError("Steps must be at least 1.")135    if steps == 1:136        return [colors[0]]137    if len(colors) == 1:138        return [colors[0]] * steps139    result = []140    segments = len(colors) - 1141    for i in range(steps):142        pos = i / (steps - 1)143        seg = int(pos * segments)144        if seg >= segments:145            seg = segments - 1146        ratio = (pos * segments) - seg147        start = colors[seg]148        end = colors[seg + 1]149        result.append(mix(start, end, ratio))150    return result151152def ink(name: str) -> Color:153    if name not in PALETTE:154        raise MrlyError(f"Unknown ink {name!r}.")155    return Color(*PALETTE[name])156157alpha = Color(0, 0, 0, 0)158black = ink("black")159white = ink("white")160gray = ink("gray")161red = ink("red")162green = ink("green")163blue = ink("blue")164cyan = ink("cyan")165purple = ink("purple")166yellow = ink("yellow")