Source code for fdtdx.colors

"""Color module for fdtdx framework.

This module provides a Color class for representing and converting between different
color formats, along with a comprehensive collection of named colors from the XKCD
color survey. The Color class is a subclass of fdtdx.TreeClass and supports:

- Multiple input formats: RGB tuples (normalized or 8-bit), hex strings, named colors
- Multiple output formats: normalized RGB, 8-bit RGB, hex, matplotlib-compatible
- Type checking and validation
- Integration with the fdtdx framework

The module includes all XKCD colors as predefined constants for consistent visualization.

Example:
    >>> from fdtdx import colors
    >>> # Create colors from different formats
    >>> c1 = colors.Color.from_rgb(255, 0, 0)  # 8-bit RGB
    >>> c2 = colors.Color.from_hex("#00FF00")
    >>> c3 = colors.XKCD_VIBRANT_GREEN
    >>>
    >>> # Convert to different formats
    >>> c1.to_hex()  # "#FF0000"
    >>> c1.to_rgb_normalized()  # (1.0, 0.0, 0.0)
    >>> c1.to_rgb_255()  # (255, 0, 0)
"""

from fdtdx.core.jax.pytrees import TreeClass, autoinit


[docs] @autoinit class Color(TreeClass): """Color representation with multiple format support. The class contains colors which are from the XKCD color survey: https://xkcd.com/color/rgb.txt and fdtdx implements most of them. This class represents a color and provides methods to convert between different color formats. Internally, colors are stored as normalized RGB values in the range [0, 1]. """ #: r (float): Red component, normalized to [0, 1] r: float #: g (float): Green component, normalized to [0, 1] g: float #: b (float): Blue component, normalized to [0, 1] b: float def __post_init__(self): """Validate RGB components are in valid range.""" if not (0 <= self.r <= 1 and 0 <= self.g <= 1 and 0 <= self.b <= 1): raise ValueError(f"RGB components must be in range [0, 1], got ({self.r}, {self.g}, {self.b})")
[docs] @classmethod def from_rgb(cls, r: int, g: int, b: int) -> "Color": """Create a Color from 8-bit RGB values (0-255). Args: r: Red component (0-255) g: Green component (0-255) b: Blue component (0-255) Returns: Color instance """ return cls(r=r / 255.0, g=g / 255.0, b=b / 255.0)
[docs] @classmethod def from_hex(cls, hex_string: str) -> "Color": """Create a Color from a hexadecimal color string. Args: hex_string: Hex color string (e.g., "#FF0000", "FF0000", "#F00") Returns: Color instance Raises: ValueError: If hex string is invalid """ hex_string = hex_string.lstrip("#") # Handle 3-character hex codes if len(hex_string) == 3: hex_string = "".join([c * 2 for c in hex_string]) if len(hex_string) != 6: raise ValueError(f"Invalid hex color: {hex_string}") try: r = int(hex_string[0:2], 16) g = int(hex_string[2:4], 16) b = int(hex_string[4:6], 16) return cls.from_rgb(r, g, b) except ValueError: raise ValueError(f"Invalid hex color: {hex_string}")
[docs] def to_rgb_normalized(self) -> tuple[float, float, float]: """Return color as normalized RGB tuple [0, 1]. Returns: Tuple of (r, g, b) with values in [0, 1] """ return (self.r, self.g, self.b)
[docs] def to_rgb_255(self) -> tuple[int, int, int]: """Return color as 8-bit RGB tuple (0-255). Returns: Tuple of (r, g, b) with integer values in [0, 255] """ return (round(self.r * 255), round(self.g * 255), round(self.b * 255))
[docs] def to_hex(self) -> str: """Return color as hexadecimal string. Returns: Hex color string with leading # (e.g., "#FF0000") """ r, g, b = self.to_rgb_255() return f"#{r:02X}{g:02X}{b:02X}"
[docs] def to_mpl(self) -> tuple[float, float, float]: """Return color in matplotlib-compatible format. This is an alias for to_rgb_normalized() for clarity when using with matplotlib. Returns: Tuple of (r, g, b) with values in [0, 1] """ return self.to_rgb_normalized()
def __str__(self) -> str: """Return hex string representation of the Color.""" return self.to_hex() def __iter__(self): """Make Color iterable for matplotlib compatibility. This allows Color objects to be used directly with matplotlib by yielding the normalized RGB values as a tuple. """ return iter((self.r, self.g, self.b)) def __len__(self) -> int: """Return length of 3 for matplotlib compatibility.""" return 3
# ============================================================================ # XKCD Color Survey Colors # ============================================================================ # The following colors are from the XKCD color survey: # https://xkcd.com/color/rgb.txt # # These colors represent the results of asking 200,000+ participants to name # random colors. They provide a comprehensive, crowdsourced palette with # intuitive names. # ============================================================================ # Bright and Vibrant Colors XKCD_CLOUDY_BLUE = Color.from_rgb(172, 194, 217) XKCD_DARK_PASTEL_GREEN = Color.from_rgb(86, 174, 87) XKCD_DUST = Color.from_rgb(178, 153, 110) XKCD_ELECTRIC_LIME = Color.from_rgb(168, 255, 4) XKCD_FRESH_GREEN = Color.from_rgb(105, 216, 79) XKCD_LIGHT_EGGPLANT = Color.from_rgb(137, 69, 133) XKCD_NASTY_GREEN = Color.from_rgb(112, 178, 63) XKCD_REALLY_LIGHT_BLUE = Color.from_rgb(212, 255, 255) XKCD_TEA = Color.from_rgb(101, 171, 124) XKCD_WARM_PURPLE = Color.from_rgb(149, 46, 143) XKCD_YELLOWISH_TAN = Color.from_rgb(252, 252, 129) XKCD_CEMENT = Color.from_rgb(165, 163, 145) XKCD_DARK_GRASS_GREEN = Color.from_rgb(56, 128, 4) XKCD_DUSTY_TEAL = Color.from_rgb(76, 144, 133) XKCD_GREY_TEAL = Color.from_rgb(94, 155, 138) XKCD_MACARONI_AND_CHEESE = Color.from_rgb(239, 180, 53) XKCD_PINKISH_TAN = Color.from_rgb(217, 155, 130) XKCD_SPRUCE = Color.from_rgb(10, 95, 56) XKCD_STRONG_BLUE = Color.from_rgb(12, 6, 247) XKCD_TOXIC_GREEN = Color.from_rgb(97, 222, 42) XKCD_WINDOWS_BLUE = Color.from_rgb(55, 120, 191) XKCD_BLUE_BLUE = Color.from_rgb(34, 66, 199) XKCD_BLUE_WITH_A_HINT_OF_PURPLE = Color.from_rgb(83, 60, 198) XKCD_BOOGER = Color.from_rgb(155, 181, 60) XKCD_BRIGHT_SEA_GREEN = Color.from_rgb(5, 255, 166) XKCD_DARK_GREEN_BLUE = Color.from_rgb(31, 99, 87) XKCD_DEEP_TURQUOISE = Color.from_rgb(1, 115, 116) XKCD_GREEN_TEAL = Color.from_rgb(12, 181, 119) XKCD_STRONG_PINK = Color.from_rgb(255, 7, 137) XKCD_BLAND = Color.from_rgb(175, 168, 139) XKCD_DEEP_AQUA = Color.from_rgb(8, 120, 127) XKCD_LAVENDER_PINK = Color.from_rgb(221, 133, 215) XKCD_LIGHT_MOSS_GREEN = Color.from_rgb(166, 200, 117) XKCD_LIGHT_SEAFOAM_GREEN = Color.from_rgb(167, 255, 181) XKCD_OLIVE_YELLOW = Color.from_rgb(194, 183, 9) XKCD_PIG_PINK = Color.from_rgb(231, 142, 165) XKCD_DEEP_LILAC = Color.from_rgb(150, 110, 189) XKCD_DESERT = Color.from_rgb(204, 173, 96) XKCD_DUSTY_LAVENDER = Color.from_rgb(172, 134, 168) XKCD_PURPLEY_GREY = Color.from_rgb(148, 126, 148) XKCD_PURPLY = Color.from_rgb(152, 63, 178) XKCD_CANDY_PINK = Color.from_rgb(255, 99, 233) XKCD_LIGHT_PASTEL_GREEN = Color.from_rgb(178, 251, 165) XKCD_BORING_GREEN = Color.from_rgb(99, 179, 101) XKCD_KIWI_GREEN = Color.from_rgb(142, 229, 63) XKCD_LIGHT_GREY_GREEN = Color.from_rgb(183, 225, 161) XKCD_ORANGE_PINK = Color.from_rgb(255, 111, 82) XKCD_TEA_GREEN = Color.from_rgb(189, 248, 163) XKCD_VERY_LIGHT_BROWN = Color.from_rgb(211, 182, 131) XKCD_EGG_SHELL = Color.from_rgb(255, 252, 196) XKCD_EGGPLANT_PURPLE = Color.from_rgb(67, 5, 65) # Primary and Secondary Colors XKCD_RED = Color.from_rgb(229, 0, 0) XKCD_ORANGE = Color.from_rgb(249, 115, 6) XKCD_YELLOW = Color.from_rgb(255, 255, 20) XKCD_GREEN = Color.from_rgb(21, 176, 26) XKCD_BLUE = Color.from_rgb(3, 67, 223) XKCD_PURPLE = Color.from_rgb(126, 30, 156) XKCD_PINK = Color.from_rgb(255, 129, 192) XKCD_BROWN = Color.from_rgb(101, 55, 0) XKCD_GREY = Color.from_rgb(146, 149, 145) XKCD_WHITE = Color.from_rgb(255, 255, 255) XKCD_BLACK = Color.from_rgb(0, 0, 0) # Shades of Red XKCD_DARK_RED = Color.from_rgb(132, 0, 0) XKCD_LIGHT_RED = Color.from_rgb(255, 71, 76) XKCD_BRIGHT_RED = Color.from_rgb(255, 0, 13) XKCD_DEEP_RED = Color.from_rgb(154, 2, 0) XKCD_BLOOD_RED = Color.from_rgb(152, 0, 2) XKCD_CRIMSON = Color.from_rgb(140, 0, 15) XKCD_SCARLET = Color.from_rgb(190, 1, 25) XKCD_CHERRY_RED = Color.from_rgb(247, 2, 42) XKCD_WINE_RED = Color.from_rgb(123, 3, 35) XKCD_BRICK_RED = Color.from_rgb(143, 20, 2) # Shades of Orange XKCD_DARK_ORANGE = Color.from_rgb(198, 81, 2) XKCD_LIGHT_ORANGE = Color.from_rgb(253, 170, 72) XKCD_BRIGHT_ORANGE = Color.from_rgb(255, 91, 0) XKCD_BURNT_ORANGE = Color.from_rgb(192, 78, 1) XKCD_RUST_ORANGE = Color.from_rgb(196, 85, 8) XKCD_PUMPKIN_ORANGE = Color.from_rgb(251, 125, 7) XKCD_TANGERINE = Color.from_rgb(255, 148, 8) # Shades of Yellow XKCD_DARK_YELLOW = Color.from_rgb(213, 182, 10) XKCD_LIGHT_YELLOW = Color.from_rgb(255, 254, 122) XKCD_BRIGHT_YELLOW = Color.from_rgb(255, 253, 1) XKCD_GOLDEN_YELLOW = Color.from_rgb(254, 193, 7) XKCD_MUSTARD_YELLOW = Color.from_rgb(210, 189, 10) XKCD_LEMON_YELLOW = Color.from_rgb(253, 255, 82) XKCD_SUNSHINE_YELLOW = Color.from_rgb(255, 253, 55) # Shades of Green XKCD_DARK_GREEN = Color.from_rgb(3, 53, 0) XKCD_LIGHT_GREEN = Color.from_rgb(150, 249, 123) XKCD_BRIGHT_GREEN = Color.from_rgb(1, 255, 7) XKCD_FOREST_GREEN = Color.from_rgb(6, 71, 12) XKCD_LIME_GREEN = Color.from_rgb(137, 254, 5) XKCD_EMERALD_GREEN = Color.from_rgb(2, 143, 30) XKCD_KELLY_GREEN = Color.from_rgb(2, 171, 46) XKCD_GRASS_GREEN = Color.from_rgb(63, 155, 11) XKCD_OLIVE_GREEN = Color.from_rgb(103, 122, 4) XKCD_MINT_GREEN = Color.from_rgb(143, 255, 159) XKCD_SEA_GREEN = Color.from_rgb(83, 252, 161) XKCD_SAGE_GREEN = Color.from_rgb(136, 179, 120) XKCD_HUNTER_GREEN = Color.from_rgb(11, 64, 8) # Shades of Blue XKCD_DARK_BLUE = Color.from_rgb(0, 3, 91) XKCD_LIGHT_BLUE = Color.from_rgb(149, 208, 252) XKCD_BRIGHT_BLUE = Color.from_rgb(1, 101, 252) XKCD_NAVY_BLUE = Color.from_rgb(0, 17, 70) XKCD_SKY_BLUE = Color.from_rgb(117, 187, 253) XKCD_ROYAL_BLUE = Color.from_rgb(5, 4, 170) XKCD_COBALT_BLUE = Color.from_rgb(3, 10, 167) XKCD_AZURE = Color.from_rgb(6, 154, 243) XKCD_ELECTRIC_BLUE = Color.from_rgb(6, 82, 255) XKCD_TURQUOISE = Color.from_rgb(6, 194, 172) XKCD_CYAN = Color.from_rgb(0, 255, 255) XKCD_TEAL = Color.from_rgb(2, 147, 134) XKCD_AQUA = Color.from_rgb(19, 234, 201) # Shades of Purple XKCD_DARK_PURPLE = Color.from_rgb(53, 6, 62) XKCD_LIGHT_PURPLE = Color.from_rgb(191, 119, 246) XKCD_BRIGHT_PURPLE = Color.from_rgb(190, 3, 253) XKCD_DEEP_PURPLE = Color.from_rgb(54, 1, 63) XKCD_LAVENDER = Color.from_rgb(199, 159, 239) XKCD_VIOLET = Color.from_rgb(154, 14, 234) XKCD_MAGENTA = Color.from_rgb(194, 0, 120) XKCD_PLUM = Color.from_rgb(88, 15, 65) XKCD_INDIGO = Color.from_rgb(56, 2, 130) XKCD_MAUVE = Color.from_rgb(174, 113, 129) # Shades of Pink XKCD_DARK_PINK = Color.from_rgb(203, 65, 107) XKCD_LIGHT_PINK = Color.from_rgb(255, 209, 223) XKCD_BRIGHT_PINK = Color.from_rgb(254, 1, 177) XKCD_HOT_PINK = Color.from_rgb(255, 2, 141) XKCD_ROSE = Color.from_rgb(207, 98, 117) XKCD_FUCHSIA = Color.from_rgb(237, 13, 217) XKCD_SALMON = Color.from_rgb(255, 121, 108) XKCD_CORAL = Color.from_rgb(252, 90, 80) XKCD_PEACH = Color.from_rgb(255, 176, 124) # Shades of Brown XKCD_DARK_BROWN = Color.from_rgb(52, 28, 2) XKCD_LIGHT_BROWN = Color.from_rgb(173, 129, 80) XKCD_CHOCOLATE_BROWN = Color.from_rgb(65, 25, 0) XKCD_COFFEE = Color.from_rgb(166, 129, 76) XKCD_CARAMEL = Color.from_rgb(175, 111, 9) XKCD_TAN = Color.from_rgb(209, 178, 111) XKCD_BEIGE = Color.from_rgb(230, 218, 166) XKCD_TAUPE = Color.from_rgb(185, 162, 129) XKCD_KHAKI = Color.from_rgb(170, 166, 98) XKCD_SAND = Color.from_rgb(226, 202, 118) # Shades of Grey XKCD_DARK_GREY = Color.from_rgb(54, 55, 55) XKCD_LIGHT_GREY = Color.from_rgb(216, 220, 214) XKCD_CHARCOAL = Color.from_rgb(52, 56, 55) XKCD_SLATE_GREY = Color.from_rgb(89, 101, 109) XKCD_SILVER = Color.from_rgb(197, 201, 199) XKCD_ASH_GREY = Color.from_rgb(178, 190, 181) # Nature-inspired Colors XKCD_LEAF_GREEN = Color.from_rgb(92, 169, 4) XKCD_MOSS_GREEN = Color.from_rgb(101, 139, 56) XKCD_SEAFOAM = Color.from_rgb(128, 249, 173) XKCD_OCEAN_BLUE = Color.from_rgb(3, 113, 156) XKCD_STONE = Color.from_rgb(172, 170, 148) XKCD_DIRT = Color.from_rgb(138, 110, 69) XKCD_CLAY = Color.from_rgb(182, 106, 80) XKCD_RUST = Color.from_rgb(168, 60, 9) # Pastel Colors XKCD_PASTEL_BLUE = Color.from_rgb(162, 191, 254) XKCD_PASTEL_GREEN = Color.from_rgb(176, 255, 157) XKCD_PASTEL_PINK = Color.from_rgb(255, 186, 205) XKCD_PASTEL_PURPLE = Color.from_rgb(202, 160, 255) XKCD_PASTEL_YELLOW = Color.from_rgb(255, 254, 113) XKCD_PASTEL_ORANGE = Color.from_rgb(255, 150, 79) # Neon Colors XKCD_NEON_BLUE = Color.from_rgb(4, 217, 255) XKCD_NEON_GREEN = Color.from_rgb(12, 255, 12) XKCD_NEON_PINK = Color.from_rgb(254, 1, 154) XKCD_NEON_PURPLE = Color.from_rgb(188, 19, 254) XKCD_NEON_YELLOW = Color.from_rgb(207, 255, 4) # Metallic Colors XKCD_GOLD = Color.from_rgb(219, 180, 12) XKCD_BRONZE = Color.from_rgb(168, 121, 0) XKCD_COPPER = Color.from_rgb(182, 99, 37) # Additional Popular Colors XKCD_CREAM = Color.from_rgb(255, 255, 194) XKCD_IVORY = Color.from_rgb(255, 255, 203) XKCD_BURGUNDY = Color.from_rgb(97, 0, 35) XKCD_MAROON = Color.from_rgb(101, 0, 33) XKCD_AVOCADO = Color.from_rgb(144, 177, 52) XKCD_PERIWINKLE = Color.from_rgb(142, 130, 254) XKCD_LILAC = Color.from_rgb(206, 162, 253) # ============================================================================ # Legacy Color Compatibility # ============================================================================ # For backward compatibility with existing code, maintain the original # constant names with their original values # Bright and primary colors GREEN = XKCD_GREEN PINK = XKCD_PINK MAGENTA = XKCD_MAGENTA CYAN = XKCD_CYAN LIGHT_BLUE = XKCD_LIGHT_BLUE ORANGE = XKCD_ORANGE LIGHT_GREEN = XKCD_LIGHT_GREEN # Grayscale colors GREY = XKCD_GREY DARK_GREY = XKCD_DARK_GREY LIGHT_GREY = XKCD_LIGHT_GREY # Earth tones BROWN = XKCD_BROWN LIGHT_BROWN = XKCD_LIGHT_BROWN TAN = XKCD_TAN BEIGE = XKCD_BEIGE # ============================================================================ # Convenience Lists # ============================================================================ PRIMARY_COLORS = [XKCD_RED, XKCD_ORANGE, XKCD_YELLOW, XKCD_GREEN, XKCD_BLUE, XKCD_PURPLE, XKCD_PINK] VIBRANT_COLORS = [ XKCD_BRIGHT_RED, XKCD_BRIGHT_ORANGE, XKCD_BRIGHT_YELLOW, XKCD_BRIGHT_GREEN, XKCD_BRIGHT_BLUE, XKCD_BRIGHT_PURPLE, XKCD_BRIGHT_PINK, ] PASTEL_COLORS = [ XKCD_PASTEL_BLUE, XKCD_PASTEL_GREEN, XKCD_PASTEL_PINK, XKCD_PASTEL_PURPLE, XKCD_PASTEL_YELLOW, XKCD_PASTEL_ORANGE, ] NEON_COLORS = [XKCD_NEON_BLUE, XKCD_NEON_GREEN, XKCD_NEON_PINK, XKCD_NEON_PURPLE, XKCD_NEON_YELLOW] EARTH_TONES = [XKCD_BROWN, XKCD_TAN, XKCD_BEIGE, XKCD_SAND, XKCD_CLAY, XKCD_DIRT, XKCD_STONE] GREYSCALE = [XKCD_BLACK, XKCD_CHARCOAL, XKCD_DARK_GREY, XKCD_GREY, XKCD_SILVER, XKCD_LIGHT_GREY, XKCD_WHITE]