Refine lighting color look and feel

This commit is contained in:
ltadeu6 2026-04-15 17:21:37 -03:00
parent d7af92f6b1
commit be697a6d1c
No known key found for this signature in database
GPG key ID: FB9FDAB6B6F3418D

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import colorsys
import math
import subprocess
import time
from io import BytesIO
@ -20,6 +21,11 @@ SATURATION_BOOST = 1.15
MIN_SATURATION = 0.20
MIN_VALUE = 0.18
REQUEST_TIMEOUT = 10
MAX_VALUE = 0.82
PALETTE_SIZE = 32
OKLCH_LIGHTNESS = 0.72
OKLCH_CHROMA = 0.24
MAX_BRIGHTNESS_STEP = 0.10
def run_playerctl(field: str, player: str | None = None) -> str:
@ -153,6 +159,120 @@ def image_pixels(img: Image.Image) -> list[tuple[int, int, int]]:
return list(img.getdata())
def hue_distance(a: float, b: float) -> float:
diff = abs(a - b)
return min(diff, 1.0 - diff)
def rgb_to_hsv(rgb: tuple[int, int, int]) -> tuple[float, float, float]:
r, g, b = rgb
return colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
def hsv_to_rgb(h: float, s: float, v: float) -> tuple[int, int, int]:
r, g, b = colorsys.hsv_to_rgb(h, s, v)
return (int(r * 255), int(g * 255), int(b * 255))
def srgb_channel_to_linear(channel: float) -> float:
if channel <= 0.04045:
return channel / 12.92
return ((channel + 0.055) / 1.055) ** 2.4
def linear_channel_to_srgb(channel: float) -> float:
if channel <= 0.0031308:
return 12.92 * channel
return 1.055 * (channel ** (1 / 2.4)) - 0.055
def rgb_to_oklab(rgb: tuple[int, int, int]) -> tuple[float, float, float]:
r, g, b = rgb
rl = srgb_channel_to_linear(r / 255.0)
gl = srgb_channel_to_linear(g / 255.0)
bl = srgb_channel_to_linear(b / 255.0)
l = 0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl
m = 0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl
s = 0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl
l_ = l ** (1 / 3)
m_ = m ** (1 / 3)
s_ = s ** (1 / 3)
return (
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
)
def oklab_to_rgb(lightness: float, a: float, b: float) -> tuple[int, int, int]:
l_ = lightness + 0.3963377774 * a + 0.2158037573 * b
m_ = lightness - 0.1055613458 * a - 0.0638541728 * b
s_ = lightness - 0.0894841775 * a - 1.2914855480 * b
l = l_ ** 3
m = m_ ** 3
s = s_ ** 3
rl = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
gl = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
bl = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
r = min(1.0, max(0.0, linear_channel_to_srgb(rl)))
g = min(1.0, max(0.0, linear_channel_to_srgb(gl)))
b = min(1.0, max(0.0, linear_channel_to_srgb(bl)))
return (int(r * 255), int(g * 255), int(b * 255))
def rgb_to_oklch(rgb: tuple[int, int, int]) -> tuple[float, float, float]:
lightness, a, b = rgb_to_oklab(rgb)
chroma = (a * a + b * b) ** 0.5
hue = (0.0 if chroma == 0 else (math.atan2(b, a) / (2 * math.pi)) % 1.0)
return (lightness, chroma, hue)
def oklch_to_rgb(lightness: float, chroma: float, hue: float) -> tuple[int, int, int]:
angle = 2 * math.pi * hue
a = chroma * math.cos(angle)
b = chroma * math.sin(angle)
return oklab_to_rgb(lightness, a, b)
def pure_tone_palette() -> list[tuple[int, int, int]]:
return [
oklch_to_rgb(OKLCH_LIGHTNESS, OKLCH_CHROMA, index / PALETTE_SIZE)
for index in range(PALETTE_SIZE)
]
PURE_TONE_PALETTE = pure_tone_palette()
def clamp_brightness(rgb: tuple[int, int, int], previous_rgb: tuple[int, int, int] | None) -> tuple[int, int, int]:
if previous_rgb is None:
return rgb
h, s, v = rgb_to_hsv(rgb)
_, _, prev_v = rgb_to_hsv(previous_rgb)
limited_v = max(prev_v - MAX_BRIGHTNESS_STEP, min(prev_v + MAX_BRIGHTNESS_STEP, v))
return hsv_to_rgb(h, s, limited_v)
def snap_to_palette(rgb: tuple[int, int, int]) -> tuple[int, int, int]:
_, _, hue = rgb_to_oklch(rgb)
best = min(
PURE_TONE_PALETTE,
key=lambda candidate: hue_distance(hue, rgb_to_oklch(candidate)[2]),
)
best_lightness, _, best_hue = rgb_to_oklch(best)
return oklch_to_rgb(best_lightness, OKLCH_CHROMA, best_hue)
def good_pixels(img: Image.Image) -> list[tuple[int, int, int]]:
img = img.resize((80, 80))
pixels = image_pixels(img)
@ -167,6 +287,8 @@ def good_pixels(img: Image.Image) -> list[tuple[int, int, int]]:
continue
if s < MIN_SATURATION:
continue
if v > MAX_VALUE and s < 0.45:
continue
filtered.append((r, g, b))
@ -190,20 +312,29 @@ def dominant_color(img: Image.Image) -> tuple[int, int, int]:
key = (r // 16 * 16, g // 16 * 16, b // 16 * 16)
buckets[key] = buckets.get(key, 0) + 1
best = max(buckets.items(), key=lambda x: x[1])[0]
best = max(
buckets.items(),
key=lambda item: item[1] * bucket_weight(item[0]),
)[0]
return boost_color(best)
def bucket_weight(rgb: tuple[int, int, int]) -> float:
_, saturation, value = rgb_to_hsv(rgb)
saturation_score = 0.6 + saturation
brightness_midpoint = 0.62
brightness_penalty = abs(value - brightness_midpoint) * 1.2
return max(0.2, saturation_score - brightness_penalty)
def boost_color(rgb: tuple[int, int, int]) -> tuple[int, int, int]:
r, g, b = rgb
rf, gf, bf = r / 255.0, g / 255.0, b / 255.0
h, s, v = colorsys.rgb_to_hsv(rf, gf, bf)
h, s, v = rgb_to_hsv(rgb)
s = min(1.0, s * SATURATION_BOOST)
v = max(v, 0.45)
v = min(0.74, max(v, 0.48))
nr, ng, nb = colorsys.hsv_to_rgb(h, s, v)
return (int(nr * 255), int(ng * 255), int(nb * 255))
return hsv_to_rgb(h, s, v)
def set_light_color(rgb: tuple[int, int, int]) -> None:
@ -226,6 +357,7 @@ def set_light_color(rgb: tuple[int, int, int]) -> None:
def main() -> None:
print("Monitorando música atual. Ctrl+C para sair.")
last_track = ""
last_rgb: tuple[int, int, int] | None = None
while True:
try:
@ -250,10 +382,14 @@ def main() -> None:
print(f"Nova faixa detectada: {track_id}")
img = load_image_from_url(art_url)
rgb = dominant_color(img)
raw_rgb = dominant_color(img)
palette_rgb = snap_to_palette(raw_rgb)
rgb = clamp_brightness(palette_rgb, last_rgb)
print(f"Cor base: {raw_rgb} -> paleta: {palette_rgb}")
print(f"Aplicando cor: {rgb}")
set_light_color(rgb)
last_track = track_id
last_rgb = rgb
time.sleep(2)