mods that hopefully work for a TechAge-based pack
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
-- colors.lua
|
||||
-- Pure color math utilities for eye_spy.
|
||||
-- No minetest API calls: safe to use from any context.
|
||||
|
||||
eye_spy.colors = {}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Clamp
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Clamp v to [minv, maxv].
|
||||
local function clamp(v, minv, maxv)
|
||||
return math.max(minv, math.min(maxv, v))
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- RGB packing / unpacking
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Split a packed 24-bit integer into its R, G, B byte components.
|
||||
local function split_rgb(color)
|
||||
local r = math.floor(color / 65536) % 256
|
||||
local g = math.floor(color / 256) % 256
|
||||
local b = color % 256
|
||||
return r, g, b
|
||||
end
|
||||
|
||||
--- Pack R, G, B byte components into a 24-bit integer.
|
||||
local function join_rgb(r, g, b)
|
||||
return (r * 65536) + (g * 256) + b
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Luminance helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Linearise a single 0–255 channel value for WCAG relative-luminance maths.
|
||||
local function channel_luminance(channel)
|
||||
local c = channel / 255
|
||||
|
||||
if c <= 0.03928 then
|
||||
return c / 12.92
|
||||
end
|
||||
|
||||
return ((c + 0.055) / 1.055) ^ 2.4
|
||||
end
|
||||
|
||||
--- WCAG 2.1 relative luminance of a packed 24-bit RGB integer.
|
||||
local function color_luminance(color)
|
||||
local r, g, b = split_rgb(color)
|
||||
|
||||
return (0.2126 * channel_luminance(r))
|
||||
+ (0.7152 * channel_luminance(g))
|
||||
+ (0.0722 * channel_luminance(b))
|
||||
end
|
||||
|
||||
--- WCAG 2.1 contrast ratio between two packed 24-bit colours.
|
||||
local function contrast_ratio(color_a, color_b)
|
||||
local a_l = color_luminance(color_a)
|
||||
local b_l = color_luminance(color_b)
|
||||
|
||||
return (math.max(a_l, b_l) + 0.05) / (math.min(a_l, b_l) + 0.05)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Colour arithmetic
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Multiply every channel of a packed colour by factor and re-pack.
|
||||
local function scale_color(color, factor)
|
||||
local r, g, b = split_rgb(color)
|
||||
r = clamp(math.floor(r * factor), 0, 255)
|
||||
g = clamp(math.floor(g * factor), 0, 255)
|
||||
b = clamp(math.floor(b * factor), 0, 255)
|
||||
return join_rgb(r, g, b)
|
||||
end
|
||||
|
||||
--- Linear interpolation between two packed colours.
|
||||
-- t = 0 → color_a, t = 1 → color_b.
|
||||
local function blend_color(color_a, color_b, t)
|
||||
local factor = clamp(t or 0.5, 0, 1)
|
||||
local ar, ag, ab = split_rgb(color_a)
|
||||
local br, bg, bb = split_rgb(color_b)
|
||||
|
||||
return join_rgb(
|
||||
math.floor((ar * (1 - factor)) + (br * factor) + 0.5),
|
||||
math.floor((ag * (1 - factor)) + (bg * factor) + 0.5),
|
||||
math.floor((ab * (1 - factor)) + (bb * factor) + 0.5)
|
||||
)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- HSL conversion
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Convert R, G, B bytes to HSL (hue 0–360, saturation 0–1, lightness 0–1).
|
||||
local function rgb_to_hsl(r, g, b)
|
||||
local rn, gn, bn = r / 255, g / 255, b / 255
|
||||
local max_c = math.max(rn, gn, bn)
|
||||
local min_c = math.min(rn, gn, bn)
|
||||
local l = (max_c + min_c) * 0.5
|
||||
|
||||
if max_c == min_c then
|
||||
return 0, 0, l
|
||||
end
|
||||
|
||||
local d = max_c - min_c
|
||||
local s = l > 0.5 and d / (2 - max_c - min_c) or d / (max_c + min_c)
|
||||
local h
|
||||
|
||||
if max_c == rn then
|
||||
h = ((gn - bn) / d) % 6
|
||||
elseif max_c == gn then
|
||||
h = (bn - rn) / d + 2
|
||||
else
|
||||
h = (rn - gn) / d + 4
|
||||
end
|
||||
|
||||
return (h / 6) * 360, s, l
|
||||
end
|
||||
|
||||
-- Hoisted helper for hsl_to_rgb — avoids allocating a new closure per call.
|
||||
-- Converts a hue position p/q at offset t (all normalised 0–1) to a channel value.
|
||||
local function hue2rgb(p, q, t)
|
||||
t = t % 1.0
|
||||
if t < 1 / 6 then return p + (q - p) * 6 * t end
|
||||
if t < 1 / 2 then return q end
|
||||
if t < 2 / 3 then return p + (q - p) * (2 / 3 - t) * 6 end
|
||||
return p
|
||||
end
|
||||
|
||||
--- Convert HSL (hue 0–360, saturation 0–1, lightness 0–1) to R, G, B bytes.
|
||||
local function hsl_to_rgb(h, s, l)
|
||||
if s <= 0 then
|
||||
local v = clamp(math.floor(l * 255 + 0.5), 0, 255)
|
||||
return v, v, v
|
||||
end
|
||||
|
||||
h = h / 360
|
||||
local q = l < 0.5 and l * (1 + s) or l + s - l * s
|
||||
local p = 2 * l - q
|
||||
|
||||
return clamp(math.floor(hue2rgb(p, q, h + 1 / 3) * 255 + 0.5), 0, 255),
|
||||
clamp(math.floor(hue2rgb(p, q, h) * 255 + 0.5), 0, 255),
|
||||
clamp(math.floor(hue2rgb(p, q, h - 1 / 3) * 255 + 0.5), 0, 255)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Background luminance (BT.601 perceived brightness, not WCAG)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Perceived brightness (BT.601) from R, G, B bytes (0–255 range).
|
||||
-- Returns a value in [0, 255].
|
||||
local function get_bg_luminance_rgb(r, g, b)
|
||||
return (r * 0.299) + (g * 0.587) + (b * 0.114)
|
||||
end
|
||||
|
||||
--- Perceived brightness (BT.601) from a 6-character lowercase hex string
|
||||
-- such as "1a1a1b". Returns a value in [0, 255].
|
||||
local function get_bg_luminance(rgb_hex)
|
||||
local r = tonumber(rgb_hex:sub(1, 2), 16) or 26
|
||||
local g = tonumber(rgb_hex:sub(3, 4), 16) or 26
|
||||
local b = tonumber(rgb_hex:sub(5, 6), 16) or 27
|
||||
return get_bg_luminance_rgb(r, g, b)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Foreground selection helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Return whichever of near-black or near-white has the higher contrast ratio
|
||||
-- against bg_color (packed 24-bit integer).
|
||||
local function best_monochrome_for_bg(bg_color)
|
||||
local dark = 0x111111
|
||||
local light = 0xF0F0F0
|
||||
|
||||
if contrast_ratio(light, bg_color) >= contrast_ratio(dark, bg_color) then
|
||||
return light
|
||||
end
|
||||
|
||||
return dark
|
||||
end
|
||||
|
||||
--- Map a value in [0, 255] to a spectrum colour (red → green → blue → white).
|
||||
local function spectrum_color(val)
|
||||
local v = clamp(val or 255, 0, 255)
|
||||
local r, g, b = 0, 0, 0
|
||||
|
||||
if v < 85 then
|
||||
r = 255 - (v * 3)
|
||||
g = v * 3
|
||||
elseif v < 170 then
|
||||
local x = v - 85
|
||||
g = 255 - (x * 3)
|
||||
b = x * 3
|
||||
elseif v < 230 then
|
||||
local x = v - 170
|
||||
r = x * 4
|
||||
b = 255 - (x * 4)
|
||||
else
|
||||
local x = math.floor((v - 230) * (255 / 25))
|
||||
r, g, b = x, x, x
|
||||
end
|
||||
|
||||
return join_rgb(r, g, b)
|
||||
end
|
||||
|
||||
--- Darken color when the background (given as a hex string) is light,
|
||||
-- so the colour remains legible.
|
||||
-- Uses get_bg_luminance_rgb + split_rgb to avoid duplicating the BT.601 formula.
|
||||
local function adapt_for_bg(color, bg_hex)
|
||||
if get_bg_luminance(bg_hex) <= 140 then
|
||||
return color
|
||||
end
|
||||
|
||||
local luminance = get_bg_luminance_rgb(split_rgb(color))
|
||||
|
||||
if luminance > 130 then
|
||||
return scale_color(color, 0.45)
|
||||
end
|
||||
|
||||
return color
|
||||
end
|
||||
|
||||
--- Iteratively blend color toward black or white until it reaches min_ratio
|
||||
-- contrast against bg_color. Makes up to 5 nudge passes.
|
||||
local function nudge_contrast(color, bg_color, min_ratio)
|
||||
local candidate = color
|
||||
local bg_l = color_luminance(bg_color)
|
||||
|
||||
for _ = 1, 5 do
|
||||
if contrast_ratio(candidate, bg_color) >= (min_ratio or 4.5) then
|
||||
return candidate
|
||||
end
|
||||
|
||||
local fg_l = color_luminance(candidate)
|
||||
|
||||
if fg_l < bg_l then
|
||||
candidate = blend_color(candidate, 0x000000, 0.32)
|
||||
else
|
||||
candidate = blend_color(candidate, 0xFFFFFF, 0.32)
|
||||
end
|
||||
end
|
||||
|
||||
return candidate
|
||||
end
|
||||
|
||||
--- Adjust color to meet min_ratio contrast against bg_color while
|
||||
-- preserving hue by blending toward white or black (whichever gives
|
||||
-- the better contrast) in up to 8 steps.
|
||||
local function tune_contrast_preserve_hue(color, bg_color, min_ratio)
|
||||
local required_ratio = min_ratio or 4.0
|
||||
local candidate = color
|
||||
|
||||
if contrast_ratio(candidate, bg_color) >= required_ratio then
|
||||
return candidate
|
||||
end
|
||||
|
||||
for step = 1, 8 do
|
||||
if contrast_ratio(candidate, bg_color) >= required_ratio then
|
||||
return candidate
|
||||
end
|
||||
|
||||
local t = clamp(step * 0.09, 0, 0.70)
|
||||
local lighter = blend_color(color, 0xFFFFFF, t)
|
||||
local darker = blend_color(color, 0x000000, t)
|
||||
|
||||
if contrast_ratio(lighter, bg_color) >= contrast_ratio(darker, bg_color) then
|
||||
candidate = lighter
|
||||
else
|
||||
candidate = darker
|
||||
end
|
||||
end
|
||||
|
||||
return candidate
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Public API
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
eye_spy.colors.clamp = clamp
|
||||
eye_spy.colors.split_rgb = split_rgb
|
||||
eye_spy.colors.join_rgb = join_rgb
|
||||
eye_spy.colors.channel_luminance = channel_luminance
|
||||
eye_spy.colors.color_luminance = color_luminance
|
||||
eye_spy.colors.contrast_ratio = contrast_ratio
|
||||
eye_spy.colors.scale_color = scale_color
|
||||
eye_spy.colors.blend_color = blend_color
|
||||
eye_spy.colors.rgb_to_hsl = rgb_to_hsl
|
||||
eye_spy.colors.hsl_to_rgb = hsl_to_rgb
|
||||
eye_spy.colors.get_bg_luminance_rgb = get_bg_luminance_rgb
|
||||
eye_spy.colors.get_bg_luminance = get_bg_luminance
|
||||
eye_spy.colors.best_monochrome_for_bg = best_monochrome_for_bg
|
||||
eye_spy.colors.spectrum_color = spectrum_color
|
||||
eye_spy.colors.adapt_for_bg = adapt_for_bg
|
||||
eye_spy.colors.nudge_contrast = nudge_contrast
|
||||
eye_spy.colors.tune_contrast_preserve_hue = tune_contrast_preserve_hue
|
||||
Reference in New Issue
Block a user