Skip to content

Commit

Permalink
Update gears.color with new color functions; Add new gears.color tests
Browse files Browse the repository at this point in the history
* Rename interpolate to mix
* update mix and stringify
* add RGB to/from HSL functions
* add darken/lighten functions
* add normalize/unnormalize functions
* add tests for stringify, rgba_to_hsla, hsla_to_rgba, lighten, darken, invert, and mix
  • Loading branch information
Veratil committed Dec 31, 2018
1 parent 4d01b3d commit c3ffbcc
Show file tree
Hide file tree
Showing 2 changed files with 428 additions and 21 deletions.
221 changes: 200 additions & 21 deletions lib/gears/color.lua
Original file line number Diff line number Diff line change
Expand Up @@ -108,47 +108,226 @@ function color.parse_color(col)
return unpack(rgb)
end

--- Interpolate between two colors
-- Note: This does interpolate the alpha channel if there is one in a or b.
-- @tparam number val Value between 0 and 1
-- @param a Starting color point
-- @param b Ending color point
--- Mix two colors
-- Note: This does mix the alpha channel if there is one in a or b.
-- @param col1 An RGB/RGBA color string able to be parsed by `gears.color.parse_color`
-- @param col2 An RGB/RGBA color string able to be parsed by `gears.color.parse_color`
-- @tparam[opt=0.5] number val Value between 0 and 1. 0.5 means use half first, and half second. 0.25 means use 1/4 first, 3/4 second.
-- @treturn table 4 values representing color in RGBA format (each of them in
-- [0, 1] range) or nil if input is incorrect.
function color.interpolate(val, a, b)
local ar,ag,ab,aa = color.parse_color(a)
local br,bg,bb,ba = color.parse_color(b)
if ar == nil or br == nil then return nil end
function color.mix(col1, col2, val)
local ar,ag,ab,aa = color.parse_color(col1)
if ar == nil then return nil end
local br,bg,bb,ba = color.parse_color(col2)
if br == nil then return nil end
local p = 0.5
if val ~= nil and 0 <= val and val <= 1 then p = (1 - val) end
local function interp(v,s,e)
return v * (e - s) + s
end
return unpack({
interp(val,ar,br),
interp(val,ag,bg),
interp(val,ab,bb),
interp(val,aa,ba)
interp(p,ar,br),
interp(p,ag,bg),
interp(p,ab,bb),
interp(p,aa,ba)
})
end

--- Convert an RGBA color to HSLA
-- @param col An RGBA color string able to be parsed by `gears.color.parse_color`
-- @treturn table 4 values representing color in HSLA format or nil if input is incorrect.
function color.rgba_to_hsla(col)
-- parse_color returns in normalized form
local rp,gp,bp,a = color.parse_color(col)
if rp == nil then return nil end
-- ignoring luacheck next line because it says s and l are unused
local h,s,l = 0,0,0 -- luacheck: ignore
local M = math.max(rp, gp, bp)
local m = math.min(rp, gp, bp)
local C = M - m
-- get Hue
if C == 0 then
h = 0 -- undefined technically
elseif M == rp then
h = ((gp - bp) / C) % 6
elseif M == gp then
h = ((bp - rp) / C) + 2
elseif M == bp then
h = ((rp - gp) / C) + 4
end
h = 60 * h
-- Lightness/Luminosity
l = (M + m) / 2
-- Saturation
if l == 1 or l == 0 then
s = 0
else
s = C / (1 - math.abs((2 * l) - 1))
end
return unpack({h, s, l, a})
end

--- Convert an RGB color to HSL
-- Note: This calls color.rgba_to_hsla which returns a 1 for Opacity
-- @param col An RGB color string able to be parsed by `gears.color.parse_color`
-- @treturn table 4 values representing color in HSLA format or nil if input is incorrect
function color.rgb_to_hsl(col)
return color.rgba_to_hsla({col[1], col[2], col[3], 1})
end

--- Convert an HSLA color to RGBA
-- Note: This does round the final RGB numbers to remove the possible decimal point.
-- @tparam table col A table of 3 or 4 values describing an HSL or HSLA color
-- @treturn table 4 values representing color in RGBA format or nil if input is incorrect
function color.hsla_to_rgba(col)
local h,s,l,a = col[1], col[2], col[3], col[4]
if h == nil or s == nil or l == nil or a == nil then return nil end
a = round(a * 255)
-- If s = 0 then it's a grey
if s == 0 then
local v = round(l * 255)
return unpack({v, v, v, a})
end
if h > 360 then h = 0 end
local C = (1 - math.abs(2 * l - 1)) * s
local X = C * (1 - math.abs((h / 60) % 2 - 1))
local m = l - (C / 2)
local r,g,b = 0,0,0
if 0 <= h and h < 60 then
r,g,b = C,X,0
elseif 60 <= h and h < 120 then
r,g,b = X,C,0
elseif 120 <= h and h < 180 then
r,g,b = 0,C,X
elseif 180 <= h and h < 240 then
r,g,b = 0,X,C
elseif 240 <= h and h < 300 then
r,g,b = X,0,C
elseif 300 <= h and h < 360 then
r,g,b = C,0,X
end
return unpack({
round((r+m) * 255),
round((g+m) * 255),
round((b+m) * 255),
a
})
end

--- Convert an HSL color to RGB
-- Note: This calls color.hsla_to_rgba with a 255 Opacity value
-- @treturn table 4 values representing color in RGBA format or nil if input is incorrect
function color.hsl_to_rgb(col)
return color.hsla_to_rgba({col[1], col[2], col[3], 255})
end

-- Keep a value between a low and high bound
local function adjust(val, lbound, hbound)
if val < lbound then return lbound end
if val > hbound then return hbound end
return val
end

--- Darken a color
-- @param col An RGB/RGBA color string able to be parsed by `gears.color.parse_color`
-- @tparam number val Percentage to darken [0,1]
-- @treturn table 4 values representing color in RGBA format or nil if input is incorrect.
function color.darken(col, val)
if val == nil or val < 0 or val > 1 then return nil end
local h, s, l, a = color.rgba_to_hsla(col)
if l == nil then return nil end
l = adjust(l - val, 0, 1)
return color.hsla_to_rgba({h, s, l, a})
end

--- Lighten a color
-- @param col An RGB/RGBA color string able to be parsed by `gears.color.parse_color`
-- @tparam number val Percentage to light [0,1]
-- @treturn table 4 values representing color in RGBA format or nil if input is incorrect.
function color.lighten(col, val)
if val == nil or val < 0 or val > 1 then return nil end
local h, s, l, a = color.rgba_to_hsla(col)
if l == nil then return nil end
l = adjust(l + val, 0, 1)
return color.hsla_to_rgba({h, s, l, a})
end

--- Invert a color
-- Does not affect alpha channel.
-- @param col An RGB/RGBA color string able to be parsed by `gears.color.parse_color`
-- @treturn table 4 values representing color in RGBA format (each of them in
-- [0, 1] range) or nil if input is incorrect.
function color.invert(col)
local ar,ag,ab,aa = color.parse_color(col)
if ar == nil then return nil end
return unpack({
1 - ar,
1 - ag,
1 - ab,
aa
})
end

--- Normalizes RGBA colors from 0-depth to 0-1
-- @tparam table col A table of 3 or 4 values
-- @tparam[opt=8] number depth 8, 12, or 16 bit depth to return (2, 3, or 4
-- chars per channel, respectively)
-- @treturn table 4 values representing color in RGBA format (each of them in
-- [0, 1] range) or nil if input is incorrect.
function color.normalize(col, depth)
depth = depth or 8
local tdepth = { [8] = 0xff, [12] = 0xfff, [16] = 0xfff }
local range = tdepth[depth]
if range == nil then return nil end
return unpack({
col[1] / range,
col[2] / range,
col[3] / range,
col[4] and col[4] / range or 1
})
end

--- Unnormalizes RGBA colors from 0-1 to 0-range
-- @tparam table col A table of 3 or 4 values
-- @tparam[opt=8] number depth 8, 12, or 16 bit depth to return (2, 3, or 4
-- chars per channel, respectively)
function color.unnormalize(col, depth)
depth = depth or 8
local tdepth = { [8] = 0xff, [12] = 0xfff, [16] = 0xfff }
local range = tdepth[depth]
if range == nil then return nil end
return unpack({
round(col[1] * range),
round(col[2] * range),
round(col[3] * range),
col[4] and round(col[4] * range) or range
})
end

--- Stringify a color table
--
-- @tparam table col A table of 3 or 4 values in range [0, 1]
-- @tparam table col A table of 3 or 4 values
-- @tparam[opt=true] bool normalized The values in col are normalized (between 0 and 1) or not
-- @tparam[opt=8] number depth 8, 12, or 16 bit depth to return (2, 3, or 4
-- chars per channel, respectively)
-- @treturn string String representing the color, or nil if table is invalid
function color.stringify(col, depth)
function color.stringify(col, normalized, depth)
if #col < 3 or #col > 4 then return nil end
depth = depth or 8
if normalized == nil then normalized = true end
local tdepth = {
[8] = { 0xff, "#%02x%02x%02x%02x" },
[12] = { 0xfff, "#%03x%03x%03x%03x" },
[16] = { 0xffff, "#%04x%04x%04x%04x" },
}
depth = depth or 8
if tdepth[depth] == nil then return nil end
local range = tdepth[depth][1]
local r = round(col[1] * range)
local g = round(col[2] * range)
local b = round(col[3] * range)
local a = col[4] and round(col[4] * range) or 1
local r,g,b,a = col[1], col[2], col[3], col[4] or range
if normalized then
r = round(col[1] * range)
g = round(col[2] * range)
b = round(col[3] * range)
a = col[4] and round(col[4] * range) or range
end
return string.format(tdepth[depth][2],r,g,b,a)
end

Expand Down
Loading

0 comments on commit c3ffbcc

Please sign in to comment.