Index: lib/color.lua ================================================================== --- lib/color.lua +++ lib/color.lua @@ -53,10 +53,20 @@ construct = function(r,g,b,a) local clip = function(v) return math.max(0,math.min(255,v)) end; + local from_hsl = function(hsl, alpha) + -- convert from a hsl table and alpha value to a color + local weird = function(n) + -- Yeah... this is a really weird function, only named f + local k = math.fmod(n + hsl.hue/(math.pi/6),12) + local a = hsl.saturation * math.min(hsl.luminosity, 1 - hsl.luminosity) + return hsl.luminosity * math.max(-1, math.min(k-3,9-k,1)) + end + return color(clip(weird(0)*255), clip(weird(8)*255), clip(weird(4)*255), alpha) + end; local warp = function(f) return function(self, ...) local n = color(self) f(n, ...) return n @@ -72,25 +82,43 @@ end; luminosity = function(self) return (self.red + self.green + self.blue) / 3 end; + + to_hsl = function(self) + -- https://en.wikipedia.org/wiki/HSL_and_HSV + -- www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ + -- https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.211.6425 + -- https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.413.9004 + -- We need the rgb between 0 and 1 + local rgb = { r = self.red/255, g = self.green/255, b = self.blue/255 } + -- First, the hue. + -- This version of the calculation can be up to 1.12deg off at the right few colors + -- but is overall very close, easier to implement, and runs much faster + -- TODO: consider memoizing something to do with this all? + local alpha = 0.5 * (2*rgb.r - rgb.g - rgb.b) + local beta = (math.sqrt(3)/2)*(rgb.g - rgb.b) + local hue = math.atan2(beta, alpha) + -- Next the luminosity/lightness. This one's easy enough + local luminosity = 0.5*(math.max(rgb.r, rgb.g, rgb.b) + math.min(rgb.r, rgb.g, rgb.b)) + -- Finally, saturation + local saturation = 0 + -- If luminosity isn't essentially 1 or 0 + if math.abs(luminosity - 1) < 1e-18 or math.abs(luminosity) < 1e-18 then + -- need the chroma + local chroma = math.max(rgb.r,rgb.g,rgb.b) - math.min(rgb.r,rgb.g,rgb.b) + saturation = chroma / (1 - math.abs(2*luminosity - 1)) + end + return { hue = hue, saturation = saturation, luminosity = luminosity } + end; readable = function(self, target) target = target or 200 - local lum = self:luminosity() - if lum < target then - local f = 1.0 + (target - lum) / 255 - local nc = self:brighten(f * 1.5) - -- i don't know why the *1.5 is necessary. it's - -- an ugly hack to work around broken math, - -- because i'm too much of a mathtard to actually - -- find what's wrong - return nc - else - return self - end + local hsl = self:to_hsl() + hsl.luminosity = target + return from_hsl(hsl, self.alpha) end; bg = function(self, text) return text .. minetest.get_background_escape_sequence(self:hex()) end; @@ -97,20 +125,22 @@ fade = warp(function(new, fac) new.alpha = math.min(255, (new.alpha or 255) * fac) end); - brighten = warp(function(new, fac) - local lum = new:luminosity() - local newlum = lum * fac - local delta = (newlum - lum) - new.red = clip(new.red + delta) - new.blue = clip(new.blue + delta) - new.green = clip(new.green + delta) - end); + brighten = function(self, fac) + -- Use HSL to brighten + -- To HSL + local hsl = self:to_hsl() + -- Do the calculation, clamp to 0-1 instead of the clamp fn + hsl.luminosity = math.max(math.min(hsl.luminosity * fac, 0), 1) + -- Turn back into RGB color + return from_hsl(hsl, self.alpha) + end; darken = warp(function(new, fac) + -- TODO: is there any point to this being different than brighten? Probably especially not now. new.red = clip(new.red - (new.red * fac)) new.blue = clip(new.blue - (new.blue * fac)) new.green = clip(new.green - (new.green * fac)) end); }