Index: lib/color.lua ================================================================== --- lib/color.lua +++ lib/color.lua @@ -54,18 +54,52 @@ 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)) + -- Based on the algorithm in Computer Graphics: Principles and Practice, by + -- James D. Foley et. al., 2nd ed., p. 596 + -- Degree version, though radian is more natural, I don't want to translate it yet + local h = hsl.hue + local s = hsl.saturation + local l = hsl.luminosity + local value = function(n1, n2, hue) + if hue > 360 then + hue = hue - 360 + elseif hue < 0 then + hue = hue + 360 + end + if hue < 60 then + return n1 + (n2 - n1) * hue/60 + elseif hue < 180 then + return n2 + elseif hue < 240 then + return n1 + (n2 - n1) * (240 - hue)/60 + else + return n1 + end + end + local m2 + if l < 0.5 then + m2 = l * (1 + s) + else + m2 = l + s - l * s end - return color(clip(weird(0)*255), clip(weird(8)*255), clip(weird(4)*255), alpha) + local m1 = 2 * l - m2 + if s == 0 then + -- Achromatic, there is no hue + -- In book this errors if hue is not undefined, but we set hue to 0 in this case, not nil or something, so + return color(l, l, l, alpha) + else + -- Chromatic case, so there is a hue + return color( + clip(value(m1, m2, h + 120)*255), + clip(value(m1, m2, h)*255), + clip(value(m1, m2, h - 120)*255), + alpha + ) + end end; local warp = function(f) return function(self, ...) local n = color(self) f(n, ...) @@ -84,38 +118,55 @@ 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 + -- Based on the algorithm in Computer Graphics: Principles and Practice, by + -- James D. Foley et. al., 2nd ed., p. 595 -- 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 r = self.red/255 + local g = self.green/255 + local b = self.blue/255 + local max = math.max(r, g, b) + local min = math.min(r, g, b) + local luminosity = (max + min)/2 + local hue = 0 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)) + if max == min then + -- Achromatic case, because r=g=b + saturation = 0 + hue = 0 -- Undefined, so just replace w/ 0 for usability + else + -- Chromatic case + if luminosity <= 0.5 then + saturation = (max - min)/(max + min) + else + saturation = (max - min)/(2 - max - min) + end + -- Next calculate the hue + local delta = max - min + if r == max then + hue = (g - b)/delta + elseif g == max then + hue = 2 + (b - r)/delta + else -- blue must be max, so no point in checking + hue = 4 + (r - g)/delta + end + hue = hue * 60 -- degrees + --hue = hue * (math.pi / 3) -- for hue in radians instead of degrees + if hue < 0 then + hue = hue + 2 * math.pi + end end + print("r"..self.red.."g"..self.green.."b"..self.blue.." is h"..hue.."s"..saturation.."l"..luminosity) + local temp = from_hsl({hue=hue,saturation=saturation,luminosity=luminosity},self.alpha) + print("back is r"..temp.red.."g"..temp.green.."b"..temp.blue) return { hue = hue, saturation = saturation, luminosity = luminosity } end; readable = function(self, target) - target = target or 200 + target = target or 0.5 local hsl = self:to_hsl() hsl.luminosity = target return from_hsl(hsl, self.alpha) end; @@ -130,12 +181,14 @@ 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) + hsl.luminosity = math.min(math.max(hsl.luminosity * fac, 0), 1) -- Turn back into RGB color + local t = from_hsl(hsl, self.alpha) + print("brighten is r"..t.red.."g"..t.green.."b"..t.blue) 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.