starlit  color.lua at [a810a756ce]

File mods/vtlib/color.lua artifact d9e5a5527c part of check-in a810a756ce


local lib = ...

local color
local function warp(f)
	return function(self, ...)
		local n = color(self)
		f(n, ...)
		return n
	end;
end

local function clip(v)
	return math.max(0,math.min(255,v))
end
local colorStruct do local T,G = lib.marshal.t, lib.marshal.g
	colorStruct = G.struct {
		red = T.u8;
		green = T.u8;
		blue = T.u8;
		alpha = T.u8;
	}
end

local function from_hsl(hsl, alpha)
	-- 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.sat
	local l = hsl.lum
	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
	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)),
			clip(value(m1, m2, h)),
			clip(value(m1, m2, h - 120)),
			alpha
		)
	end
end

color = lib.class {
	__tostring = function(self)
		local hex = function(val)
			return string.format('%02X',math.max(0,math.min(0xff,math.floor(0xff*val))))
		end
		local str = '#' ..
			hex(self.red) ..
			hex(self.green) ..
			hex(self.blue)
		if self.alpha and self.alpha < 1.0 then str = str .. hex(self.alpha) end
		return str
	end;

	__add = function(self, other)
		local sfac = (self.alpha or 1.0) / 1.0
		local ofac = (other.alpha or 1.0) / 1.0
		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(1.0, a+b))
		end
		local alpha = nil
		if self.alpha and other.alpha then
			alpha = saturate(self.alpha or 1.0, other.alpha or 1.0)
		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]; alpha = t[4];
		} end;
	};

	__index = {
		hex = function(self) return
			getmetatable(self).__tostring(self)
		end;

		int24 = function(self)
			return bit.bor(
				bit.lshift(math.floor(0xff*self.red),   16),
				bit.lshift(math.floor(0xff*self.green),  8),
				           math.floor(0xff*self.blue))
		end;

		int = function(self)
			return bit.bor(bit.lshift(self:int24(), 8), math.floor(0xff*(self.alpha or 1.0)))
		end;

		fmt = function(self, text) return
			minetest.colorize(self:hex(), text)
		end;

		bg = function(self, text) return
			text .. minetest.get_background_escape_sequence(self:hex())
		end;

		lum = function(self) return
			(self.red + self.green + self.blue) / 3
		end;

		pair = function(self) --> bg, fg
			if self:to_hsl().lum > 0.5 then -- dark on light
				return self:brighten(1.2), self:brighten(0.1)
			else -- light on dark
				return self:brighten(0.6), self:readable(.9, 1.0)
			end
		end;

		marshal = function(self)
			local raw = colorStruct.enc {
				red   = math.floor(self.red   * 0xff);
				green = math.floor(self.green * 0xff);
				blue  = math.floor(self.blue  * 0xff);
				alpha = math.floor(self.alpha * 0xff);
			}
			return lib.str.meta_armor(raw)
		end;
		to_hsl_o = function(self)
			local s = self:to_hsl()
			return {
				hue = s.hue;
				sat = s.sat*2-1;
				lum = s.lum*2-1;
			}
		end;

		to_hsl = function(self)
			-- THIS DOESN'T WORK. color(hsl):to_hsl() ~= hsl.
			-- has ugly implications for light control

			-- 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 r = self.red
			local g = self.green
			local b = self.blue
			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 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,sat=saturation,lum=luminosity},self.alpha)
			-- print("back is r"..temp.red.."g"..temp.green.."b"..temp.blue)
			if hue < 0 then
				hue = 360 + hue
			end
			return { hue = hue, sat = saturation, lum = luminosity, alpha = self.alpha }
		end;

		readable = function(self, target, minalpha)
			target = target or 0.6
			local hsl = self:to_hsl()
			hsl.lum = target
			local worstHue = 230
			local nearness = math.abs(worstHue - hsl.hue)
			if nearness <= 70 then
				local boost = 1.0 - (nearness / 70)
				hsl.lum = math.min(1, hsl.lum * (1 + (boost*0.4)))
			end

			return from_hsl(hsl, math.max(self.alpha or 1.0, minalpha))
		end;

		fade = warp(function(new, fac)
			new.alpha = math.min(1.0, (new.alpha or 1.0) * fac)
		end);

		warp = warp(function(new, fn) fn(new) end);

		lerp = warp(function(self, new, fac) -- uses rgb color space
			local function lerp(t, a, b) return (1-t)*a + t*b end
			self.red = lerp(fac, self.red,   new.red)
			self.green = lerp(fac, self.green, new.green)
			self.blue = lerp(fac, self.blue,  new.blue)
			if self.alpha ~= nil or new.alpha ~= nil then
				if new.alpha == nil and fac >= 1.0 then
					self.alpha = nil
				elseif self.alpha == nil and fac <= 0.0 then
					self.alpha = nil
				else
					self.alpha = lerp(fac, self.alpha or 1.0,  new.alpha or 1.0)
				end
			end
		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.lum = math.min(math.max(hsl.lum * fac, 0), 1)
			-- Turn back into RGB color
			-- local t = from_hsl(hsl, self.alpha)
			-- print("darker is r"..hsl.red.."g"..hsl.green.."b"..hsl.blue)
			-- 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.
			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);
	};

	construct = function(r,g,b,a)
		local new = {}
		if g == nil then
			if type(r) == 'string' then
				assert(false) -- TODO parse color string
			elseif type(r) == 'table' then
				if r.hue then
					return from_hsl(r, r.alpha or g)
				elseif r.r and r.g and r.b then
					new.red = r.r
					new.green = r.g
					new.blue = r.b
					new.alpha = r.a
				else
					new.red = r[1]
					new.green = r[2]
					new.blue = r[3]
					new.alpha = r[4]
				end
			else assert(false) end
		else
			if color.id(r) then
				new.red = r.red
				new.green = r.green
				new.blue = r.blue
				new.alpha = g
			else
				new.red = r
				new.green = g
				new.blue = b
				new.alpha = a
			end
		end
		return new
	end
}

function color.unmarshal(str)
	local raw = lib.str.meta_dearmor(raw)
	local o = colorStruct.dec(raw)
	return color(
		o.red   / 0xff,
		o.green / 0xff,
		o.blue  / 0xff,
		o.alpha / 0xff
	)
end;

return color