Compare commits
7 Commits
main
...
lendemor/a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bebe49a6a5 | ||
![]() |
be58a4c17e | ||
![]() |
68999a6f7c | ||
![]() |
f36d1a0226 | ||
![]() |
d56e2ae532 | ||
![]() |
e93279890c | ||
![]() |
d885cf6a66 |
40
poetry.lock
generated
40
poetry.lock
generated
@ -363,6 +363,17 @@ files = [
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "coloraide"
|
||||
version = "3.3.1"
|
||||
description = "A color library for Python."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "coloraide-3.3.1-py3-none-any.whl", hash = "sha256:b09183ea0f85cb48c9fabd9f77daf09d681d6a4cecad55793e413d27f5fcea42"},
|
||||
{file = "coloraide-3.3.1.tar.gz", hash = "sha256:bb945bc61aa08ac0927fdaec5a1052a8b30e64e5e31c4c94f1e706a7617297d8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@ -582,13 +593,13 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.114.1"
|
||||
version = "0.114.2"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi-0.114.1-py3-none-any.whl", hash = "sha256:5d4746f6e4b7dff0b4f6b6c6d5445645285f662fe75886e99af7ee2d6b58bb3e"},
|
||||
{file = "fastapi-0.114.1.tar.gz", hash = "sha256:1d7bbbeabbaae0acb0c22f0ab0b040f642d3093ca3645f8c876b6f91391861d8"},
|
||||
{file = "fastapi-0.114.2-py3-none-any.whl", hash = "sha256:44474a22913057b1acb973ab90f4b671ba5200482e7622816d79105dcece1ac5"},
|
||||
{file = "fastapi-0.114.2.tar.gz", hash = "sha256:0adb148b62edb09e8c6eeefa3ea934e8f276dabc038c5a82989ea6346050c3da"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -775,13 +786,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.0"
|
||||
version = "2.6.1"
|
||||
description = "File identification library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
|
||||
{file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
|
||||
{file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"},
|
||||
{file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -789,15 +800,18 @@ license = ["ukkonen"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.8"
|
||||
version = "3.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
|
||||
{file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.5.0"
|
||||
@ -1592,13 +1606,13 @@ testing = ["pytest", "pytest-cov", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.2"
|
||||
version = "4.3.3"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"},
|
||||
{file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"},
|
||||
{file = "platformdirs-4.3.3-py3-none-any.whl", hash = "sha256:50a5450e2e84f44539718293cbb1da0a0885c9d14adf21b77bae4e66fc99d9b5"},
|
||||
{file = "platformdirs-4.3.3.tar.gz", hash = "sha256:d4e0b7d8ec176b341fb03cb11ca12d0276faa8c485f9cd218f613840463fc2c0"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -3009,4 +3023,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "7a8990e432a404802c3ace9a81c3c8c33cdd60596f26cdc38e2de424cb1126dd"
|
||||
content-hash = "1ff3b9e0623564e88c348df348e2d9bc1e6f76c21961ded3102441067f02c1a0"
|
||||
|
@ -82,6 +82,7 @@ asynctest = ">=0.13.0,<1.0"
|
||||
pre-commit = {version = ">=3.2.1", python = ">=3.8,<4.0"}
|
||||
selenium = ">=4.11.0,<5.0"
|
||||
pytest-benchmark = ">=4.0.0,<5.0"
|
||||
coloraide=">=3.3.1"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
reflex = "reflex.reflex:cli"
|
||||
|
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
608
reflex/experimental/palette.py
Normal file
608
reflex/experimental/palette.py
Normal file
@ -0,0 +1,608 @@
|
||||
"""A module for generating Radix colors.
|
||||
|
||||
Converted from https://github.com/radix-ui/website/blob/main/components/generateRadixColors.tsx
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
from reflex.utils import console
|
||||
|
||||
try:
|
||||
from coloraide import Color
|
||||
except ImportError:
|
||||
console.error(
|
||||
"coloraide is not installed. Please install it to use this module. `pip install coloraide`"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
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