David Briscoe 7d99a08134 [Breaking] color: Convert hue [0,359] -> [0,1]
imgui [uses 0-1 for everything internally](
https://github.com/ocornut/imgui/blob/master/imgui.cpp#L2045).

[Both Unity](https://docs.unity3d.com/ScriptReference/Color.RGBToHSV.html)
[and Godot](https://docs.godotengine.org/en/stable/classes/class_color.html#class-color-property-h)
use H, S, and V in the range 0.0 to 1.0

Since we're switching colours out of 0,255 we should also switch hue out
of 0,359. Now all of our r,g,b,a,h,s,v values are all in 0,1.
2022-04-01 21:55:40 -07:00

401 lines
9.1 KiB
Lua

--- Color utilities
-- @module color
local modules = (...):gsub('%.[^%.]+$', '') .. "."
local constants = require(modules .. "constants")
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] * 6 -- 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
-- Prevent division by zero.
if delta == 0 then
delta = constants.DBL_EPSILON
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 / 6 -- normalize from segment 0..5
if h < 0 then
h = h + 1
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-1, 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-1, 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-1
-- @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-1
-- @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
--- Interpolate between two colors.
-- @tparam color at start
-- @tparam color at end
-- @tparam number s in 0-1 progress between the two colors
-- @treturn color out
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-1
-- @treturn color out
function color.hue(col, hue)
local c = color_to_hsv(col)
c[1] = (hue + 1) % 1
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
-- https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ
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] = r[4]
return c
else
return convert(r), convert(g), convert(b), a or 1
end
end
-- https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB
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] = 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)