sorcery  Artifact [1e12e4a44b]

Artifact 1e12e4a44be43541dc4c94c23747da7e25bc1515dfee57f4c69e8dbc97f5f95f:

  • File lib/color.lua — part of check-in [0a49ac4849] at 2020-08-13 05:11:22 on branch glowpelt/hsl — feat(color): Change color lightening to use HSL Change color lightening, including the readable utility, to use HSL. This is because the earlier implementation was broken and hacky, and using HSL is a way to implement this in a much more natural-feeling way (being closer to percieved lightness), especially for the current uses. Add a color:to_hsl() function to make this easier, as well as a from_hsl utility that is only in color, for now, but maybe should be exposed as an alternate constructor? (user: glowpelt, size: 6197) [annotate] [blame] [check-ins using]

local color
color = sorcery.lib.class {
	__tostring = function(self)
		-- return minetest.rgba(
		-- 	self.red,
		-- 	self.green,
		-- 	self.blue,
		-- 	self.alpha
		-- )
		local hex = function(val)
			return string.format('%02X',math.max(0,math.min(255,math.floor(val))))
		end
		local str = '#' ..
			hex(self.red) ..
			hex(self.green) ..
			hex(self.blue)
		if self.alpha then str = str .. hex(self.alpha) end
		return str
	end;

	__add = function(self, other)
		local sfac = (self.alpha or 255) / 255
		local ofac = (other.alpha or 255) / 255
		if self.alpha == other.alpha then
			sfac = 1 ofac = 1
		end

		local sr, sg, sb = other.red * ofac, other.blue * ofac, other.green * ofac
		local nr, ng, nb =  self.red * sfac,  self.blue * sfac,  self.green * sfac
		local saturate = function(a,b)
			return math.max(0, math.min(255, a+b))
		end
		local alpha = nil
		if self.alpha and other.alpha then
			alpha = saturate(self.alpha or 255, other.alpha or 255)
		end
		return color(
			saturate(sr, nr),
			saturate(sg, ng),
			saturate(sb, nb),
			alpha
		)
	end;
	
	cast = {
		number = function(n) return {
			red = n; green = n; blue = n;
		} end;
		table = function(t) return {
			red = t[1]; green = t[2]; blue = t[3];
		} end;
	};

	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
			end;
		end;
		local new = {
			hex = function(self) return
				getmetatable(self).__tostring(self)
			end;

			fmt = function(self, text) return
				minetest.colorize(self:hex(), text)
			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 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;

			fade = warp(function(new, fac)
				new.alpha = math.min(255, (new.alpha or 255) * fac)
			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);
		}
		if g == nil then
			if type(r) == 'string' then
				assert(false) -- TODO parse color string
			elseif type(r) == 'table' then
				new.red = r[1]
				new.green = r[2]
				new.blue = r[3]
				new.alpha = r[4]
			else assert(false) end
		else
			new.red = r
			new.green = g
			new.blue = b
			new.alpha = a
		end
		return new
	end
}
return color