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