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")