fix(vivid): Keep colors from washing out when brightened

HSLuv preserves *relative* saturation, but the sRGB gamut narrows toward
white, so holding S constant while raising lightness silently drained real
chroma: red lost 29.8%, violet 26.6% and magenta 18.2%.

Work in LCh instead and never let chroma fall below where it started,
capped at what the gamut can hold at the new lightness. Colors already on
the boundary keep riding it up, so the hues that were not losing chroma
are untouched and near-gray tokens stay neutral.

red500     #f37574 -> #ff6c6a  (+19.7% chroma)
magenta500 #ef73a6 -> #fe67a8  (+22.1%)
violet500  #989ad7 -> #9498ea  (+35.6%)

Contrast is unchanged, with the lowest still at 6.21:1.
This commit is contained in:
Takuya Matsuyama 2026-08-11 14:03:20 +09:00
parent b08592c1bb
commit 308edfd998

View file

@ -36,7 +36,8 @@ function M.lighten(hex, amount, fg)
return M.blend(hex, fg or M.fg, amount)
end
--- Raises a color's perceptual lightness toward white, leaving hue and saturation untouched
--- Raises a color's perceptual lightness without draining its colorfulness.
---
---@param hex string
---@param amount number between 0 and 1, from unchanged to white
---@return string
@ -45,9 +46,23 @@ function M.brighten(hex, amount)
return hex
end
local hsluv = require("solarized-osaka.hsluv")
local color = hsluv.hex_to_hsluv(hex)
color[3] = color[3] + (100 - color[3]) * amount
return hsluv.hsluv_to_hex(color)
local lch = hsluv.rgb_to_lch(hsluv.hex_to_rgb(hex))
local chroma, hue = lch[2], lch[3]
local ceiling_before = hsluv.max_safe_chroma_for_lh(lch[1], hue)
lch[1] = lch[1] + (100 - lch[1]) * amount
-- Colors already sitting on the gamut boundary should ride it up rather than
-- stay pinned to their old chroma, so track the ceiling as well as the floor.
local ceiling = hsluv.max_safe_chroma_for_lh(lch[1], hue)
local scaled = ceiling_before > 0 and chroma * ceiling / ceiling_before or 0
lch[2] = math.min(math.max(chroma, scaled), ceiling)
local rgb = hsluv.lch_to_rgb(lch)
for i = 1, 3 do
rgb[i] = math.min(math.max(rgb[i], 0), 1)
end
return hsluv.rgb_to_hex(rgb)
end
function M.invert_color(color)