add generate palette function in experimental
This commit is contained in:
parent
13a6d538a9
commit
d885cf6a66
199
reflex/experimental/bezier.py
Normal file
199
reflex/experimental/bezier.py
Normal file
@ -0,0 +1,199 @@
|
||||
"""A module for generating Bezier easing functions."""
|
||||
|
||||
# These values are established by empiricism with tests (tradeoff: performance VS precision)
|
||||
NEWTON_ITERATIONS = 4
|
||||
NEWTON_MIN_SLOPE = 0.001
|
||||
SUBDIVISION_PRECISION = 0.0000001
|
||||
SUBDIVISION_MAX_ITERATIONS = 10
|
||||
kSplineTableSize = 11
|
||||
kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
|
||||
|
||||
|
||||
def A(aA1, aA2):
|
||||
"""Calculate A.
|
||||
|
||||
Args:
|
||||
aA1: The first value.
|
||||
aA2: The second value.
|
||||
|
||||
Returns:
|
||||
The calculated value.
|
||||
"""
|
||||
return 1.0 - 3.0 * aA2 + 3.0 * aA1
|
||||
|
||||
|
||||
def B(aA1, aA2):
|
||||
"""Calculate B.
|
||||
|
||||
Args:
|
||||
aA1: The first value.
|
||||
aA2: The second value.
|
||||
|
||||
Returns:
|
||||
The calculated value.
|
||||
"""
|
||||
return 3.0 * aA2 - 6.0 * aA1
|
||||
|
||||
|
||||
def C(aA1):
|
||||
"""Calculate C.
|
||||
|
||||
Args:
|
||||
aA1: The first value.
|
||||
|
||||
Returns:
|
||||
The calculated value.
|
||||
"""
|
||||
return 3.0 * aA1
|
||||
|
||||
|
||||
def calcBezier(aT, aA1, aA2):
|
||||
"""Calculate Bezier.
|
||||
|
||||
Args:
|
||||
aT: The time.
|
||||
aA1: The first value.
|
||||
aA2: The second value.
|
||||
|
||||
Returns:
|
||||
x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
|
||||
"""
|
||||
return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT
|
||||
|
||||
|
||||
def getSlope(aT, aA1, aA2):
|
||||
"""Calculate slope.
|
||||
|
||||
Args:
|
||||
aT: The time.
|
||||
aA1: The first value.
|
||||
aA2: The second value.
|
||||
|
||||
Returns:
|
||||
dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
|
||||
"""
|
||||
return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1)
|
||||
|
||||
|
||||
def binarySubdivide(aX, aA, aB, mX1, mX2):
|
||||
"""Perform a binary subdivide.
|
||||
|
||||
Args:
|
||||
aX: The x value.
|
||||
aA: The a value.
|
||||
aB: The b value.
|
||||
mX1: The x1 value.
|
||||
mX2: The x2 value.
|
||||
|
||||
Returns:
|
||||
The t value.
|
||||
"""
|
||||
current_x = aA
|
||||
current_t = 0
|
||||
i = 0
|
||||
while True:
|
||||
i += 1
|
||||
if i >= SUBDIVISION_MAX_ITERATIONS:
|
||||
break
|
||||
current_t = aA + (aB - aA) / 2.0
|
||||
current_x = calcBezier(current_t, mX1, mX2) - aX
|
||||
if current_x > 0.0:
|
||||
aB = current_t
|
||||
else:
|
||||
aA = current_t
|
||||
if abs(current_x) <= SUBDIVISION_PRECISION:
|
||||
break
|
||||
return current_t
|
||||
|
||||
|
||||
def newtonRaphsonIterate(aX, aGuessT, mX1, mX2):
|
||||
"""Perform a Newton-Raphson iteration.
|
||||
|
||||
Args:
|
||||
aX: The x value.
|
||||
aGuessT: The guess value.
|
||||
mX1: The x1 value.
|
||||
mX2: The x2 value.
|
||||
|
||||
Returns:
|
||||
The t value.
|
||||
"""
|
||||
for _ in range(NEWTON_ITERATIONS):
|
||||
current_slope = getSlope(aGuessT, mX1, mX2)
|
||||
if current_slope == 0.0:
|
||||
return aGuessT
|
||||
current_x = calcBezier(aGuessT, mX1, mX2) - aX
|
||||
aGuessT -= current_x / current_slope
|
||||
return aGuessT
|
||||
|
||||
|
||||
def LinearEasing(x):
|
||||
"""Linear easing function.
|
||||
|
||||
Args:
|
||||
x: The x value.
|
||||
|
||||
Returns:
|
||||
The x value.
|
||||
"""
|
||||
return x
|
||||
|
||||
|
||||
def bezier(mX1, mY1, mX2, mY2):
|
||||
"""Generate a Bezier easing function.
|
||||
|
||||
Args:
|
||||
mX1: The x1 value.
|
||||
mY1: The y1 value.
|
||||
mX2: The x2 value.
|
||||
mY2: The y2 value.
|
||||
|
||||
Raises:
|
||||
ValueError: If the x values are not in the [0, 1] range.
|
||||
|
||||
Returns:
|
||||
The Bezier easing function.
|
||||
"""
|
||||
if not (0 <= mX1 <= 1 and 0 <= mX2 <= 1):
|
||||
raise ValueError("bezier x values must be in [0, 1] range")
|
||||
|
||||
if mX1 == mY1 and mX2 == mY2:
|
||||
return LinearEasing
|
||||
|
||||
# Precompute samples table
|
||||
sampleValues = [
|
||||
calcBezier(i * kSampleStepSize, mX1, mX2) for i in range(kSplineTableSize)
|
||||
]
|
||||
|
||||
def getTForX(aX):
|
||||
intervalStart = 0.0
|
||||
currentSample = 1
|
||||
lastSample = kSplineTableSize - 1
|
||||
|
||||
while currentSample != lastSample and sampleValues[currentSample] <= aX:
|
||||
intervalStart += kSampleStepSize
|
||||
currentSample += 1
|
||||
currentSample -= 1
|
||||
|
||||
# Interpolate to provide an initial guess for t
|
||||
dist = (aX - sampleValues[currentSample]) / (
|
||||
sampleValues[currentSample + 1] - sampleValues[currentSample]
|
||||
)
|
||||
guessForT = intervalStart + dist * kSampleStepSize
|
||||
|
||||
initialSlope = getSlope(guessForT, mX1, mX2)
|
||||
if initialSlope >= NEWTON_MIN_SLOPE:
|
||||
return newtonRaphsonIterate(aX, guessForT, mX1, mX2)
|
||||
elif initialSlope == 0.0:
|
||||
return guessForT
|
||||
else:
|
||||
return binarySubdivide(
|
||||
aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2
|
||||
)
|
||||
|
||||
def BezierEasing(x):
|
||||
if x == 0 or x == 1:
|
||||
return x
|
||||
return calcBezier(getTForX(x), mY1, mY2)
|
||||
|
||||
return BezierEasing
|
3603
reflex/experimental/default_colors.py
Normal file
3603
reflex/experimental/default_colors.py
Normal file
File diff suppressed because it is too large
Load Diff
600
reflex/experimental/palette.py
Normal file
600
reflex/experimental/palette.py
Normal file
@ -0,0 +1,600 @@
|
||||
"""A module for generating Radix colors.
|
||||
|
||||
Converted from https://github.com/radix-ui/website/blob/main/components/generateRadixColors.tsx
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
from coloraide import Color
|
||||
|
||||
from .bezier import bezier
|
||||
from .default_colors import DEFAULT_DARK_COLORS, DEFAULT_LIGHT_COLORS
|
||||
|
||||
ArrayOf12 = list[float]
|
||||
gray_scale_names = ["gray", "mauve", "slate", "sage", "olive", "sand"]
|
||||
scale_names = gray_scale_names + [
|
||||
"tomato",
|
||||
"red",
|
||||
"ruby",
|
||||
"crimson",
|
||||
"pink",
|
||||
"plum",
|
||||
"purple",
|
||||
"violet",
|
||||
"iris",
|
||||
"indigo",
|
||||
"blue",
|
||||
"cyan",
|
||||
"teal",
|
||||
"jade",
|
||||
"green",
|
||||
"grass",
|
||||
"brown",
|
||||
"orange",
|
||||
"sky",
|
||||
"mint",
|
||||
"lime",
|
||||
"yellow",
|
||||
"amber",
|
||||
]
|
||||
|
||||
|
||||
def get_base_colors(appearance: str):
|
||||
"""Get the base colors from the default dicts.
|
||||
|
||||
Args:
|
||||
appearance: The appearance of the colors.
|
||||
|
||||
Returns:
|
||||
dict: The base colors.
|
||||
"""
|
||||
raw_colors = DEFAULT_LIGHT_COLORS if appearance == "light" else DEFAULT_DARK_COLORS
|
||||
colors = {}
|
||||
for color_name, color_scale in raw_colors.items():
|
||||
f_scale = []
|
||||
for scale in color_scale:
|
||||
scale["coords"] = [
|
||||
float("nan") if x is None else x for x in scale["coords"]
|
||||
]
|
||||
f_scale.append(Color(scale))
|
||||
colors[color_name] = f_scale
|
||||
return colors
|
||||
|
||||
|
||||
light_colors = get_base_colors("light")
|
||||
dark_colors = get_base_colors("dark")
|
||||
light_gray_colors = {name: light_colors[name] for name in gray_scale_names}
|
||||
dark_gray_colors = {name: dark_colors[name] for name in gray_scale_names}
|
||||
|
||||
|
||||
def generate_radix_colors(
|
||||
appearance: str, accent: str, gray: str, background: str
|
||||
) -> dict:
|
||||
"""Generate Radix colors.
|
||||
|
||||
Args:
|
||||
appearance: The appearance of the colors.
|
||||
accent: The accent color.
|
||||
gray: The gray color.
|
||||
background: The background color.
|
||||
|
||||
Returns:
|
||||
dict: The generated colors.
|
||||
"""
|
||||
all_scales = light_colors if appearance == "light" else dark_colors
|
||||
gray_scales = light_gray_colors if appearance == "light" else dark_gray_colors
|
||||
background_color = Color(background).convert("oklch")
|
||||
|
||||
gray_base_color = Color(gray).convert("oklch")
|
||||
gray_scale_colors = get_scale_from_color(
|
||||
gray_base_color, gray_scales, background_color
|
||||
)
|
||||
|
||||
accent_base_color = Color(accent).convert("oklch")
|
||||
accent_scale_colors = get_scale_from_color(
|
||||
accent_base_color, all_scales, background_color
|
||||
)
|
||||
|
||||
background_hex = background_color.convert("srgb").to_string(hex=True)
|
||||
|
||||
accent_base_hex = accent_base_color.convert("srgb").to_string(hex=True)
|
||||
if accent_base_hex == "#000000" or accent_base_hex == "#ffffff":
|
||||
accent_scale_colors = [color.clone() for color in gray_scale_colors]
|
||||
|
||||
accent9_color, accent_contrast_color = get_step9_colors(
|
||||
accent_scale_colors, accent_base_color
|
||||
)
|
||||
|
||||
accent_scale_colors[8] = accent9_color
|
||||
accent_scale_colors[9] = get_button_hover_color(
|
||||
accent9_color, [accent_scale_colors]
|
||||
)
|
||||
|
||||
# Limit saturation of the text colors
|
||||
accent_scale_colors[10] = accent_scale_colors[10].set(
|
||||
"oklch.c",
|
||||
min(
|
||||
max(
|
||||
accent_scale_colors[8].get("oklch.c"),
|
||||
accent_scale_colors[7].get("oklch.c"),
|
||||
),
|
||||
accent_scale_colors[10].get("oklch.c"),
|
||||
),
|
||||
)
|
||||
accent_scale_colors[11] = accent_scale_colors[11].set(
|
||||
"oklch.c",
|
||||
min(
|
||||
max(
|
||||
accent_scale_colors[8].get("oklch.c"),
|
||||
accent_scale_colors[7].get("oklch.c"),
|
||||
),
|
||||
accent_scale_colors[11].get("oklch.c"),
|
||||
),
|
||||
)
|
||||
accent_scale_hex = [
|
||||
color.convert("srgb").to_string(hex=True) for color in accent_scale_colors
|
||||
]
|
||||
accent_scale_wide_gamut = [to_oklch_string(color) for color in accent_scale_colors]
|
||||
accent_scale_alpha_hex = [
|
||||
get_alpha_color_srgb(color, background_hex) for color in accent_scale_hex
|
||||
]
|
||||
accent_scale_alpha_wide_gamut_string = [
|
||||
get_alpha_color_p3(color, background_hex) for color in accent_scale_hex
|
||||
]
|
||||
|
||||
accent_contrast_color_hex = accent_contrast_color.convert("srgb").to_string(
|
||||
hex=True
|
||||
)
|
||||
|
||||
gray_scale_hex = [
|
||||
color.convert("srgb").to_string(hex=True) for color in gray_scale_colors
|
||||
]
|
||||
gray_scale_wide_gamut = [to_oklch_string(color) for color in gray_scale_colors]
|
||||
gray_scale_alpha_hex = [
|
||||
get_alpha_color_srgb(color, background_hex) for color in gray_scale_hex
|
||||
]
|
||||
gray_scale_alpha_wide_gamut_string = [
|
||||
get_alpha_color_p3(color, background_hex) for color in gray_scale_hex
|
||||
]
|
||||
|
||||
accent_surface_hex = (
|
||||
get_alpha_color_srgb(accent_scale_hex[1], background_hex, 0.8)
|
||||
if appearance == "light"
|
||||
else get_alpha_color_srgb(accent_scale_hex[1], background_hex, 0.5)
|
||||
)
|
||||
|
||||
accent_surface_wide_gamut_string = (
|
||||
get_alpha_color_p3(accent_scale_wide_gamut[1], background_hex, 0.8)
|
||||
if appearance == "light"
|
||||
else get_alpha_color_p3(accent_scale_wide_gamut[1], background_hex, 0.5)
|
||||
)
|
||||
|
||||
return {
|
||||
"accentScale": accent_scale_hex,
|
||||
"accentScaleAlpha": accent_scale_alpha_hex,
|
||||
"accentScaleWideGamut": accent_scale_wide_gamut,
|
||||
"accentScaleAlphaWideGamut": accent_scale_alpha_wide_gamut_string,
|
||||
"accentContrast": accent_contrast_color_hex,
|
||||
"grayScale": gray_scale_hex,
|
||||
"grayScaleAlpha": gray_scale_alpha_hex,
|
||||
"grayScaleWideGamut": gray_scale_wide_gamut,
|
||||
"grayScaleAlphaWideGamut": gray_scale_alpha_wide_gamut_string,
|
||||
"graySurface": "#ffffffcc" if appearance == "light" else "rgba(0, 0, 0, 0.05)",
|
||||
"graySurfaceWideGamut": "color(display-p3 1 1 1 / 80%)"
|
||||
if appearance == "light"
|
||||
else "color(display-p3 0 0 0 / 5%)",
|
||||
"accentSurface": accent_surface_hex,
|
||||
"accentSurfaceWideGamut": accent_surface_wide_gamut_string,
|
||||
"background": background_hex,
|
||||
}
|
||||
|
||||
|
||||
def get_step9_colors(
|
||||
scale: list[Color], accent_base_color: Color
|
||||
) -> tuple[Color, Color]:
|
||||
"""Get the step 9 colors.
|
||||
|
||||
Args:
|
||||
scale: The scale of colors.
|
||||
accent_base_color: The accent base color.
|
||||
|
||||
Returns:
|
||||
The step 9 colors.
|
||||
"""
|
||||
reference_background_color = scale[0]
|
||||
distance = accent_base_color.delta_e(reference_background_color) * 100
|
||||
|
||||
if distance < 25:
|
||||
return scale[8], get_text_color(scale[8])
|
||||
|
||||
return accent_base_color, get_text_color(accent_base_color)
|
||||
|
||||
|
||||
def get_button_hover_color(source: Color, scales: list[list[Color]]) -> Color:
|
||||
"""Get the button hover color.
|
||||
|
||||
Args:
|
||||
source: The source color.
|
||||
scales: The scales of colors.
|
||||
|
||||
Returns:
|
||||
The button hover color.
|
||||
"""
|
||||
L, C, H = source["lightness"], source["chroma"], source["hue"]
|
||||
|
||||
new_L = L - 0.03 / (L + 0.1) if L > 0.4 else L + 0.03 / (L + 0.1)
|
||||
new_C = C * 0.93 if L > 0.4 and not math.isnan(H) else C
|
||||
button_hover_color = Color("oklch", [new_L, new_C, H])
|
||||
|
||||
closest_color = button_hover_color
|
||||
min_distance = float("inf")
|
||||
|
||||
for scale in scales:
|
||||
for color in scale:
|
||||
distance = button_hover_color.delta_e(color)
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
closest_color = color
|
||||
|
||||
button_hover_color["chroma"] = closest_color["chroma"]
|
||||
button_hover_color["hue"] = closest_color["hue"]
|
||||
return button_hover_color
|
||||
|
||||
|
||||
def get_text_color(background: Color) -> Color:
|
||||
"""Get the text color.
|
||||
|
||||
Args:
|
||||
background: The background color.
|
||||
|
||||
Returns:
|
||||
The text color.
|
||||
"""
|
||||
white = Color("oklch", [1, 0, 0])
|
||||
|
||||
if abs(white.contrast(background)) < 40:
|
||||
_, C, H = background["lightness"], background["chroma"], background["hue"]
|
||||
return Color("oklch", [0.25, max(0.08 * C, 0.04), H])
|
||||
|
||||
return white
|
||||
|
||||
|
||||
def get_alpha_color(
|
||||
target_rgb: list[float],
|
||||
background_rgb: list[float],
|
||||
rgb_precision: int,
|
||||
alpha_precision: int,
|
||||
target_alpha: float | None = None,
|
||||
) -> tuple[float, float, float, float]:
|
||||
"""Get the alpha color.
|
||||
|
||||
Args:
|
||||
target_rgb: The target RGB.
|
||||
background_rgb: The background RGB.
|
||||
rgb_precision: The RGB precision.
|
||||
alpha_precision: The alpha precision.
|
||||
target_alpha: The target alpha.
|
||||
|
||||
Raises:
|
||||
ValueError: If the color is undefined.
|
||||
|
||||
Returns:
|
||||
The alpha color.
|
||||
"""
|
||||
tr, tg, tb = [round(c * rgb_precision) for c in target_rgb]
|
||||
br, bg, bb = [round(c * rgb_precision) for c in background_rgb]
|
||||
|
||||
if any(c is None for c in [tr, tg, tb, br, bg, bb]):
|
||||
raise ValueError("Color is undefined")
|
||||
|
||||
desired_rgb = 0
|
||||
if tr > br or tg > bg or tb > bb:
|
||||
desired_rgb = rgb_precision
|
||||
|
||||
alpha_r = (tr - br) / (desired_rgb - br)
|
||||
alpha_g = (tg - bg) / (desired_rgb - bg)
|
||||
alpha_b = (tb - bb) / (desired_rgb - bb)
|
||||
|
||||
is_pure_gray = all(alpha == alpha_r for alpha in [alpha_r, alpha_g, alpha_b])
|
||||
|
||||
if not target_alpha and is_pure_gray:
|
||||
v = desired_rgb / rgb_precision
|
||||
return v, v, v, alpha_r
|
||||
|
||||
def clamp_rgb(n):
|
||||
return 0 if n is None else min(rgb_precision, max(0, n))
|
||||
|
||||
def clamp_a(n):
|
||||
return 0 if n is None else min(alpha_precision, max(0, n))
|
||||
|
||||
max_alpha = (
|
||||
target_alpha if target_alpha is not None else max(alpha_r, alpha_g, alpha_b)
|
||||
)
|
||||
A = clamp_a(math.ceil(max_alpha * alpha_precision)) / alpha_precision
|
||||
|
||||
R = clamp_rgb(((br * (1 - A) - tr) / A) * -1)
|
||||
G = clamp_rgb(((bg * (1 - A) - tg) / A) * -1)
|
||||
B = clamp_rgb(((bb * (1 - A) - tb) / A) * -1)
|
||||
|
||||
R, G, B = map(math.ceil, [R, G, B])
|
||||
|
||||
blended_r = blend_alpha(R, A, br)
|
||||
blended_g = blend_alpha(G, A, bg)
|
||||
blended_b = blend_alpha(B, A, bb)
|
||||
|
||||
if desired_rgb == 0:
|
||||
if tr <= br and tr != blended_r:
|
||||
R += 1 if tr > blended_r else -1
|
||||
if tg <= bg and tg != blended_g:
|
||||
G += 1 if tg > blended_g else -1
|
||||
if tb <= bb and tb != blended_b:
|
||||
B += 1 if tb > blended_b else -1
|
||||
|
||||
if desired_rgb == rgb_precision:
|
||||
if tr >= br and tr != blended_r:
|
||||
R += 1 if tr > blended_r else -1
|
||||
if tg >= bg and tg != blended_g:
|
||||
G += 1 if tg > blended_g else -1
|
||||
if tb >= bb and tb != blended_b:
|
||||
B += 1 if tb > blended_b else -1
|
||||
|
||||
R /= rgb_precision
|
||||
G /= rgb_precision
|
||||
B /= rgb_precision
|
||||
|
||||
return R, G, B, A
|
||||
|
||||
|
||||
def blend_alpha(foreground, alpha, background, _round=True) -> float:
|
||||
"""Blend the alpha.
|
||||
|
||||
Args:
|
||||
foreground: The foreground.
|
||||
alpha: The alpha.
|
||||
background: The background.
|
||||
_round: Whether to round the result.
|
||||
|
||||
Returns:
|
||||
The blended alpha.
|
||||
"""
|
||||
if _round:
|
||||
return round(background * (1 - alpha)) + round(foreground * alpha)
|
||||
|
||||
return background * (1 - alpha) + foreground * alpha
|
||||
|
||||
|
||||
def get_alpha_color_srgb(
|
||||
target_color: str, background_color: str, target_alpha: float | None = None
|
||||
) -> str:
|
||||
"""Get the alpha color in srgb.
|
||||
|
||||
Args:
|
||||
target_color: The target color.
|
||||
background_color: The background color.
|
||||
target_alpha: The target alpha.
|
||||
|
||||
Returns:
|
||||
The alpha color.
|
||||
"""
|
||||
r, g, b, a = get_alpha_color(
|
||||
Color(target_color).convert("srgb").coords(),
|
||||
Color(background_color).convert("srgb").coords(),
|
||||
255,
|
||||
255,
|
||||
target_alpha,
|
||||
)
|
||||
return Color("srgb", [r, g, b], a).to_string(format="hex")
|
||||
|
||||
|
||||
def get_alpha_color_p3(
|
||||
target_color: str, background_color: str, target_alpha: float | None = None
|
||||
) -> str:
|
||||
"""Get the alpha color in display-p3.
|
||||
|
||||
Args:
|
||||
target_color: The target color.
|
||||
background_color: The background color.
|
||||
target_alpha: The target alpha.
|
||||
|
||||
Returns:
|
||||
The alpha color.
|
||||
"""
|
||||
r, g, b, a = get_alpha_color(
|
||||
Color(target_color).convert("display-p3").coords(),
|
||||
Color(background_color).convert("display-p3").coords(),
|
||||
255,
|
||||
1000,
|
||||
target_alpha,
|
||||
)
|
||||
return Color("display-p3", [r, g, b], a).to_string(precision=4)
|
||||
|
||||
|
||||
def format_hex(s: str) -> str:
|
||||
"""Format shortform hex to longform.
|
||||
|
||||
Args:
|
||||
s: The hex color.
|
||||
|
||||
Returns:
|
||||
The formatted hex color.
|
||||
"""
|
||||
if not s.startswith("#"):
|
||||
return s
|
||||
|
||||
if len(s) == 4:
|
||||
return f"#{s[1]}{s[1]}{s[2]}{s[2]}{s[3]}{s[3]}"
|
||||
|
||||
if len(s) == 5:
|
||||
return f"#{s[1]}{s[1]}{s[2]}{s[2]}{s[3]}{s[3]}{s[4]}{s[4]}"
|
||||
|
||||
return s
|
||||
|
||||
|
||||
dark_mode_easing = [1, 0, 1, 0]
|
||||
light_mode_easing = [0, 2, 0, 2]
|
||||
|
||||
|
||||
def to_oklch_string(color: Color) -> str:
|
||||
"""Convert a color to an oklch string for CSS.
|
||||
|
||||
Args:
|
||||
color: The color to convert.
|
||||
|
||||
Returns:
|
||||
The oklch string.
|
||||
"""
|
||||
L = round(color["lightness"] * 100, 1)
|
||||
return f"oklch({L}% {color['chroma']:.4f} {color['hue']:.4f})"
|
||||
|
||||
|
||||
def get_scale_from_color(
|
||||
source: Color, scales: dict[str, list[Color]], background_color: Color
|
||||
) -> list[Color]:
|
||||
"""Get a scale from a color.
|
||||
|
||||
Args:
|
||||
source: The source color.
|
||||
scales: The scales of colors.
|
||||
background_color: The background color.
|
||||
|
||||
Returns:
|
||||
The generated scale.
|
||||
"""
|
||||
all_colors = []
|
||||
for name, scale in scales.items():
|
||||
for color in scale:
|
||||
distance = source.delta_e(color)
|
||||
all_colors.append({"scale": name, "distance": distance, "color": color})
|
||||
|
||||
all_colors.sort(key=lambda x: x["distance"])
|
||||
|
||||
# Remove non-unique scales
|
||||
closest_colors = []
|
||||
seen_scales = set()
|
||||
for color in all_colors:
|
||||
if color["scale"] not in seen_scales:
|
||||
closest_colors.append(color)
|
||||
seen_scales.add(color["scale"])
|
||||
|
||||
# Handle gray scales
|
||||
gray_scale_names = ["gray", "mauve", "slate", "sage", "olive", "sand"]
|
||||
all_are_grays = all(color["scale"] in gray_scale_names for color in closest_colors)
|
||||
if not all_are_grays and closest_colors[0]["scale"] in gray_scale_names:
|
||||
while closest_colors[1]["scale"] in gray_scale_names:
|
||||
del closest_colors[1]
|
||||
|
||||
color_a = closest_colors[0]
|
||||
color_b = closest_colors[1]
|
||||
|
||||
# Calculate triangle sides
|
||||
a = color_b["distance"]
|
||||
b = color_a["distance"]
|
||||
c = color_a["color"].delta_e(color_b["color"])
|
||||
|
||||
# Calculate angles
|
||||
cos_a = (b**2 + c**2 - a**2) / (2 * b * c)
|
||||
rad_a = math.acos(cos_a)
|
||||
sin_a = math.sin(rad_a)
|
||||
|
||||
cos_b = (a**2 + c**2 - b**2) / (2 * a * c)
|
||||
rad_b = math.acos(cos_b)
|
||||
sin_b = math.sin(rad_b)
|
||||
|
||||
# Calculate tangents
|
||||
tan_c1 = cos_a / sin_a
|
||||
tan_c2 = cos_b / sin_b
|
||||
|
||||
# Calculate ratio
|
||||
ratio = max(0, tan_c1 / tan_c2) * 0.5
|
||||
|
||||
# Mix scales
|
||||
scale_a = scales[color_a["scale"]]
|
||||
scale_b = scales[color_b["scale"]]
|
||||
scale = [
|
||||
Color.mix(scale_a[i], scale_b[i], ratio).convert("oklch") for i in range(12)
|
||||
]
|
||||
|
||||
# Find base color
|
||||
base_color = min(scale, key=lambda color: source.delta_e(color))
|
||||
|
||||
# Adjust chroma ratio
|
||||
ratio_c = source.get("oklch.c") / base_color.get("oklch.c")
|
||||
|
||||
# Modify hue and chroma of the scale
|
||||
for color in scale:
|
||||
color = color.set(
|
||||
"oklch.c", min(source.get("oklch.c") * 1.5, color.get("oklch.c") * ratio_c)
|
||||
)
|
||||
color = color.set("oklch.h", source.get("oklch.h"))
|
||||
|
||||
# Handle light and dark modes
|
||||
if scale[0].get("oklch.l") > 0.5: # Light mode
|
||||
lightness_scale = [color.get("oklch.l") for color in scale]
|
||||
background_l = max(0, min(1, background_color.get("oklch.l")))
|
||||
new_lightness_scale = transpose_progression_start(
|
||||
background_l, lightness_scale, light_mode_easing
|
||||
)
|
||||
new_lightness_scale = new_lightness_scale[1:] # Remove the added step
|
||||
|
||||
for i, lightness in enumerate(new_lightness_scale):
|
||||
scale[i] = scale[i].set("oklch.l", lightness)
|
||||
else: # Dark mode
|
||||
ease = list(dark_mode_easing)
|
||||
reference_background_color_l = scale[0].get("oklch.l")
|
||||
background_color_l = max(0, min(1, background_color.get("oklch.l")))
|
||||
ratio_l = background_color_l / reference_background_color_l
|
||||
|
||||
if ratio_l > 1:
|
||||
max_ratio = 1.5
|
||||
for i in range(len(ease)):
|
||||
meta_ratio = (ratio_l - 1) * (max_ratio / (max_ratio - 1))
|
||||
ease[i] = ( # type: ignore
|
||||
0 if ratio_l > max_ratio else max(0, ease[i] * (1 - meta_ratio))
|
||||
)
|
||||
|
||||
lightness_scale = [color.get("oklch.l") for color in scale]
|
||||
background_l = background_color.get("oklch.l")
|
||||
new_lightness_scale = transpose_progression_start(
|
||||
background_l, lightness_scale, ease
|
||||
)
|
||||
|
||||
for i, lightness in enumerate(new_lightness_scale):
|
||||
scale[i] = scale[i].set("oklch.l", lightness)
|
||||
|
||||
return scale
|
||||
|
||||
|
||||
def transpose_progression_start(to: float, arr: list, curve: list) -> list[float]:
|
||||
"""Transpose a progression to a new start point.
|
||||
|
||||
Args:
|
||||
to: The new start point.
|
||||
arr: The progression.
|
||||
curve: The bezier curve.
|
||||
|
||||
Returns:
|
||||
The transposed progression.
|
||||
"""
|
||||
last_index = len(arr) - 1
|
||||
diff = arr[0] - to
|
||||
fn = bezier(*curve)
|
||||
return [n - diff * fn(1 - i / last_index) for i, n in enumerate(arr)]
|
||||
|
||||
|
||||
def transpose_progression_end(
|
||||
to: float, arr: list[float], curve: list[float]
|
||||
) -> list[float]:
|
||||
"""Transpose a progression to a new end point.
|
||||
|
||||
Args:
|
||||
to: The new end point.
|
||||
arr: The progression.
|
||||
curve: The bezier curve.
|
||||
|
||||
Returns:
|
||||
The transposed progression.
|
||||
"""
|
||||
last_index = len(arr) - 1
|
||||
diff = arr[-1] - to
|
||||
fn = bezier(*curve)
|
||||
return [n - diff * fn(i / last_index) for i, n in enumerate(arr)]
|
Loading…
Reference in New Issue
Block a user