aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--awesome.lua31
-rw-r--r--css.lua30
-rw-r--r--lib/json.lua388
-rw-r--r--lib/oklab.lua623
-rw-r--r--lib/utils.lua47
-rw-r--r--nvim.lua18
-rw-r--r--palette.json154
8 files changed, 1292 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..23ebe35
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+Source for the prismite palette
diff --git a/awesome.lua b/awesome.lua
new file mode 100644
index 0000000..f64fb46
--- /dev/null
+++ b/awesome.lua
@@ -0,0 +1,31 @@
+local utils = require "lib.utils"
+local oklab = require "lib.oklab"
+
+local palette = utils.get_palette()
+
+local output = [[-- autogenerated from palette.json
+local M = {}
+
+local function tone_set(colors)
+ return setmetatable(colors, {
+ __call = function(self)
+ return self[1]
+ end,
+ })
+end
+
+M.palette = {
+]]
+
+for name, value in utils.opairs(palette) do
+ output = output .. " " .. name .. " = tone_set {\n"
+ for variant, variant_value in utils.opairs(value) do
+ local color = oklab.rgb2hex(oklab.oklab2rgb(oklab.oklch2oklab(variant_value)))
+ output = output .. " " .. (variant == "normal" and [["]] or variant .. [[ = "]]) .. color .. "\",\n"
+ end
+ output = output .. " },\n"
+end
+
+output = output .. " transparent = \"#00000000\"\n}\n\nreturn M"
+
+print(output)
diff --git a/css.lua b/css.lua
new file mode 100644
index 0000000..9201f62
--- /dev/null
+++ b/css.lua
@@ -0,0 +1,30 @@
+local utils = require "lib.utils"
+local oklab = require "lib.oklab"
+
+local palette = utils.get_palette()
+
+local output = {}
+
+local function generate(supports)
+ table.insert(output, ":root{")
+ for name, value in utils.opairs(palette) do
+ for variant, v_value in utils.opairs(value) do
+ local color
+ if supports then
+ color = "oklch(" .. v_value.l .. "%," .. v_value.c .. "," .. v_value.h .. ")"
+ else
+ color = oklab.rgb2hex(oklab.oklab2rgb(oklab.oklch2oklab(v_value)))
+ end
+ table.insert(output, "--prismite-" .. name .. (variant == "normal" and "" or "-" .. variant) .. ":" .. color .. ";")
+ end
+ end
+ table.insert(output, "}")
+end
+
+generate(false)
+table.insert(output, "")
+generate(true)
+
+for _, line in ipairs(output) do
+ io.write(line)
+end
diff --git a/lib/json.lua b/lib/json.lua
new file mode 100644
index 0000000..711ef78
--- /dev/null
+++ b/lib/json.lua
@@ -0,0 +1,388 @@
+--
+-- json.lua
+--
+-- Copyright (c) 2020 rxi
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy of
+-- this software and associated documentation files (the "Software"), to deal in
+-- the Software without restriction, including without limitation the rights to
+-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+-- of the Software, and to permit persons to whom the Software is furnished to do
+-- so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in all
+-- copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+-- SOFTWARE.
+--
+
+local json = { _version = "0.1.2" }
+
+-------------------------------------------------------------------------------
+-- Encode
+-------------------------------------------------------------------------------
+
+local encode
+
+local escape_char_map = {
+ [ "\\" ] = "\\",
+ [ "\"" ] = "\"",
+ [ "\b" ] = "b",
+ [ "\f" ] = "f",
+ [ "\n" ] = "n",
+ [ "\r" ] = "r",
+ [ "\t" ] = "t",
+}
+
+local escape_char_map_inv = { [ "/" ] = "/" }
+for k, v in pairs(escape_char_map) do
+ escape_char_map_inv[v] = k
+end
+
+
+local function escape_char(c)
+ return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
+end
+
+
+local function encode_nil(val)
+ return "null"
+end
+
+
+local function encode_table(val, stack)
+ local res = {}
+ stack = stack or {}
+
+ -- Circular reference?
+ if stack[val] then error("circular reference") end
+
+ stack[val] = true
+
+ if rawget(val, 1) ~= nil or next(val) == nil then
+ -- Treat as array -- check keys are valid and it is not sparse
+ local n = 0
+ for k in pairs(val) do
+ if type(k) ~= "number" then
+ error("invalid table: mixed or invalid key types")
+ end
+ n = n + 1
+ end
+ if n ~= #val then
+ error("invalid table: sparse array")
+ end
+ -- Encode
+ for i, v in ipairs(val) do
+ table.insert(res, encode(v, stack))
+ end
+ stack[val] = nil
+ return "[" .. table.concat(res, ",") .. "]"
+
+ else
+ -- Treat as an object
+ for k, v in pairs(val) do
+ if type(k) ~= "string" then
+ error("invalid table: mixed or invalid key types")
+ end
+ table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
+ end
+ stack[val] = nil
+ return "{" .. table.concat(res, ",") .. "}"
+ end
+end
+
+
+local function encode_string(val)
+ return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
+end
+
+
+local function encode_number(val)
+ -- Check for NaN, -inf and inf
+ if val ~= val or val <= -math.huge or val >= math.huge then
+ error("unexpected number value '" .. tostring(val) .. "'")
+ end
+ return string.format("%.14g", val)
+end
+
+
+local type_func_map = {
+ [ "nil" ] = encode_nil,
+ [ "table" ] = encode_table,
+ [ "string" ] = encode_string,
+ [ "number" ] = encode_number,
+ [ "boolean" ] = tostring,
+}
+
+
+encode = function(val, stack)
+ local t = type(val)
+ local f = type_func_map[t]
+ if f then
+ return f(val, stack)
+ end
+ error("unexpected type '" .. t .. "'")
+end
+
+
+function json.encode(val)
+ return ( encode(val) )
+end
+
+
+-------------------------------------------------------------------------------
+-- Decode
+-------------------------------------------------------------------------------
+
+local parse
+
+local function create_set(...)
+ local res = {}
+ for i = 1, select("#", ...) do
+ res[ select(i, ...) ] = true
+ end
+ return res
+end
+
+local space_chars = create_set(" ", "\t", "\r", "\n")
+local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
+local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
+local literals = create_set("true", "false", "null")
+
+local literal_map = {
+ [ "true" ] = true,
+ [ "false" ] = false,
+ [ "null" ] = nil,
+}
+
+
+local function next_char(str, idx, set, negate)
+ for i = idx, #str do
+ if set[str:sub(i, i)] ~= negate then
+ return i
+ end
+ end
+ return #str + 1
+end
+
+
+local function decode_error(str, idx, msg)
+ local line_count = 1
+ local col_count = 1
+ for i = 1, idx - 1 do
+ col_count = col_count + 1
+ if str:sub(i, i) == "\n" then
+ line_count = line_count + 1
+ col_count = 1
+ end
+ end
+ error( string.format("%s at line %d col %d", msg, line_count, col_count) )
+end
+
+
+local function codepoint_to_utf8(n)
+ -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
+ local f = math.floor
+ if n <= 0x7f then
+ return string.char(n)
+ elseif n <= 0x7ff then
+ return string.char(f(n / 64) + 192, n % 64 + 128)
+ elseif n <= 0xffff then
+ return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
+ elseif n <= 0x10ffff then
+ return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
+ f(n % 4096 / 64) + 128, n % 64 + 128)
+ end
+ error( string.format("invalid unicode codepoint '%x'", n) )
+end
+
+
+local function parse_unicode_escape(s)
+ local n1 = tonumber( s:sub(1, 4), 16 )
+ local n2 = tonumber( s:sub(7, 10), 16 )
+ -- Surrogate pair?
+ if n2 then
+ return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
+ else
+ return codepoint_to_utf8(n1)
+ end
+end
+
+
+local function parse_string(str, i)
+ local res = ""
+ local j = i + 1
+ local k = j
+
+ while j <= #str do
+ local x = str:byte(j)
+
+ if x < 32 then
+ decode_error(str, j, "control character in string")
+
+ elseif x == 92 then -- `\`: Escape
+ res = res .. str:sub(k, j - 1)
+ j = j + 1
+ local c = str:sub(j, j)
+ if c == "u" then
+ local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
+ or str:match("^%x%x%x%x", j + 1)
+ or decode_error(str, j - 1, "invalid unicode escape in string")
+ res = res .. parse_unicode_escape(hex)
+ j = j + #hex
+ else
+ if not escape_chars[c] then
+ decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
+ end
+ res = res .. escape_char_map_inv[c]
+ end
+ k = j + 1
+
+ elseif x == 34 then -- `"`: End of string
+ res = res .. str:sub(k, j - 1)
+ return res, j + 1
+ end
+
+ j = j + 1
+ end
+
+ decode_error(str, i, "expected closing quote for string")
+end
+
+
+local function parse_number(str, i)
+ local x = next_char(str, i, delim_chars)
+ local s = str:sub(i, x - 1)
+ local n = tonumber(s)
+ if not n then
+ decode_error(str, i, "invalid number '" .. s .. "'")
+ end
+ return n, x
+end
+
+
+local function parse_literal(str, i)
+ local x = next_char(str, i, delim_chars)
+ local word = str:sub(i, x - 1)
+ if not literals[word] then
+ decode_error(str, i, "invalid literal '" .. word .. "'")
+ end
+ return literal_map[word], x
+end
+
+
+local function parse_array(str, i)
+ local res = {}
+ local n = 1
+ i = i + 1
+ while 1 do
+ local x
+ i = next_char(str, i, space_chars, true)
+ -- Empty / end of array?
+ if str:sub(i, i) == "]" then
+ i = i + 1
+ break
+ end
+ -- Read token
+ x, i = parse(str, i)
+ res[n] = x
+ n = n + 1
+ -- Next token
+ i = next_char(str, i, space_chars, true)
+ local chr = str:sub(i, i)
+ i = i + 1
+ if chr == "]" then break end
+ if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
+ end
+ return res, i
+end
+
+
+local function parse_object(str, i)
+ local res = {}
+ i = i + 1
+ while 1 do
+ local key, val
+ i = next_char(str, i, space_chars, true)
+ -- Empty / end of object?
+ if str:sub(i, i) == "}" then
+ i = i + 1
+ break
+ end
+ -- Read key
+ if str:sub(i, i) ~= '"' then
+ decode_error(str, i, "expected string for key")
+ end
+ key, i = parse(str, i)
+ -- Read ':' delimiter
+ i = next_char(str, i, space_chars, true)
+ if str:sub(i, i) ~= ":" then
+ decode_error(str, i, "expected ':' after key")
+ end
+ i = next_char(str, i + 1, space_chars, true)
+ -- Read value
+ val, i = parse(str, i)
+ -- Set
+ res[key] = val
+ -- Next token
+ i = next_char(str, i, space_chars, true)
+ local chr = str:sub(i, i)
+ i = i + 1
+ if chr == "}" then break end
+ if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
+ end
+ return res, i
+end
+
+
+local char_func_map = {
+ [ '"' ] = parse_string,
+ [ "0" ] = parse_number,
+ [ "1" ] = parse_number,
+ [ "2" ] = parse_number,
+ [ "3" ] = parse_number,
+ [ "4" ] = parse_number,
+ [ "5" ] = parse_number,
+ [ "6" ] = parse_number,
+ [ "7" ] = parse_number,
+ [ "8" ] = parse_number,
+ [ "9" ] = parse_number,
+ [ "-" ] = parse_number,
+ [ "t" ] = parse_literal,
+ [ "f" ] = parse_literal,
+ [ "n" ] = parse_literal,
+ [ "[" ] = parse_array,
+ [ "{" ] = parse_object,
+}
+
+
+parse = function(str, idx)
+ local chr = str:sub(idx, idx)
+ local f = char_func_map[chr]
+ if f then
+ return f(str, idx)
+ end
+ decode_error(str, idx, "unexpected character '" .. chr .. "'")
+end
+
+
+function json.decode(str)
+ if type(str) ~= "string" then
+ error("expected argument of type string, got " .. type(str))
+ end
+ local res, idx = parse(str, next_char(str, 1, space_chars, true))
+ idx = next_char(str, idx, space_chars, true)
+ if idx <= #str then
+ decode_error(str, idx, "trailing garbage")
+ end
+ return res
+end
+
+
+return json
diff --git a/lib/oklab.lua b/lib/oklab.lua
new file mode 100644
index 0000000..c35f96c
--- /dev/null
+++ b/lib/oklab.lua
@@ -0,0 +1,623 @@
+-- Source: https://github.com/ramojus/mellifluous.nvim/blob/9add9975a067e448da077dab92b70473aab41a76/lua/mellifluous/utils/oklab.lua
+-- modifications:
+-- - lightness calculation adjustments: https://github.com/echasnovski/mini.nvim/issues/1628#issuecomment-2692645938
+-- - chroma normalization
+
+-- MIT License
+--
+-- Copyright (c) 2024 Ramojus Lapinskas
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy
+-- of this software and associated documentation files (the "Software"), to deal
+-- in the Software without restriction, including without limitation the rights
+-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+-- copies of the Software, and to permit persons to whom the Software is
+-- furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in all
+-- copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+-- SOFTWARE.
+
+-- Oklab color space: https://bottosson.github.io/posts/oklab/
+
+-- Original code taken from:
+-- https://github.com/echasnovski/mini.colors/blob/main/lua/mini/colors.lua,
+-- then slightly refactored and restyled to fit project coding style.
+-- Parts of code unrelated to conversion of colors were removed.
+
+local M = {}
+
+local tau = 2 * math.pi
+
+-- Cusps for Oklch color space. These represent (c, l) points of Oklch space
+-- (with **not corrected lightness**) inside a hue leaf (points with
+-- `math.floor(h) = <index>`) with the highest value of chroma (`c`).
+-- They are used to model the whole RGB gamut (region inside which sRGB colors
+-- are converted in Oklch space). It is modelled as triangle with vertices:
+-- (0, 0), (0, 100) and cusp. NOTE: this is an approximation, i.e. not all RGB
+-- colors lie inside this triangle **AND** not all points inside triangle are
+-- RGB colors. But both proportions are small: around 0.5% with similar modeled
+-- RGB color for first one and around 2.16% for second one.
+--stylua: ignore start
+---@diagnostic disable start
+---@private
+local cusps = {
+ [0] = { 26.23, 64.74 },
+ { 26.14, 64.65 },
+ { 26.06, 64.56 },
+ { 25.98, 64.48 },
+ { 25.91, 64.39 },
+ { 25.82, 64.29 },
+ { 25.76, 64.21 },
+ { 25.70, 64.13 },
+ { 25.65, 64.06 },
+ { 25.59, 63.97 },
+ { 25.55, 63.90 },
+ { 25.52, 63.83 },
+ { 25.48, 63.77 },
+ { 25.45, 63.69 },
+ { 25.43, 63.63 },
+ { 25.41, 63.55 },
+ { 25.40, 63.50 },
+ { 25.39, 63.43 },
+ { 25.40, 63.33 },
+ { 25.40, 63.27 },
+ { 25.42, 63.22 },
+ { 25.44, 63.15 },
+ { 25.46, 63.11 },
+ { 25.50, 63.05 },
+ { 25.53, 63.00 },
+ { 25.58, 62.95 },
+ { 25.63, 62.90 },
+ { 25.69, 62.85 },
+ { 25.75, 62.81 },
+ { 25.77, 62.80 },
+ { 25.34, 63.25 },
+ { 24.84, 63.79 },
+ { 24.37, 64.32 },
+ { 23.92, 64.83 },
+ { 23.48, 65.35 },
+ { 23.08, 65.85 },
+ { 22.65, 66.38 },
+ { 22.28, 66.86 },
+ { 21.98, 67.27 },
+ { 21.67, 67.70 },
+ { 21.36, 68.14 },
+ { 21.05, 68.60 },
+ { 20.74, 69.08 },
+ { 20.50, 69.45 },
+ { 20.27, 69.83 },
+ { 20.04, 70.22 },
+ { 19.82, 70.62 },
+ { 19.60, 71.03 },
+ { 19.38, 71.44 },
+ { 19.17, 71.87 },
+ { 19.03, 72.16 },
+ { 18.83, 72.59 },
+ { 18.71, 72.89 },
+ { 18.52, 73.34 },
+ { 18.40, 73.64 },
+ { 18.28, 73.95 },
+ { 18.17, 74.26 },
+ { 18.01, 74.74 },
+ { 17.91, 75.05 },
+ { 17.82, 75.38 },
+ { 17.72, 75.70 },
+ { 17.64, 76.03 },
+ { 17.56, 76.36 },
+ { 17.48, 76.69 },
+ { 17.41, 77.03 },
+ { 17.35, 77.36 },
+ { 17.29, 77.71 },
+ { 17.24, 78.05 },
+ { 17.19, 78.39 },
+ { 17.15, 78.74 },
+ { 17.12, 79.09 },
+ { 17.09, 79.45 },
+ { 17.07, 79.80 },
+ { 17.05, 80.16 },
+ { 17.04, 80.52 },
+ { 17.04, 81.06 },
+ { 17.04, 81.42 },
+ { 17.05, 81.79 },
+ { 17.07, 82.16 },
+ { 17.08, 82.53 },
+ { 17.11, 82.72 },
+ { 17.14, 83.09 },
+ { 17.18, 83.46 },
+ { 17.22, 83.84 },
+ { 17.27, 84.22 },
+ { 17.33, 84.60 },
+ { 17.39, 84.98 },
+ { 17.48, 85.56 },
+ { 17.56, 85.94 },
+ { 17.64, 86.33 },
+ { 17.73, 86.72 },
+ { 17.81, 87.10 },
+ { 17.91, 87.50 },
+ { 18.04, 88.09 },
+ { 18.16, 88.48 },
+ { 18.27, 88.88 },
+ { 18.40, 89.48 },
+ { 18.57, 89.87 },
+ { 18.69, 90.27 },
+ { 18.88, 90.87 },
+ { 19.03, 91.48 },
+ { 19.22, 91.88 },
+ { 19.44, 92.49 },
+ { 19.66, 93.10 },
+ { 19.85, 93.71 },
+ { 20.04, 94.33 },
+ { 20.33, 94.94 },
+ { 20.60, 95.56 },
+ { 20.85, 96.18 },
+ { 21.10, 96.80 },
+ { 21.19, 96.48 },
+ { 21.27, 96.24 },
+ { 21.38, 95.93 },
+ { 21.47, 95.70 },
+ { 21.59, 95.40 },
+ { 21.72, 95.10 },
+ { 21.86, 94.80 },
+ { 21.97, 94.58 },
+ { 22.12, 94.30 },
+ { 22.27, 94.02 },
+ { 22.43, 93.74 },
+ { 22.64, 93.40 },
+ { 22.81, 93.14 },
+ { 23.04, 92.81 },
+ { 23.22, 92.56 },
+ { 23.45, 92.25 },
+ { 23.68, 91.95 },
+ { 23.92, 91.65 },
+ { 24.21, 91.31 },
+ { 24.45, 91.04 },
+ { 24.74, 90.72 },
+ { 25.08, 90.36 },
+ { 25.37, 90.07 },
+ { 25.70, 89.74 },
+ { 26.08, 89.39 },
+ { 26.44, 89.07 },
+ { 26.87, 88.69 },
+ { 27.27, 88.34 },
+ { 27.72, 87.98 },
+ { 28.19, 87.61 },
+ { 28.68, 87.23 },
+ { 29.21, 86.84 },
+ { 29.48, 86.64 },
+ { 28.99, 86.70 },
+ { 28.13, 86.81 },
+ { 27.28, 86.92 },
+ { 26.56, 87.02 },
+ { 25.83, 87.12 },
+ { 25.18, 87.22 },
+ { 24.57, 87.32 },
+ { 24.01, 87.41 },
+ { 23.53, 87.49 },
+ { 23.03, 87.58 },
+ { 22.53, 87.68 },
+ { 22.10, 87.76 },
+ { 21.68, 87.84 },
+ { 21.26, 87.93 },
+ { 20.92, 88.01 },
+ { 20.58, 88.08 },
+ { 20.25, 88.16 },
+ { 19.92, 88.24 },
+ { 19.59, 88.33 },
+ { 19.35, 88.39 },
+ { 19.12, 88.46 },
+ { 18.81, 88.55 },
+ { 18.58, 88.61 },
+ { 18.36, 88.68 },
+ { 18.14, 88.76 },
+ { 17.93, 88.83 },
+ { 17.79, 88.88 },
+ { 17.59, 88.95 },
+ { 17.39, 89.03 },
+ { 17.26, 89.08 },
+ { 17.08, 89.16 },
+ { 16.96, 89.21 },
+ { 16.79, 89.29 },
+ { 16.68, 89.35 },
+ { 16.58, 89.41 },
+ { 16.43, 89.49 },
+ { 16.33, 89.55 },
+ { 16.24, 89.60 },
+ { 16.16, 89.66 },
+ { 16.04, 89.75 },
+ { 15.96, 89.81 },
+ { 15.89, 89.87 },
+ { 15.83, 89.93 },
+ { 15.77, 89.99 },
+ { 15.71, 90.05 },
+ { 15.66, 90.12 },
+ { 15.61, 90.18 },
+ { 15.57, 90.24 },
+ { 15.54, 90.31 },
+ { 15.51, 90.37 },
+ { 15.48, 90.44 },
+ { 15.46, 90.51 },
+ { 15.40, 90.30 },
+ { 15.30, 89.83 },
+ { 15.21, 89.36 },
+ { 15.12, 88.89 },
+ { 15.03, 88.67 },
+ { 14.99, 88.18 },
+ { 14.92, 87.71 },
+ { 14.85, 87.24 },
+ { 14.78, 86.77 },
+ { 14.75, 86.53 },
+ { 14.70, 86.06 },
+ { 14.65, 85.59 },
+ { 14.61, 85.12 },
+ { 14.60, 84.89 },
+ { 14.57, 84.42 },
+ { 14.54, 83.94 },
+ { 14.53, 83.71 },
+ { 14.52, 83.24 },
+ { 14.51, 82.77 },
+ { 14.52, 82.30 },
+ { 14.52, 81.83 },
+ { 14.53, 81.60 },
+ { 14.55, 81.13 },
+ { 14.58, 80.66 },
+ { 14.59, 80.43 },
+ { 14.63, 79.96 },
+ { 14.68, 79.49 },
+ { 14.70, 79.26 },
+ { 14.76, 78.79 },
+ { 14.82, 78.32 },
+ { 14.85, 78.09 },
+ { 14.93, 77.62 },
+ { 15.01, 77.16 },
+ { 15.10, 76.69 },
+ { 15.19, 76.23 },
+ { 15.24, 76.00 },
+ { 15.34, 75.54 },
+ { 15.45, 75.07 },
+ { 15.57, 74.61 },
+ { 15.69, 74.15 },
+ { 15.82, 73.69 },
+ { 15.96, 73.23 },
+ { 16.10, 72.77 },
+ { 16.24, 72.31 },
+ { 16.39, 71.86 },
+ { 16.55, 71.40 },
+ { 16.71, 70.95 },
+ { 16.96, 70.26 },
+ { 17.14, 69.81 },
+ { 17.32, 69.36 },
+ { 17.59, 68.69 },
+ { 17.88, 68.02 },
+ { 18.07, 67.57 },
+ { 18.37, 66.90 },
+ { 18.67, 66.24 },
+ { 18.99, 65.58 },
+ { 19.30, 64.93 },
+ { 19.74, 64.06 },
+ { 20.07, 63.42 },
+ { 20.51, 62.57 },
+ { 20.97, 61.73 },
+ { 21.54, 60.69 },
+ { 22.00, 59.87 },
+ { 22.70, 58.66 },
+ { 23.39, 57.49 },
+ { 24.19, 56.16 },
+ { 25.20, 54.52 },
+ { 26.38, 52.66 },
+ { 28.55, 49.32 },
+ { 31.32, 45.20 },
+ { 31.15, 45.42 },
+ { 30.99, 45.64 },
+ { 30.85, 45.85 },
+ { 30.72, 46.06 },
+ { 30.57, 46.31 },
+ { 30.47, 46.50 },
+ { 30.34, 46.75 },
+ { 30.23, 46.97 },
+ { 30.13, 47.20 },
+ { 30.03, 47.45 },
+ { 29.93, 47.71 },
+ { 29.86, 47.91 },
+ { 29.77, 48.20 },
+ { 29.71, 48.43 },
+ { 29.65, 48.66 },
+ { 29.58, 48.98 },
+ { 29.53, 49.23 },
+ { 29.48, 49.48 },
+ { 29.44, 49.74 },
+ { 29.41, 50.01 },
+ { 29.37, 50.29 },
+ { 29.35, 50.57 },
+ { 29.33, 50.86 },
+ { 29.31, 51.16 },
+ { 29.30, 51.56 },
+ { 29.29, 51.87 },
+ { 29.29, 52.39 },
+ { 29.30, 52.72 },
+ { 29.31, 53.05 },
+ { 29.33, 53.38 },
+ { 29.35, 53.72 },
+ { 29.37, 54.06 },
+ { 29.40, 54.41 },
+ { 29.43, 54.76 },
+ { 29.47, 55.12 },
+ { 29.52, 55.60 },
+ { 29.56, 55.97 },
+ { 29.61, 56.34 },
+ { 29.66, 56.72 },
+ { 29.73, 57.22 },
+ { 29.79, 57.61 },
+ { 29.84, 57.99 },
+ { 29.93, 58.52 },
+ { 29.99, 58.91 },
+ { 30.08, 59.44 },
+ { 30.15, 59.84 },
+ { 30.24, 60.38 },
+ { 30.34, 60.93 },
+ { 30.42, 61.34 },
+ { 30.52, 61.90 },
+ { 30.63, 62.45 },
+ { 30.73, 63.02 },
+ { 30.85, 63.58 },
+ { 30.96, 64.15 },
+ { 31.08, 64.72 },
+ { 31.19, 65.30 },
+ { 31.31, 65.88 },
+ { 31.44, 66.46 },
+ { 31.59, 67.20 },
+ { 31.72, 67.79 },
+ { 31.88, 68.53 },
+ { 32.01, 69.12 },
+ { 32.18, 69.87 },
+ { 32.25, 70.17 },
+ { 32.06, 69.99 },
+ { 31.76, 69.70 },
+ { 31.45, 69.42 },
+ { 31.21, 69.20 },
+ { 30.97, 68.98 },
+ { 30.68, 68.71 },
+ { 30.44, 68.50 },
+ { 30.21, 68.29 },
+ { 29.98, 68.09 },
+ { 29.75, 67.89 },
+ { 29.53, 67.69 },
+ { 29.31, 67.50 },
+ { 29.09, 67.31 },
+ { 28.88, 67.12 },
+ { 28.72, 66.98 },
+ { 28.52, 66.80 },
+ { 28.31, 66.63 },
+ { 28.16, 66.50 },
+ { 27.97, 66.33 },
+ { 27.78, 66.17 },
+ { 27.64, 66.05 },
+ { 27.49, 65.94 },
+ { 27.33, 65.77 },
+ { 27.20, 65.66 },
+ { 27.04, 65.51 },
+ { 26.92, 65.40 },
+ { 26.81, 65.30 },
+ { 26.66, 65.16 },
+ { 26.55, 65.06 },
+ { 26.45, 64.96 },
+ { 26.35, 64.87 },
+}
+
+local function cuberoot(x)
+ return x^(1 / 3)
+end
+
+local function correct_channel(x)
+ return 0.04045 < x and (((x + 0.055) / 1.055)^2.4) or (x / 12.92)
+end
+
+local function correct_channel_inv(x)
+ return (0.0031308 >= x) and (12.92 * x) or (1.055 * (x^0.416666667) - 0.055)
+end
+
+local function clip(x, from, to)
+ return math.min(math.max(x, from), to)
+end
+
+local function round(x)
+ return math.floor(x + 0.5)
+end
+
+local function degree2rad(x)
+ return (x % 360) * tau / 360
+end
+
+local function rad2degree(x)
+ return (x % tau) * 360 / tau
+end
+
+local function correct_lightness(x)
+ x = 0.01 * x
+ local k1, k2 = 0.206, 0.03
+ local k3 = (1 + k1) / (1 + k2)
+
+ local res = 0.5 * (k3 * x - k1 + math.sqrt((k3 * x - k1) ^ 2 + 4 * k2 * k3 * x))
+ return 100 * res
+end
+
+local function correct_lightness_inv(x)
+ x = 0.01 * x
+ local k1, k2 = 0.206, 0.03
+ local k3 = (1 + k1) / (1 + k2)
+ local res = (x / k3) * (x + k1) / (x + k2)
+ return 100 * res
+end
+
+-- Get gamut ranges for Lch point. They are computed for its hue leaf as
+-- segments of triangle in (c, l) coordinates ((0, 0), (0, 100), cusp).
+-- Equations for triangle parts:
+-- - Lower segment ((0; 0) to cusp): y * c_cusp = x * L_cusp
+-- - Upper segment ((0; 100) to cusp): (100 - y) * c_cusp = x * (100 - L_cusp)
+-- NOTEs:
+-- - It is **very important** that this triangle is computed for **not
+-- corrected** lightness. But it is assumed **corrected lightness** in input.
+-- - This approach is not entirely accurate and can results in ranges outside
+-- of input `lch` for in-gamut point. Put it should be pretty rare: ~0.5%
+-- cases for most saturated colors.
+local function get_gamut_points(lch)
+ local c, l = lch.c, clip(lch.l, 0, 100)
+ l = correct_lightness_inv(l)
+ local cusp = cusps[math.floor(lch.h % 360)]
+ local c_cusp, l_cusp = cusp[1], cusp[2]
+
+ -- Maximum allowed chroma is computed based on current lightness and depends
+ -- on whether `l` is below or above cusp's `l`:
+ -- - If below, then it is from lower triangle segment.
+ -- - If above - from upper segment.
+ local c_upper = l <= l_cusp and (c_cusp * l / l_cusp) or (c_cusp * (100 - l) / (100 - l_cusp))
+ -- - Don't allow negative chroma (can happen if `l` is out of [0; 100])
+ c_upper = clip(c_upper, 0, math.huge)
+
+ -- Other points can be computed only in presence of actual chroma
+ if c == nil then return { c_upper = c_upper } end
+
+ -- Range of allowed lightness is computed based on current chroma:
+ -- - Lower is from segment between (0, 0) and cusp.
+ -- - Upper is from segment between (0, 100) and cusp.
+ local l_lower, l_upper
+ if c < 0 then
+ l_lower, l_upper = 0, 100
+ elseif c_cusp < c then
+ l_lower, l_upper = l_cusp, l_cusp
+ else
+ local saturation = c / c_cusp
+ l_lower = saturation * l_cusp
+ l_upper = saturation * (l_cusp - 100) + 100
+ end
+
+ -- Intersection of segment between (c, l) and (0, l_cusp) with gamut boundary
+ local c_cusp_clip, l_cusp_clip
+ if c <= 0 then
+ c_cusp_clip, l_cusp_clip = c, l
+ elseif l <= l_cusp then
+ -- Intersection with lower segment
+ local prop = 1 - l / l_cusp
+ c_cusp_clip = c_cusp * c / (c_cusp * prop + c)
+ l_cusp_clip = l_cusp * c_cusp_clip / c_cusp
+ else
+ -- Intersection with upper segment
+ local prop = 1 - (l - 100) / (l_cusp - 100)
+ c_cusp_clip = c_cusp * c / (c_cusp * prop + c)
+ l_cusp_clip = 100 + c_cusp_clip * (l_cusp - 100) / c_cusp
+ end
+
+ return {
+ l_lower = correct_lightness(l_lower),
+ l_upper = correct_lightness(l_upper),
+ c_upper = c_upper,
+ l_cusp_clip = correct_lightness(l_cusp_clip),
+ c_cusp_clip = c_cusp_clip,
+ }
+end
+
+function M.oklch2okhsl(lch)
+ if lch.c <= 0 or lch.h == nil then return { l = lch.l, s = 0 } end
+
+ local gamut_points = get_gamut_points(lch)
+ local percent = 100 * lch.c / gamut_points.c_upper
+
+ return { l = lch.l, s = clip(percent, 0, 100), h = lch.h }
+end
+
+function M.okhsl2oklch(lsh)
+ if lsh.s <= 0 or lsh.h == nil then return { l = lsh.l, c = 0 } end
+
+ local gamut_points = get_gamut_points(lsh)
+ local c = 0.01 * lsh.s * gamut_points.c_upper
+
+ return { l = lsh.l, c = clip(c, 0, math.huge), h = lsh.h }
+end
+
+function M.oklab2oklch(lab)
+ local c = math.sqrt(lab.a ^ 2 + lab.b ^ 2)
+ -- Treat grays specially
+ local h = nil
+ if c > 0 then h = rad2degree(math.atan2(lab.b, lab.a)) end
+ return { l = lab.l, c = c, h = h }
+end
+
+function M.oklch2oklab(lch)
+ -- Treat grays specially
+ if lch.c <= 0 or lch.h == nil then return { l = lch.l, a = 0, b = 0 } end
+
+ local a = lch.c * math.cos(degree2rad(lch.h))
+ local b = lch.c * math.sin(degree2rad(lch.h))
+ return { l = lch.l, a = a, b = b }
+end
+
+function M.rgb2oklab(rgb)
+ -- Convert to linear RGB
+ local r, g, b = correct_channel(rgb.r / 255), correct_channel(rgb.g / 255), correct_channel(rgb.b / 255)
+
+ -- Convert to Oklab
+ local l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b
+ local m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b
+ local s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b
+
+ local l_, m_, s_ = cuberoot(l), cuberoot(m), cuberoot(s)
+
+ local L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_
+ local A = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_
+ local B = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
+
+ -- Explicitly convert for nearly achromatic colors
+ if math.abs(A) < 1e-4 then A = 0 end
+ if math.abs(B) < 1e-4 then B = 0 end
+
+ -- Normalize to appropriate range
+ return { l = 100 * L, a = A, b = B }
+end
+
+function M.oklab2rgb(oklab)
+ local L, A, B = 0.01 * oklab.l, oklab.a, oklab.b
+
+ local l_ = L + 0.3963377774 * A + 0.2158037573 * B
+ local m_ = L - 0.1055613458 * A - 0.0638541728 * B
+ local s_ = L - 0.0894841775 * A - 1.2914855480 * B
+
+ local l = l_ * l_ * l_
+ local m = m_ * m_ * m_
+ local s = s_ * s_ * s_
+
+ --stylua: ignore
+ local r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
+ local g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
+ local b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
+
+ return { r = 255 * correct_channel_inv(r), g = 255 * correct_channel_inv(g), b = 255 * correct_channel_inv(b) }
+end
+
+function M.hex2rgb(hex)
+ local dec = tonumber(hex:sub(2), 16)
+
+ local b = math.fmod(dec, 256)
+ local g = math.fmod((dec - b) / 256, 256)
+ local r = math.floor(dec / 65536)
+
+ return { r = r, g = g, b = b }
+end
+
+function M.rgb2hex(rgb)
+ -- Use straightforward clipping to [0; 255] here to ensure correctness.
+ -- Modify `rgb` prior to this to ensure only a small distortion.
+ local r = clip(round(rgb.r), 0, 255)
+ local g = clip(round(rgb.g), 0, 255)
+ local b = clip(round(rgb.b), 0, 255)
+
+ return string.format('#%02x%02x%02x', r, g, b)
+end
+
+return M
diff --git a/lib/utils.lua b/lib/utils.lua
new file mode 100644
index 0000000..909ec49
--- /dev/null
+++ b/lib/utils.lua
@@ -0,0 +1,47 @@
+local json = require "lib.json"
+
+local M = {}
+
+function M.get_palette()
+ local f = io.open("palette.json", "r")
+ if not f then error("couldn't open palette.json") end
+ local data = f:read("a")
+ f:close()
+ return json.decode(data)
+end
+
+-- This is taken from http://lua-users.org/wiki/SortedIteration
+-- This version is stripped of comments and empty lines + some stuff is renamed
+
+local function __gen_oindex(t)
+ local oindex = {}
+ for key in pairs(t) do
+ table.insert(oindex, key)
+ end
+ table.sort(oindex)
+ return oindex
+end
+
+function M.onext(t, state)
+ local key = nil
+ if state == nil then
+ t.__oindex = __gen_oindex(t)
+ key = t.__oindex[1]
+ else
+ for i = 1, #t.__oindex do
+ if t.__oindex[i] == state then
+ key = t.__oindex[i + 1]
+ end
+ end
+ end
+ if key then
+ return key, t[key]
+ end
+ t.__oindex = nil
+end
+
+function M.opairs(t)
+ return M.onext, t, nil
+end
+
+return M
diff --git a/nvim.lua b/nvim.lua
new file mode 100644
index 0000000..d17abe5
--- /dev/null
+++ b/nvim.lua
@@ -0,0 +1,18 @@
+local utils = require "lib.utils"
+
+local palette = utils.get_palette()
+
+local output = [[-- autogenerated from palette.json
+return {
+]]
+
+for name, value in utils.opairs(palette) do
+ for variant, v_value in utils.opairs(value) do
+ local color = "{ l = " .. v_value.l .. ", c = " .. v_value.c .. ", h = " .. v_value.h .. "}"
+ output = output .. " " .. name .. (variant == "normal" and "" or "_" .. variant) .. " = " .. color .. ",\n"
+ end
+end
+
+output = output .. "}"
+
+print(output)
diff --git a/palette.json b/palette.json
new file mode 100644
index 0000000..de1ac9f
--- /dev/null
+++ b/palette.json
@@ -0,0 +1,154 @@
+{
+ "fg": {
+ "low": {
+ "l": 75,
+ "c": 0.01368,
+ "h": 253.09
+ },
+ "normal": {
+ "l": 85,
+ "c": 0.01368,
+ "h": 253.09
+ },
+ "high": {
+ "l": 95,
+ "c": 0.01368,
+ "h": 253.09
+ },
+ },
+ "bg": {
+ "lowest": {
+ "l": 15,
+ "c": 0.01368,
+ "h": 253.09
+ },
+ "low": {
+ "l": 17.5,
+ "c": 0.01368,
+ "h": 253.09
+ },
+ "normal": {
+ "l": 20,
+ "c": 0.01368,
+ "h": 253.09
+ },
+ "high": {
+ "l": 22.5,
+ "c": 0.01368,
+ "h": 253.09
+ },
+ "highest": {
+ "l": 25,
+ "c": 0.01368,
+ "h": 253.09
+ },
+ },
+ "border": {
+ "normal": {
+ "l": 35,
+ "c": 0.023,
+ "h": 253.09
+ },
+ "variant": {
+ "l": 30,
+ "c": 0.023,
+ "h": 253.09
+ },
+ },
+ "red": {
+ "normal": {
+ "l": 80,
+ "c": 0.15765,
+ "h": 24.728
+ },
+ "bright": {
+ "l": 90,
+ "c": 0.15765,
+ "h": 24.728
+ },
+ },
+ "orange": {
+ "normal": {
+ "l": 80,
+ "c": 0.139,
+ "h": 45.216
+ },
+ "bright": {
+ "l": 90,
+ "c": 0.139,
+ "h": 45.216
+ },
+ },
+ "yellow": {
+ "normal": {
+ "l": 80,
+ "c": 0.12758,
+ "h": 76.402
+ },
+ "bright": {
+ "l": 90,
+ "c": 0.12758,
+ "h": 76.402
+ },
+ },
+ "green": {
+ "normal": {
+ "l": 80,
+ "c": 0.11211,
+ "h": 147.63
+ },
+ "bright": {
+ "l": 90,
+ "c": 0.11211,
+ "h": 147.63
+ },
+ },
+ "cyan": {
+ "normal": {
+ "l": 80,
+ "c": 0.09919,
+ "h": 181.49
+ },
+ "bright": {
+ "l": 90,
+ "c": 0.09919,
+ "h": 181.49
+ },
+ },
+ "blue": {
+ "normal": {
+ "l": 80,
+ "c": 0.09994,
+ "h": 249.58
+ },
+ "bright": {
+ "l": 90,
+ "c": 0.09994,
+ "h": 249.58
+ },
+ },
+ "purple": {
+ "normal": {
+ "l": 80,
+ "c": 0.09923,
+ "h": 296.61
+ },
+ "bright": {
+ "l": 90,
+ "c": 0.09923,
+ "h": 296.61
+ },
+ },
+ "pink": {
+ "normal": {
+ "l": 80,
+ "c": 0.06662,
+ "h": 9.2146
+ },
+ "bright": {
+ "l": 90,
+ "c": 0.06662,
+ "h": 9.2146
+ }
+ }
+}