From 8627ab441f6313a40fc3f4162d46bbca8d1e4bbc Mon Sep 17 00:00:00 2001 From: Kevin Zander Date: Fri, 4 Jan 2019 17:13:06 -0600 Subject: [PATCH 1/2] Add beautiful.colorful module and tests; Update themes to use colorful functions --- lib/beautiful/colorful.lua | 1395 ++++++++++++++++++++++++++++++ lib/beautiful/gtk.lua | 20 +- lib/beautiful/init.lua | 2 + spec/beautiful/colorful_spec.lua | 494 +++++++++++ themes/gtk/theme.lua | 55 +- 5 files changed, 1908 insertions(+), 58 deletions(-) create mode 100644 lib/beautiful/colorful.lua create mode 100644 spec/beautiful/colorful_spec.lua diff --git a/lib/beautiful/colorful.lua b/lib/beautiful/colorful.lua new file mode 100644 index 0000000000..c4374599f2 --- /dev/null +++ b/lib/beautiful/colorful.lua @@ -0,0 +1,1395 @@ +--------------------------------------------------------------------------- +-- This module simplifies the creation and modification of colors. +-- +-- A color can be created from a normal hexadecimal value such as 0x33CCFF, a +-- table of 3 or 4 values which describe a color in either RGB[A] or HSL[A], or +-- a string which will take any form a CSS color can be described with. +-- +-- The easiest way to create a color is with a string: +-- colorful.color("#FFCC99") +-- colorful.color("rgb(255, 204, 153, 1)") +-- colorful.color("rgb(100%, 80%, 60%)") +-- colorful.color("hsl(30, 100%, 80%, 100%)") +-- +-- Compatible CSS color strings. Spaces between values and commas are optional. +-- For the whitespace functional notations (no commas), at least one space is +-- required between values. +-- -- Hex codes +-- #rgb +-- #rgba +-- #rrggbb +-- #rrggbbaa +-- -- RGB[A] +-- rgb([0,255], [0,255], [0,255]) +-- rgb([0,255], [0,255], [0,255], [0,1]) +-- rgb([0,255], [0,255], [0,255], [0,100]%) +-- rgb([0,255] [0,255] [0,255]) +-- rgb([0,255] [0,255] [0,255] / [0,1]) +-- rgb([0,255] [0,255] [0,255] / [0,100]%) +-- rgb([0,100]%, [0,100]%, [0,100]%) +-- rgb([0,100]%, [0,100]%, [0,100]%, [0,1]) +-- rgb([0,100]%, [0,100]%, [0,100]%, [0,100]%) +-- rgb([0,100]% [0,100]% [0,100]%) +-- rgb([0,100]% [0,100]% [0,100]% / [0,1]) +-- rgb([0,100]% [0,100]% [0,100]% / [0,100]%) +-- rgba([0,255], [0,255], [0,255], [0,1]) +-- rgba([0,255], [0,255], [0,255], [0,100]%) +-- rgba([0,255] [0,255] [0,255] / [0,1]) +-- rgba([0,255] [0,255] [0,255] / [0,100]%) +-- rgba([0,100]%, [0,100]%, [0,100]%, [0,1]) +-- rgba([0,100]%, [0,100]%, [0,100]%, [0,100]%) +-- rgba([0,100]% [0,100]% [0,100]% / [0,1]) +-- rgba([0,100]% [0,100]% [0,100]% / [0,100]%) +-- -- HSL[A] +-- hsl([0,360], [0,100]%, [0,100]%) +-- hsl([0,360], [0,100]%, [0,100]%, [0,1]) +-- hsl([0,360], [0,100]%, [0,100]%, [0,100]%) +-- hsl([0,360] [0,100]% [0,100]%) +-- hsl([0,360] [0,100]% [0,100]% / [0,1]) +-- hsl([0,360] [0,100]% [0,100]% / [0,100]%) +-- hsla([0,360], [0,100]%, [0,100]%, [0,1]) +-- hsla([0,360], [0,100]%, [0,100]%, [0,100]%) +-- hsla([0,360] [0,100]% [0,100]% / [0,1]) +-- hsla([0,360] [0,100]% [0,100]% / [0,100]%) +-- +-- Additionally you can create a color from a hexadecimal value: +-- colorful.color(0xFFCC99) +-- colorful.color(0x336699AA) +-- +-- Or you can create it from a table: +-- colorful.color({30, 1, 0.8}) +-- colorful.color({0xff, 0xcc, 0x99}) +-- colorful.color({red=255, green=204, blue=153}) +-- colorful.color({hue=30, saturation=1, lightness=0.8}) +-- +-- One note about creating from a table, the new function will try to determine +-- what values you're passing in by checking the range of the values, or if the +-- table has named fields. It first checks for red, green, blue fields (optional +-- alpha field); then for hue, saturation, lightness; then if it has 3 or 4 +-- fields. If it gets to a 3 or 4 field table, it will check the first for HSL +-- values, then for RGB. +-- +-- After a color is created you can access it's individual elements as needed. +-- c = colorful.color.new("#ffcc99") +-- c.red -- 1.0 +-- c.green -- 0.8 +-- c.blue -- 0.6 +-- c.hue -- 30 +-- c.saturation -- 80% +-- c.lightness -- 60% +-- c.alpha -- 1.0 Note this defaults to no transparency +-- +-- Once a color is created you can do transformations on it. Note that they do +-- not modify the original color object, but instead return a new one. +-- c2 = colorful.color("#99ccff") +-- c3 = c:mix(c2, 0.4) -- mix c with c2, giving c 40% and c2 60% +-- c:darken(0.25) -- darken 25% +-- c:lighten(0.25) -- lighten 25% +-- c:saturate(0.1) -- saturate 10% +-- c:desaturate(0.1) -- desaturate 10% +-- c:grayscale() -- desaturate color completely to grayscale +-- c:invert() -- inverts color +-- c:complement() -- the compliment of the color (180deg hue shift) +-- +-- If you want to create a color scheme you can use the color harmony functions: +-- local left, right = c:analogous() +-- local left, right = c:split_complementary() +-- local left, right = c:triadic() +-- local complement, analogous, analogous_complement = c:tetradic() +-- +-- @author Kevin Zander +-- @copyright 2019 Kevin Zander +-- @module beautiful.colorful +--------------------------------------------------------------------------- + +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) + +local colorful = {} + +local color = { + -- set the from and as tables + from = {}, + as = {} +} + +-- Metatable for color instances. This is set up near the end of the file. +local color_mt = {} + +-- {{{ Helper functions + +-- Lua 5.1 does not support bit shifting operators +local function lshift(x, disp) + return (x * 2^disp) % 2^32 +end + +local function rshift(x, disp) + return math.floor(x % 2^32 / 2^disp) +end + +-- Do we want to pull in gears.math.round? +local function round(x) + local d,_ = math.modf(x + (x >= 0 and 1 or -1) * 0.5) + return d +end + +-- Clamp a number between a low and high +local function clamp(v, l, h) + if v < l then return l end + if v > h then return h end + return v +end + +-- Return true/false if a number is between a low and high +local function between(v, l, h) + if l <= v and v <= h then return true end + return false +end + +-- Expects Hue [0,360], Saturation [0,1], Lightness [0,1] +-- Does NOT check bounds +local function hsl_to_rgb(hue, saturation, lightness) + -- If s = 0 then it's a grey + if saturation == 0 then + return unpack({lightness, lightness, lightness}) + end + local C = (1 - math.abs(2 * lightness - 1)) * saturation + local X = C * (1 - math.abs((hue / 60) % 2 - 1)) + local m = lightness - (C / 2) + local r,g,b = 0,0,0 + if 0 <= hue and hue <= 60 then + r,g,b = C,X,0 + elseif 60 <= hue and hue <= 120 then + r,g,b = X,C,0 + elseif 120 <= hue and hue <= 180 then + r,g,b = 0,C,X + elseif 180 <= hue and hue <= 240 then + r,g,b = 0,X,C + elseif 240 <= hue and hue <= 300 then + r,g,b = X,0,C + elseif 300 <= hue and hue <= 360 then + r,g,b = C,0,X + end + return unpack({r+m, g+m, b+m}) +end + +-- Expects Red, Blue, and Green [0,1] +-- Does NOT check bounds +local function rgb_to_hsl(red, green, blue) + local h,s,l = 0,0,0 -- luacheck: ignore + local M = math.max(red, green, blue) + local m = math.min(red, green, blue) + local C = M - m + -- get Hue + if C == 0 then + h = 0 -- undefined technically + elseif M == red then + h = ((green - blue) / C) % 6 + elseif M == green then + h = ((blue - red) / C) + 2 + elseif M == blue then + h = ((red - green) / 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}) +end + +-- Create a color object +local function create_color_object(r,g,b,h,s,l,a) + local o = setmetatable({ + red = r, green = g, blue = b, + hue = h, saturation = s, lightness = l, + alpha = a + }, color_mt) + -- allow as functions to use color object variables + setmetatable(o.as, { + __index = o + }) + return o +end + +-- }}} Helper functions + +-- {{{ New Color functions + +--- Color object creation functions +-- @section color_creation + +-- {{{ color.new() + +--- Create a new color instance +-- @param val A CSS color compatible string, decimal color value, or a table +-- containing 3 or 4 values. +-- @return A new color object, or nil if invalid +-- @usage local c = colorful.color.new({30, 0.8, 0.6}) +-- -- For more examples see the top of this page +function color.new(val) + if type(val) == "string" then return color.from.string(val) end + if type(val) == "number" then + if val > 0xFFFFFF then return color.from.hex4(val) end + return color.from.hex3(val) + end + if type(val) == "table" then + -- do we have red/green/blue/alpha fields? + if val.red ~= nil and val.green ~= nil and val.blue ~= nil then + local alpha = val.alpha or 255 + return color.from.rgba(val.red, val.green, val.blue, alpha) + -- do we have hue/saturation/lightness/alpha fields? + elseif val.hue ~= nil and val.saturation ~= nil and val.lightness ~= nil then + local alpha = val.alpha or 1 + return color.from.hsla(val.hue, val.saturation, val.lightness, alpha) + -- do we have a table of 3 or 4 values? + elseif #val == 3 or #val == 4 then + -- if val[1] [0,360] and val[2] and val[3] [0,1] we deduce HSL + if (between(val[1], 0, 360) and + between(val[2], 0, 1) and + between(val[3], 0, 1) ) + then + local alpha = val[4] and clamp(val[4], 0, 1) or 1 + return color.from.hsla( + val[1], + val[2], + val[3], + alpha + ) + end + if (between(val[1], 0, 255) and + between(val[2], 0, 255) and + between(val[3], 0, 255) ) + then + local alpha = val[4] and clamp(val[4], 0, 255) or 255 + return color.from.rgba( + val[1], + val[2], + val[3], + alpha + ) + end + -- if values aren't in range, escape + end + -- don't know the table type, escape + end + -- check for C userdata (GdkRGBA) with values + if type(val) == "userdata" then + if val.red ~= nil and val.green ~= nil and val.blue ~= nil then + local alpha = val.alpha or 1 + return color.from.rgba( + val.red * 255, + val.green * 255, + val.blue * 255, + alpha * 255 + ) + end + end + -- not a valid type, quit + return nil +end + +-- }}} color.new() + +-- {{{ String +-- {{{ Helper tables and functions + +local css = { + integer = "%d?%d?%d", -- 0-999 (but 0-255 for values) + --percent = "%d?%d?%d%%", -- 0-999% (but 0-100% for values) + float = "%d?%.?%d+", -- this is _very_ crude, but probably fast + comma = "%s*,%s*", -- whitespace optional comma + space = "%s+", -- whitespace required + slash = "%s*/%s*", -- whitespace optional slash + rgb = "^rgb%(%s*", -- rgb functional header + rgba = "^rgba%(%s*", -- rgba functional header + hsl = "^hsl%(%s*", -- hsl functional header + hsla = "^hsla%(%s*", -- hsla functional header + eol = "%s*%)$", -- functional EOL +} +-- capture a string +local function capt(s) + return "(" .. s .. ")" +end +-- match string followed by % +local function perc(s) + return s .. "%%" +end + +-- }}} Helper tables and functions +-- {{{ Color patterns + +-- identifier string = 3 char type, value type, alpha type (if present) +local color_patterns = { + -- {{{ Hexadecimal patterns + -- shorthand #rgb + { "hexs", "^#(%x)(%x)(%x)$" }, + -- shorthand #rgba + { "hexsa", "^#(%x)(%x)(%x)(%x)$" }, + -- longform #rrggbb + { "hexd", "%#(%x%x)(%x%x)(%x%x)$" }, + -- longform #rrggbbaa + { "hexda", "%#(%x%x)(%x%x)(%x%x)(%x%x)$" }, + -- }}} + -- {{{ Functional patterns + -- {{{ rgb(d, d, d) integers 0-255 + { "rgbi", table.concat({ + css.rgb, -- functional header + capt(css.integer), -- red + css.comma, -- separator + capt(css.integer), -- green + css.comma, -- separator + capt(css.integer), -- blue + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(d, d, d, f) integers 0-255, float 0-1 + { "rgbif", table.concat({ + css.rgb, -- functional header + capt(css.integer), -- red + css.comma, -- separator + capt(css.integer), -- green + css.comma, -- separator + capt(css.integer), -- blue + css.comma, -- separator + capt(css.float), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(d, d, d, %) integers 0-255, percentage 0-100 + { "rgbip", table.concat({ + css.rgb, -- functional header + capt(css.integer), -- red + css.comma, -- separator + capt(css.integer), -- green + css.comma, -- separator + capt(css.integer), -- blue + css.comma, + perc(capt(css.integer)), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(d d d) integers 0-255 + { "rgbi", table.concat({ + css.rgb, -- functional header + capt(css.integer), -- red + css.space, -- separator + capt(css.integer), -- green + css.space, -- separator + capt(css.integer), -- blue + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(d d d / f) integers 0-255, float 0-1 + { "rgbif", table.concat({ + css.rgb, -- functional header + capt(css.integer), -- red + css.space, -- separator + capt(css.integer), -- green + css.space, -- separator + capt(css.integer), -- blue + css.slash, + capt(css.float), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(d d d / %) integers 0-255, percentage 0-100 + { "rgbip", table.concat({ + css.rgb, -- functional header + capt(css.integer), -- red + css.space, -- separator + capt(css.integer), -- green + css.space, -- separator + capt(css.integer), -- blue + css.slash, + perc(capt(css.integer)), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(%, %, %) percentage 0-100 + { "rgbp", table.concat({ + css.rgb, -- functional header + perc(capt(css.integer)), -- red + css.comma, -- separator + perc(capt(css.integer)), -- green + css.comma, -- separator + perc(capt(css.integer)), -- blue + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(%, %, %, f) percentage 0-100, float 0-1 + { "rgbpf", table.concat({ + css.rgb, -- functional header + perc(capt(css.integer)), -- red + css.comma, -- separator + perc(capt(css.integer)), -- green + css.comma, -- separator + perc(capt(css.integer)), -- blue + css.comma, + capt(css.float), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(%, %, %, %) percentage 0-100 + { "rgbpp", table.concat({ + css.rgb, -- functional header + perc(capt(css.integer)), -- red + css.comma, -- separator + perc(capt(css.integer)), -- green + css.comma, -- separator + perc(capt(css.integer)), -- blue + css.comma, -- separator + perc(capt(css.integer)), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(% % %) percentage 0-100 + { "rgbp", table.concat({ + css.rgb, -- functional header + perc(capt(css.integer)), -- red + css.space, -- separator + perc(capt(css.integer)), -- green + css.space, -- separator + perc(capt(css.integer)), -- blue + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(% % % / f) percentage 0-100, float 0-1 + { "rgbpf", table.concat({ + css.rgb, -- functional header + perc(capt(css.integer)), -- red + css.space, -- separator + perc(capt(css.integer)), -- green + css.space, -- separator + perc(capt(css.integer)), -- blue + css.slash, + capt(css.float), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgb(% % % / %) percentage 0-100 + { "rgbpp", table.concat({ + css.rgb, -- functional header + perc(capt(css.integer)), -- red + css.space, -- separator + perc(capt(css.integer)), -- green + css.space, -- separator + perc(capt(css.integer)), -- blue + css.slash, + perc(capt(css.integer)), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgba(d, d, d, f) integers 0-255, float 0-1 + { "rgbif", table.concat({ + css.rgba, -- functional header + capt(css.integer), -- red + css.comma, -- separator + capt(css.integer), -- green + css.comma, -- separator + capt(css.integer), -- blue + css.comma, -- separator + capt(css.float), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgba(d, d, d, %) integers 0-255, percentage 0-100 + { "rgbip", table.concat({ + css.rgba, -- functional header + capt(css.integer), -- red + css.comma, -- separator + capt(css.integer), -- green + css.comma, -- separator + capt(css.integer), -- blue + css.comma, + perc(capt(css.integer)), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgba(d d d / f) integers 0-255, float 0-1 + { "rgbif", table.concat({ + css.rgba, -- functional header + capt(css.integer), -- red + css.space, -- separator + capt(css.integer), -- green + css.space, -- separator + capt(css.integer), -- blue + css.slash, + capt(css.float), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgba(d d d / %) integers 0-255, percentage 0-100 + { "rgbip", table.concat({ + css.rgba, -- functional header + capt(css.integer), -- red + css.space, -- separator + capt(css.integer), -- green + css.space, -- separator + capt(css.integer), -- blue + css.slash, + perc(capt(css.integer)), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgba(%, %, %, f) percentage 0-100, float 0-1 + { "rgbpf", table.concat({ + css.rgba, -- functional header + perc(capt(css.integer)), -- red + css.comma, -- separator + perc(capt(css.integer)), -- green + css.comma, -- separator + perc(capt(css.integer)), -- blue + css.comma, + capt(css.float), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgba(%, %, %, %) percentage 0-100 + { "rgbpp", table.concat({ + css.rgba, -- functional header + perc(capt(css.integer)), -- red + css.comma, -- separator + perc(capt(css.integer)), -- green + css.comma, -- separator + perc(capt(css.integer)), -- blue + css.comma, -- separator + perc(capt(css.integer)), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgba(% % % / f) percentage 0-100, float 0-1 + { "rgbpf", table.concat({ + css.rgba, -- functional header + perc(capt(css.integer)), -- red + css.space, -- separator + perc(capt(css.integer)), -- green + css.space, -- separator + perc(capt(css.integer)), -- blue + css.slash, + capt(css.float), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ rgba(% % % / %) percentage 0-100 + { "rgbpp", table.concat({ + css.rgba, -- functional header + perc(capt(css.integer)), -- red + css.space, -- separator + perc(capt(css.integer)), -- green + css.space, -- separator + perc(capt(css.integer)), -- blue + css.slash, + perc(capt(css.integer)), -- alpha + css.eol -- EOL + }, "") + }, + -- }}} + -- {{{ hsl(d, %, %) integers 0-360, percentage 0-100 + { "hsl", table.concat({ + css.hsl, + capt(css.integer), -- hue + css.comma, + perc(capt(css.integer)), -- saturation + css.comma, + perc(capt(css.integer)), -- lightness + css.eol + }, "") + }, + -- }}} + -- {{{ hsl(d, %, %, f) integers 0-360, percentage 0-100, float 0-1 + { "hslxf", table.concat({ + css.hsl, + capt(css.integer), -- hue + css.comma, + perc(capt(css.integer)), -- saturation + css.comma, + perc(capt(css.integer)), -- lightness + css.comma, + capt(css.float), -- alpha + css.eol + }, "") + }, + -- }}} + -- {{{ hsl(d, %, %, %) integers 0-360, percentage 0-100 + { "hslxp", table.concat({ + css.hsl, + capt(css.integer), -- hue + css.comma, + perc(capt(css.integer)), -- saturation + css.comma, + perc(capt(css.integer)), -- lightness + css.comma, + perc(capt(css.integer)), -- alpha + css.eol + }, "") + }, + -- }}} + -- {{{ hsl(d % %) integers 0-360, percentage 0-100 + { "hsl", table.concat({ + css.hsl, + capt(css.integer), -- hue + css.space, + perc(capt(css.integer)), -- saturation + css.space, + perc(capt(css.integer)), -- lightness + css.eol + }, "") + }, + -- }}} + -- {{{ hsl(d % % / f) integers 0-360, percentage 0-100, float 0-1 + { "hslxf", table.concat({ + css.hsl, + capt(css.integer), -- hue + css.space, + perc(capt(css.integer)), -- saturation + css.space, + perc(capt(css.integer)), -- lightness + css.slash, + capt(css.float), -- alpha + css.eol + }, "") + }, + -- }}} + -- {{{ hsl(d % % / %) integers 0-360, percentage 0-100 + { "hslxp", table.concat({ + css.hsl, + capt(css.integer), -- hue + css.space, + perc(capt(css.integer)), -- saturation + css.space, + perc(capt(css.integer)), -- lightness + css.slash, + perc(capt(css.integer)), -- alpha + css.eol + }, "") + }, + -- }}} + -- {{{ hsla(d, %, %, f) integers 0-360, percentage 0-100, float 0-1 + { "hslxf", table.concat({ + css.hsla, + capt(css.integer), -- hue + css.comma, + perc(capt(css.integer)), -- saturation + css.comma, + perc(capt(css.integer)), -- lightness + css.comma, + capt(css.float), -- alpha + css.eol + }, "") + }, + -- }}} + -- {{{ hsla(d, %, %, %) integers 0-360, percentage 0-100 + { "hslxp", table.concat({ + css.hsla, + capt(css.integer), -- hue + css.comma, + perc(capt(css.integer)), -- saturation + css.comma, + perc(capt(css.integer)), -- lightness + css.comma, + perc(capt(css.integer)), -- alpha + css.eol + }, "") + }, + -- }}} + -- {{{ hsla(d % % / f) integers 0-360, percentage 0-100, float 0-1 + { "hslxf", table.concat({ + css.hsla, + capt(css.integer), -- hue + css.space, + perc(capt(css.integer)), -- saturation + css.space, + perc(capt(css.integer)), -- lightness + css.slash, + capt(css.float), -- alpha + css.eol + }, "") + }, + -- }}} + -- {{{ hsla(d % % / %) integers 0-360, percentage 0-100 + { "hslxp", table.concat({ + css.hsla, + capt(css.integer), -- hue + css.space, + perc(capt(css.integer)), -- saturation + css.space, + perc(capt(css.integer)), -- lightness + css.slash, + perc(capt(css.integer)), -- alpha + css.eol + }, "") + }, + -- }}} + -- }}} +} +-- }}} Color patterns + +--- Create a new color instance from a string +-- @tparam string str A CSS color compatible string. +-- @return A new color object, or nil if invalid +-- @usage local c = colorful.color.from.string("rgb(255, 204, 153)") +function color.from.string(str) + for _,pat in pairs(color_patterns) do + local t = pat[1] + local m = {str:match(pat[2])} + if m[1] ~= nil then + local th = t:sub(1,3) + local tv = t:sub(4,4) + local ta = #t == 5 and t:sub(5,5) or nil + if th == "hex" then + -- r,g,b convert to number + local r = tonumber(m[1], 16) + local g = tonumber(m[2], 16) + local b = tonumber(m[3], 16) + local a = 0xFF + -- if alpha present + if ta ~= nil then + a = tonumber(m[4], 16) + end + -- if type is shorthand + if tv == "s" then + r = r * 16 + r + g = g * 16 + g + b = b * 16 + b + a = a * 16 + a + end + return color.from.rgba(r,g,b,a) + elseif th == "rgb" then + local r = tonumber(m[1]) + local g = tonumber(m[2]) + local b = tonumber(m[3]) + local a = 255 + -- if values percentages + if tv == "p" then + r,g,b = r/100 * 255, g/100 * 255, b/100 * 255 + end + -- check for alpha + if ta ~= nil then + a = tonumber(m[4]) + if ta == "p" then + a = a/100 + end + a = a * 255 -- both float and percentage need this + end + if r == nil or g == nil or b == nil or a == nil then return nil end + return color.from.rgba(r,g,b,a) + elseif th == "hsl" then + local h = tonumber(m[1]) + local s = tonumber(m[2]) / 100 -- always percentage + local l = tonumber(m[3]) / 100 -- always percentage + local a = 1 + if ta ~= nil then + a = tonumber(m[4]) + if ta == "p" then + a = a / 100 + end + end + if h == nil or s == nil or l == nil or a == nil then return nil end + return color.from.hsla(h,s,l,a) + end + end + end + return nil +end + +-- }}} String + +-- {{{ Decimal + +--- Create a new color instance from an RGB decimal value. +-- Will bitshift decimal 2 bytes left and call `color.from.hex4` +-- @tparam number hex A decimal describing an RGB color +-- @return A new color object, or nil if invalid +-- @usage local c = colorful.color.from.hex3(0xffcc99) +function color.from.hex3(hex) + return color.from.hex4(lshift(hex, 8) + 0xff) +end + +--- Create a new color instance from an RGBA decimal value. +-- @tparam number hex A decimal describing an RGBA color +-- @return A new color object, or nil if invalid +-- @usage local c = colorful.color.from.hex4(0xffcc99ff) +function color.from.hex4(hex) + -- shift right 24 bits, 0s shifted in + local r = rshift(hex, 24) + -- clear upper bits as needed, and shift remaining bits down + local g = rshift(lshift(hex, 8), 24) + local b = rshift(lshift(hex, 16), 24) + local a = rshift(lshift(hex, 24), 24) + return color.from.rgba(r,g,b,a) +end + +-- }}} Decimal + +-- {{{ RGB + +--- Create a new color instance from RGBA values. +-- @tparam number r Red decimal value 0-255 +-- @tparam number g Green decimal value 0-255 +-- @tparam number b Blue decimal value 0-255 +-- @tparam number a Alpha decimal value 0-255 +-- @return A new color object, or nil if invalid +-- @usage local c = colorful.color.from.rgba(255, 204, 153, 255) +function color.from.rgba(r,g,b,a) + -- put r,g,b,a into [0,1] + r = clamp(r, 0, 255) / 255 + g = clamp(g, 0, 255) / 255 + b = clamp(b, 0, 255) / 255 + a = clamp(a, 0, 255) / 255 + -- make sure values are valid + if r == nil or g == nil or b == nil or a == nil then return nil end + -- hsl function expects RGB in range [0,1] + local h,s,l = rgb_to_hsl(r,g,b) + return create_color_object(r,g,b,h,s,l,a) +end + +--- Create a new color instance from RGB values. +-- Will call `color.from.rgba` with an Alpha value of 255 +-- @tparam number r Red decimal value 0-255 +-- @tparam number g Green decimal value 0-255 +-- @tparam number b Blue decimal value 0-255 +-- @return A new color object, or nil if invalid +-- @usage local c = colorful.color.from.rgb(255, 204, 153) +function color.from.rgb(r,g,b) + return color.from.rgba(r,g,b,255) +end + +-- }}} RGB + +-- {{{ HSL + +--- Create a new color instance from HSLA values. +-- @tparam number h Hue decimal value 0-360 +-- @tparam number s Saturation float value 0-1 +-- @tparam number l Lightness float value 0-1 +-- @tparam number a Alpha float value 0-1 +-- @return A new color object, or nil if invalid +-- @usage local c = colorful.color.from.hsla(30, 0.8, 0.6, 1) +function color.from.hsla(h,s,l,a) + -- hue is adjustable as it's degrees + if not between(h, 0, 360) then + while h < 0 do + h = h + 360 + end + while h > 360 do + h = h - 360 + end + end + s = clamp(s, 0.0, 1.0) + l = clamp(l, 0.0, 1.0) + if s == nil or l == nil then return nil end + local r,g,b = hsl_to_rgb(h,s,l) + return create_color_object(r,g,b,h,s,l,a) +end + +--- Create a new color instance from HSL values. +-- Will call `color.from.hsla` with an Alpha value of 1 +-- @tparam number h Hue decimal value 0-360 +-- @tparam number s Saturation float value 0-1 +-- @tparam number l Lightness float value 0-1 +-- @return A new color object, or nil if invalid +-- @usage local c = colorful.color.from.hsl(30, 0.8, 0.6) +function color.from.hsl(h,s,l) + return color.from.hsla(h,s,l,1) +end + +-- }}} HSL + +-- @section end + +-- }}} New Color functions + +-- {{{ Color transformation functions + +--- Color object transformation functions +-- @section color_change + +--- Update the HSL values after editing any of the RGB values +function color:hsl_update() + self.hue, self.saturation, self.lightness = rgb_to_hsl(self.red, self.green, self.blue) +end + +--- Update the RGB values after editing any of the HSL values +function color:rgb_update() + self.red, self.green, self.blue = hsl_to_rgb(self.hue, self.saturation, self.lightness) +end + +-- {{{ RGB-adjusting functions + +-- Helper function for color:mix +local function calcval(v,s,e) + local w = 1 - v + return s * v + e * w +end + +--- Returns the mix of a color with another by a specified amount +-- @tparam beautiful.colorful.color c A color object +-- @tparam[opt=0.5] number v A number [0,1] of how much of the origin color +-- should be mixed with c. +-- @return A new color object representing the mix of the two colors, or nil if invalid +-- @usage local mixed_color = c:mix(c2, 0.5) +function color:mix(c, v) + if v == nil then v = 0.5 end + if v < 0 or v > 1 then return nil end + local red = calcval(v, self.red, c.red) * 255 + local green = calcval(v, self.green, c.green) * 255 + local blue = calcval(v, self.blue, c.blue) * 255 + local alpha = calcval(v, self.alpha, c.alpha) * 255 + return color.from.rgba(red, green, blue, alpha) +end + +--- Returns the inverse of a color +-- @return A new color object representing the inverted color, or nil if invalid +-- @usage local inverted_color = c:invert() +function color:invert() + local red = clamp(1 - self.red, 0, 1) * 255 + local green = clamp(1 - self.green, 0, 1) * 255 + local blue = clamp(1 - self.blue, 0, 1) * 255 + return color.from.rgba(red, green, blue, self.alpha * 255) +end + +-- }}} RGB-adjusting functions + +-- {{{ HSL-adjusting functions + +--- Returns a color darkened by a specified amount +-- @tparam number v A number [0,1] of how much to decrease Lightness +-- @return A new color object representing the darkened color, or nil if invalid +-- @usage local darker_color = c:darken(0.25) +function color:darken(v) + local lightness = clamp(self.lightness - v, 0, 1) + return color.from.hsla(self.hue, self.saturation, lightness, self.alpha) +end + +--- Returns a color lightened by a specified amount +-- @tparam number v A number [0,1] of how much to increase Lightness +-- @return A new color object representing the lightened color, or nil if invalid +-- @usage local lighter_color = c:lighten(0.25) +function color:lighten(v) + local lightness = clamp(self.lightness + v, 0, 1) + return color.from.hsla(self.hue, self.saturation, lightness, self.alpha) +end + +--- Returns a color saturated by a specified amount +-- @tparam number v A number [0,1] of how much to increase Saturation +-- @return A new color object representing the saturated color, or nil if invalid +-- @usage local saturated_color = c:saturate(0.25) +function color:saturate(v) + local saturation = clamp(self.saturation + v, 0, 1) + return color.from.hsla(self.hue, saturation, self.lightness, self.alpha) +end + +--- Returns a color desaturated by a specified amount +-- @tparam number v A number [0,1] of how much to decrease Saturation +-- @return A new color object representing the desaturated color, or nil if invalid +-- @usage local desaturated_color = c:desaturate(0.25) +function color:desaturate(v) + local saturation = clamp(self.saturation - v, 0, 1) + return color.from.hsla(self.hue, saturation, self.lightness, self.alpha) +end + +--- Returns the grayscale of a color +-- Reduces Saturation to 0 +-- @return A new color object representing the grayscaled color, or nil if invalid +-- @usage local grayscaled_color = c:grayscale() +function color:grayscale() + return color.from.hsla(self.hue, 0, self.lightness, self.alpha) +end + +-- }}} HSL-adjusting functions + +-- {{{ Color harmony functions + +--- Returns the complement of a color +-- Adds 180 to Hue (hue will self-adjust to be within [0,360]) +-- @return A new color object representing the complementary color, or nil if invalid +-- @usage local complemented_color = c:complement() +function color:complement() + local hue = self.hue + 180 + return color.from.hsla(hue, self.saturation, self.lightness, self.alpha) +end + +--- Returns the left and right analogous colors (30deg hue shift) +-- @return A new color object representing a -30 degree hue shift of the color, or nil if invalid +-- @return A new color object representing a +30 degree hue shift of the color, or nil if invalid +-- @usage local left, right = c:analogous() +function color:analogous() + local left = self.hue - 30 + local right = self.hue + 30 + return color.from.hsla( + left, + self.saturation, + self.lightness, + self.alpha), + color.from.hsla( + right, + self.saturation, + self.lightness, + self.alpha) +end + +--- Returns the left and right complementary colors (30deg shift from +--complementary color) +-- @return A new color object representing the +210 degree hue shift (180 + 30) +-- of the color, or nil if invalid +-- @return A new color object representing the +150 degree hue shift (180 - 30) +-- of the color, or nil if invalid +-- @usage local left, right = c:split_complementary() +function color:split_complementary() + local left = self.hue + 180 + 30 + local right = self.hue + 180 - 30 + return color.from.hsla( + left, + self.saturation, + self.lightness, + self.alpha), + color.from.hsla( + right, + self.saturation, + self.lightness, + self.alpha) +end + +--- Returns the left and right triadic colors (120deg shift from base color) +-- @return A new color object representing a +120 degree hue shift of the color, or nil if invalid +-- @return A new color object representing a -120 degree hue shift of the color, or nil if invalid +-- @usage local left, right = c:triadic() +function color:triadic() + local left = self.hue + 120 + local right = self.hue - 120 + return color.from.hsla( + left, + self.saturation, + self.lightness, + self.alpha), + color.from.hsla( + right, + self.saturation, + self.lightness, + self.alpha) +end + +--- Returns three colors making up the tetradic colors (base complement, base +--+ 60deg, base + 60deg complement) +-- @return A new color object representing the complementary color, or nil if +-- invalid +-- @return A new color object representing a +60 degree shift of the color, or +-- nil if invalid +-- @return A new color object representing the complement of the previous +-- returned color, or nil if invalid +-- @usage local complement, analogous, analogous_complement = c:tetradic() +function color:tetradic() + local complement = self:complement() + local base_left = color.from.hsla(self.hue + 60, self.saturation, self.lightness, self.alpha) + return complement, base_left, base_left:complement() +end + +-- @section end + +-- }}} Color harmony functions + +-- }}} Color transformation functions + +-- {{{ Color output functions + +--- Color object output functions +-- @section color_output + +-- {{{ String output + +--- Return the string representation of the color in #RRGGBBAA format +-- @treturn string A string represention of the color in #RRGGBBAA format +-- @usage local hexa_str = c.as:hexa() +function color.as:hexa() + local r = round(self.red * 255) + local g = round(self.green * 255) + local b = round(self.blue * 255) + local a = round(self.alpha * 255) + return string.format("#%02x%02x%02x%02x", r, g, b, a) +end + +--- Return the string representation of the color in #RRGGBB format +-- @treturn string A string represention of the color in #RRGGBB format +-- @usage local hex_str = c.as:hex() +function color.as:hex() + local r = round(self.red * 255) + local g = round(self.green * 255) + local b = round(self.blue * 255) + return string.format("#%02x%02x%02x", r, g, b) +end + +-- }}} String output + +-- {{{ Decimal output + +--- Return the decimal representation of the color including the alpha channel +-- @treturn number The decimal represention of the color including the alpha +-- channel +-- @usage local deca_color = c.as:deca() +function color.as:deca() + return math.floor( + lshift(round(self.red * 0xFF), 24) + + lshift(round(self.green * 0xFF), 16) + + lshift(round(self.blue * 0xFF), 8) + + round(self.alpha * 0xFF) + ) +end + +--- Return the decimal representation of the color +-- @treturn number The decimal represention of the color not including the +-- alpha channel +-- @usage local dec_color = c.as:dec() +function color.as:dec() + return math.floor( + lshift(round(self.red * 0xFF), 16) + + lshift(round(self.green * 0xFF), 8) + + round(self.blue * 0xFF) + ) +end + +-- }}} Decimal output + +-- {{{ Multi-return output + +--- Return the Hue, Saturation, Lightness, and Alpha +-- @treturn number Hue [0,360] +-- @treturn number Saturation [0,1] +-- @treturn number Lightness [0,1] +-- @treturn number Alpha [0,1] +-- @usage local h,s,l,a = c.as:hsla() +function color.as:hsla() + return unpack({ + self.hue, + self.saturation, + self.lightness, + self.alpha + }) +end + +--- Return the Hue, Saturation, and Lightness +-- @treturn number Hue [0,360] +-- @treturn number Saturation [0,1] +-- @treturn number Lightness [0,1] +-- @usage local h,s,l = c.as:hsl() +function color.as:hsl() + return unpack({ + self.hue, + self.saturation, + self.lightness, + }) +end + +--- Return the Red, Green, Blue, and Alpha +-- @treturn number Red [0,1] +-- @treturn number Green [0,1] +-- @treturn number Blue [0,1] +-- @treturn number Alpha [0,1] +-- @usage local r,g,b,a = c.as:rgba() +function color.as:rgba() + return unpack({ + self.red, + self.green, + self.blue, + self.alpha + }) +end + +--- Return the Red, Green, and Blue +-- @treturn number Red [0,1] +-- @treturn number Green [0,1] +-- @treturn number Blue [0,1] +-- @usage local r,g,b = c.as:rgb() +function color.as:rgb() + return unpack({ + self.red, + self.green, + self.blue, + }) +end + +-- }}} Multi-return output + +-- @section end + +-- }}} Color output functions + +--- Color metamethods +-- @section color_meta + +color_mt.__index = color +color_mt.__tostring = color.as.hex + +--- Shorthand invert of a color +-- @function color.__unm +-- @see beautiful.colorful.color.invert +color_mt.__unm = color.invert + +--- Add the RGBA values of two colors +-- @function color.__add +-- @tparam lhs beautiful.colorful.color A color object +-- @tparam rhs beautiful.colorful.color A color object +-- @treturn beautiful.colorful.color A new color object with RGBA values added +-- @usage colorful.color("#123") + colorful.color("#321") -- returns #444444 +color_mt.__add = function(lhs, rhs) + if type(rhs) ~= "table" then return nil end + return color.from.rgba( + (lhs.red + rhs.red) * 255, + (lhs.green + rhs.green) * 255, + (lhs.blue + rhs.blue) * 255, + (lhs.alpha + rhs.alpha) * 255 + ) +end + +--- Subtract the RGBA values of two colors +-- @function color.__sub +-- @tparam lhs beautiful.colorful.color A color object +-- @tparam rhs beautiful.colorful.color A color object +-- @treturn beautiful.colorful.color A new color object with RGBA values +-- subtracted +-- @usage colorful.color("#123") - colorful.color("#321") -- returns #000022 +color_mt.__sub = function(lhs, rhs) + if type(rhs) ~= "table" then return nil end + return color.from.rgba( + (lhs.red - rhs.red) * 255, + (lhs.green - rhs.green) * 255, + (lhs.blue - rhs.blue) * 255, + (lhs.alpha - rhs.alpha) * 255 + ) +end + +--- Compare the lightness value of two colors +-- @function color.__lt +-- @tparam lhs beautiful.colorful.color A color object +-- @tparam rhs beautiful.colorful.color A color object +-- @treturn boolean True if lhs lightness is less than rhs lightness +-- @usage colorful.color("#ffcc99") < colorful.color("#aabbcc") -- returns false (0.8 < 0.73333) +color_mt.__lt = function(lhs, rhs) + if type(rhs) ~= "table" then return nil end + return lhs.lightness < rhs.lightness +end + +--- Compare the lightness value of two colors +-- @function color.__gt +-- @tparam lhs beautiful.colorful.color A color object +-- @tparam rhs beautiful.colorful.color A color object +-- @treturn boolean True if lhs lightness is greater than rhs lightness +-- @usage colorful.color("#ffcc99") > colorful.color("#aabbcc") -- returns true (0.8 > 0.73333) +color_mt.__gt = function(lhs, rhs) + if type(rhs) ~= "table" then return nil end + return lhs.lightness > rhs.lightness +end + +--- A color object +-- @type color +colorful.color = color + +setmetatable(color, { + __call = function(_,...) return color.new(...) end, +}) +-- {{{ Color changing wrapper functions + +--- Wrappers for Color object functions +-- @section wrappers + +--- Mix two colors with optional mix amount +-- @param color1 A CSS color compatible string, decimal color value, or a table +-- containing 3 or 4 values. +-- @param color2 A CSS color compatible string, decimal color value, or a table +-- containing 3 or 4 values. +-- @tparam number amount A number [0,1] of how much of color1 should be mixed +-- with color2. +-- @treturn string The string representation of the mixed color in `#rrggbbaa` +-- format. +-- @see beautiful.colorful.color.mix +-- @usage local mix_str = colorful.mix("#ffcc99", "#99ccff", 0.5) +function colorful.mix(color1, color2, amount) + local c1 = color.new(color1) + local c2 = color.new(color2) + if c1 == nil or c2 == nil then return nil end + return tostring(c1:mix(c2, amount)) +end + +--- Darken a color by an amount +-- @param _color A CSS color compatible string, decimal color value, or a table +-- containing 3 or 4 values. +-- @tparam number amount A number [0,1] of how much to darken color. +-- @treturn string The string representation of the mixed color in `#rrggbbaa` +-- format. +-- @see beautiful.colorful.color.darken +-- @usage local dark_str = colorful.darken("#ffcc99", 0.25) +function colorful.darken(_color, amount) + local c1 = color.new(_color) + if c1 == nil then return nil end + return tostring(c1:darken(amount)) +end + +--- Lighten a color by an amount +-- @param _color A CSS color compatible string, decimal color value, or a table +-- containing 3 or 4 values. +-- @tparam number amount A number [0,1] of how much to lighten color. +-- @treturn string The string representation of the mixed color in `#rrggbbaa` +-- format. +-- @see beautiful.colorful.color.lighten +-- @usage local light_str = colorful.lighten("#ffcc99", 0.25) +function colorful.lighten(_color, amount) + local c1 = color.new(_color) + if c1 == nil then return nil end + return tostring(c1:lighten(amount)) +end + +--- Invert a color +-- @param _color A CSS color compatible string, decimal color value, or a table +-- containing 3 or 4 values. +-- @treturn string The string representation of the mixed color in `#rrggbbaa` +-- format. +-- @see beautiful.colorful.color.invert +-- @usage = local invert_str = colorful.invert("#ffcc99") +function colorful.invert(_color) + local c1 = color.new(_color) + if c1 == nil then return nil end + return tostring(c1:invert()) +end + +--- Grayscale a color +-- @param _color A CSS color compatible string, decimal color value, or a table +-- containing 3 or 4 values. +-- @treturn string The string representation of the mixed color in `#rrggbbaa` +-- format. +-- @see beautiful.colorful.color.grayscale +-- @usage local grayed_str = colorful.grayscale("#ffcc99") +function colorful.grayscale(_color) + local c1 = color.new(_color) + if c1 == nil then return nil end + return tostring(c1:grayscale()) +end + +-- @section end + +-- }}} Color changing wrapper functions + +return colorful + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/beautiful/gtk.lua b/lib/beautiful/gtk.lua index df71b1a388..89e39fb116 100644 --- a/lib/beautiful/gtk.lua +++ b/lib/beautiful/gtk.lua @@ -7,7 +7,7 @@ --------------------------------------------------------------------------- local get_dpi = require("beautiful.xresources").get_dpi local gears_debug = require("gears.debug") -local gears_math = require("gears.math") +local colorful = require("beautiful.colorful") local join = require("gears.table").join local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) @@ -17,24 +17,12 @@ local gtk = { } -local function convert_gtk_channel_to_hex(channel_value) - return string.format("%02x", gears_math.round(channel_value * 255)) -end - -local function convert_gtk_color_to_hex(gtk_color) - return "#" .. - convert_gtk_channel_to_hex(gtk_color.red) .. - convert_gtk_channel_to_hex(gtk_color.green) .. - convert_gtk_channel_to_hex(gtk_color.blue) .. - convert_gtk_channel_to_hex(gtk_color.alpha) -end - local function lookup_gtk_color_to_hex(_style_context, color_name) local gtk_color = _style_context:lookup_color(color_name) if not gtk_color then return nil end - return convert_gtk_color_to_hex(gtk_color) + return tostring(colorful.color.new(gtk_color)) end local function get_gtk_property(_style_context, property_name) @@ -47,9 +35,9 @@ local function get_gtk_property(_style_context, property_name) end local function get_gtk_color_property_to_hex(_style_context, property_name) - return convert_gtk_color_to_hex( + return tostring(colorful.color.new( get_gtk_property(_style_context, property_name) - ) + )) end local function read_gtk_color_properties_from_widget(gtk_widget, properties) diff --git a/lib/beautiful/init.lua b/lib/beautiful/init.lua index f37aefd701..fee80324f8 100644 --- a/lib/beautiful/init.lua +++ b/lib/beautiful/init.lua @@ -23,11 +23,13 @@ local protected_call = require("gears.protected_call") local xresources = require("beautiful.xresources") local theme_assets = require("beautiful.theme_assets") local gtk = require("beautiful.gtk") +local colorful = require("beautiful.colorful") local beautiful = { xresources = xresources, theme_assets = theme_assets, gtk = gtk, + colorful = colorful, mt = {} } diff --git a/spec/beautiful/colorful_spec.lua b/spec/beautiful/colorful_spec.lua new file mode 100644 index 0000000000..9a03efc953 --- /dev/null +++ b/spec/beautiful/colorful_spec.lua @@ -0,0 +1,494 @@ +--------------------------------------------------------------------------- +-- @author Kevin Zander +-- @copyright 2019 Kevin Zander +--------------------------------------------------------------------------- + +local color = require 'gears.color' +local colorful = require "beautiful.colorful" +local Color = colorful.color + +local colors = { + -- { + -- hex str, + -- hsl table, + -- lighten 25%, + -- darken 25%, + -- invert, + -- mix 25%, + -- mix 75%, + -- saturate 25%, + -- desaturate 25%, + -- grayscale, + -- complement + -- } + -- For HSL we use the 0-100 range and convert to the 0-1 range + -- + -- a few random colors + { + "#dbf7a6", -- originally #daf7a6, bumped red channel for floating precision error + {81, 84, 81}, + "#ffffff", + "#acec31", + "#240859", + "#52446c", + "#adbb93", + "#ddff9e", + "#d7ebb2", + "#cfcfcf", + "#c2a5f7", + }, + { + "#944fb0", + {283, 38, 50}, + "#caa7d8", + "#4a2858", + "#6bb04f", + "#759867", + "#8a6798", + "#a12fd0", + "#876f90", + "#808080", + "#6bb04f", + }, + { + "#ff5833", -- originally #ff5733, bumped green channel for floating precision error + {11, 100, 60}, + "#ffc0b3", + "#b32000", + "#00a7cc", + "#4093a6", + "#bf6c59", + "#ff5833", + "#e6684c", + "#999999", + "#32d9ff", + }, + { + "#c70038", + {343, 100, 39}, + "#ff477b", -- scss lighten returns #ff487b, rounding error + "#480014", + "#38ffc7", + "#5cbfa3", + "#a3405c", + "#c70038", + "#ae1943", + "#646464", + "#00c78f", + }, + { + "#920c3f", -- originally #900c3f, bumped red channel for floating precision error + {337, 85, 31}, + "#ee3078", + "#1c020c", + "#6df3c0", + "#76b9a0", + "#89465f", + "#9e003c", + "#7e2044", + "#4f4f4f", + "#0c925f", + }, + { + "#581845", + {318, 57, 22}, + "#bc3394", + "#000000", + "#a7e7ba", + "#93b39d", + "#6c4c62", + "#660a4b", + "#4a263f", + "#383838", + "#18582b", + }, + -- red + { + "#ff0000", + {0, 100, 50}, + "#ff8080", + "#800000", + "#00ffff", + "#40bfbf", + "#bf4040", + "#ff0000", + "#df2020", + "#808080", + "#00ffff", + }, + -- cyan + { + "#00ffff", + {180, 100, 50}, + "#80ffff", + "#008080", + "#ff0000", + "#bf4040", + "#40bfbf", + "#00ffff", + "#20dfdf", + "#808080", + "#ff0000", + }, + -- white + { + "#ffffff", + {0, 0, 100}, -- if saturation == 0 white/black/grey + "#ffffff", + "#bfbfbf", + "#000000", + "#404040", + "#bfbfbf", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + }, + -- black + { + "#000000", + {0, 0, 0}, -- if saturation == 0 white/black/grey + "#404040", + "#000000", + "#ffffff", + "#bfbfbf", + "#404040", + "#000000", + "#000000", + "#000000", + "#000000", + }, + -- grey + { + "#808080", + {0, 0, 50}, -- if saturation == 0 white/black/grey + "#c0c0c0", + "#404040", + "#7f7f7f", + "#7f7f7f", + "#808080", + "#a06060", + "#808080", + "#808080", + "#808080", + }, +} + +local color_functional = { + -- rgb(d, d, d) + { "#bffea2ff", "rgb(191, 254, 162)" }, + { "#568482ff", "rgb(86, 132, 130)" }, + { "#4b3575ff", "rgb(75, 53, 117)" }, + { "#d328adff", "rgb(211, 40, 173)" }, + { "#e3ddfbff", "rgb(227, 221, 251)" }, + -- rgb(d, d, d, f) + { "#bffea200", "rgb(191, 254, 162, 0)" }, + { "#568482ff", "rgb(86, 132, 130, 1)" }, + { "#4b357580", "rgb(75, 53, 117, 0.5)" }, + { "#d328ad54", "rgb(211, 40, 173, 0.33)" }, + { "#e3ddfba8", "rgb(227, 221, 251, 0.66)" }, + -- rgb(d, d, d, %) + { "#83d69d4d", "rgb(131, 214, 157, 30%)" }, + { "#a3867efa", "rgb(163, 134, 126, 98%)" }, + { "#4bc587c4", "rgb(75, 197, 135, 77%)" }, + { "#67e4495c", "rgb(103, 228, 73, 36%)" }, + { "#ceeb12f2", "rgb(206, 235, 18, 95%)" }, + -- rgb(%, %, %) + { "#4a5ccfff", "rgb(29%, 36%, 81%)" }, + { "#12f287ff", "rgb(7%, 95%, 53%)" }, + { "#33abe6ff", "rgb(20%, 67%, 90%)" }, + { "#120875ff", "rgb(7%, 3%, 46%)" }, + { "#3dfae8ff", "rgb(24%, 98%, 91%)" }, + -- rgb(%, %, %, f) + { "#8a1745e0", "rgb(54%, 9%, 27%, 0.88)" }, + { "#b01a1f5c", "rgb(69%, 10%, 12%, 0.36)" }, + { "#9499ab4a", "rgb(58%, 60%, 67%, 0.29)" }, + { "#c74a5430", "rgb(78%, 29%, 33%, 0.19)" }, + { "#fc03d454", "rgb(99%, 1%, 83%, 0.33)" }, + -- rgb(%, %, %, %) + { "#458a61c4", "rgb(27%, 54%, 38%, 77%)" }, + { "#85ab8a0a", "rgb(52%, 67%, 54%, 4%)" }, + { "#70f0f0ba", "rgb(44%, 94%, 94%, 73%)" }, + { "#4abda35c", "rgb(29%, 74%, 64%, 36%)" }, + { "#b02b73e3", "rgb(69%, 17%, 45%, 89%)" }, + -- rgb(d d d) + { "#d765c8ff", "rgb(215 101 200)" }, + { "#e93356ff", "rgb(233 51 86)" }, + { "#478e7aff", "rgb(71 142 122)" }, + { "#5e83f3ff", "rgb(94 131 243)" }, + { "#a3b725ff", "rgb(163 183 37)" }, + -- rgb(d d d / f) + { "#e93356c4", "rgb(233 51 86 / 0.77)" }, + { "#478e7aa1", "rgb(71 142 122 / 0.63)" }, + { "#5e83f3eb", "rgb(94 131 243 / 0.92)" }, + { "#2867221c", "rgb(40 103 34 / 0.11)" }, + { "#9d4ca385", "rgb(157 76 163 / 0.52)" }, + -- rgb(d d d / %) + { "#053e23cf", "rgb(5 62 35 / 81%)" }, + { "#2867221c", "rgb(40 103 34 / 11%)" }, + { "#ff3883d6", "rgb(255 56 131 / 84%)" }, + { "#9d4ca387", "rgb(157 76 163 / 53%)" }, + { "#7ef94bc7", "rgb(126 249 75 / 78%)" }, + -- rgb(% % %) + { "#d966c9ff", "rgb(85% 40% 79%)" }, + { "#eb3357ff", "rgb(92% 20% 34%)" }, + { "#478f7aff", "rgb(28% 56% 48%)" }, + { "#5e85f5ff", "rgb(37% 52% 96%)" }, + { "#a3b826ff", "rgb(64% 72% 15%)" }, + -- rgb(% % % / f) + { "#054024cc", "rgb(2% 25% 14% / 0.80)" }, + { "#2969211c", "rgb(16% 41% 13% / 0.11)" }, + { "#ff3885d6", "rgb(100% 22% 52% / 0.84)" }, + { "#9e4da385", "rgb(62% 30% 64% / 0.52)" }, + { "#80fa4dc4", "rgb(50% 98% 30% / 0.77)" }, + -- rgb(% % % / %) + { "#87c469e6", "rgb(53% 77% 41% / 90%)" }, + { "#4a5ccfeb", "rgb(29% 36% 81% / 92%)" }, + { "#12f28717", "rgb(7% 95% 53% / 9%)" }, + { "#33abe659", "rgb(20% 67% 90% / 35%)" }, + { "#12087512", "rgb(7% 3% 46% / 7%)" }, + -- rgba(d, d, d, f) + { "#b02b71e0", "rgba(176, 43, 113, 0.88)" }, + { "#d4553be3", "rgba(212, 85, 59, 0.89)" }, + { "#5ab0f496", "rgba(90, 176, 244, 0.59)" }, + { "#a8db71eb", "rgba(168, 219, 113, 0.92)" }, + { "#66d0afe8", "rgba(102, 208, 175, 0.91)" }, + -- rgba(d, d, d, %) + { "#ff35e321", "rgba(255, 53, 227, 13%)" }, + { "#ff0ede14", "rgba(255, 14, 222, 8%)" }, + { "#02ec9830", "rgba(2, 236, 152, 19%)" }, + { "#2a64e9d1", "rgba(42, 100, 233, 82%)" }, + { "#5c8d9475", "rgba(92, 141, 148, 46%)" }, + -- rgba(%, %, %, f) + { "#3070f5eb", "rgb(19%, 44%, 96%, 0.92)" }, + { "#c4b321b0", "rgb(77%, 70%, 13%, 0.69)" }, + { "#63c7f2eb", "rgb(39%, 78%, 95%, 0.92)" }, + { "#de36cc8c", "rgb(87%, 21%, 80%, 0.55)" }, + { "#4de8e8de", "rgb(30%, 91%, 91%, 0.87)" }, + -- rgba(%, %, %, %) + { "#b01a8ac2", "rgba(69%, 10%, 54%, 76%)" }, + { "#4fff94e0", "rgba(31%, 100%, 58%, 88%)" }, + { "#bfa10abf", "rgba(75%, 63%, 4%, 75%)" }, + { "#d6ede0d6", "rgba(84%, 93%, 88%, 84%)" }, + { "#fabfe8fc", "rgba(98%, 75%, 91%, 99%)" }, + -- rgba(d d d / f) + { "#26e1a46e", "rgba(38 225 164 / 0.43)" }, + { "#9e48c94f", "rgba(158 72 201 / 0.31)" }, + { "#723a3047", "rgba(114 58 48 / 0.28)" }, + { "#8e6b2ce8", "rgba(142 107 44 / 0.91)" }, + { "#1b217fc2", "rgba(27 33 127 / 0.76)" }, + -- rgba(d d d / %) + { "#488cb81f", "rgba(72 140 184 / 12%)" }, + { "#7998f175", "rgba(121 152 241 / 46%)" }, + { "#56d96f03", "rgba(86 217 111 / 1%)" }, + { "#5899d53d", "rgba(88 153 213 / 24%)" }, + { "#ad7c7b4f", "rgba(173 124 123 / 31%)" }, + -- rgba(% % % / f) + { "#b830a10a", "rgba(72% 19% 63% / 0.04)" }, + { "#6bb3ada3", "rgba(42% 70% 68% / 0.64)" }, + { "#59309ca1", "rgba(35% 19% 61% / 0.63)" }, + { "#bd54bf33", "rgba(74% 33% 75% / 0.20)" }, + { "#edb0a842", "rgba(93% 69% 66% / 0.26)" }, + -- rgba(% % % / %) + { "#ab802bd6", "rgba(67% 50% 17% / 84%)" }, + { "#e314a640", "rgba(89% 8% 65% / 25%)" }, + { "#a13bb552", "rgba(63% 23% 71% / 32%)" }, + { "#543d14a3", "rgba(33% 24% 8% / 64%)" }, + { "#3ba885fa", "rgba(23% 66% 52% / 98%)" }, + -- hsl(d, %, %) + { "#124444ff", "hsl(180, 58%, 17%)" }, + { "#b43c9cff", "hsl(312, 50%, 47%)" }, + { "#223f3fff", "hsl(179, 30%, 19%)" }, + { "#988ea9ff", "hsl(262, 14%, 61%)" }, + { "#4f0c4dff", "hsl(302, 73%, 18%)" }, + -- hsl(d, %, %, f) + { "#1f28285c", "hsl(180, 13%, 14%, 0.36)" }, + { "#d5fed29e", "hsl(117, 94%, 91%, 0.62)" }, + { "#e817e154", "hsl(302, 82%, 50%, 0.33)" }, + { "#5add8a42", "hsl(142, 66%, 61%, 0.26)" }, + { "#1e1e1aa6", "hsl(55, 8%, 11%, 0.65)" }, + -- hsl(d, %, %, %) + { "#3e70471a", "hsl(131, 29%, 34%, 10%)" }, + { "#34f9a445", "hsl(154, 94%, 59%, 27%)" }, + { "#1d23dd29", "hsl(238, 77%, 49%, 16%)" }, + { "#d237a336", "hsl(318, 63%, 52%, 21%)" }, + { "#c1d9e666", "hsl(201, 43%, 83%, 40%)" }, + -- hsl(d % %) + { "#bcd1a3ff", "hsl(88 33% 73%)" }, + { "#eedddeff", "hsl(355 34% 90%)" }, + { "#c9cac9ff", "hsl(148 1% 79%)" }, + { "#dce2daff", "hsl(106 12% 87%)" }, + { "#fefcfbff", "hsl(18 45% 99%)" }, + -- hsl(d % % / f) + { "#e5eece17", "hsl(76 48% 87% / 0.09)" }, + { "#6e5530a8", "hsl(36 39% 31% / 0.66)" }, + { "#110d110d", "hsl(292 14% 6% / 0.05)" }, + { "#76efd170", "hsl(165 79% 70% / 0.44)" }, + { "#d3af5587", "hsl(43 59% 58% / 0.53)" }, + -- hsl(d % % / %) + { "#324a6ce3", "hsl(215 37% 31% / 89%)" }, + { "#8baca887", "hsl(172 17% 61% / 53%)" }, + { "#182d62d4", "hsl(223 60% 24% / 83%)" }, + { "#efedeb2b", "hsl(26 10% 93% / 17%)" }, + { "#cadedc4d", "hsl(174 23% 83% / 30%)" }, + -- hsla(d, %, %, f) + { "#0ba822cf", "hsla(129, 88%, 35%, 0.81)" }, + { "#404045c7", "hsla(238, 4%, 26%, 0.78)" }, + { "#0d2c9138", "hsla(226, 84%, 31%, 0.22)" }, + { "#262d0bab", "hsla(72, 62%, 11%, 0.67)" }, + { "#450f5c66", "hsla(282, 72%, 21%, 0.40)" }, + -- hsla(d, %, %, %) + { "#28582263", "hsla(114, 44%, 24%, 39%)" }, + { "#7d9aa105", "hsla(192, 16%, 56%, 2%)" }, + { "#306e42bd", "hsla(137, 39%, 31%, 74%)" }, + { "#85d846eb", "hsla(94, 65%, 56%, 92%)" }, + { "#8375f052", "hsla(247, 81%, 70%, 32%)" }, + -- hsla(d % % / f) + { "#89898bd6", "hsla(233 1% 54% / 0.84)" }, + { "#3562d466", "hsla(223 65% 52% / 0.40)" }, + { "#acf1b8ad", "hsla(131 72% 81% / 0.68)" }, + { "#131211b0", "hsla(56 4% 7% / 0.69)" }, + { "#d7e38791", "hsla(68 62% 71% / 0.57)" }, + -- hsla(d % % / %) + { "#504e4e45", "hsla(1 1% 31% / 27%)" }, + { "#070c5a59", "hsla(236 86% 19% / 35%)" }, + { "#5f5cf552", "hsla(241 88% 66% / 32%)" }, + { "#31212c82", "hsla(319 19% 16% / 51%)" }, + { "#f7d8f833", "hsla(299 68% 91% / 20%)" }, +} + +describe("Colorful", function() + + describe("creates object with new", function() + it("from #rrggbb string", function() + for _,col in ipairs(colors) do + local c = Color.new(col[1]) + -- we're going to verify against the original parser for things + local pc = {color.parse_color(col[1])} + assert.is.same(c.red, pc[1]) + assert.is.same(c.green, pc[2]) + assert.is.same(c.blue, pc[3]) + assert.is.same(c.alpha, pc[4]) + end + end) + + it("from HSL table", function() + for _,col in ipairs(colors) do + -- we convert to [0,1] + local h,s,l = col[2][1], col[2][2]/100, col[2][3]/100 + local c = Color.new({h,s,l}) + assert.is.same(col[1], c.as:hex()) + end + end) + + it("from number", function() + for _,col in ipairs(colors) do + local c = Color.new(col[1]) + local dec = tonumber(col[1]:sub(2), 16) + assert.is.same(c.as:dec(), dec) + end + end) + + it("from functional string", function() + for _,col in ipairs(color_functional) do + local c= Color.new(col[2]) + assert.is.same(col[1], c.as:hexa()) + end + end) + end) + + describe("adjusts color by", function() + it("lightening 25%", function() + for _,col in ipairs(colors) do + local c = Color.new(col[1]) + c = c:lighten(0.25) + assert.is.same(col[3], c.as:hex()) + end + end) + + it("darkening 25%", function() + for _,col in ipairs(colors) do + local c = Color.new(col[1]) + c = c:darken(0.25) + assert.is.same(col[4], c.as:hex()) + end + end) + + it("inverting", function() + for _,col in ipairs(colors) do + local c = Color.new(col[1]) + c = c:invert() + assert.is.same(col[5], c.as:hex()) + end + end) + + it("mixing 0%", function() + for _,col in ipairs(colors) do + local c1 = Color.new(col[1]) + local c2 = Color.new(col[5]) + local mix = c1:mix(c2, 0) + assert.is.same(col[5], mix.as:hex()) + end + end) + + it("mixing 25%", function() + for _,col in ipairs(colors) do + local c1 = Color.new(col[1]) + local c2 = Color.new(col[5]) + local mix = c1:mix(c2, 0.25) + assert.is.same(col[6], mix.as:hex()) + end + end) + + it("mixing 50%", function() + for _,col in ipairs(colors) do + local c1 = Color.new(col[1]) + local c2 = Color.new(col[5]) + local mix = c1:mix(c2, 0.5) + assert.is.same("#808080", mix.as:hex()) + end + end) + + it("mixing 75%", function() + for _,col in ipairs(colors) do + local c1 = Color.new(col[1]) + local c2 = Color.new(col[5]) + local mix = c1:mix(c2, 0.75) + assert.is.same(col[7], mix.as:hex()) + end + end) + + it("mixing 100%", function() + for _,col in ipairs(colors) do + local c1 = Color.new(col[1]) + local c2 = Color.new(col[5]) + local mix = c1:mix(c2, 1) + assert.is.same(col[1], mix.as:hex()) + end + end) + + it("saturates 25%", function() + for _,col in ipairs(colors) do + local c = Color.new(col[1]) + c = c:saturate(0.25) + assert.is.same(col[8], c.as:hex()) + end + end) + + it("desaturates 25%", function() + for _,col in ipairs(colors) do + local c = Color.new(col[1]) + c = c:desaturate(0.25) + assert.is.same(col[9], c.as:hex()) + end + end) + + it("grayscales", function() + for _,col in ipairs(colors) do + local c = Color.new(col[1]) + c = c:grayscale() + assert.is.same(col[10], c.as:hex()) + end + end) + end) +end) diff --git a/themes/gtk/theme.lua b/themes/gtk/theme.lua index ca31be8f06..a97f5b80f3 100644 --- a/themes/gtk/theme.lua +++ b/themes/gtk/theme.lua @@ -6,6 +6,7 @@ local theme_assets = require("beautiful.theme_assets") local dpi = require("beautiful.xresources").apply_dpi local gfs = require("gears.filesystem") +local colorful = require("beautiful.colorful") local themes_path = gfs.get_themes_dir() local gears_shape = require("gears.shape") local wibox = require("wibox") @@ -16,21 +17,6 @@ local gtk = require("beautiful.gtk") -- Helper functions for modifying hex colors: -- local hex_color_match = "[a-fA-F0-9][a-fA-F0-9]" -local function darker(color_value, darker_n) - local result = "#" - local channel_counter = 1 - for s in color_value:gmatch(hex_color_match) do - local bg_numeric_value = tonumber("0x"..s) - if channel_counter <= 3 then - bg_numeric_value = bg_numeric_value - darker_n - end - if bg_numeric_value < 0 then bg_numeric_value = 0 end - if bg_numeric_value > 255 then bg_numeric_value = 255 end - result = result .. string.format("%02x", bg_numeric_value) - channel_counter = channel_counter + 1 - end - return result -end local function is_dark(color_value) local bg_numeric_value = 0; local channel_counter = 1 @@ -44,25 +30,10 @@ local function is_dark(color_value) local is_dark_bg = (bg_numeric_value < 383) return is_dark_bg end -local function mix(color1, color2, ratio) - ratio = ratio or 0.5 - local result = "#" - local channels1 = color1:gmatch(hex_color_match) - local channels2 = color2:gmatch(hex_color_match) - for _ = 1,3 do - local bg_numeric_value = math.ceil( - tonumber("0x"..channels1())*ratio + - tonumber("0x"..channels2())*(1-ratio) - ) - if bg_numeric_value < 0 then bg_numeric_value = 0 end - if bg_numeric_value > 255 then bg_numeric_value = 255 end - result = result .. string.format("%02x", bg_numeric_value) - end - return result -end local function reduce_contrast(color, ratio) - ratio = ratio or 50 - return darker(color, is_dark(color) and -ratio or ratio) + ratio = (ratio or 127) / 255 + -- Negative number here will lighten + return colorful.darken(color, is_dark(tostring(color)) and -ratio or ratio) end local function choose_contrast_color(reference, candidate1, candidate2) -- luacheck: no unused @@ -94,7 +65,7 @@ end theme.gtk.button_border_radius = dpi(theme.gtk.button_border_radius or 0) theme.gtk.button_border_width = dpi(theme.gtk.button_border_width or 1) theme.gtk.bold_font = theme.gtk.font_family .. ' Bold ' .. theme.gtk.font_size -theme.gtk.menubar_border_color = mix( +theme.gtk.menubar_border_color = colorful.mix( theme.gtk.menubar_bg_color, theme.gtk.menubar_fg_color, 0.7 @@ -115,8 +86,8 @@ theme.fg_focus = theme.gtk.selected_fg_color theme.bg_urgent = theme.gtk.error_bg_color theme.fg_urgent = theme.gtk.error_fg_color -theme.bg_minimize = mix(theme.wibar_fg, theme.wibar_bg, 0.3) -theme.fg_minimize = mix(theme.wibar_fg, theme.wibar_bg, 0.9) +theme.bg_minimize = colorful.mix(theme.wibar_fg, theme.wibar_bg, 0.3) +theme.fg_minimize = colorful.mix(theme.wibar_fg, theme.wibar_bg, 0.9) theme.bg_systray = theme.wibar_bg @@ -152,7 +123,7 @@ theme.tasklist_bg_focus = theme.tasklist_bg_normal theme.tasklist_font_focus = theme.gtk.bold_font theme.tasklist_shape_minimized = rounded_rect_shape -theme.tasklist_shape_border_color_minimized = mix( +theme.tasklist_shape_border_color_minimized = colorful.mix( theme.bg_minimize, theme.fg_minimize, 0.85 @@ -233,12 +204,12 @@ theme.taglist_shape_border_color_container = theme.gtk.header_button_border_colo theme.taglist_bg_occupied = theme.gtk.header_button_bg_color theme.taglist_fg_occupied = theme.gtk.header_button_fg_color -theme.taglist_bg_empty = mix( +theme.taglist_bg_empty = colorful.mix( theme.gtk.menubar_bg_color, theme.gtk.header_button_bg_color, 0.3 ) -theme.taglist_fg_empty = mix( +theme.taglist_fg_empty = colorful.mix( theme.gtk.menubar_bg_color, theme.gtk.header_button_fg_color ) @@ -315,7 +286,7 @@ theme.icon_theme = nil -- Generate Awesome icon: theme.awesome_icon = theme_assets.awesome_icon( - theme.menu_height, mix(theme.bg_focus, theme.fg_normal), theme.wibar_bg + theme.menu_height, colorful.mix(theme.bg_focus, theme.fg_normal), theme.wibar_bg ) -- Generate taglist squares: @@ -339,8 +310,8 @@ if not is_dark(theme.bg_normal) then end wallpaper_bg = reduce_contrast(wallpaper_bg, 50) wallpaper_fg = reduce_contrast(wallpaper_fg, 30) -wallpaper_fg = mix(wallpaper_fg, wallpaper_bg, 0.4) -wallpaper_alt_fg = mix(wallpaper_alt_fg, wallpaper_fg, 0.4) +wallpaper_fg = colorful.mix(wallpaper_fg, wallpaper_bg, 0.4) +wallpaper_alt_fg = colorful.mix(wallpaper_alt_fg, wallpaper_fg, 0.4) theme.wallpaper = function(s) return theme_assets.wallpaper(wallpaper_bg, wallpaper_fg, wallpaper_alt_fg, s) end From eb413d5766fe496311fbf7c5cb748872c2f5c017 Mon Sep 17 00:00:00 2001 From: Kevin Zander Date: Sun, 27 Jan 2019 15:34:23 -0600 Subject: [PATCH 2/2] Change color.as functions to just color functions; Remove unpack, and clarify docs --- lib/beautiful/colorful.lua | 711 +++++++++++++------------------ spec/beautiful/colorful_spec.lua | 28 +- themes/gtk/theme.lua | 3 +- 3 files changed, 318 insertions(+), 424 deletions(-) diff --git a/lib/beautiful/colorful.lua b/lib/beautiful/colorful.lua index c4374599f2..4b42b3b836 100644 --- a/lib/beautiful/colorful.lua +++ b/lib/beautiful/colorful.lua @@ -102,14 +102,11 @@ -- @module beautiful.colorful --------------------------------------------------------------------------- -local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) - local colorful = {} local color = { -- set the from and as tables from = {}, - as = {} } -- Metatable for color instances. This is set up near the end of the file. @@ -145,12 +142,15 @@ local function between(v, l, h) return false end +-- The formulas in hsl_to_rgb and rgb_to_hsl are described at the +-- Wikipedia page https://en.wikipedia.org/wiki/HSL_and_HSV + -- Expects Hue [0,360], Saturation [0,1], Lightness [0,1] -- Does NOT check bounds local function hsl_to_rgb(hue, saturation, lightness) -- If s = 0 then it's a grey if saturation == 0 then - return unpack({lightness, lightness, lightness}) + return lightness, lightness, lightness end local C = (1 - math.abs(2 * lightness - 1)) * saturation local X = C * (1 - math.abs((hue / 60) % 2 - 1)) @@ -169,13 +169,13 @@ local function hsl_to_rgb(hue, saturation, lightness) elseif 300 <= hue and hue <= 360 then r,g,b = C,0,X end - return unpack({r+m, g+m, b+m}) + return r+m, g+m, b+m end -- Expects Red, Blue, and Green [0,1] -- Does NOT check bounds local function rgb_to_hsl(red, green, blue) - local h,s,l = 0,0,0 -- luacheck: ignore + local h,s,l local M = math.max(red, green, blue) local m = math.min(red, green, blue) local C = M - m @@ -198,7 +198,7 @@ local function rgb_to_hsl(red, green, blue) else s = C / (1 - math.abs((2 * l) - 1)) end - return unpack({h, s, l}) + return h, s, l end -- Create a color object @@ -208,10 +208,6 @@ local function create_color_object(r,g,b,h,s,l,a) hue = h, saturation = s, lightness = l, alpha = a }, color_mt) - -- allow as functions to use color object variables - setmetatable(o.as, { - __index = o - }) return o end @@ -299,7 +295,6 @@ end local css = { integer = "%d?%d?%d", -- 0-999 (but 0-255 for values) - --percent = "%d?%d?%d%%", -- 0-999% (but 0-100% for values) float = "%d?%.?%d+", -- this is _very_ crude, but probably fast comma = "%s*,%s*", -- whitespace optional comma space = "%s+", -- whitespace required @@ -336,411 +331,381 @@ local color_patterns = { -- }}} -- {{{ Functional patterns -- {{{ rgb(d, d, d) integers 0-255 - { "rgbi", table.concat({ - css.rgb, -- functional header - capt(css.integer), -- red - css.comma, -- separator - capt(css.integer), -- green - css.comma, -- separator - capt(css.integer), -- blue + { "rgbi", + css.rgb .. -- functional header + capt(css.integer) .. -- red + css.comma .. -- separator + capt(css.integer) .. -- green + css.comma .. -- separator + capt(css.integer) .. -- blue css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(d, d, d, f) integers 0-255, float 0-1 - { "rgbif", table.concat({ - css.rgb, -- functional header - capt(css.integer), -- red - css.comma, -- separator - capt(css.integer), -- green - css.comma, -- separator - capt(css.integer), -- blue - css.comma, -- separator - capt(css.float), -- alpha + { "rgbif", + css.rgb .. -- functional header + capt(css.integer) .. -- red + css.comma .. -- separator + capt(css.integer) .. -- green + css.comma .. -- separator + capt(css.integer) .. -- blue + css.comma .. -- separator + capt(css.float) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(d, d, d, %) integers 0-255, percentage 0-100 - { "rgbip", table.concat({ - css.rgb, -- functional header - capt(css.integer), -- red - css.comma, -- separator - capt(css.integer), -- green - css.comma, -- separator - capt(css.integer), -- blue - css.comma, - perc(capt(css.integer)), -- alpha + { "rgbip", + css.rgb .. -- functional header + capt(css.integer) .. -- red + css.comma .. -- separator + capt(css.integer) .. -- green + css.comma .. -- separator + capt(css.integer) .. -- blue + css.comma .. + perc(capt(css.integer)) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(d d d) integers 0-255 - { "rgbi", table.concat({ - css.rgb, -- functional header - capt(css.integer), -- red - css.space, -- separator - capt(css.integer), -- green - css.space, -- separator - capt(css.integer), -- blue + { "rgbi", + css.rgb .. -- functional header + capt(css.integer) .. -- red + css.space .. -- separator + capt(css.integer) .. -- green + css.space .. -- separator + capt(css.integer) .. -- blue css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(d d d / f) integers 0-255, float 0-1 - { "rgbif", table.concat({ - css.rgb, -- functional header - capt(css.integer), -- red - css.space, -- separator - capt(css.integer), -- green - css.space, -- separator - capt(css.integer), -- blue - css.slash, - capt(css.float), -- alpha + { "rgbif", + css.rgb .. -- functional header + capt(css.integer) .. -- red + css.space .. -- separator + capt(css.integer) .. -- green + css.space .. -- separator + capt(css.integer) .. -- blue + css.slash .. + capt(css.float) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(d d d / %) integers 0-255, percentage 0-100 - { "rgbip", table.concat({ - css.rgb, -- functional header - capt(css.integer), -- red - css.space, -- separator - capt(css.integer), -- green - css.space, -- separator - capt(css.integer), -- blue - css.slash, - perc(capt(css.integer)), -- alpha + { "rgbip", + css.rgb .. -- functional header + capt(css.integer) .. -- red + css.space .. -- separator + capt(css.integer) .. -- green + css.space .. -- separator + capt(css.integer) .. -- blue + css.slash .. + perc(capt(css.integer)) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(%, %, %) percentage 0-100 - { "rgbp", table.concat({ - css.rgb, -- functional header - perc(capt(css.integer)), -- red - css.comma, -- separator - perc(capt(css.integer)), -- green - css.comma, -- separator - perc(capt(css.integer)), -- blue + { "rgbp", + css.rgb .. -- functional header + perc(capt(css.integer)) .. -- red + css.comma .. -- separator + perc(capt(css.integer)) .. -- green + css.comma .. -- separator + perc(capt(css.integer)) .. -- blue css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(%, %, %, f) percentage 0-100, float 0-1 - { "rgbpf", table.concat({ - css.rgb, -- functional header - perc(capt(css.integer)), -- red - css.comma, -- separator - perc(capt(css.integer)), -- green - css.comma, -- separator - perc(capt(css.integer)), -- blue - css.comma, - capt(css.float), -- alpha + { "rgbpf", + css.rgb .. -- functional header + perc(capt(css.integer)) .. -- red + css.comma .. -- separator + perc(capt(css.integer)) .. -- green + css.comma .. -- separator + perc(capt(css.integer)) .. -- blue + css.comma .. + capt(css.float) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(%, %, %, %) percentage 0-100 - { "rgbpp", table.concat({ - css.rgb, -- functional header - perc(capt(css.integer)), -- red - css.comma, -- separator - perc(capt(css.integer)), -- green - css.comma, -- separator - perc(capt(css.integer)), -- blue - css.comma, -- separator - perc(capt(css.integer)), -- alpha + { "rgbpp", + css.rgb .. -- functional header + perc(capt(css.integer)) .. -- red + css.comma .. -- separator + perc(capt(css.integer)) .. -- green + css.comma .. -- separator + perc(capt(css.integer)) .. -- blue + css.comma .. -- separator + perc(capt(css.integer)) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(% % %) percentage 0-100 - { "rgbp", table.concat({ - css.rgb, -- functional header - perc(capt(css.integer)), -- red - css.space, -- separator - perc(capt(css.integer)), -- green - css.space, -- separator - perc(capt(css.integer)), -- blue + { "rgbp", + css.rgb .. -- functional header + perc(capt(css.integer)) .. -- red + css.space .. -- separator + perc(capt(css.integer)) .. -- green + css.space .. -- separator + perc(capt(css.integer)) .. -- blue css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(% % % / f) percentage 0-100, float 0-1 - { "rgbpf", table.concat({ - css.rgb, -- functional header - perc(capt(css.integer)), -- red - css.space, -- separator - perc(capt(css.integer)), -- green - css.space, -- separator - perc(capt(css.integer)), -- blue - css.slash, - capt(css.float), -- alpha + { "rgbpf", + css.rgb .. -- functional header + perc(capt(css.integer)) .. -- red + css.space .. -- separator + perc(capt(css.integer)) .. -- green + css.space .. -- separator + perc(capt(css.integer)) .. -- blue + css.slash .. + capt(css.float) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgb(% % % / %) percentage 0-100 - { "rgbpp", table.concat({ - css.rgb, -- functional header - perc(capt(css.integer)), -- red - css.space, -- separator - perc(capt(css.integer)), -- green - css.space, -- separator - perc(capt(css.integer)), -- blue - css.slash, - perc(capt(css.integer)), -- alpha + { "rgbpp", + css.rgb .. -- functional header + perc(capt(css.integer)) .. -- red + css.space .. -- separator + perc(capt(css.integer)) .. -- green + css.space .. -- separator + perc(capt(css.integer)) .. -- blue + css.slash .. + perc(capt(css.integer)) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgba(d, d, d, f) integers 0-255, float 0-1 - { "rgbif", table.concat({ - css.rgba, -- functional header - capt(css.integer), -- red - css.comma, -- separator - capt(css.integer), -- green - css.comma, -- separator - capt(css.integer), -- blue - css.comma, -- separator - capt(css.float), -- alpha + { "rgbif", + css.rgba .. -- functional header + capt(css.integer) .. -- red + css.comma .. -- separator + capt(css.integer) .. -- green + css.comma .. -- separator + capt(css.integer) .. -- blue + css.comma .. -- separator + capt(css.float) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgba(d, d, d, %) integers 0-255, percentage 0-100 - { "rgbip", table.concat({ - css.rgba, -- functional header - capt(css.integer), -- red - css.comma, -- separator - capt(css.integer), -- green - css.comma, -- separator - capt(css.integer), -- blue - css.comma, - perc(capt(css.integer)), -- alpha + { "rgbip", + css.rgba .. -- functional header + capt(css.integer) .. -- red + css.comma .. -- separator + capt(css.integer) .. -- green + css.comma .. -- separator + capt(css.integer) .. -- blue + css.comma .. + perc(capt(css.integer)) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgba(d d d / f) integers 0-255, float 0-1 - { "rgbif", table.concat({ - css.rgba, -- functional header - capt(css.integer), -- red - css.space, -- separator - capt(css.integer), -- green - css.space, -- separator - capt(css.integer), -- blue - css.slash, - capt(css.float), -- alpha + { "rgbif", + css.rgba .. -- functional header + capt(css.integer) .. -- red + css.space .. -- separator + capt(css.integer) .. -- green + css.space .. -- separator + capt(css.integer) .. -- blue + css.slash .. + capt(css.float) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgba(d d d / %) integers 0-255, percentage 0-100 - { "rgbip", table.concat({ - css.rgba, -- functional header - capt(css.integer), -- red - css.space, -- separator - capt(css.integer), -- green - css.space, -- separator - capt(css.integer), -- blue - css.slash, - perc(capt(css.integer)), -- alpha + { "rgbip", + css.rgba .. -- functional header + capt(css.integer) .. -- red + css.space .. -- separator + capt(css.integer) .. -- green + css.space .. -- separator + capt(css.integer) .. -- blue + css.slash .. + perc(capt(css.integer)) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgba(%, %, %, f) percentage 0-100, float 0-1 - { "rgbpf", table.concat({ - css.rgba, -- functional header - perc(capt(css.integer)), -- red - css.comma, -- separator - perc(capt(css.integer)), -- green - css.comma, -- separator - perc(capt(css.integer)), -- blue - css.comma, - capt(css.float), -- alpha + { "rgbpf", + css.rgba .. -- functional header + perc(capt(css.integer)) .. -- red + css.comma .. -- separator + perc(capt(css.integer)) .. -- green + css.comma .. -- separator + perc(capt(css.integer)) .. -- blue + css.comma .. + capt(css.float) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgba(%, %, %, %) percentage 0-100 - { "rgbpp", table.concat({ - css.rgba, -- functional header - perc(capt(css.integer)), -- red - css.comma, -- separator - perc(capt(css.integer)), -- green - css.comma, -- separator - perc(capt(css.integer)), -- blue - css.comma, -- separator - perc(capt(css.integer)), -- alpha + { "rgbpp", + css.rgba .. -- functional header + perc(capt(css.integer)) .. -- red + css.comma .. -- separator + perc(capt(css.integer)) .. -- green + css.comma .. -- separator + perc(capt(css.integer)) .. -- blue + css.comma .. -- separator + perc(capt(css.integer)) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgba(% % % / f) percentage 0-100, float 0-1 - { "rgbpf", table.concat({ - css.rgba, -- functional header - perc(capt(css.integer)), -- red - css.space, -- separator - perc(capt(css.integer)), -- green - css.space, -- separator - perc(capt(css.integer)), -- blue - css.slash, - capt(css.float), -- alpha + { "rgbpf", + css.rgba .. -- functional header + perc(capt(css.integer)) .. -- red + css.space .. -- separator + perc(capt(css.integer)) .. -- green + css.space .. -- separator + perc(capt(css.integer)) .. -- blue + css.slash .. + capt(css.float) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ rgba(% % % / %) percentage 0-100 - { "rgbpp", table.concat({ - css.rgba, -- functional header - perc(capt(css.integer)), -- red - css.space, -- separator - perc(capt(css.integer)), -- green - css.space, -- separator - perc(capt(css.integer)), -- blue - css.slash, - perc(capt(css.integer)), -- alpha + { "rgbpp", + css.rgba .. -- functional header + perc(capt(css.integer)) .. -- red + css.space .. -- separator + perc(capt(css.integer)) .. -- green + css.space .. -- separator + perc(capt(css.integer)) .. -- blue + css.slash .. + perc(capt(css.integer)) .. -- alpha css.eol -- EOL - }, "") }, -- }}} -- {{{ hsl(d, %, %) integers 0-360, percentage 0-100 - { "hsl", table.concat({ - css.hsl, - capt(css.integer), -- hue - css.comma, - perc(capt(css.integer)), -- saturation - css.comma, - perc(capt(css.integer)), -- lightness + { "hsl", + css.hsl .. + capt(css.integer) .. -- hue + css.comma .. + perc(capt(css.integer)) .. -- saturation + css.comma .. + perc(capt(css.integer)) .. -- lightness css.eol - }, "") }, -- }}} -- {{{ hsl(d, %, %, f) integers 0-360, percentage 0-100, float 0-1 - { "hslxf", table.concat({ - css.hsl, - capt(css.integer), -- hue - css.comma, - perc(capt(css.integer)), -- saturation - css.comma, - perc(capt(css.integer)), -- lightness - css.comma, - capt(css.float), -- alpha + { "hslxf", + css.hsl .. + capt(css.integer) .. -- hue + css.comma .. + perc(capt(css.integer)) .. -- saturation + css.comma .. + perc(capt(css.integer)) .. -- lightness + css.comma .. + capt(css.float) .. -- alpha css.eol - }, "") }, -- }}} -- {{{ hsl(d, %, %, %) integers 0-360, percentage 0-100 - { "hslxp", table.concat({ - css.hsl, - capt(css.integer), -- hue - css.comma, - perc(capt(css.integer)), -- saturation - css.comma, - perc(capt(css.integer)), -- lightness - css.comma, - perc(capt(css.integer)), -- alpha + { "hslxp", + css.hsl .. + capt(css.integer) .. -- hue + css.comma .. + perc(capt(css.integer)) .. -- saturation + css.comma .. + perc(capt(css.integer)) .. -- lightness + css.comma .. + perc(capt(css.integer)) .. -- alpha css.eol - }, "") }, -- }}} -- {{{ hsl(d % %) integers 0-360, percentage 0-100 - { "hsl", table.concat({ - css.hsl, - capt(css.integer), -- hue - css.space, - perc(capt(css.integer)), -- saturation - css.space, - perc(capt(css.integer)), -- lightness + { "hsl", + css.hsl .. + capt(css.integer) .. -- hue + css.space .. + perc(capt(css.integer)) .. -- saturation + css.space .. + perc(capt(css.integer)) .. -- lightness css.eol - }, "") }, -- }}} -- {{{ hsl(d % % / f) integers 0-360, percentage 0-100, float 0-1 - { "hslxf", table.concat({ - css.hsl, - capt(css.integer), -- hue - css.space, - perc(capt(css.integer)), -- saturation - css.space, - perc(capt(css.integer)), -- lightness - css.slash, - capt(css.float), -- alpha + { "hslxf", + css.hsl .. + capt(css.integer) .. -- hue + css.space .. + perc(capt(css.integer)) .. -- saturation + css.space .. + perc(capt(css.integer)) .. -- lightness + css.slash .. + capt(css.float) .. -- alpha css.eol - }, "") }, -- }}} -- {{{ hsl(d % % / %) integers 0-360, percentage 0-100 - { "hslxp", table.concat({ - css.hsl, - capt(css.integer), -- hue - css.space, - perc(capt(css.integer)), -- saturation - css.space, - perc(capt(css.integer)), -- lightness - css.slash, - perc(capt(css.integer)), -- alpha + { "hslxp", + css.hsl .. + capt(css.integer) .. -- hue + css.space .. + perc(capt(css.integer)) .. -- saturation + css.space .. + perc(capt(css.integer)) .. -- lightness + css.slash .. + perc(capt(css.integer)) .. -- alpha css.eol - }, "") }, -- }}} -- {{{ hsla(d, %, %, f) integers 0-360, percentage 0-100, float 0-1 - { "hslxf", table.concat({ - css.hsla, - capt(css.integer), -- hue - css.comma, - perc(capt(css.integer)), -- saturation - css.comma, - perc(capt(css.integer)), -- lightness - css.comma, - capt(css.float), -- alpha + { "hslxf", + css.hsla .. + capt(css.integer) .. -- hue + css.comma .. + perc(capt(css.integer)) .. -- saturation + css.comma .. + perc(capt(css.integer)) .. -- lightness + css.comma .. + capt(css.float) .. -- alpha css.eol - }, "") }, -- }}} -- {{{ hsla(d, %, %, %) integers 0-360, percentage 0-100 - { "hslxp", table.concat({ - css.hsla, - capt(css.integer), -- hue - css.comma, - perc(capt(css.integer)), -- saturation - css.comma, - perc(capt(css.integer)), -- lightness - css.comma, - perc(capt(css.integer)), -- alpha + { "hslxp", + css.hsla .. + capt(css.integer) .. -- hue + css.comma .. + perc(capt(css.integer)) .. -- saturation + css.comma .. + perc(capt(css.integer)) .. -- lightness + css.comma .. + perc(capt(css.integer)) .. -- alpha css.eol - }, "") }, -- }}} -- {{{ hsla(d % % / f) integers 0-360, percentage 0-100, float 0-1 - { "hslxf", table.concat({ - css.hsla, - capt(css.integer), -- hue - css.space, - perc(capt(css.integer)), -- saturation - css.space, - perc(capt(css.integer)), -- lightness - css.slash, - capt(css.float), -- alpha + { "hslxf", + css.hsla .. + capt(css.integer) .. -- hue + css.space .. + perc(capt(css.integer)) .. -- saturation + css.space .. + perc(capt(css.integer)) .. -- lightness + css.slash .. + capt(css.float) .. -- alpha css.eol - }, "") }, -- }}} -- {{{ hsla(d % % / %) integers 0-360, percentage 0-100 - { "hslxp", table.concat({ - css.hsla, - capt(css.integer), -- hue - css.space, - perc(capt(css.integer)), -- saturation - css.space, - perc(capt(css.integer)), -- lightness - css.slash, - perc(capt(css.integer)), -- alpha + { "hslxp", + css.hsla .. + capt(css.integer) .. -- hue + css.space .. + perc(capt(css.integer)) .. -- saturation + css.space .. + perc(capt(css.integer)) .. -- lightness + css.slash .. + perc(capt(css.integer)) .. -- alpha css.eol - }, "") }, -- }}} -- }}} @@ -769,6 +734,7 @@ function color.from.string(str) if ta ~= nil then a = tonumber(m[4], 16) end + if r == nil or g == nil or b == nil or a == nil then return nil end -- if type is shorthand if tv == "s" then r = r * 16 + r @@ -781,33 +747,35 @@ function color.from.string(str) local r = tonumber(m[1]) local g = tonumber(m[2]) local b = tonumber(m[3]) - local a = 255 + if r == nil or g == nil or b == nil then return nil end -- if values percentages if tv == "p" then r,g,b = r/100 * 255, g/100 * 255, b/100 * 255 end -- check for alpha + local a = 255 if ta ~= nil then a = tonumber(m[4]) + if a == nil then return nil end if ta == "p" then a = a/100 end a = a * 255 -- both float and percentage need this end - if r == nil or g == nil or b == nil or a == nil then return nil end return color.from.rgba(r,g,b,a) elseif th == "hsl" then local h = tonumber(m[1]) local s = tonumber(m[2]) / 100 -- always percentage local l = tonumber(m[3]) / 100 -- always percentage local a = 1 + if h == nil or s == nil or l == nil then return nil end if ta ~= nil then a = tonumber(m[4]) + if a == nil then return nil end if ta == "p" then a = a / 100 end end - if h == nil or s == nil or l == nil or a == nil then return nil end return color.from.hsla(h,s,l,a) end end @@ -859,8 +827,6 @@ function color.from.rgba(r,g,b,a) g = clamp(g, 0, 255) / 255 b = clamp(b, 0, 255) / 255 a = clamp(a, 0, 255) / 255 - -- make sure values are valid - if r == nil or g == nil or b == nil or a == nil then return nil end -- hsl function expects RGB in range [0,1] local h,s,l = rgb_to_hsl(r,g,b) return create_color_object(r,g,b,h,s,l,a) @@ -900,7 +866,6 @@ function color.from.hsla(h,s,l,a) end s = clamp(s, 0.0, 1.0) l = clamp(l, 0.0, 1.0) - if s == nil or l == nil then return nil end local r,g,b = hsl_to_rgb(h,s,l) return create_color_object(r,g,b,h,s,l,a) end @@ -1023,7 +988,7 @@ end -- {{{ Color harmony functions ---- Returns the complement of a color +--- Returns the complement of a color (+180deg hue) -- Adds 180 to Hue (hue will self-adjust to be within [0,360]) -- @return A new color object representing the complementary color, or nil if invalid -- @usage local complemented_color = c:complement() @@ -1032,13 +997,13 @@ function color:complement() return color.from.hsla(hue, self.saturation, self.lightness, self.alpha) end ---- Returns the left and right analogous colors (30deg hue shift) +--- Returns the left and right analogous colors (+30deg, -30deg hue) -- @return A new color object representing a -30 degree hue shift of the color, or nil if invalid -- @return A new color object representing a +30 degree hue shift of the color, or nil if invalid -- @usage local left, right = c:analogous() function color:analogous() - local left = self.hue - 30 - local right = self.hue + 30 + local left = self.hue + 30 + local right = self.hue - 30 return color.from.hsla( left, self.saturation, @@ -1051,8 +1016,7 @@ function color:analogous() self.alpha) end ---- Returns the left and right complementary colors (30deg shift from ---complementary color) +--- Returns the left and right complementary colors (+210deg, +150deg hue) -- @return A new color object representing the +210 degree hue shift (180 + 30) -- of the color, or nil if invalid -- @return A new color object representing the +150 degree hue shift (180 - 30) @@ -1073,7 +1037,7 @@ function color:split_complementary() self.alpha) end ---- Returns the left and right triadic colors (120deg shift from base color) +--- Returns the left and right triadic colors (+120deg, -120deg hue) -- @return A new color object representing a +120 degree hue shift of the color, or nil if invalid -- @return A new color object representing a -120 degree hue shift of the color, or nil if invalid -- @usage local left, right = c:triadic() @@ -1092,8 +1056,7 @@ function color:triadic() self.alpha) end ---- Returns three colors making up the tetradic colors (base complement, base ---+ 60deg, base + 60deg complement) +--- Returns three colors making up the tetradic colors (+180deg, +60deg, +240deg hue) -- @return A new color object representing the complementary color, or nil if -- invalid -- @return A new color object representing a +60 degree shift of the color, or @@ -1122,8 +1085,8 @@ end --- Return the string representation of the color in #RRGGBBAA format -- @treturn string A string represention of the color in #RRGGBBAA format --- @usage local hexa_str = c.as:hexa() -function color.as:hexa() +-- @usage local hexa_str = c:hexa() +function color:hexa() local r = round(self.red * 255) local g = round(self.green * 255) local b = round(self.blue * 255) @@ -1133,8 +1096,8 @@ end --- Return the string representation of the color in #RRGGBB format -- @treturn string A string represention of the color in #RRGGBB format --- @usage local hex_str = c.as:hex() -function color.as:hex() +-- @usage local hex_str = c:hex() +function color:hex() local r = round(self.red * 255) local g = round(self.green * 255) local b = round(self.blue * 255) @@ -1148,8 +1111,8 @@ end --- Return the decimal representation of the color including the alpha channel -- @treturn number The decimal represention of the color including the alpha -- channel --- @usage local deca_color = c.as:deca() -function color.as:deca() +-- @usage local deca_color = c:deca() +function color:deca() return math.floor( lshift(round(self.red * 0xFF), 24) + lshift(round(self.green * 0xFF), 16) @@ -1161,8 +1124,8 @@ end --- Return the decimal representation of the color -- @treturn number The decimal represention of the color not including the -- alpha channel --- @usage local dec_color = c.as:dec() -function color.as:dec() +-- @usage local dec_color = c:dec() +function color:dec() return math.floor( lshift(round(self.red * 0xFF), 16) + lshift(round(self.green * 0xFF), 8) @@ -1179,27 +1142,23 @@ end -- @treturn number Saturation [0,1] -- @treturn number Lightness [0,1] -- @treturn number Alpha [0,1] --- @usage local h,s,l,a = c.as:hsla() -function color.as:hsla() - return unpack({ - self.hue, +-- @usage local h,s,l,a = c:hsla() +function color:hsla() + return self.hue, self.saturation, self.lightness, self.alpha - }) end --- Return the Hue, Saturation, and Lightness -- @treturn number Hue [0,360] -- @treturn number Saturation [0,1] -- @treturn number Lightness [0,1] --- @usage local h,s,l = c.as:hsl() -function color.as:hsl() - return unpack({ - self.hue, +-- @usage local h,s,l = c:hsl() +function color:hsl() + return self.hue, self.saturation, - self.lightness, - }) + self.lightness end --- Return the Red, Green, Blue, and Alpha @@ -1207,27 +1166,23 @@ end -- @treturn number Green [0,1] -- @treturn number Blue [0,1] -- @treturn number Alpha [0,1] --- @usage local r,g,b,a = c.as:rgba() -function color.as:rgba() - return unpack({ - self.red, +-- @usage local r,g,b,a = c:rgba() +function color:rgba() + return self.red, self.green, self.blue, self.alpha - }) end --- Return the Red, Green, and Blue -- @treturn number Red [0,1] -- @treturn number Green [0,1] -- @treturn number Blue [0,1] --- @usage local r,g,b = c.as:rgb() -function color.as:rgb() - return unpack({ - self.red, +-- @usage local r,g,b = c:rgb() +function color:rgb() + return self.red, self.green, - self.blue, - }) + self.blue end -- }}} Multi-return output @@ -1236,71 +1191,8 @@ end -- }}} Color output functions ---- Color metamethods --- @section color_meta - color_mt.__index = color -color_mt.__tostring = color.as.hex - ---- Shorthand invert of a color --- @function color.__unm --- @see beautiful.colorful.color.invert -color_mt.__unm = color.invert - ---- Add the RGBA values of two colors --- @function color.__add --- @tparam lhs beautiful.colorful.color A color object --- @tparam rhs beautiful.colorful.color A color object --- @treturn beautiful.colorful.color A new color object with RGBA values added --- @usage colorful.color("#123") + colorful.color("#321") -- returns #444444 -color_mt.__add = function(lhs, rhs) - if type(rhs) ~= "table" then return nil end - return color.from.rgba( - (lhs.red + rhs.red) * 255, - (lhs.green + rhs.green) * 255, - (lhs.blue + rhs.blue) * 255, - (lhs.alpha + rhs.alpha) * 255 - ) -end - ---- Subtract the RGBA values of two colors --- @function color.__sub --- @tparam lhs beautiful.colorful.color A color object --- @tparam rhs beautiful.colorful.color A color object --- @treturn beautiful.colorful.color A new color object with RGBA values --- subtracted --- @usage colorful.color("#123") - colorful.color("#321") -- returns #000022 -color_mt.__sub = function(lhs, rhs) - if type(rhs) ~= "table" then return nil end - return color.from.rgba( - (lhs.red - rhs.red) * 255, - (lhs.green - rhs.green) * 255, - (lhs.blue - rhs.blue) * 255, - (lhs.alpha - rhs.alpha) * 255 - ) -end - ---- Compare the lightness value of two colors --- @function color.__lt --- @tparam lhs beautiful.colorful.color A color object --- @tparam rhs beautiful.colorful.color A color object --- @treturn boolean True if lhs lightness is less than rhs lightness --- @usage colorful.color("#ffcc99") < colorful.color("#aabbcc") -- returns false (0.8 < 0.73333) -color_mt.__lt = function(lhs, rhs) - if type(rhs) ~= "table" then return nil end - return lhs.lightness < rhs.lightness -end - ---- Compare the lightness value of two colors --- @function color.__gt --- @tparam lhs beautiful.colorful.color A color object --- @tparam rhs beautiful.colorful.color A color object --- @treturn boolean True if lhs lightness is greater than rhs lightness --- @usage colorful.color("#ffcc99") > colorful.color("#aabbcc") -- returns true (0.8 > 0.73333) -color_mt.__gt = function(lhs, rhs) - if type(rhs) ~= "table" then return nil end - return lhs.lightness > rhs.lightness -end +color_mt.__tostring = color.hex --- A color object -- @type color @@ -1309,6 +1201,7 @@ colorful.color = color setmetatable(color, { __call = function(_,...) return color.new(...) end, }) + -- {{{ Color changing wrapper functions --- Wrappers for Color object functions diff --git a/spec/beautiful/colorful_spec.lua b/spec/beautiful/colorful_spec.lua index 9a03efc953..275bffe520 100644 --- a/spec/beautiful/colorful_spec.lua +++ b/spec/beautiful/colorful_spec.lua @@ -377,7 +377,7 @@ describe("Colorful", function() -- we convert to [0,1] local h,s,l = col[2][1], col[2][2]/100, col[2][3]/100 local c = Color.new({h,s,l}) - assert.is.same(col[1], c.as:hex()) + assert.is.same(col[1], c:hex()) end end) @@ -385,14 +385,14 @@ describe("Colorful", function() for _,col in ipairs(colors) do local c = Color.new(col[1]) local dec = tonumber(col[1]:sub(2), 16) - assert.is.same(c.as:dec(), dec) + assert.is.same(c:dec(), dec) end end) it("from functional string", function() for _,col in ipairs(color_functional) do local c= Color.new(col[2]) - assert.is.same(col[1], c.as:hexa()) + assert.is.same(col[1], c:hexa()) end end) end) @@ -402,7 +402,7 @@ describe("Colorful", function() for _,col in ipairs(colors) do local c = Color.new(col[1]) c = c:lighten(0.25) - assert.is.same(col[3], c.as:hex()) + assert.is.same(col[3], c:hex()) end end) @@ -410,7 +410,7 @@ describe("Colorful", function() for _,col in ipairs(colors) do local c = Color.new(col[1]) c = c:darken(0.25) - assert.is.same(col[4], c.as:hex()) + assert.is.same(col[4], c:hex()) end end) @@ -418,7 +418,7 @@ describe("Colorful", function() for _,col in ipairs(colors) do local c = Color.new(col[1]) c = c:invert() - assert.is.same(col[5], c.as:hex()) + assert.is.same(col[5], c:hex()) end end) @@ -427,7 +427,7 @@ describe("Colorful", function() local c1 = Color.new(col[1]) local c2 = Color.new(col[5]) local mix = c1:mix(c2, 0) - assert.is.same(col[5], mix.as:hex()) + assert.is.same(col[5], mix:hex()) end end) @@ -436,7 +436,7 @@ describe("Colorful", function() local c1 = Color.new(col[1]) local c2 = Color.new(col[5]) local mix = c1:mix(c2, 0.25) - assert.is.same(col[6], mix.as:hex()) + assert.is.same(col[6], mix:hex()) end end) @@ -445,7 +445,7 @@ describe("Colorful", function() local c1 = Color.new(col[1]) local c2 = Color.new(col[5]) local mix = c1:mix(c2, 0.5) - assert.is.same("#808080", mix.as:hex()) + assert.is.same("#808080", mix:hex()) end end) @@ -454,7 +454,7 @@ describe("Colorful", function() local c1 = Color.new(col[1]) local c2 = Color.new(col[5]) local mix = c1:mix(c2, 0.75) - assert.is.same(col[7], mix.as:hex()) + assert.is.same(col[7], mix:hex()) end end) @@ -463,7 +463,7 @@ describe("Colorful", function() local c1 = Color.new(col[1]) local c2 = Color.new(col[5]) local mix = c1:mix(c2, 1) - assert.is.same(col[1], mix.as:hex()) + assert.is.same(col[1], mix:hex()) end end) @@ -471,7 +471,7 @@ describe("Colorful", function() for _,col in ipairs(colors) do local c = Color.new(col[1]) c = c:saturate(0.25) - assert.is.same(col[8], c.as:hex()) + assert.is.same(col[8], c:hex()) end end) @@ -479,7 +479,7 @@ describe("Colorful", function() for _,col in ipairs(colors) do local c = Color.new(col[1]) c = c:desaturate(0.25) - assert.is.same(col[9], c.as:hex()) + assert.is.same(col[9], c:hex()) end end) @@ -487,7 +487,7 @@ describe("Colorful", function() for _,col in ipairs(colors) do local c = Color.new(col[1]) c = c:grayscale() - assert.is.same(col[10], c.as:hex()) + assert.is.same(col[10], c:hex()) end end) end) diff --git a/themes/gtk/theme.lua b/themes/gtk/theme.lua index a97f5b80f3..488f629c01 100644 --- a/themes/gtk/theme.lua +++ b/themes/gtk/theme.lua @@ -31,7 +31,8 @@ local function is_dark(color_value) return is_dark_bg end local function reduce_contrast(color, ratio) - ratio = (ratio or 127) / 255 + -- turn ratio to a percentage range [0,1] + ratio = (ratio or 50) / 100 -- Negative number here will lighten return colorful.darken(color, is_dark(tostring(color)) and -ratio or ratio) end