From 8ff37e2a9877c4c128c37f05e138b34d62078be5 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sun, 21 Jul 2024 14:33:31 +0800 Subject: [PATCH] feat(cpicker): add linear and lab color space --- bundle/cpicker.nvim/lua/cpicker.lua | 9 +- .../cpicker.nvim/lua/cpicker/formats/cmyk.lua | 8 +- .../cpicker.nvim/lua/cpicker/formats/hsl.lua | 6 +- .../cpicker.nvim/lua/cpicker/formats/hsv.lua | 6 +- .../cpicker.nvim/lua/cpicker/formats/hwb.lua | 6 +- .../cpicker.nvim/lua/cpicker/formats/lab.lua | 108 ++++++++++ .../lua/cpicker/formats/linear.lua | 108 ++++++++++ .../cpicker.nvim/lua/cpicker/formats/rgb.lua | 6 +- bundle/cpicker.nvim/plugin/cpicker.lua | 2 +- lua/spacevim/api/color.lua | 202 ++++++++++++++++++ 10 files changed, 438 insertions(+), 23 deletions(-) create mode 100644 bundle/cpicker.nvim/lua/cpicker/formats/lab.lua create mode 100644 bundle/cpicker.nvim/lua/cpicker/formats/linear.lua diff --git a/bundle/cpicker.nvim/lua/cpicker.lua b/bundle/cpicker.nvim/lua/cpicker.lua index f5981e32c..cf3c197a6 100644 --- a/bundle/cpicker.nvim/lua/cpicker.lua +++ b/bundle/cpicker.nvim/lua/cpicker.lua @@ -20,6 +20,7 @@ local util = require('cpicker.util') local enabled_formats = {} local increase_keys = {} local reduce_keys = {} +local color_code_regex = {} local function update_buf_text() local rst = {} @@ -35,7 +36,7 @@ local function update_buf_text() end end table.insert(rst, '') - local color_code_regex = {} + color_code_regex = {} for _, format in ipairs(enabled_formats) do local ok, f = pcall(require, 'cpicker.formats.' .. format) if ok then @@ -85,11 +86,7 @@ end -- https://zenn.dev/kawarimidoll/articles/a8ac50a17477bd local function copy_color() - local from, to = vim - .regex( - [[#[0123456789ABCDEF]\+\|rgb(\d\+,\s\d\+,\s\d\+)\|hsl(\d\+,\s\d\+%,\s\d\+%)\|hsv(\d\+,\s\d\+%,\s\d\+%)\|cmyk(\d\+%,\s\d\+%,\s\d\+%,\s\d\+%)\|hwb(\d\+,\s\d\+%,\s\d\+%)]] - ) - :match_str(vim.fn.getline('.')) + local from, to = vim.regex(table.concat(vim.tbl_map(function(t) return t[2] end, color_code_regex), '\\|')):match_str(vim.fn.getline('.')) if from then vim.fn.setreg('+', string.sub(vim.fn.getline('.'), from, to + 1)) notify.notify('copied:' .. string.sub(vim.fn.getline('.'), from, to + 1)) diff --git a/bundle/cpicker.nvim/lua/cpicker/formats/cmyk.lua b/bundle/cpicker.nvim/lua/cpicker/formats/cmyk.lua index 23aa27ad5..7a037be23 100644 --- a/bundle/cpicker.nvim/lua/cpicker/formats/cmyk.lua +++ b/bundle/cpicker.nvim/lua/cpicker/formats/cmyk.lua @@ -27,10 +27,10 @@ function M.buf_text() local m_bar = util.generate_bar(magenta, '+') local y_bar = util.generate_bar(yellow, '+') local k_bar = util.generate_bar(black, '+') - table.insert(rst, 'CMYK: C: ' .. string.format('%4s', math.floor(cyan * 100 + 0.5)) .. ' ' .. c_bar) - table.insert(rst, ' M: ' .. string.format('%4s', math.floor(magenta * 100 + 0.5)) .. ' ' .. m_bar) - table.insert(rst, ' Y: ' .. string.format('%4s', math.floor(yellow * 100 + 0.5)) .. ' ' .. y_bar) - table.insert(rst, ' K: ' .. string.format('%4s', math.floor(black * 100 + 0.5)) .. ' ' .. k_bar) + table.insert(rst, 'CMYK: C: ' .. string.format('%4s', math.floor(cyan * 100 + 0.5)) .. ' ' .. c_bar) + table.insert(rst, ' M: ' .. string.format('%4s', math.floor(magenta * 100 + 0.5)) .. ' ' .. m_bar) + table.insert(rst, ' Y: ' .. string.format('%4s', math.floor(yellow * 100 + 0.5)) .. ' ' .. y_bar) + table.insert(rst, ' K: ' .. string.format('%4s', math.floor(black * 100 + 0.5)) .. ' ' .. k_bar) return rst end diff --git a/bundle/cpicker.nvim/lua/cpicker/formats/hsl.lua b/bundle/cpicker.nvim/lua/cpicker/formats/hsl.lua index 1b593962e..edccd6365 100644 --- a/bundle/cpicker.nvim/lua/cpicker/formats/hsl.lua +++ b/bundle/cpicker.nvim/lua/cpicker/formats/hsl.lua @@ -25,14 +25,14 @@ function M.buf_text() local h_bar = util.generate_bar(hue, '+', 360) local s_bar = util.generate_bar(saturation, '+') local l_bar = util.generate_bar(lightness, '+') - table.insert(rst, 'HSL: H: ' .. string.format('%4s', math.floor(hue + 0.5)) .. ' ' .. h_bar) + table.insert(rst, 'HSL: H: ' .. string.format('%4s', math.floor(hue + 0.5)) .. ' ' .. h_bar) table.insert( rst, - ' S: ' .. string.format('%3s', math.floor(saturation * 100 + 0.5)) .. '% ' .. s_bar + ' S: ' .. string.format('%3s', math.floor(saturation * 100 + 0.5)) .. '% ' .. s_bar ) table.insert( rst, - ' L: ' .. string.format('%3s', math.floor(lightness * 100 + 0.5)) .. '% ' .. l_bar + ' L: ' .. string.format('%3s', math.floor(lightness * 100 + 0.5)) .. '% ' .. l_bar ) return rst end diff --git a/bundle/cpicker.nvim/lua/cpicker/formats/hsv.lua b/bundle/cpicker.nvim/lua/cpicker/formats/hsv.lua index 979085c29..adfd600b4 100644 --- a/bundle/cpicker.nvim/lua/cpicker/formats/hsv.lua +++ b/bundle/cpicker.nvim/lua/cpicker/formats/hsv.lua @@ -26,14 +26,14 @@ function M.buf_text() local h_bar = util.generate_bar(hue, '+', 360) local s_bar = util.generate_bar(saturation, '+') local l_bar = util.generate_bar(value, '+') - table.insert(rst, 'HSV: H: ' .. string.format('%4s', math.floor(hue + 0.5)) .. ' ' .. h_bar) + table.insert(rst, 'HSV: H: ' .. string.format('%4s', math.floor(hue + 0.5)) .. ' ' .. h_bar) table.insert( rst, - ' S: ' .. string.format('%3s', math.floor(saturation * 100 + 0.5)) .. '% ' .. s_bar + ' S: ' .. string.format('%3s', math.floor(saturation * 100 + 0.5)) .. '% ' .. s_bar ) table.insert( rst, - ' V: ' .. string.format('%3s', math.floor(value * 100 + 0.5)) .. '% ' .. l_bar + ' V: ' .. string.format('%3s', math.floor(value * 100 + 0.5)) .. '% ' .. l_bar ) return rst end diff --git a/bundle/cpicker.nvim/lua/cpicker/formats/hwb.lua b/bundle/cpicker.nvim/lua/cpicker/formats/hwb.lua index f717f8047..5d27f49ab 100644 --- a/bundle/cpicker.nvim/lua/cpicker/formats/hwb.lua +++ b/bundle/cpicker.nvim/lua/cpicker/formats/hwb.lua @@ -25,14 +25,14 @@ function M.buf_text() local h_bar = util.generate_bar(hue, '+', 360) local w_bar = util.generate_bar(whiteness, '+') local b_bar = util.generate_bar(blackness, '+') - table.insert(rst, 'HWB: H: ' .. string.format('%4s', math.floor(hue + 0.5)) .. ' ' .. h_bar) + table.insert(rst, 'HWB: H: ' .. string.format('%4s', math.floor(hue + 0.5)) .. ' ' .. h_bar) table.insert( rst, - ' W: ' .. string.format('%3s', math.floor(whiteness * 100 + 0.5)) .. '% ' .. w_bar + ' W: ' .. string.format('%3s', math.floor(whiteness * 100 + 0.5)) .. '% ' .. w_bar ) table.insert( rst, - ' B: ' .. string.format('%3s', math.floor(blackness * 100 + 0.5)) .. '% ' .. b_bar + ' B: ' .. string.format('%3s', math.floor(blackness * 100 + 0.5)) .. '% ' .. b_bar ) return rst end diff --git a/bundle/cpicker.nvim/lua/cpicker/formats/lab.lua b/bundle/cpicker.nvim/lua/cpicker/formats/lab.lua new file mode 100644 index 000000000..3c6e942e4 --- /dev/null +++ b/bundle/cpicker.nvim/lua/cpicker/formats/lab.lua @@ -0,0 +1,108 @@ +--============================================================================= +-- lab.lua +-- Copyright (c) 2019-2024 Wang Shidong & Contributors +-- Author: Wang Shidong < wsdjeg@outlook.com > +-- URL: https://spacevim.org +-- License: GPLv3 +--============================================================================= +local M = {} + +local color = require('spacevim.api.color') +local util = require('cpicker.util') + +local l = 0 -- [0, 360] +local a = 0 -- [0, 100%] +local b = 0 -- [0, 100%] + +M.color_code_regex = [[\slab(\d\+,\s-\?\d\+%,\s-\?\d\+%)]] + +local function on_change_argv() + return 'lab', { l, a, b } +end + +function M.buf_text() + local rst = {} + local h_bar = util.generate_bar(l, '+', 100) + local s_bar = util.generate_bar(a, '+', 250) + local l_bar = util.generate_bar(b, '+', 250) + table.insert(rst, 'Lab: L: ' .. string.format('%3s', math.floor(l + 0.5)) .. ' ' .. h_bar) + table.insert(rst, ' a: ' .. string.format('%3s', math.floor(a + 0.5)) .. '% ' .. s_bar) + table.insert(rst, ' b: ' .. string.format('%3s', math.floor(b + 0.5)) .. '% ' .. l_bar) + return rst +end + +function M.color_code() + return + ' =========' .. string.format( + ' lab(%s, %s%%, %s%%)', + math.floor(l + 0.5), + math.floor(a + 0.5), + math.floor(b + 0.5) + ) +end + +local function increase_l() + if l <= 99 then + l = l + 1 + elseif l < 100 then + l = 100 + end + return on_change_argv() +end +local function reduce_l() + if l >= 1 then + l = l - 1 + elseif l > 0 then + l = 0 + end + return on_change_argv() +end +local function increase_a() + if a <= 99 then + a = a + 1 + elseif a < 100 then + a = 100 + end + return on_change_argv() +end +local function reduce_a() + if a >= -99 then + a = a - 1 + elseif a > -100 then + a = -100 + end + return on_change_argv() +end +local function increase_b() + if b <= 99 then + b = b + 1 + elseif b < 100 then + b = 100 + end + return on_change_argv() +end +local function reduce_b() + if b >= -99 then + b = b - 1 + elseif b > -100 then + b = -100 + end + return on_change_argv() +end +function M.increase_reduce_functions() + return { + { increase_l, reduce_l }, + { increase_a, reduce_a }, + { increase_b, reduce_b }, + } +end + +function M.on_change(f, code) + if f == 'lab' then + l, a, b = unpack(code) + return + end + l, a, b = color[f .. '2lab'](unpack(code)) +end + +return M diff --git a/bundle/cpicker.nvim/lua/cpicker/formats/linear.lua b/bundle/cpicker.nvim/lua/cpicker/formats/linear.lua new file mode 100644 index 000000000..52191eedd --- /dev/null +++ b/bundle/cpicker.nvim/lua/cpicker/formats/linear.lua @@ -0,0 +1,108 @@ +--============================================================================= +-- rgb-linear +-- Copyright (c) 2019-2024 Wang Shidong & Contributors +-- Author: Wang Shidong < wsdjeg@outlook.com > +-- URL: https://spacevim.org +-- License: GPLv3 +--============================================================================= +local M = {} + +local color = require('spacevim.api.color') +local util = require('cpicker.util') + +local red = 0 -- [0, 1] +local green = 0 -- [0, 1] +local blue = 0 -- [0, 1] + +M.color_code_regex = [[\s#[0123456789ABCDEF]\+]] + +local function on_change_argv() + return 'linear', { red, green, blue } +end + +function M.buf_text() + local rst = {} + local r_bar = util.generate_bar(red, '+') + local g_bar = util.generate_bar(green, '+') + local b_bar = util.generate_bar(blue, '+') + table.insert( + rst, + 'Linear: R: ' .. string.format('%4s', math.floor(red * 255 + 0.5)) .. ' ' .. r_bar + ) + table.insert( + rst, + ' G: ' .. string.format('%4s', math.floor(green * 255 + 0.5)) .. ' ' .. g_bar + ) + table.insert( + rst, + ' B: ' .. string.format('%4s', math.floor(blue * 255 + 0.5)) .. ' ' .. b_bar + ) + return rst +end + +function M.color_code() + return ' =========' .. ' ' .. color.linear2hex(red, green, blue) +end + +local function increase(c) + if c <= 1 - 1 / 255 then + c = c + 1 / 255 + elseif c < 1 then + c = 1 + end + return c +end + +local function reduce(c) + if c >= 1 / 255 then + c = c - 1 / 255 + elseif c > 0 then + c = 0 + end + return c +end + +local function increase_rgb_red() + red = increase(red) + return on_change_argv() +end +local function reduce_rgb_red() + red = reduce(red) + return on_change_argv() +end +local function increase_rgb_green() + green = increase(green) + return on_change_argv() +end +local function reduce_rgb_green() + green = reduce(green) + return on_change_argv() +end + +local function increase_rgb_blue() + blue = increase(blue) + return on_change_argv() +end + +local function reduce_rgb_blue() + blue = reduce(blue) + return on_change_argv() +end +function M.increase_reduce_functions() + return { + { increase_rgb_red, reduce_rgb_red }, + { increase_rgb_green, reduce_rgb_green }, + { increase_rgb_blue, reduce_rgb_blue }, + } +end + +function M.on_change(f, code) + if f == 'linear' then + red, green, blue = unpack(code) + return + end + red, green, blue = color[f .. '2linear'](unpack(code)) +end + +return M + diff --git a/bundle/cpicker.nvim/lua/cpicker/formats/rgb.lua b/bundle/cpicker.nvim/lua/cpicker/formats/rgb.lua index c5c445677..da2e05ecf 100644 --- a/bundle/cpicker.nvim/lua/cpicker/formats/rgb.lua +++ b/bundle/cpicker.nvim/lua/cpicker/formats/rgb.lua @@ -27,15 +27,15 @@ function M.buf_text() local b_bar = util.generate_bar(blue, '+') table.insert( rst, - 'RGB: R: ' .. string.format('%4s', math.floor(red * 255 + 0.5)) .. ' ' .. r_bar + 'RGB: R: ' .. string.format('%4s', math.floor(red * 255 + 0.5)) .. ' ' .. r_bar ) table.insert( rst, - ' G: ' .. string.format('%4s', math.floor(green * 255 + 0.5)) .. ' ' .. g_bar + ' G: ' .. string.format('%4s', math.floor(green * 255 + 0.5)) .. ' ' .. g_bar ) table.insert( rst, - ' B: ' .. string.format('%4s', math.floor(blue * 255 + 0.5)) .. ' ' .. b_bar + ' B: ' .. string.format('%4s', math.floor(blue * 255 + 0.5)) .. ' ' .. b_bar ) return rst end diff --git a/bundle/cpicker.nvim/plugin/cpicker.lua b/bundle/cpicker.nvim/plugin/cpicker.lua index 9c8812b8d..35b305a3c 100644 --- a/bundle/cpicker.nvim/plugin/cpicker.lua +++ b/bundle/cpicker.nvim/plugin/cpicker.lua @@ -8,7 +8,7 @@ if vim.api.nvim_create_user_command then local function complete() - return { 'rgb', 'hsl', 'hsv', 'cmyk', 'hwb' } + return { 'rgb', 'hsl', 'hsv', 'cmyk', 'hwb', 'linear', 'lab' } end vim.api.nvim_create_user_command('Cpicker', function(opt) require('cpicker').picker(opt.fargs) diff --git a/lua/spacevim/api/color.lua b/lua/spacevim/api/color.lua index 606b5fdd9..539d7ff1f 100644 --- a/lua/spacevim/api/color.lua +++ b/lua/spacevim/api/color.lua @@ -8,6 +8,12 @@ local color = {} +-- rgb <-> hsl - hwb +-- rgb <-> cmyk +-- rgb <-> hsv +-- rgb <-> hex +-- rgb <-> linear <-> xyz <-> lab <-> lch + -- 参考: https://blog.csdn.net/Sunshine_in_Moon/article/details/45131285 color.rgb2hsl = function(r, g, b) @@ -268,4 +274,200 @@ color.hwb2hex = function(h, w, b) return color.rgb2hex(color.hwb2rgb(h, w, b)) end +color.rgb2linear = function(r, g, b) + return unpack(vim.tbl_map(function(x) + if x <= 0.04045 then + return x / 12.92 + end + return ((x + 0.055) / 1.055) ^ 2.4 + end, { r, g, b })) +end + +color.linear2rgb = function(r, g, b) + return unpack(vim.tbl_map(function(x) + if x <= 0.0031308 then + local a = 12.92 * x + if a > 1 then + return 1 + elseif a < 0 then + return 0 + else + return a + end + else + local a = 1.055 * x ^ (1 / 2.4) - 0.055 + if a > 1 then + return 1 + elseif a < 0 then + return 0 + else + return a + end + end + end, { r, g, b })) +end + +color.linear2hsl = function(r, g, b) + return color.rgb2hsl(color.linear2rgb(r, g, b)) +end + +color.linear2hwb = function(r, g, b) + return color.rgb2hwb(color.linear2rgb(r, g, b)) +end +color.linear2cmyk = function(r, g, b) + return color.rgb2cmyk(color.linear2rgb(r, g, b)) +end +color.linear2hsv = function(r, g, b) + return color.rgb2hsv(color.linear2rgb(r, g, b)) +end +color.linear2hex = function(r, g, b) + return color.rgb2hex(color.linear2rgb(r, g, b)) +end +color.linear2lab = function(r, g, b) + return color.xyz2lab(color.linear2xyz(r, g, b)) +end + +color.hsl2linear = function(h, s, l) + return color.rgb2linear(color.hsl2rgb(h, s, l)) +end + +color.hwb2linear = function(h, w, b) + return color.rgb2linear(color.hwb2rgb(h, w, b)) +end + +color.cmyk2linear = function(c, m, y, k) + return color.rgb2linear(color.cmyk2rgb(c, m, y, k)) +end + +color.hsv2linear = function(h, s, v) + return color.rgb2linear(color.hsv2rgb(h, s, v)) +end + +-------------------------------------------------------------------------------- +-- XYZ color space +-------------------------------------------------------------------------------- + +local linear2xyz = { + { 0.41239079926595, 0.35758433938387, 0.18048078840183 }, + { 0.21263900587151, 0.71516867876775, 0.072192315360733 }, + { 0.019330818715591, 0.11919477979462, 0.95053215224966 }, +} +local xyz2linear = { + { 3.240969941904521, -1.537383177570093, -0.498610760293 }, + { -0.96924363628087, 1.87596750150772, 0.041555057407175 }, + { 0.055630079696993, -0.20397695888897, 1.056971514242878 }, +} + +local function dot(a, b) + assert(#a == #b) + local result = 0 + for i = 1, #a do + result = result + a[i] * b[i] + end + return result +end + +local function product(m, v) + local row = #m + local result = {} + for i = 1, row do + result[i] = dot(m[i], v) + end + return unpack(result) +end + +function color.linear2xyz(r, g, b) + return product(linear2xyz, { r, g, b }) +end + +function color.xyz2linear(x, y, z) + return product(xyz2linear, { x, y, z }) +end + +-------------------------------------------------------------------------------- +-- Lab color space +-------------------------------------------------------------------------------- +-- What Does CIE L*a*b* Stand for? +-- +-- The CIE in CIELAB is the abbreviation for the International Commission on +-- Illumination’s French name, Commission Internationale de l´Eclairage. +-- The letters L*, a* and b* represent each of the three values the CIELAB color +-- space uses to measure objective color and calculate color differences. +-- L* represents lightness from black to white on a scale of zero to 100, +-- while a* and b* represent chromaticity with no specific numeric limits. +-- Negative a* corresponds with green, positive a* corresponds with red, +-- negative b* corresponds with blue and positive b* corresponds with yellow. +-- +-- https://www.hunterlab.com/blog/what-is-cielab-color-space/ +-- https://zh.wikipedia.org/wiki/CIELAB%E8%89%B2%E5%BD%A9%E7%A9%BA%E9%97%B4 +-- https://en.wikipedia.org/wiki/CIELAB_color_space +-- https://developer.mozilla.org/zh-CN/docs/Web/CSS/color_value/lab + +function color.xyz2lab(x, y, z) + local Xn, Yn, Zn = 0.9505, 1, 1.089 + local function f(t) + if t > (6 / 29) ^ 3 then + return 116 * t ^ (1 / 3) - 16 + end + return (29 / 3) ^ 3 * t + end + return f(y / Yn), (500 / 116) * (f(x / Xn) - f(y / Yn)), (200 / 116) * (f(y / Yn) - f(z / Zn)) +end + +function color.lab2xyz(l, a, b) + local Xn, Yn, Zn = 0.9505, 1, 1.089 + local fy = (l + 16) / 116 + local fx = fy + (a / 500) + local fz = fy - (b / 200) + local function t(f) + if f > 6 / 29 then + return f ^ 3 + end + return (116 * f - 16) * (3 / 29) ^ 3 + end + return t(fx) * Xn, t(fy) * Yn, t(fz) * Zn +end + +function color.rgb2lab(r, g, b) + return color.xyz2lab(color.linear2xyz(color.rgb2linear(r, g, b))) +end + +function color.lab2rgb(l, a, b) + return color.linear2rgb(color.xyz2linear(color.lab2xyz(l, a, b))) +end + +function color.lab2hex(l, a, b) + return color.rgb2hex(color.lab2rgb(l, a, b)) +end + +function color.lab2hsl(l, a, b) + return color.rgb2hsl(color.lab2rgb(l, a, b)) +end +function color.lab2hsv(l, a, b) + return color.rgb2hsv(color.lab2rgb(l, a, b)) +end +function color.lab2hwb(l, a, b) + return color.rgb2hwb(color.lab2rgb(l, a, b)) +end +function color.lab2cmyk(l, a, b) + return color.rgb2cmyk(color.lab2rgb(l, a, b)) +end +function color.lab2linear(l, a, b) + return color.xyz2linear(color.lab2xyz(l, a, b)) +end + +function color.lab2lch(L, a, b) + local H = math.atan2(b, a) + local C = math.sqrt(a ^ 2 + b ^ 2) + H = H / (2 * math.pi) * 360 -- [rad] -> [deg] + H = H % 360 + return L, C, H +end + +function color.lch2lab(L, C, H) + H = H / 360 * (2 * math.pi) -- [deg] -> [rad] + local a = C * math.cos(H) + local b = C * math.sin(H) + return L, a, b +end return color