David Briscoe 343f320e2f [Breaking] color: Convert 0-255 -> 0-1 range
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.
2022-04-01 21:47:56 -07:00

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)