local utils package.preload["lua-color.utils"] = package.preload["lua-color.utils"] or function(...) local function min_index(first, ...) local min, index = first, 1 for i, v in ipairs {...} do if v < min then min, index = v, i + 1 end end return min, index end local function max_index(first, ...) local max, index = first, 1 for i, v in ipairs {...} do if v > max then max, index = v, i + 1 end end return max, index end local function round(x) return x + 0.5 - (x + 0.5) % 1 end local function clamp(x, min, max) return x < min and min or x > max and max or x end local function map(t, cb) local n = {} for i, v in ipairs(t) do n[i] = cb(v) end return n end return { min = min_index, max = max_index, round = round, clamp = clamp, map = map, } end utils = require("lua-color.utils") local class package.preload["lua-color.utils.class"] = package.preload["lua-color.utils.class"] or function(...) -- Code based on: -- http://lua-users.org/wiki/SimpleLuaClasses --- Helper function to create classes -- -- @usage local Color = class(function () --[[ constructor ]] end) -- @usage local Color2 = class( -- Color, -- function () --[[ constructor ]] end, -- { prop_a = "some value" } -- ) local function class(base, init, defaults) local c = defaults or {} -- a new class instance if not init and type(base) == 'function' then init = base base = nil elseif type(base) == 'table' then -- our new class is a shallow copy of the base class! for i,v in pairs(base) do c[i] = v end c._base = base end -- the class will be the metatable for all its objects, -- and they will look up their methods in it. c.__index = c -- expose a constructor which can be called by () local mt = {} mt.__call = function(class_tbl, ...) local obj = {} setmetatable(obj,c) if init then init(obj,...) else -- make sure that any stuff from the base class is initialized! if base and base.init then base.init(obj, ...) end end return obj end c.init = init c.is_a = function(self, klass) local m = getmetatable(self) while m do if m == klass then return true end m = m._base end return false end setmetatable(c, mt) return c end return class end class = require("lua-color.utils.class") local bitwise package.preload["lua-color.utils.bitwise"] = package.preload["lua-color.utils.bitwise"] or function(...) -- Implementations of bitwise operators so that lua-color can be used -- with Lua 5.1 and LuaJIT 2.1.0-beta3 (e.g. inside Neovim). -- Code taken directly from: -- https://stackoverflow.com/questions/5977654/how-do-i-use-the-bitwise-operator-xor-in-lua local function bit_xor(a, b) local p, c = 1, 0 while a > 0 and b > 0 do local ra, rb = a % 2, b % 2 if ra ~= rb then c = c + p end a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2 end if a < b then a = b end while a > 0 do local ra = a % 2 if ra > 0 then c = c + p end a, p = (a - ra) / 2, p * 2 end return c end local function bit_or(a, b) local p, c = 1, 0 while a + b > 0 do local ra, rb = a % 2, b % 2 if ra + rb > 0 then c = c + p end a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2 end return c end local function bit_not(n) local p, c = 1, 0 while n > 0 do local r = n % 2 if r < 1 then c = c + p end n, p = (n - r) / 2, p * 2 end return c end local function bit_and(a, b) local p, c = 1, 0 while a > 0 and b > 0 do local ra, rb = a % 2, b % 2 if ra + rb > 1 then c = c + p end a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2 end return c end local function bit_lshift(x, by) return x * 2 ^ by end local function bit_rshift(x, by) return math.floor(x / 2 ^ by) end return { bit_xor = bit_xor, bit_or = bit_or, bit_not = bit_not, bit_and = bit_and, bit_lshift = bit_lshift, bit_rshift = bit_rshift, } end bitwise = require("lua-color.utils.bitwise") local Color package.preload["lua-color"] = package.preload["lua-color"] or function(...) --- Parse, convert and manipulate color values. -- -- @classmod Color local utils = require "lua-color.utils" local class = require "lua-color.utils.class" -- Lua 5.1 compat local bitwise = require "lua-color.utils.bitwise" local bit_and = bitwise.bit_and local bit_lshift = bitwise.bit_lshift local bit_rshift = bitwise.bit_rshift -- Utils local function hcm_to_rgb(h, c, m) local r, g, b = 0, 0, 0 h = h * 6 local x = c * (1 - math.abs(h % 2 - 1)) if h <= 1 then r, g, b = c, x, 0 elseif h <= 2 then r, g, b = x, c, 0 elseif h <= 3 then r, g, b = 0, c, x elseif h <= 4 then r, g, b = 0, x, c elseif h <= 5 then r, g, b = x, 0, c elseif h <= 6 then r, g, b = c, 0, x end return r + m, g + m, b + m end local function tonumPercent(str) if str:sub(-1) == "%" then return tonumber(str:sub(1, #str - 1)) / 100 end return tonumber(str) end -- Color --- Color constructor. -- -- @function Color:__call -- -- @tparam ?string|table|Color value Color value (default: `nil`) -- -- @see Color:set --- Red component. -- @field r --- Green component. -- @field g --- Blue component. -- @field b --- Alpha component. -- @field a --- Color class local Color = class(nil, function (this, value) if value then this:set(value) end end, { __is_color = true, r = 0, g = 0, b = 0, a = 1, }) --- Table of color names. --
-- Can be set to a table containing named colors to be used by `Color:set` --
-- Values must be compatible with `Color:set` --
-- Default: `nil` -- -- @usage Color.colorNames = { red = "#ff0000", green = "#00ff00", blue = "#0000ff" } --local color = Color "green" Color.colorNames = nil --- Clone color -- -- @treturn Color copy function Color:clone() return Color(self) end --- Set color to value. --
-- Called by constructor --

-- Possible value types: -- -- -- @see Color:__call -- -- @tparam string|table|Color value Color -- -- @treturn Color self -- -- @usage color:set "#f1f1f1" -- @usage color:set "rgba(241, 241, 241, 0.5)" -- @usage color:set "hsl 180 100% 20%" -- @usage color:set { r = 0.255, g = 0.729, b = 0.412 } -- @usage color:set { 0.255, 0.729, 0.412 } -- same as above -- @usage color:set { h = 0.389, s = 0.65, v = 0.73 } function Color:set(value) assert(value) -- from Color if value.__is_color then self.r = value.r self.g = value.g self.b = value.b self.a = value.a elseif type(value) == "string" then self.a = 1 if value:sub(1, 1) ~= "#" then if Color.colorNames then local c = Color.colorNames[value] if c then return self:set(c) end end local func, values = value:match "(%w+)[ %(]+([x ,.%x%%]+)" if func ~= nil then if func == "rgb" then local r, g, b = values:match "([x.%x]+)[ ,]+([x.%x]+)[ ,]+([x.%x]+)" assert(r and g and b) self.r = tonumber(r) / 0xff self.g = tonumber(g) / 0xff self.b = tonumber(b) / 0xff return self elseif func == "rgba" then local r, g, b, a = values:match "([x.%x]+)[ ,]+([x.%x]+)[ ,]+([x.%x]+)[ ,]+([x.%x]+%%?)" assert(r and g and b and a) self.r = tonumber(r) / 0xff self.g = tonumber(g) / 0xff self.b = tonumber(b) / 0xff self.a = tonumPercent(a) return self elseif func == "hsv" then local h, s, v = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)" assert(h and s and v) return self:set { h = tonumber(h) / 360, s = tonumPercent(s), v = tonumPercent(v), } elseif func == "hsva" then local h, s, v, a = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)" assert(h and s and v and a) return self:set { h = tonumber(h) / 360, s = tonumPercent(s), v = tonumPercent(v), a = tonumPercent(a) } elseif func == "hsl" then local h, s, l = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)" assert(h and s and l) return self:set { h = tonumber(h) / 360, s = tonumPercent(s), l = tonumPercent(l), } elseif func == "hsla" then local h, s, l, a = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)" assert(h and s and l and a) return self:set { h = tonumber(h) / 360, s = tonumPercent(s), l = tonumPercent(l), a = tonumPercent(a) } elseif func == "hwb" then local h, w, b = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)" assert(h and w and b) return self:set { h = tonumber(h) / 360, w = tonumPercent(w), b = tonumPercent(b), } elseif func == "hwba" then local h, w, b, a = values:match "([x.%x]+)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)" assert(h and w and b and a) return self:set { h = tonumber(h) / 360, w = tonumPercent(w), b = tonumPercent(b), a = tonumPercent(a) } elseif func == "cmyk" then local c, m, y, k = values:match "([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)" assert(c and m and y and k) return self:set { c = tonumPercent(c), m = tonumPercent(m), y = tonumPercent(y), k = tonumPercent(k), } end else local col, dist, w, b, a = value:match "([RGBCMYrgbcmy])(%d*)[, ]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)" if col == nil then col, dist, w, b, a = value:match "([RGBCMYrgbcmy])(%d*)[, ]+([x.%x]+%%?)[ ,]+([x.%x]+%%?)" end if col then col = col:lower() local h if col == "r" then h = 0 elseif col == "y" then h = 1/6 elseif col == "g" then h = 2/6 elseif col == "c" then h = 3/6 elseif col == "b" then h = 4/6 elseif col == "m" then h = 5/6 end if #dist > 0 then h = h + tonumber(dist) / 600 end return self:set { h = h, w = tonumPercent(w), b = tonumPercent(b), a = a and tonumPercent(a) or 1 } end end else value = value:sub(2) end local pattern local div = 0xff if #value == 3 then pattern = "(%x)(%x)(%x)" div = 0xf elseif #value == 4 then pattern = "(%x)(%x)(%x)(%x)" div = 0xf elseif #value == 6 then pattern = "(%x%x)(%x%x)(%x%x)" elseif #value == 8 then pattern = "(%x%x)(%x%x)(%x%x)(%x%x)" else error "Not a valid color" end local r, g, b, a = value:match(pattern) assert(r ~= nil, "Not a valid color") self.r = tonumber(r, 16) / div self.g = tonumber(g, 16) / div self.b = tonumber(b, 16) / div self.a = a ~= nil and tonumber(a, 16) / div or 1 -- table with rgb elseif value[1] ~= nil then self.r = value[1] self.g = value[2] self.b = value[3] self.a = value[4] or self.a or 1 elseif value.r ~= nil then self.r = value.r self.g = value.g or self.g self.b = value.b or self.b self.a = value.a or self.a elseif value.c ~= nil then local k = 1 - value.k self.r = (1 - value.c) * k self.g = (1 - value.m) * k self.b = (1 - value.y) * k self.a = 1 -- table with hs[vl] elseif value.h ~= nil then if value.w ~= nil then -- hwb value.v = 1 - value.b value.s = 1 - value.w / value.v end local hue, saturation = value.h, value.s assert(hue ~= nil, saturation ~= nil) local r, g, b = 0, 0, 0 if value.v ~= nil then local v = value.v local chroma = saturation * v r, g, b = hcm_to_rgb(hue, chroma, v - chroma) elseif value.l ~= nil then local lightness = value.l local chroma = (1 - math.abs(2 * lightness - 1)) * saturation r, g, b = hcm_to_rgb(hue, chroma, lightness - chroma / 2) end self.r = r self.g = g self.b = b self.a = value.a or self.a or 1 else -- Single set mode if value.red then self.r = value.red end if value.green then self.g = value.green end if value.blue then self.b = value.blue end if value.alpha then self.a = value.alpha end if value.lightness then local h, s, l = self:hsl() self:set {h= value.hue or h, s= value.saturation or s, l= value.lightness or l} value.hue = nil value.saturation = nil end if value.whiteness or value.blackness then local h, w, b = self:hwb() self:set {h= value.hue or h, w= value.whiteness or w, b= value.backness or b} value.hue = nil end if value.hue or value.saturation or value.value then local h, s, v = self:hsv() self:set {h= value.hue or h, s= value.saturation or s, v= value.value or v} end if value.cyan or value.magenta or value.yellow or value.key then local c, m, y, k = self:cmyk() self:set { c = value.cyan or c, m = value.magenta or m, y = value.yellow or y, k = value.key or k } end end local r, g, b, a = utils.clamp(self.r, 0, 1), utils.clamp(self.g, 0, 1), utils.clamp(self.b, 0, 1), utils.clamp(self.a, 0, 1) assert(r and g and b and a, "Color invalid") return self end --- Get rgb values. -- -- @treturn number[0;1] red -- @treturn number[0;1] green -- @treturn number[0;1] blue function Color:rgb() return self.r, self.g, self.b end --- Get rgba values. -- -- @treturn number[0;1] red -- @treturn number[0;1] green -- @treturn number[0;1] blue -- @treturn number[0;1] alpha function Color:rgba() return self.r, self.g, self.b, self.a end function Color:_hsvm() local r, g, b = self.r, self.g, self.b local max, max_i = utils.max(r, g, b) local min = math.min(r, g, b) local chroma = max - min local hue if chroma == 0 then hue = 0 elseif max_i == 1 then hue = ( (g - b) / chroma) / 6 elseif max_i == 2 then hue = (2 + (b - r) / chroma) / 6 elseif max_i == 3 then hue = (4 + (r - g) / chroma) / 6 end local saturation = max == 0 and 0 or chroma / max return hue % 1, saturation, max, min end --- Get hsv values. -- -- @treturn number[0;1] hue -- @treturn number[0;1] saturation -- @treturn number[0;1] value function Color:hsv() local h, s, v = self:_hsvm() return h, s, v end --- Get hsv values. -- -- @treturn number[0;1] hue -- @treturn number[0;1] saturation -- @treturn number[0;1] value -- @treturn number[0;1] alpha function Color:hsva() local h, s, v = self:_hsvm() return h, s, v, self.a end --- Get hsl values. -- -- @treturn number[0;1] hue -- @treturn number[0;1] saturation -- @treturn number[0;1] lightness function Color:hsl() local hue, _, max, min = self:_hsvm() local lightness = (max + min) / 2 local saturation = lightness == 0 and 0 or (max - lightness) / math.min(lightness, 1 - lightness) if saturation ~= saturation then saturation = 0 end return hue, saturation, lightness end --- Get hsl values. -- -- @treturn number[0;1] hue -- @treturn number[0;1] saturation -- @treturn number[0;1] lightness -- @treturn number[0;1] alpha function Color:hsla() local h, s, l = self:hsl() return h, s, l, self.a end --- Get hwb values. -- -- @treturn number[0;1] hue -- @treturn number[0;1] whiteness -- @treturn number[0;1] blackness function Color:hwb() local h, s, v = self:hsv() local w = (1 - s) * v local b = 1 - v return h, w, b end --- Get hwb values. -- -- @treturn number[0;1] hue -- @treturn number[0;1] whiteness -- @treturn number[0;1] blackness -- @treturn number[0;1] alpha function Color:hwba() local h, w, b = self:hwb() return h, w, b, self.a end --- Get cmyk values. -- -- @treturn number[0;1] cyan -- @treturn number[0;1] magenta -- @treturn number[0;1] yellow -- @treturn number[0;1] key function Color:cmyk() local r, g, b = self.r, self.g, self.b local K = math.max(r, g, b) if K == 0 then return 0.0, 0.0, 0.0, 1.0 end local k = 1 - K local c = (K - r) / K local m = (K - g) / K local y = (K - b) / K return c, m, y, k end --- Rotate hue of color. -- -- @tparam number[0;1]|table value Part of full turn or table containing degree or radians -- -- @treturn Color self -- -- @usage color:rotate(0.5) -- @usage color:rotate {deg=180} -- @usage color:rotate {rad=math.pi} function Color:rotate(value) local r if type(value) == "number" then r = value elseif value.rad ~= nil then r = value.rad / (math.pi * 2) elseif value.deg ~= nil then r = value.deg / 360 else error("No valid argument") end local h, s, v = self:hsv() h = (h + r) % 1 self:set {h = h, s = s, v = v, a = self.a} return self end --- Invert the color. -- -- @treturn Color self function Color:invert() self.r = 1 - self.r self.g = 1 - self.g self.b = 1 - self.b return self end --- Reduce saturation to 0. -- -- @treturn Color self function Color:grey() local h, _, v = self:hsv() self:set {h=h, s=0, v=v, a=self.a} return self end --- Set to black or white depending on lightness. -- -- @tparam ?number[0;1] lightness Cutoff point (Default: 0.5) -- -- @treturn Color self function Color:blackOrWhite(lightness) local _, _, l = self:hsl() local v = l > lightness and 1 or 0 self.r = v self.g = v self.b = v return self end --- Mix two colors together. -- -- @tparam Color other -- @tparam ?number strength 0 results in self, 1 results in other (Default: 0.5) -- -- @treturn Color self function Color:mix(other, strength) if strength == nil then strength = 0.5 end self.r = self.r * (1 - strength) + other.r * strength self.g = self.g * (1 - strength) + other.g * strength self.b = self.b * (1 - strength) + other.b * strength self.a = self.a * (1 - strength) + other.a * strength return self end --- Generate complementary color. -- -- @treturn Color function Color:complement() return Color(self):rotate(0.5) end --- Generate analogous color scheme. -- -- @treturn Color -- @treturn Color self -- @treturn Color function Color:analogous() local h, s, v = self:hsv() return Color {h = (h - 1/12) % 1, s = s, v = v, a = self.a}, self, Color {h = (h + 1/12) % 1, s = s, v = v, a = self.a} end --- Generate triadic color scheme. -- -- @treturn Color self -- @treturn Color -- @treturn Color function Color:triad() local h, s, v = self:hsv() return self, Color {h = (h + 1/3) % 1, s = s, v = v, a = self.a}, Color {h = (h + 2/3) % 1, s = s, v = v, a = self.a} end --- Generate tetradic color scheme. -- -- @treturn Color self -- @treturn Color -- @treturn Color -- @treturn Color function Color:tetrad() local h, s, v = self:hsv() return self, Color {h = (h + 1/4) % 1, s = s, v = v, a = self.a}, Color {h = (h + 2/4) % 1, s = s, v = v, a = self.a}, Color {h = (h + 3/4) % 1, s = s, v = v, a = self.a} end --- Generate compound color scheme. -- -- @treturn Color -- @treturn Color self -- @treturn Color function Color:compound() local ca, _, cb = self:complement():analogous() return ca, self, cb end --- Generate evenly spaced color scheme. --
-- Generalization of `triad` and `tetrad`. -- -- @tparam int n Return n colors -- @tparam ?number r Space colors over r rotations (Default: 1) -- -- @treturn {Color,...} Table with n colors including self at index 1 function Color:evenlySpaced(n, r) assert(n > 0, "n needs to be greater than 0") r = r or 1 local res = {self} local rot = r / n local h, s, v = self:hsv() local a = self.a for i = 1, n - 1 do h = (h + rot) % 1 table.insert(res, Color {h=h, s=s, v=v, a=a}) end return res end --- Get string representation of color. -- -- If `format` is `nil`, `color:tostring()` is the same as `tostring(color)`. -- -- @tparam ?string format One of: `#fff`, `#ffff`, `#ffffff`, `#ffffffff`, -- rgb, rgba, hsv, hsva, hsl, hsla, hwb, hwba, ncol, cmyk -- -- @treturn string -- -- @see Color:__tostring function Color:tostring(format) if format == nil then return tostring(self) end format = format:lower() if format:sub(1,1) == "#" then if #format == 4 then return string.format("#%x%x%x", utils.round(self.r * 0xf), utils.round(self.g * 0xf), utils.round(self.b * 0xf)) elseif #format == 5 then return string.format("#%x%x%x%x", utils.round(self.r * 0xf), utils.round(self.g * 0xf), utils.round(self.b * 0xf), utils.round(self.a * 0xf)) elseif #format == 7 then return string.format("#%02x%02x%02x", utils.round(self.r * 0xff), utils.round(self.g * 0xff), utils.round(self.b * 0xff)) elseif #format == 9 then return string.format("#%02x%02x%02x%02x", utils.round(self.r * 0xff), utils.round(self.g * 0xff), utils.round(self.b * 0xff), utils.round(self.a * 0xff)) end elseif format == "rgb" then return string.format("rgb(%d, %d, %d)", utils.round(self.r * 0xff), utils.round(self.g * 0xff), utils.round(self.b * 0xff)) elseif format == "rgba" then return string.format("rgba(%d, %d, %d, %s)", utils.round(self.r * 0xff), utils.round(self.g * 0xff), utils.round(self.b * 0xff), self.a) elseif format == "hsv" then local h, s, v = self:hsv() return string.format("hsv(%d, %d%%, %d%%)", utils.round(h * 360), utils.round(s * 100), utils.round(v * 100)) elseif format == "hsva" then local h, s, v, a = self:hsva() return string.format("hsva(%d, %d%%, %d%%, %s)", utils.round(h * 360), utils.round(s * 100), utils.round(v * 100), a) elseif format == "hsl" then local h, s, l = self:hsl() return string.format("hsl(%d, %d%%, %d%%)", utils.round(h * 360), utils.round(s * 100), utils.round(l * 100)) elseif format == "hsla" then local h, s, l, a = self:hsla() return string.format("hsla(%d, %d%%, %d%%, %s)", utils.round(h * 360), utils.round(s * 100), utils.round(l * 100), a) elseif format == "hwb" then local h, w, b = self:hwb() return string.format("hwb(%d, %d%%, %d%%)", utils.round(h * 360), utils.round(w * 100), utils.round(b * 100)) elseif format == "hwba" then local h, w, b, a = self:hwba() return string.format("hwba(%d, %d%%, %d%%, %s)", utils.round(h * 360), utils.round(w * 100), utils.round(b * 100), a) elseif format == "ncol" then local h, w, b = self:hwb() local h_maj, h_min = math.modf(h * 6) h_maj = h_maj % 6 local col if h_maj == 0 then col = "R" elseif h_maj == 1 then col = "Y" elseif h_maj == 2 then col = "G" elseif h_maj == 3 then col = "C" elseif h_maj == 4 then col = "B" else col = "M" end return string.format("%s%d, %d%%, %d%%", col, utils.round(h_min * 100), utils.round(w * 100), utils.round(b * 100)) elseif format == "cmyk" then local c, m, y, k = self:cmyk() return string.format("cymk(%d%%, %d%%, %d%%, %d%%)", utils.round(c * 100), utils.round(m * 100), utils.round(y * 100), utils.round(k * 100)) end return tostring(self) end --- Get color in rgb hex notation. --
-- only adds alpha value if `color.a < 1` -- -- @treturn string `#rrggbb` | `#rrggbbaa` -- -- @see Color:tostring function Color:__tostring() if self.a < 1 then return string.format( "#%02x%02x%02x%02x", utils.round(self.r * 0xff), utils.round(self.g * 0xff), utils.round(self.b * 0xff), utils.round(self.a * 0xff) ) else return string.format( "#%02x%02x%02x", utils.round(self.r * 0xff), utils.round(self.g * 0xff), utils.round(self.b * 0xff) ) end end --- Check if colors are equal. -- -- @tparam Color other -- -- @treturn boolean all values are equal function Color:__eq(other) return self.r == other.r and self.g == other.g and self.b == other.b and self.a == other.a end --- Checks whether color is darker. -- -- @tparam Color other -- -- @treturn boolean self is darker than other function Color:__lt(other) local _, _, la = self:hsl() local _, _, lb = other:hsl() return la < lb end --- Checks whether color is as dark or darker. -- -- @tparam Color other -- -- @treturn boolean self is as dark or darker than other function Color:__le(other) local _, _, la = self:hsl() local _, _, lb = other:hsl() return la <= lb end --- Iterate through color. -- -- Iterates through r, g, b, and a. function Color:__pairs() local function iter(tbl, k) if k == nil then return "r", self.r elseif k == "r" then return "g", self.g elseif k == "g" then return "b", self.b elseif k == "b" then return "a", self.a end end return iter, self, nil end --- Get inverted clone of color. -- -- @treturn Color function Color:__unm() return Color(self):invert() end --- Mix two colors evenly. -- -- @tparam Color a first color -- @tparam Color b second color -- -- @treturn Color new color -- -- @see Color:mix function Color.__add(a, b) assert(Color.isColor(a) and Color.isColor(b), "Can only add two colors.") return Color(a):mix(b) end --- Complement of even mix. -- -- @tparam Color a first color -- @tparam Color b second color -- -- @treturn Color new color -- -- @see Color:mix -- @see Color.__add function Color.__sub(a, b) assert(Color.isColor(a) and Color.isColor(b), "Can only add two colors.") return Color(a):mix(b):rotate(0.5) end --- Apply rgb mask to color. -- -- @tparam Color|number a color or mask -- @tparam Color|number b color or mask (if a and b are colors b is used as mask) -- -- @treturn Color new color -- -- @usage local new_col = color & 0xff00ff -- get new color without the green channel function Color.__band(a, b) local color, mask if Color.isColor(a) and type(b) == "number" then color = a mask = b elseif Color.isColor(b) and type(a) == "number" then color = b mask = a elseif Color.isColor(a) and Color.isColor(b) then color = a mask = bit_lshift(utils.round(b.r * 0xff), 16) + bit_lshift(utils.round(b.g * 0xff), 8) + utils.round(b.b * 0xff) else error("Required arguments: Color|number,Color|number Received: "..type(a)..","..type(b)) end return Color { bit_and(utils.round(color.r * 0xff), bit_rshift(mask, 16)) / 0xff, bit_and(utils.round(color.g * 0xff), bit_rshift(mask, 8)) / 0xff, bit_and(utils.round(color.b * 0xff), mask ) / 0xff, color.a } end --- Apply rgb mask to color, providing backwards compatibility for Lua 5.1 and LuaJIT 2.1.0-beta3 (e.g. inside Neovim), which don't provide native support for bitwise operators. -- -- @tparam Color|number a color or mask -- @tparam Color|number b color or mask (if a and b are colors b is used as mask) -- -- @treturn Color new color -- -- @usage local new_col = Color.band(color, 0xff00ff) -- get new color without the green channel function Color.band(a, b) return Color.__band(a, b) end --- Check whether `color` is a Color. -- -- @param color -- -- @treturn boolean is a color -- -- @usage if Color.isColor(color) then print "It's a color!" end function Color.isColor(color) return color ~= nil and color.__is_color == true end return Color end Color = require("lua-color") package.preload["html"] = package.preload["html"] or function(...) local _local_1_ = require("js") local _local_2_ = _local_1_.global local document = _local_2_.document local RV = _local_2_.RV local js = _local_1_ local Array local function _3_(...) return js.global:Array(...) end Array = _3_ local log local function _4_(...) return js.global.console:log(...) end log = _4_ local function html(doc) if (nil == doc) then _G.error("Missing argument doc on ./html.fnl:16", 2) else end local function tbl_to_obj(tbl) local obj = js.new(js.global.Object) for k, v in pairs(tbl) do if (type(v) == "table") then obj[k] = tbl_to_obj(v) else obj[k] = v end end return obj end local element = doc[1] local attrs = doc[2] local children = (function (t, k) return ((getmetatable(t) or {}).__fennelrest or function (t, k) return {(table.unpack or unpack)(t, k)} end)(t, k) end)(doc, 3) local function _7_() local tbl_26_ = {} local i_27_ = 0 for _, child in ipairs(children) do local val_28_ if (type(child) == "string") then if string.find(child, "^", generate = "", title = "", saved = "", feedback = "", done = "", coffee = ""} return icons end icons = require("icons") local i18n package.preload["i18n"] = package.preload["i18n"] or function(...) local i18n = {} local js = require("js") local log local function _12_(...) return js.global.console:log(...) end log = _12_ i18n.data = {en = {description = {"p", {}, {"b", {}, "So Many Colors!"}, " is an app that generates a variety of color schemes for you to use. It can help you find color combinations that you can use for your designs, artwork, or even spreadsheets. It does this randomly, but it also gives you the option to pick your own colors as well!"}, ["free-palestine"] = "Free Palestine \240\159\135\181\240\159\135\184", ["anti-capitalist"] = "This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles.", language = "Language", ["select-language"] = "Select language", ["color-formats"] = "Color formats", ["copy-dialog"] = "This color is available in a wide variety of formats that you can copy into your work.", done = "Done", random = "Random", pick = "Pick", save = "Save color", remove = "Remove color", brighten = "Brighter", ["brighten-desc"] = "Brighter colors that are derived by increasing the lightness of the color until it approaches white.", darken = "Darker", ["darken-desc"] = "Darker colors that are derived by decreasing the lightness of the color until it approaches black.", complementary = "Complementary", ["complementary-desc"] = "Two colors that are on opposite sides of the color wheel. When placed next to each other, they create the strongest contrast for those two colors. Complementary colors may also be called \"opposite colors\".", analogous = "Analogous", ["analogous-desc"] = "Analogous colors are groups of colors that are next to each other on the color wheel. Red, orange, and red-orange are examples. These color schemes are most often seen in nature. For example, during the fall, one might often see the changing leaves form an analogous sort of color scheme.", triad = "Triad", ["triad-desc"] = "The triadic color scheme is a three-color combination consisting of base color and two colors that are 120 degrees and 240 degrees apart from the base color. Triadic color schemes tend to be quite vibrant. Even when using pale or unsaturated versions of hues, it offers a higher degree of contrast while also retaining the color harmony.", tetrad = "Tetrad", ["tetrad-desc"] = "The tetradic color scheme is considered the richest because it uses four colors arranged into two complementary color pairs. This scheme is hard to harmonize and requires a color to dominate or subdue the colors; if all four colors are used in equal amounts, the color scheme may look unbalanced.", saturate = "Saturate", ["saturate-desc"] = "Color saturation refers to how vivid, rich, or intense a color is. These colors are derived by increasing the base color's saturation.", desaturate = "Desaturate", ["desaturate-desc"] = "Color saturation refers to how vivid, rich, or intense a color is. These colors are derived by decreasing the base color's saturation.", ["saved-colors"] = "Saved colors", ["saved-colors-desc"] = "Here you'll find the list of base colors that you saved. Tap one to show the color schemes derived from it."}, tl = {description = {"p", {}, "Ang ", {"b", {}, "So Many Colors!"}, " ay isang app na lumilikha ng ibat-ibang mga color scheme na pwede mong gamitin. Nakakatulong 'to sa paghahanap ng mga kulay para sa iyong mga disenyo, sining, o kaya mga spreadsheet. Lumilikha ito ng mga random na kulay, pero pwede ka rin namang pumili ng sarili mong kulay kung gusto mo!"}, ["free-palestine"] = "Palayain ang Palestine \240\159\135\181\240\159\135\184", ["anti-capitalist"] = "Ang software na ito ay kontra-kapitalismo, binibigay ng libre sa mga indibidwal at mga samahan na hindi naniniwala sa mga alituntunin ng kapitalismo.", language = "Wika", ["select-language"] = "Pumuli ng wika", ["color-formats"] = "Ibat-ibang pormat ng kulay", ["copy-dialog"] = "Makokopya mo din ang iba't-ibang anyo ng kulay na 'to sa iyong mga gawain.", done = "Tapos na", random = "Random", pick = "Pumili", save = "Kolektahin ang kulay", remove = "Tanggalin ang kulay", brighten = "Mas maliwanag", ["brighten-desc"] = "Ang kulay ay pinaliliwanagan ng unti-unti hanggang ito ay maging puti.", darken = "Mas madilim", ["darken-desc"] = "Ang kulay ay dinidiliman ng unti-unti hanggang ito ay maging itim.", complementary = "Magkatugma", ["complementary-desc"] = "Magkakatugma o magkakaternong kulay ay ang mga kulay na may pinakamalaking pagkakaiba lalong-lalo na kapag pinagtabi ang dalawa. Kung titingnan ang gulong ng mga kulay, ang mga magkakaternong kulay na ito ay yung mga tuwirang nasa harapan ng bawat isa.", analogous = "Magkakatabi", ["analogous-desc"] = "Ang mga magkakatabing kulay ay mga grupo ng kulay na magkakatabi sa gulong ng mga kulay. Pula, kahel, at naranghang pulahin\tay isang halimbawa nito. Madalas ito nakikita sa kalikasan. Halimbawa tuwing taglagas, nagmumukhang magkakatabi ang mga kulay ng mga dahon.", triad = "Tatluhan", ["triad-desc"] = "Ang tatluhang color scheme ay ang pagsasama ng isang pangunahing kulay at dalawa pang kulay na 120 grado at 240 grado ang layo sa isa't-isa. Karaniwang matapang ang samahan ng mga kulay na ito. Kahit tugagas man ang mga kulay, mataas parin ang grado ng kanilang contrast at pagsasama.", tetrad = "Apatan", ["tetrad-desc"] = "Ang apatang color scheme ay ang pinakamarilag dahil ito ay gumagamit ng apat na kulay at ang dalawang pares ay magkakatugmang kulay. Ngunit mahirap ipagsama ang apat na ito dahil kailangan may isang kulay na namamayapag sa lahat. Kung pantay-pantay ang gamit sa apat na kulay, hindi balanse ang itsura ng ikalalabasan.", saturate = "Mas matapang", ["saturate-desc"] = "Ang tapang ng kulay ay ang magdidikta kung gaano katindi o ang karilagan ng isang kulay. Ang mga kulay na ito ay nakukuha sa unti-unting pagtaas ng tapang ng kulay.", desaturate = "Mas tugagas o maputla", ["desaturate-desc"] = "Ang tapang ng kulay ay ang magdidikta kung gaano katindi o ang karilagan ng isang kulay. Ang mga kulay na ito ay nakukuha sa unti-unting pagbaba ng tapang ng kulay hanggang maging tugagas o maputla ito.", ["saved-colors"] = "Mga nakolektang kulay", ["saved-colors-desc"] = "Dito mo mahahanap ang mga kulay na nakolekta mo. Pindutin ang isa para makita ulit ang mga color scheme na nilikha ng app."}} local function lang_apply(lang, dir) local html_el = js.global.document:querySelector("html") i18n.locale = lang html_el:setAttribute("lang", lang) return html_el:setAttribute("dir", dir) end i18n.setLang = function(el) log("Changed language", el.value) js.global.localStorage:setItem("lang", el.value) return js.global.location:reload() end i18n.text = function(key) local text = i18n.data[i18n.locale][key] if (text ~= nil) then return text else return i18n.data.en[key] end end local function checkLang() local found = "en" for _, lang in ipairs({"en", "tl"}) do if string.match(js.global.navigator.language, ("^" .. lang)) then found = lang else end end return found end do local saved = js.global.localStorage:getItem("lang") local lang if (saved == js.null) then lang = checkLang() else lang = saved end if (lang == "en") then lang_apply(lang, "ltr") elseif (lang == "tl") then lang_apply(lang, "ltr") else end end return i18n end i18n = require("i18n") local _local_17_ = require("js") local _local_18_ = _local_17_.global local document = _local_18_.document local navigator = _local_18_.navigator local crdt = _local_18_.crdt local js = _local_17_ local log local function _19_(...) return js.global.console:log(...) end log = _19_ local app = {state = {dialog = {content = "", open = false}}} local memo = {bar = {}, foreground = {}, min = {s = {}, l = {}}, max = {s = {}, l = {}}, sections = {}} math.randomseed(os.time()) local function luminance(color) local adjust local function _20_(color0) if (color0 <= 0.03928) then return (color0 / 12.92) else return (((color0 + 0.055) / 1.055) ^ 2.4) end end adjust = _20_ local r = adjust(color.r) local g = adjust(color.g) local b = adjust(color.b) return ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)) end local function contrast(c1, c2) local l1 = math.max(luminance(c1), luminance(c2)) local l2 = math.min(luminance(c1), luminance(c2)) return ((l1 + 0.05) / (l2 + 0.05)) end local function random_color() local color = Color({r = math.random(), g = math.random(), b = math.random()}) if ((color:tostring() == "#ffffff") or (color:tostring() == "#000000")) then return Color({r = math.random(), g = math.random(), b = math.random()}) else return color end end local function max_prop_color(color, prop) if memo.max[prop][color:tostring()] then return memo.max[prop][color:tostring()] else local h, s, l = color:hsl() local key = color:tostring() local color0 = {h = h, s = s, l = l} local variations = 6 local current = color0[prop] local gap = (100 - (current * 100)) local step = math.ceil((gap / variations)) local colors = {} if (math.floor((gap / step)) > 1) then for i = math.floor((100 * current)), 100, step do do local v = (0.01 * i) if (v > 1) then color0[prop] = 1 else color0[prop] = v end end table.insert(colors, Color(color0)) end else end memo.max[key] = colors return colors end end local function min_prop_color(color, prop) if memo.min[prop][color:tostring()] then return memo.min[prop][color:tostring()] else local h, s, l = color:hsl() local key = color:tostring() local color0 = {h = h, s = s, l = l} local variations = 6 local current = color0[prop] local gap = (current * 100) local step = math.ceil((gap / variations)) local colors = {} if (math.floor((gap / step)) > 1) then for i = math.floor((100 * current)), 0, ( - step) do do local v = (0.01 * i) if (v < 0) then color0[prop] = 0 else color0[prop] = v end end table.insert(colors, Color(color0)) end else end memo.min[key] = colors return colors end end local function foreground_color(color) if memo.foreground[color:tostring()] then return memo.foreground[color:tostring()] else local contrast_finder local function _29_(colors) local a = color for i, b in ipairs(colors) do if (contrast(color, b) < contrast(color, a)) then a = a else a = b end end return a end contrast_finder = _29_ local candidates do local tbl_26_ = {} local i_27_ = 0 for i, candidate in ipairs({min_prop_color(color, "l"), max_prop_color(color, "l")}) do local val_28_ = contrast_finder(candidate) if (nil ~= val_28_) then i_27_ = (i_27_ + 1) tbl_26_[i_27_] = val_28_ else end end candidates = tbl_26_ end local final = contrast_finder(candidates) memo.foreground[color:tostring()] = final:tostring() return final:tostring() end end local function rgb565(color) local r, g, b = color:rgb() local r8 = (utils.round((r * 31)) << 11) local g8 = (utils.round((g * 63)) << 5) local b8 = utils.round((b * 31)) local color8 = (r8 | g8 | b8) return string.format("#%04X", color8) end local function save_color_template() local function _33_() local tbl_26_ = {} local i_27_ = 0 for color in js.of(js.global.Object:keys(crdt.data)) do local val_28_ do local color0 = Color(color) local function _34_() app.state.color = color0 js.global:scrollTo(0, 0) return app.render() end val_28_ = {"div", {class = "saved-color", style = {background = color0:tostring(), color = foreground_color(color0)}, onclick = _34_}, {"span", {}, color0:tostring()}} end if (nil ~= val_28_) then i_27_ = (i_27_ + 1) tbl_26_[i_27_] = val_28_ else end end return tbl_26_ end return {"div", {}, table.unpack(_33_())} end local function save_color(el, color, _3frender) local scroll_el = document:querySelector("#scroll-to-saved") if crdt.data[color:tostring()] then crdt.data[color:tostring()] = js.null js.global:sendData(color:tostring(), js.null) else crdt.data[color:tostring()] = color:tostring() js.global:sendData(color:tostring(), color:tostring()) end local function _37_() return scroll_el.classList:remove("beat-fade") end RV.id.scroll:addEventListener("animationend", _37_) RV.id.scroll.classList:add("beat-fade") if _3frender then return app.render() else return nil end end local function dialog_template(color) local select_text local function _39_(el) return el:select() end select_text = _39_ local dialog local function _40_() app.state.dialog.open = false return app.render() end local function _41_(el) save_color(el, color, false) app.state.dialog.content = dialog_template(color) return app.render() end local function _42_() if crdt.data[color:tostring()] then return i18n.text("remove") else return i18n.text("save") end end dialog = {"article", {}, {"header", {}, {"p", {}, {"b", {}, i18n.text("color-formats")}}}, {"div", {class = "sample-color", style = {background = color:tostring(), color = foreground_color(color)}}, color:tostring()}, {"p", {}, i18n.text("copy-dialog")}, {"details", {name = "hex"}, {"summary", {}, "Hex"}, {"input", {type = "text", value = color:tostring(), onclick = select_text}}}, {"details", {name = "hsl"}, {"summary", {}, "HSL"}, {"input", {type = "text", value = color:tostring("hsl"), onclick = select_text}}}, {"details", {name = "rgb"}, {"summary", {}, "RGB"}, {"input", {type = "text", value = color:tostring("rgb"), onclick = select_text}}}, {"details", {name = "rgb565"}, {"summary", {}, "RGB565 (16-bit RGB)"}, {"input", {type = "text", value = rgb565(color), onclick = select_text}}}, {"details", {name = "cmyk"}, {"summary", {}, "CMYK"}, {"input", {type = "text", value = color:tostring("cmyk"), onclick = select_text}}}, {"details", {name = "hsv"}, {"summary", {}, "HSV"}, {"input", {type = "text", value = color:tostring("hsv"), onclick = select_text}}}, {"button", {ariaLabel = "Close", onclick = _40_}, icons.done, i18n.text("done")}, {"button", {class = "contrast save", onclick = _41_}, icons.saved, _42_()}} return dialog end local dialog_donate local function _43_() app.state.dialog.open = false return app.render() end dialog_donate = {"article", {}, {"header", {}, {"strong", {}, "Support this app on itch.io!"}}, {"p", {}, "This app is free, but if you're able to afford it please support by buying or sharing my apps on ", {"code", {}, "durianbean.itch.io"}}, {"p", {}, "A little bit goes a long way. Thank you so much for using this app!"}, {"button", {ariaLabel = "close", onclick = _43_}, "Close"}} local function new_dialog(color) app.state.dialog.open = true app.state.dialog.content = dialog_template(color) return app.render() end local function color_bar_template(color) if memo.bar[color:tostring()] then return memo.bar[color:tostring()] else local bar local function _44_() return new_dialog(color) end bar = {"div", {class = "derived-color", style = {background = color:tostring(), color = foreground_color(color)}, onclick = _44_}, {"span", {}, color:tostring()}} memo.bar[color:tostring()] = bar return bar end end local function generate_gradient(colors) local hsv = "" for i, color in ipairs(colors) do if (i == 1) then hsv = ("oklab(from " .. color:tostring("hsl") .. "l a b)") else hsv = (hsv .. ",oklab(from " .. color:tostring("hsl") .. "l a b)") end end return hsv end local function section_template(title, subtitle, colors) if (#colors > 0) then local function _47_() local tbl_26_ = {} local i_27_ = 0 for _, color in ipairs(colors) do local val_28_ = color_bar_template(color) if (nil ~= val_28_) then i_27_ = (i_27_ + 1) tbl_26_[i_27_] = val_28_ else end end return tbl_26_ end return {"section", {}, {"p", {}, {"b", {}, title}, {"p", {}, subtitle}}, {"p", {}, {"div", {class = "derived-colors overflow-auto"}, table.unpack(_47_())}}, {"p", {}, {"div", {class = "gradient", style = {background = ("linear-gradient(to right," .. generate_gradient(colors))}}}}} else return "" end end app["main-template"] = function() local _50_ if app.state.dialog.open then _50_ = {"dialog", {open = true}, app.state.dialog.content} else _50_ = {"dialog", {open = false}} end local function _52_() return new_dialog(app.state.color) end local function _53_() app.state.color = random_color() return app.render() end local function _54_(this) app.state.color = Color(this.value) return app.render() end local function _55_(el) return save_color(el, app.state.color, true) end local function _56_() if crdt.data[app.state.color:tostring()] then return i18n.text("remove") else return i18n.text("save") end end local _57_ if memo.sections[app.state.color:tostring()] then _57_ = memo.sections[app.state.color:tostring()] else local sections = {"div", {}, section_template(i18n.text("brighten"), i18n.text("brighten-desc"), max_prop_color(app.state.color, "l")), section_template(i18n.text("darken"), i18n.text("darken-desc"), min_prop_color(app.state.color, "l")), section_template(i18n.text("analogous"), i18n.text("analogous-desc"), table.pack(app.state.color:analogous())), section_template(i18n.text("triad"), i18n.text("triad-desc"), table.pack(app.state.color:triad())), section_template(i18n.text("tetrad"), i18n.text("tetrad-desc"), table.pack(app.state.color:tetrad())), section_template(i18n.text("complementary"), i18n.text("complementary-desc"), {app.state.color:complement(), app.state.color}), section_template((i18n.text("brighten") .. " (" .. i18n.text("complementary") .. ")"), i18n.text("brighten-desc"), max_prop_color(app.state.color:complement(), "l")), section_template((i18n.text("darken") .. " (" .. i18n.text("complementary") .. ")"), i18n.text("darken-desc"), min_prop_color(app.state.color:complement(), "l")), section_template(i18n.text("saturate"), i18n.text("saturate-desc"), max_prop_color(app.state.color, "s")), section_template(i18n.text("desaturate"), i18n.text("desaturate-desc"), min_prop_color(app.state.color, "s"))} memo.sections[app.state.color:tostring()] = sections _57_ = sections end return {"div", {}, _50_, {"section", {}, {"div", {class = "main-color", style = {background = app.state.color:tostring(), color = foreground_color(app.state.color)}, onclick = _52_}, app.state.color:tostring()}}, {"section", {id = "controls"}, {"button", {id = "generate", onclick = _53_}, icons.generate, i18n.text("random")}, {"label", {["for"] = "picker", id = "picker-label", role = "button", class = "secondary"}, {"div", {}, icons.palette, i18n.text("pick")}, {"input", {id = "picker", type = "color", value = app.state.color:tostring(), onchange = _54_}}}}, {"section", {}, {"button", {onclick = _55_, class = "contrast save"}, icons.saved, _56_()}}, _57_, {"article", {}, {"header", {}, {"b", {}, i18n.text("saved-colors")}}, {"p", {}, i18n.text("saved-colors-desc")}, {"div", {id = "saved-colors"}, save_color_template()}}} end app.state.color = random_color() app.render = function() return render(app["main-template"](), "main") end app.render() render({"div", {}, i18n.text("description"), {"select", {name = "select", ariaLabel = i18n.text("select-language"), onchange = i18n.setLang}, {"option", {selected = "", value = "", disabled = ""}, i18n.text("language")}, {"option", {value = "en"}, "English"}, {"option", {value = "tl"}, "Tagalog"}}, {"div", {id = "version"}, {"hr", {}}, {"p", {}, "Version 0.1.19"}, {"p", {}, i18n.text("free-palestine")}, {"hr", {}}, {"p", {class = "license"}, i18n.text("anti-capitalist")}, {"p", {class = "license"}, "Anti-Capitalist Software License (v1.4)"}}}, "#footer") local function _60_() app.state.dialog.open = true app.state.dialog.content = dialog_donate return app.render() end return render({"div", {class = "container"}, {"nav", {}, {"ul", {}, {"li", {}, {"div", {id = "title"}, icons.title, {"b", {}, "So Many Colors!"}}}}, {"ul", {}, {"li", {}, {"a", {["aria-label"] = "Saved colors", href = "#saved-colors", title = "Saved colors", id = "scroll-to-saved", rvid = "scroll"}, icons.saved}}, {"li", {}, {"div", {role = "button", id = "donate", onclick = _60_}, icons.coffee}}}}}, "#nav")