Our hsv logic assumes 0-1 (cs.rit.edu says "r,g,b values are from 0 to 1") and it makes more sense to provide __mul for multiplying in 0-1 since the result will stay in that range. Additionally, love switched to 0-1 color since 11.0. Included an as_255() function to unpack the color in 0-255 for some amount of backwards compatibility. Doesn't make sense to provide any kind of color object for it since most of our functions won't work correctly. Add tests for hue(), saturation(), value() that fail (even with /255 removed) for the old code, but pass on the new 0-1 range because the hsv logic outputs colors in 0-1. Test comparisons use reduced precision because my input data has limited precision.
390 lines
8.8 KiB
Lua
390 lines
8.8 KiB
Lua
--- Color utilities
|
|
-- @module color
|
|
|
|
local modules = (...):gsub('%.[^%.]+$', '') .. "."
|
|
local utils = require(modules .. "utils")
|
|
local precond = require(modules .. "_private_precond")
|
|
local color = {}
|
|
local color_mt = {}
|
|
|
|
local function new(r, g, b, a)
|
|
local c = { r, g, b, a }
|
|
c._c = c
|
|
return setmetatable(c, color_mt)
|
|
end
|
|
|
|
-- HSV utilities (adapted from http://www.cs.rit.edu/~ncs/color/t_convert.html)
|
|
-- hsv_to_color(hsv)
|
|
-- Converts a set of HSV values to a color. hsv is a table.
|
|
-- See also: hsv(h, s, v)
|
|
local function hsv_to_color(hsv)
|
|
local i
|
|
local f, q, p, t
|
|
local h, s, v
|
|
local a = hsv[4] or 1
|
|
s = hsv[2]
|
|
v = hsv[3]
|
|
|
|
if s == 0 then
|
|
return new(v, v, v, a)
|
|
end
|
|
|
|
h = hsv[1] / 60 -- sector 0 to 5
|
|
|
|
i = math.floor(h)
|
|
f = h - i -- factorial part of h
|
|
p = v * (1-s)
|
|
q = v * (1-s*f)
|
|
t = v * (1-s*(1-f))
|
|
|
|
if i == 0 then return new(v, t, p, a)
|
|
elseif i == 1 then return new(q, v, p, a)
|
|
elseif i == 2 then return new(p, v, t, a)
|
|
elseif i == 3 then return new(p, q, v, a)
|
|
elseif i == 4 then return new(t, p, v, a)
|
|
else return new(v, p, q, a)
|
|
end
|
|
end
|
|
|
|
-- color_to_hsv(c)
|
|
-- Takes in a normal color and returns a table with the HSV values.
|
|
local function color_to_hsv(c)
|
|
local r = c[1]
|
|
local g = c[2]
|
|
local b = c[3]
|
|
local a = c[4] or 1
|
|
local h, s, v
|
|
|
|
local min = math.min(r, g, b)
|
|
local max = math.max(r, g, b)
|
|
v = max
|
|
|
|
local delta = max - min
|
|
|
|
-- black, nothing else is really possible here.
|
|
if min == 0 and max == 0 then
|
|
return { 0, 0, 0, a }
|
|
end
|
|
|
|
if max ~= 0 then
|
|
s = delta / max
|
|
else
|
|
-- r = g = b = 0 s = 0, v is undefined
|
|
s = 0
|
|
h = -1
|
|
return { h, s, v, 1 }
|
|
end
|
|
|
|
if r == max then
|
|
h = ( g - b ) / delta -- yellow/magenta
|
|
elseif g == max then
|
|
h = 2 + ( b - r ) / delta -- cyan/yellow
|
|
else
|
|
h = 4 + ( r - g ) / delta -- magenta/cyan
|
|
end
|
|
|
|
h = h * 60 -- degrees
|
|
|
|
if h < 0 then
|
|
h = h + 360
|
|
end
|
|
|
|
return { h, s, v, a }
|
|
end
|
|
|
|
--- The public constructor.
|
|
-- @param x Can be of three types: </br>
|
|
-- number red component 0-1
|
|
-- table {r, g, b, a}
|
|
-- nil for {0,0,0,0}
|
|
-- @tparam number g Green component 0-1
|
|
-- @tparam number b Blue component 0-1
|
|
-- @tparam number a Alpha component 0-1
|
|
-- @treturn color out
|
|
function color.new(r, g, b, a)
|
|
-- number, number, number, number
|
|
if r and g and b and a then
|
|
precond.typeof(r, "number", "new: Wrong argument type for r")
|
|
precond.typeof(g, "number", "new: Wrong argument type for g")
|
|
precond.typeof(b, "number", "new: Wrong argument type for b")
|
|
precond.typeof(a, "number", "new: Wrong argument type for a")
|
|
|
|
return new(r, g, b, a)
|
|
|
|
-- {r, g, b, a}
|
|
elseif type(r) == "table" then
|
|
local rr, gg, bb, aa = r[1], r[2], r[3], r[4]
|
|
precond.typeof(rr, "number", "new: Wrong argument type for r")
|
|
precond.typeof(gg, "number", "new: Wrong argument type for g")
|
|
precond.typeof(bb, "number", "new: Wrong argument type for b")
|
|
precond.typeof(aa, "number", "new: Wrong argument type for a")
|
|
|
|
return new(rr, gg, bb, aa)
|
|
end
|
|
|
|
return new(0, 0, 0, 0)
|
|
end
|
|
|
|
--- Convert hue,saturation,value table to color object.
|
|
-- @tparam table hsva {hue 0-359, saturation 0-1, value 0-1, alpha 0-1}
|
|
-- @treturn color out
|
|
color.hsv_to_color_table = hsv_to_color
|
|
|
|
--- Convert color to hue,saturation,value table
|
|
-- @tparam color in
|
|
-- @treturn table hsva {hue 0-359, saturation 0-1, value 0-1, alpha 0-1}
|
|
color.color_to_hsv_table = color_to_hsv
|
|
|
|
--- Convert hue,saturation,value to color object.
|
|
-- @tparam number h hue 0-359
|
|
-- @tparam number s saturation 0-1
|
|
-- @tparam number v value 0-1
|
|
-- @treturn color out
|
|
function color.from_hsv(h, s, v)
|
|
return hsv_to_color { h, s, v }
|
|
end
|
|
|
|
--- Convert hue,saturation,value to color object.
|
|
-- @tparam number h hue 0-359
|
|
-- @tparam number s saturation 0-1
|
|
-- @tparam number v value 0-1
|
|
-- @tparam number a alpha 0-1
|
|
-- @treturn color out
|
|
function color.from_hsva(h, s, v, a)
|
|
return hsv_to_color { h, s, v, a }
|
|
end
|
|
|
|
--- Invert a color.
|
|
-- @tparam color to invert
|
|
-- @treturn color out
|
|
function color.invert(c)
|
|
return new(1 - c[1], 1 - c[2], 1 - c[3], c[4])
|
|
end
|
|
|
|
--- Lighten a color by a component-wise fixed amount (alpha unchanged)
|
|
-- @tparam color to lighten
|
|
-- @tparam number amount to increase each component by, 0-1 scale
|
|
-- @treturn color out
|
|
function color.lighten(c, v)
|
|
return new(
|
|
utils.clamp(c[1] + v, 0, 1),
|
|
utils.clamp(c[2] + v, 0, 1),
|
|
utils.clamp(c[3] + v, 0, 1),
|
|
c[4]
|
|
)
|
|
end
|
|
|
|
function color.lerp(a, b, s)
|
|
return a + s * (b - a)
|
|
end
|
|
|
|
--- Unpack a color into individual components in 0-1.
|
|
-- @tparam color to unpack
|
|
-- @treturn number r in 0-1
|
|
-- @treturn number g in 0-1
|
|
-- @treturn number b in 0-1
|
|
-- @treturn number a in 0-1
|
|
function color.unpack(c)
|
|
return c[1], c[2], c[3], c[4]
|
|
end
|
|
|
|
--- Unpack a color into individual components in 0-255.
|
|
-- @tparam color to unpack
|
|
-- @treturn number r in 0-255
|
|
-- @treturn number g in 0-255
|
|
-- @treturn number b in 0-255
|
|
-- @treturn number a in 0-255
|
|
function color.as_255(c)
|
|
return c[1] * 255, c[2] * 255, c[3] * 255, c[4] * 255
|
|
end
|
|
|
|
--- Darken a color by a component-wise fixed amount (alpha unchanged)
|
|
-- @tparam color to darken
|
|
-- @tparam number amount to decrease each component by, 0-1 scale
|
|
-- @treturn color out
|
|
function color.darken(c, v)
|
|
return new(
|
|
utils.clamp(c[1] - v, 0, 1),
|
|
utils.clamp(c[2] - v, 0, 1),
|
|
utils.clamp(c[3] - v, 0, 1),
|
|
c[4]
|
|
)
|
|
end
|
|
|
|
--- Multiply a color's components by a value (alpha unchanged)
|
|
-- @tparam color to multiply
|
|
-- @tparam number to multiply each component by
|
|
-- @treturn color out
|
|
function color.multiply(c, v)
|
|
local t = color.new()
|
|
for i = 1, 3 do
|
|
t[i] = c[i] * v
|
|
end
|
|
|
|
t[4] = c[4]
|
|
return t
|
|
end
|
|
|
|
-- directly set alpha channel
|
|
-- @tparam color to alter
|
|
-- @tparam number new alpha 0-1
|
|
-- @treturn color out
|
|
function color.alpha(c, v)
|
|
local t = color.new()
|
|
for i = 1, 3 do
|
|
t[i] = c[i]
|
|
end
|
|
|
|
t[4] = v
|
|
return t
|
|
end
|
|
|
|
--- Multiply a color's alpha by a value
|
|
-- @tparam color to multiply
|
|
-- @tparam number to multiply alpha by
|
|
-- @treturn color out
|
|
function color.opacity(c, v)
|
|
local t = color.new()
|
|
for i = 1, 3 do
|
|
t[i] = c[i]
|
|
end
|
|
|
|
t[4] = c[4] * v
|
|
return t
|
|
end
|
|
|
|
--- Set a color's hue (saturation, value, alpha unchanged)
|
|
-- @tparam color to alter
|
|
-- @tparam hue to set 0-359
|
|
-- @treturn color out
|
|
function color.hue(col, hue)
|
|
local c = color_to_hsv(col)
|
|
c[1] = (hue + 360) % 360
|
|
return hsv_to_color(c)
|
|
end
|
|
|
|
--- Set a color's saturation (hue, value, alpha unchanged)
|
|
-- @tparam color to alter
|
|
-- @tparam saturation to set 0-1
|
|
-- @treturn color out
|
|
function color.saturation(col, percent)
|
|
local c = color_to_hsv(col)
|
|
c[2] = utils.clamp(percent, 0, 1)
|
|
return hsv_to_color(c)
|
|
end
|
|
|
|
--- Set a color's value (saturation, hue, alpha unchanged)
|
|
-- @tparam color to alter
|
|
-- @tparam value to set 0-1
|
|
-- @treturn color out
|
|
function color.value(col, percent)
|
|
local c = color_to_hsv(col)
|
|
c[3] = utils.clamp(percent, 0, 1)
|
|
return hsv_to_color(c)
|
|
end
|
|
|
|
-- http://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
|
|
function color.gamma_to_linear(r, g, b, a)
|
|
local function convert(c)
|
|
if c > 1.0 then
|
|
return 1.0
|
|
elseif c < 0.0 then
|
|
return 0.0
|
|
elseif c <= 0.04045 then
|
|
return c / 12.92
|
|
else
|
|
return math.pow((c + 0.055) / 1.055, 2.4)
|
|
end
|
|
end
|
|
|
|
if type(r) == "table" then
|
|
local c = {}
|
|
for i = 1, 3 do
|
|
c[i] = convert(r[i])
|
|
end
|
|
|
|
c[4] = convert(r[4])
|
|
return c
|
|
else
|
|
return convert(r), convert(g), convert(b), a or 1
|
|
end
|
|
end
|
|
|
|
-- http://en.wikipedia.org/wiki/SRGB#The_forward_transformation_.28CIE_xyY_or_CIE_XYZ_to_sRGB.29
|
|
function color.linear_to_gamma(r, g, b, a)
|
|
local function convert(c)
|
|
if c > 1.0 then
|
|
return 1.0
|
|
elseif c < 0.0 then
|
|
return 0.0
|
|
elseif c < 0.0031308 then
|
|
return c * 12.92
|
|
else
|
|
return 1.055 * math.pow(c, 0.41666) - 0.055
|
|
end
|
|
end
|
|
|
|
if type(r) == "table" then
|
|
local c = {}
|
|
for i = 1, 3 do
|
|
c[i] = convert(r[i])
|
|
end
|
|
|
|
c[4] = convert(r[4])
|
|
return c
|
|
else
|
|
return convert(r), convert(g), convert(b), a or 1
|
|
end
|
|
end
|
|
|
|
--- Check if color is valid
|
|
-- @tparam color to test
|
|
-- @treturn boolean is color
|
|
function color.is_color(a)
|
|
if type(a) ~= "table" then
|
|
return false
|
|
end
|
|
|
|
for i = 1, 4 do
|
|
if type(a[i]) ~= "number" then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--- Return a formatted string.
|
|
-- @tparam color a color to be turned into a string
|
|
-- @treturn string formatted
|
|
function color.to_string(a)
|
|
return string.format("[ %3.0f, %3.0f, %3.0f, %3.0f ]", a[1], a[2], a[3], a[4])
|
|
end
|
|
|
|
color_mt.__index = color
|
|
color_mt.__tostring = color.to_string
|
|
|
|
function color_mt.__call(_, r, g, b, a)
|
|
return color.new(r, g, b, a)
|
|
end
|
|
|
|
function color_mt.__add(a, b)
|
|
return new(a[1] + b[1], a[2] + b[2], a[3] + b[3], a[4] + b[4])
|
|
end
|
|
|
|
function color_mt.__sub(a, b)
|
|
return new(a[1] - b[1], a[2] - b[2], a[3] - b[3], a[4] - b[4])
|
|
end
|
|
|
|
function color_mt.__mul(a, b)
|
|
if type(a) == "number" then
|
|
return new(a * b[1], a * b[2], a * b[3], a * b[4])
|
|
elseif type(b) == "number" then
|
|
return new(b * a[1], b * a[2], b * a[3], b * a[4])
|
|
else
|
|
return new(a[1] * b[1], a[2] * b[2], a[3] * b[3], a[4] * b[4])
|
|
end
|
|
end
|
|
|
|
return setmetatable({}, color_mt)
|