CoolFace
Apppublic

chwellofficial/nt360Slides

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
theme_utils.py358 linesDownload Raw Back to utils
1from __future__ import annotations2 3import math4import random5from dataclasses import dataclass6from typing import Dict, Optional7 8from models.theme_data import GeneratedColorPalette9 10IS_DARK_BELOW = 0.6511BACKGROUND_RETRIES = 20012TEXT_RETRIES = 20013 14LIGHTNESS_VALUES: Dict[str, float] = {15    "50": 0.97,16    "100": 0.93,17    "200": 0.86,18    "300": 0.78,19    "400": 0.70,20    "500": 0.62,21    "600": 0.54,22    "700": 0.46,23    "800": 0.38,24    "900": 0.30,25}26 27 28@dataclass(frozen=True)29class Oklch:30    l: float  # noqa: E74131    c: float32    h: float33 34 35def _clamp(value: float, min_value: float = 0.0, max_value: float = 1.0) -> float:36    return max(min_value, min(max_value, value))37 38 39def _get_random_value(min_value: float, max_value: float) -> float:40    return min_value + random.random() * (max_value - min_value)41 42 43def _get_random_value_at_min_max_distance(44    base_value: float,45    min_value: float,46    max_value: float,47    min_distance: Optional[float] = None,48    max_distance: Optional[float] = None,49) -> float:50    normalized_min_distance = max(0.0, min_distance or 0.0)51    normalized_max_distance = max_distance if max_distance is not None else math.inf52    min_dist = min(normalized_min_distance, normalized_max_distance)53    max_dist = max(normalized_min_distance, normalized_max_distance)54 55    lower_start = max(min_value, base_value - max_dist)56    lower_end = min(max_value, base_value - min_dist)57    upper_start = max(min_value, base_value + min_dist)58    upper_end = min(max_value, base_value + max_dist)59 60    lower_size = max(0.0, lower_end - lower_start)61    upper_size = max(0.0, upper_end - upper_start)62    total_size = lower_size + upper_size63 64    if total_size <= 0:65        return _get_random_value(min_value, max_value)66 67    picker = random.random() * total_size68    if picker < lower_size:69        return _get_random_value(lower_start, lower_end)70 71    return _get_random_value(upper_start, upper_end)72 73 74def _srgb_to_linear(channel: float) -> float:75    if channel <= 0.04045:76        return channel / 12.9277    return ((channel + 0.055) / 1.055) ** 2.478 79 80def _linear_to_srgb(channel: float) -> float:81    if channel <= 0.0031308:82        return 12.92 * channel83    return 1.055 * (channel ** (1 / 2.4)) - 0.05584 85 86def _oklch_to_srgb(color: Oklch) -> tuple[float, float, float]:87    hue_rad = math.radians(color.h % 360)88    a = color.c * math.cos(hue_rad)89    b = color.c * math.sin(hue_rad)90 91    l_ = (color.l + 0.3963377774 * a + 0.2158037573 * b) ** 392    m_ = (color.l - 0.1055613458 * a - 0.0638541728 * b) ** 393    s_ = (color.l - 0.0894841775 * a - 1.2914855480 * b) ** 394 95    r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_96    g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_97    b = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_98 99    return (100        _clamp(_linear_to_srgb(r)),101        _clamp(_linear_to_srgb(g)),102        _clamp(_linear_to_srgb(b)),103    )104 105 106def _srgb_to_oklch(r: float, g: float, b: float) -> Oklch:107    r_lin = _srgb_to_linear(r)108    g_lin = _srgb_to_linear(g)109    b_lin = _srgb_to_linear(b)110 111    l_ = 0.4122214708 * r_lin + 0.5363325363 * g_lin + 0.0514459929 * b_lin112    m_ = 0.2119034982 * r_lin + 0.6806995451 * g_lin + 0.1073969566 * b_lin113    s_ = 0.0883024619 * r_lin + 0.2817188376 * g_lin + 0.6299787005 * b_lin114 115    l_cbrt = math.copysign(abs(l_) ** (1 / 3), l_)116    m_cbrt = math.copysign(abs(m_) ** (1 / 3), m_)117    s_cbrt = math.copysign(abs(s_) ** (1 / 3), s_)118 119    lightness = 0.2104542553 * l_cbrt + 0.7936177850 * m_cbrt - 0.0040720468 * s_cbrt120    a = 1.9779984951 * l_cbrt - 2.4285922050 * m_cbrt + 0.4505937099 * s_cbrt121    b = 0.0259040371 * l_cbrt + 0.7827717662 * m_cbrt - 0.8086757660 * s_cbrt122 123    chroma = math.hypot(a, b)124    hue = math.degrees(math.atan2(b, a)) % 360125 126    return Oklch(l=lightness, c=chroma, h=hue)127 128 129def _hex_to_oklch(hex_value: str) -> Oklch:130    hex_value = hex_value.strip().lstrip("#")131    if len(hex_value) != 6:132        raise ValueError(f"Invalid hex color: {hex_value!r}")133    r = int(hex_value[0:2], 16) / 255.0134    g = int(hex_value[2:4], 16) / 255.0135    b = int(hex_value[4:6], 16) / 255.0136    return _srgb_to_oklch(r, g, b)137 138 139def _format_hex(color: Oklch) -> str:140    r, g, b = _oklch_to_srgb(color)141    return "#{:02x}{:02x}{:02x}".format(142        int(round(r * 255)),143        int(round(g * 255)),144        int(round(b * 255)),145    )146 147 148def _relative_luminance(color: Oklch) -> float:149    r, g, b = _oklch_to_srgb(color)150    r_lin = _srgb_to_linear(r)151    g_lin = _srgb_to_linear(g)152    b_lin = _srgb_to_linear(b)153    return 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin154 155 156def _wcag_contrast(a: Oklch, b: Oklch) -> float:157    l1 = _relative_luminance(a)158    l2 = _relative_luminance(b)159    lighter = max(l1, l2)160    darker = min(l1, l2)161    return (lighter + 0.05) / (darker + 0.05)162 163 164def _get_color_for_all_lightness_values(base_color: Oklch) -> Dict[str, str]:165    colors: Dict[str, str] = {}166    for name, value in LIGHTNESS_VALUES.items():167        color = Oklch(l=value, c=base_color.c, h=base_color.h)168        colors[name] = _format_hex(color)169    return colors170 171 172def _generate_primary_color() -> Oklch:173    lightness = _get_random_value(0.0, 1.0)174    chroma = _get_random_value(0.0, 0.4)175    hue = _get_random_value(0.0, 360.0)176    return Oklch(l=lightness, c=chroma, h=hue)177 178 179def _generate_background_color(base_color: Oklch) -> Oklch:180    for _ in range(BACKGROUND_RETRIES):181        lightness = _get_random_value(0.0, 1.0)182        chroma = _get_random_value(0.0, 0.4)183        hue = _get_random_value(0.0, 360.0)184        color = Oklch(l=lightness, c=chroma, h=hue)185        if _wcag_contrast(color, base_color) >= 6:186            return color187 188    if base_color.l < IS_DARK_BELOW:189        return Oklch(l=1.0, c=0.0, h=0.0)190    return Oklch(l=0.0, c=0.0, h=0.0)191 192 193def _generate_accent_color(base_color: Oklch, n: int) -> Oklch:194    lightness = _get_random_value_at_min_max_distance(base_color.l, 0.0, 1.0, 0.0, 0.1)195    chroma = _get_random_value_at_min_max_distance(base_color.c, 0.0, 0.4, 0.0, 0.4)196    hue = _get_random_value_at_min_max_distance(197        base_color.h if base_color.h is not None else 0.0,198        0.0,199        360.0,200        n * 90.0,201        (n + 1) * 90.0,202    )203    return Oklch(l=lightness, c=chroma, h=hue)204 205 206def _generate_text_color(base_color: Oklch, text_type: str) -> Oklch:207    is_base_dark = base_color.l < IS_DARK_BELOW208 209    for _ in range(TEXT_RETRIES):210        if text_type == "text_1":211            lightness = (212                _get_random_value(0.8, 1.0)213                if is_base_dark214                else _get_random_value(0.0, 0.2)215            )216            chroma = _get_random_value(0.0, 0.02)217        elif text_type == "text_2":218            lightness = (219                _get_random_value(0.8, 1.0)220                if is_base_dark221                else _get_random_value(0.0, 0.2)222            )223            chroma = _get_random_value(0.0, 0.04)224        else:225            raise ValueError(f"Invalid text type: {text_type}")226 227        hue = _get_random_value(0.0, 360.0)228        color = Oklch(l=lightness, c=chroma, h=hue)229 230        min_contrast = 6.0231        max_contrast = None232        contrast = _wcag_contrast(color, base_color)233 234        if contrast >= min_contrast and (235            max_contrast is None or contrast <= max_contrast236        ):237            return color238 239    if base_color.l < IS_DARK_BELOW:240        return Oklch(l=1.0 if text_type == "text_1" else 0.9, c=0.0, h=0.0)241    return Oklch(l=0.0 if text_type == "text_1" else 0.1, c=0.0, h=0.0)242 243 244def get_lightness_key_at_distance(245    value: float,246    min_distance: Optional[int] = None,247    max_distance: Optional[int] = None,248    prefer_dark: Optional[bool] = None,249) -> str:250    items = sorted(LIGHTNESS_VALUES.items(), key=lambda item: item[1])251 252    nearest_index = 0253    nearest_distance = abs(items[0][1] - value)254    for index, (_, lightness) in enumerate(items[1:], start=1):255        distance = abs(lightness - value)256        if distance < nearest_distance or (257            distance == nearest_distance and lightness < items[nearest_index][1]258        ):259            nearest_index = index260            nearest_distance = distance261 262    normalized_min = max(0, min_distance or 0)263    normalized_max = max_distance if max_distance is not None else normalized_min264    if normalized_max < normalized_min:265        normalized_min, normalized_max = normalized_max, normalized_min266 267    candidate_indices = []268    for distance in range(normalized_min, normalized_max + 1):269        lower_index = nearest_index - distance270        upper_index = nearest_index + distance271        if 0 <= lower_index < len(items):272            candidate_indices.append(lower_index)273        if upper_index != lower_index and 0 <= upper_index < len(items):274            candidate_indices.append(upper_index)275 276    if not candidate_indices:277        return items[nearest_index][0]278 279    if prefer_dark is True:280        darker_candidates = [idx for idx in candidate_indices if idx <= nearest_index]281        if darker_candidates:282            return items[min(darker_candidates)][0]283        return items[min(candidate_indices)][0]284    if prefer_dark is False:285        lighter_candidates = [idx for idx in candidate_indices if idx >= nearest_index]286        if lighter_candidates:287            return items[max(lighter_candidates)][0]288        return items[max(candidate_indices)][0]289 290    def distance_to_value(idx: int) -> float:291        return abs(items[idx][1] - value)292 293    closest_index = min(candidate_indices, key=lambda idx: (distance_to_value(idx), idx))294    return items[closest_index][0]295 296 297def generate_color_palette(298    provided_primary: Optional[str] = None,299    provided_background: Optional[str] = None,300    provided_accent_1: Optional[str] = None,301    provided_accent_2: Optional[str] = None,302    provided_text_1: Optional[str] = None,303    provided_text_2: Optional[str] = None,304) -> GeneratedColorPalette:305    primary = (306        _hex_to_oklch(provided_primary) if provided_primary else _generate_primary_color()307    )308    background = (309        _hex_to_oklch(provided_background)310        if provided_background311        else _generate_background_color(primary)312    )313    accent_1 = (314        _hex_to_oklch(provided_accent_1)315        if provided_accent_1316        else _generate_accent_color(primary, 1)317    )318    accent_2 = (319        _hex_to_oklch(provided_accent_2)320        if provided_accent_2321        else _generate_accent_color(primary, 2)322    )323    text_1 = (324        _hex_to_oklch(provided_text_1)325        if provided_text_1326        else _generate_text_color(background, "text_1")327    )328    text_2 = (329        _hex_to_oklch(provided_text_2)330        if provided_text_2331        else _generate_text_color(primary, "text_2")332    )333 334    primary_variations = _get_color_for_all_lightness_values(primary)335    background_variations = _get_color_for_all_lightness_values(background)336    accent_1_variations = _get_color_for_all_lightness_values(accent_1)337    accent_2_variations = _get_color_for_all_lightness_values(accent_2)338 339    return GeneratedColorPalette(340        primary=_format_hex(primary),341        background=_format_hex(background),342        accent_1=_format_hex(accent_1),343        accent_2=_format_hex(accent_2),344        text_1=_format_hex(text_1),345        text_2=_format_hex(text_2),346        primary_variations=primary_variations,347        background_variations=background_variations,348        accent_1_variations=accent_1_variations,349        accent_2_variations=accent_2_variations,350        primary_lightness=primary.l,351        background_lightness=background.l,352        accent_1_lightness=accent_1.l,353        accent_2_lightness=accent_2.l,354        text_1_lightness=text_1.l,355        text_2_lightness=text_2.l,356    )357 358