-- Introduction: https://colorspace.r-forge.r-project.org/articles/color_spaces.html
local p =
local function isEmpty(value) return value
end
local function isNotEmpty(value) return value ~= nil and value ~= end
local function argDefault(value, default) if (value
) then return default else return value endend
local function numArgDefault(value, default) if (value
) then return default else return tonumber(value) endend
local function isArgTrue(value) return (value ~= nil and value ~= and value ~= '0')end
local function isEmpty(value) return value
end
local function isNotEmpty(value) return value ~= nil and value ~= end
local function hexToRgb(hexColor) local cleanColor = hexColor:gsub('#', '#'):match('^[%s#]*(.-)[%s;]*$') if (#cleanColor
3) then return 17 * tonumber(string.sub(cleanColor, 1, 1), 16), 17 * tonumber(string.sub(cleanColor, 2, 2), 16), 17 * tonumber(string.sub(cleanColor, 3, 3), 16) end error('Invalid hexadecimal color ' .. cleanColor, 1)end
local function round(value) if (value < 0) then return math.ceil(value - 0.5) else return math.floor(value + 0.5) endend
local function rgbToHex(r, g, b) return string.format('%02X%02X%02X', round(r), round(g), round(b))end
local function checkRgb(r, g, b) if (r > 255 or g > 255 or b > 255 or r < 0 or g < 0 or b < 0) then error('Color level out of bounds') endend
local function rgbToCmyk(r, g, b) local c = 1 - r / 255 local m = 1 - g / 255 local y = 1 - b / 255 local k = math.min(c, m, y) if (k
local function rgbToHsl(r, g, b) local channelMax = math.max(r, g, b) local channelMin = math.min(r, g, b) local range = channelMax - channelMin local h, s if (range
r) then h = 60 * ((g - b) / range) if (h < 0) then h = 360 + h end elseif (channelMax
0 or L
local function rgbToHsv(r, g, b) local channelMax = math.max(r, g, b) local channelMin = math.min(r, g, b) local range = channelMax - channelMin local h, s if (range
r) then h = 60 * ((g - b) / range) if (h < 0) then h = 360 + h end elseif (channelMax
0) then s = 0 else s = 100 * range / channelMax end return h, s, channelMax * 100 / 255end
local function checkHsv(h, s, v) if (s > 100 or v > 100 or s < 0 or v < 0) then error('Color level out of bounds') end end
local function hsvToRgb(h, s, v) local hn = (h / 60 - 6 * math.floor(h / 360)) local hi = math.floor(hn) local hr = hn - hi local sn = s / 100 local vs = v * 255 / 100 local p = vs * (1 - sn); local q = vs * (1 - sn * hr); local t = vs * (1 - sn * (1 - hr)); if (hi < 3) then if (hi
1) then return q, vs, p else return p, vs, t end else if (hi
4) then return t, p, vs else return vs, p, q end endend
-- c in [0, 255], condition tweaked for no discontinuity-- http://entropymine.com/imageworsener/srgbformula/local function toLinear(c) if (c > 10.314300250662591) then return math.pow((c + 14.025) / 269.025, 2.4) else return c / 3294.6 endend
local function toNonLinear(c) if (c > 0.00313066844250063) then return 269.025 * math.pow(c, 1.0/2.4) - 14.025 else return 3294.6 * c endend
local function srgbToCielchuvD65o2deg(r, g, b) local R = toLinear(r) local G = toLinear(g) local B = toLinear(b) -- https://github.com/w3c/csswg-drafts/issues/5922 local X = 0.1804807884018343 * B + 0.357584339383878 * G + 0.41239079926595934 * R local Y = 0.07219231536073371 * B + 0.21263900587151027 * R + 0.715168678767756 * G local Z = 0.01933081871559182 * R + 0.11919477979462598 * G + 0.9505321522496607 * B local L, C, h if (Y > 0.00885645167903563082) then L = 116 * math.pow(Y, 1/3) - 16 else L = Y * 903.2962962962962962963 end if ((r
b) or L
0) then C = 0 h = 0 else -- 0.19783... and 0.4631... computed with extra precision from (X,Y,Z) when (R,G,B) = (1,1,1), -- in which case (u,v) ≈ (0,0) local us = 4 * X / d - 0.19783000664283678994 local vs = 9 * Y / d - 0.46831999493879099801 h = math.atan2(vs, us) * 57.2957795130823208768 if (h < 0) then h = h + 360 elseif (h
0) then C = 0 h = 0 end end end return L, C, hend
local function checkInterpolationParameter(t) if (t > 1 or t < 0) then error('Interpolation parameter out of bounds') endend
local function srgbMix(t, r0, g0, b0, r1, g1, b1) local tc = 1 - t return toNonLinear(tc * toLinear(r0) + t * toLinear(r1)), toNonLinear(tc * toLinear(g0) + t * toLinear(g1)), toNonLinear(tc * toLinear(b0) + t * toLinear(b1))end
-- functions for generating gradients, inspired by OKLCH but not needing gamut mappinglocal function adjustHueToCielch(h) local n = 180 * math.floor(h / 180) local d = h - n if (d < 60) then d = 73.7 * d / 60 elseif (d < 120) then d = 0.6975 * d + 31.85 else d = 1.07416666666666666667 * d - 13.35 end return n + dend
local function unadjustHueFromCielch(h) local n = 180 * math.floor(h / 180) local d = h - n if (d < 73.7) then d = 0.81411126187245590231 * d elseif (d < 115.55) then d = 1.43369175627240143369 * d - 45.66308243727598566308 else d = 0.93095422808378588053 * d + 12.42823894491854150504 end return n + dend
local function getLightness(r, g, b) local Y = 0.07219231536073371 * toLinear(b) + 0.21263900587151027 * toLinear(r) + 0.715168678767756 * toLinear(g) if (Y > 0.00885645167903563082) then return 116 * math.pow(Y, 1/3) - 16 else return Y * 903.2962962962962962963 endend
local function adjustLightness(L, r, g, b) if (L >= 100) then return 255, 255, 255 end local Yc if (L > 8) then Yc = (L + 16) / 116 Yc = Yc * Yc * Yc else Yc = L * 0.00110705645987945385 end local R = toLinear(r) local G = toLinear(g) local B = toLinear(b) local Y = 0.07219231536073371 * B + 0.21263900587151027 * R + 0.715168678767756 * G if (Y > 0) then local scale = Yc / Y R = R * scale G = G * scale B = B * scale local cmax = math.max(R, G, B) if (cmax > 1) then R = R / cmax G = G / cmax B = B / cmax local d = 0.07219231536073371 * (1 - B) + 0.21263900587151027 * (1 - R) + 0.715168678767756 * (1 - G) if (d <= 0) then R = 1 G = 1 B = 1 else local strength = 0.5 -- 1 yields equal lightness local t = (Yc - 0.07219231536073371 * B - 0.21263900587151027 * R - 0.715168678767756 * G) / d R = R + strength * (1 - R) * t G = G + strength * (1 - G) * t B = B + strength * (1 - B) * t end end else R = Yc G = Yc B = Yc end return toNonLinear(R), toNonLinear(G), toNonLinear(B)end
local function interpolateHue(t, r0, g0, b0, r1, g1, b1, direction) local h0, s0, v0 = rgbToHsv(r0, g0, b0) local h1, s1, v1 = rgbToHsv(r1, g1, b1) if (s0
0) then s0 = s1 end end if (s1
0) then s1 = s1 end end local hn0 = h0 / 360 local hn1 = h1 / 360 if (direction
local function formatToPrecision(value, p) return string.format('%.' .. p .. 'f', value)end
local function getFractionalZeros(p) if (p > 0) then return '.' .. string.rep('0', p) else return endend
local function polyMix(t, palette) if (t <= 0) then return palette[1] elseif (t >= 1) then return palette[#palette] end local n, f = math.modf(t * (#palette - 1)) if (f
-- same principle: https://colorspace.r-forge.r-project.org/articles/hcl_palettes.html-- the darkest colors do not yield an WCAG AA contrast with text, maybe this can be solved by using HCL Wizard from R's Colorspace package-- https://colorspace.r-forge.r-project.org/articles/approximations.html-- R's Colorspace does gamut mapping through simple clipping (as do most other color libraries, such as chroma.js and colorio), which is fast but not goodlocal function brewerGradient(t, palette) local colors = return polyMix(t, colors[palette])end
local function softSigmoid(x) local ax = math.abs(x) if (ax > 0.000000000000000111) then return x / (1 + ax) else return x endend
function p.hexToRgbTriplet(frame) local args = frame.args or frame:getParent.args local hex = args[1] if (isEmpty(hex)) then return end local r, g, b = hexToRgb(hex) return r .. ', ' .. g .. ', ' .. bend
function p.hexToCmyk(frame) local args = frame.args or frame:getParent.args local hex = args[1] if (isEmpty(hex)) then return end local p = numArgDefault(args.precision, 0) local s = args.pctsign or '1' local c, m, y, k = rgbToCmyk(hexToRgb(hex)) local fk = formatToPrecision(k, p) local fc, fm, fy local fracZeros = getFractionalZeros(p) if (fk
function p.hexToHsl(frame) local args = frame.args or frame:getParent.args local hex = args[1] if (isEmpty(hex)) then return end local p = numArgDefault(args.precision, 0) local h, s, l = rgbToHsl(hexToRgb(hex)) local fl = formatToPrecision(l, p) local fs, fh local fracZeros = getFractionalZeros(p) local fZero = 0 .. fracZeros if (fl
100 .. fracZeros) then fs = fZero fh = fZero else fs = formatToPrecision(s, p) if (fs
360 .. fracZeros) then fh = fZero -- handle rounding to 360 end end end return fh .. '°, ' .. fs .. '%, ' .. fl .. '%'end
function p.hexToHsv(frame) local args = frame.args or frame:getParent.args local hex = args[1] if (isEmpty(hex)) then return end local p = numArgDefault(args.precision, 0) local h, s, v = rgbToHsv(hexToRgb(hex)) local fv = formatToPrecision(v, p) local fs, fh local fracZeros = getFractionalZeros(p) local fZero = 0 .. fracZeros if (fv
fZero) then fh = fZero else fh = formatToPrecision(h, p) if (fh
function p.hexToCielch(frame) local args = frame.args or frame:getParent.args local hex = args[1] if (isEmpty(hex)) then return end local p = numArgDefault(args.precision, 0) local L, C, h = srgbToCielchuvD65o2deg(hexToRgb(hex)) local fL = formatToPrecision(L, p) local fC, fh local fracZeros = getFractionalZeros(p) local fZero = 0 .. fracZeros if (fL
100 .. fracZeros) then fC = fZero fh = fZero else fC = formatToPrecision(C, p) if (fC
360 .. fracZeros) then fh = fZero -- handle rounding to 360 end end end return fL .. ', ' .. fC .. ', ' .. fh .. '°'end
function p.hexMix(frame) local args = frame.args or frame:getParent.args local hex0 = args[1] local hex1 = args[2] if (isEmpty(hex0) or isEmpty(hex1)) then return end local t = args[3] if (isEmpty(t)) then t = 0.5 else t = tonumber(t) local amin = numArgDefault(args.min, 0) local amax = numArgDefault(args.max, 100) if (amax
function p.hexInterpolate(frame) local args = frame.args or frame:getParent.args local hex0 = args[1] local hex1 = args[2] if (isEmpty(hex0)) then return hex1 elseif (isEmpty(hex1)) then return hex0 end local t = args[3] if (isEmpty(t)) then t = 0.5 else t = tonumber(t) local amin = numArgDefault(args.min, 0) local amax = numArgDefault(args.max, 100) if (amax
function p.hexBrewerGradient(frame) local args = frame.args or frame:getParent.args local pal = argDefault(args.pal, 'spectral'):lower local value = args[1] local t if (isEmpty(value)) then t = 0.5 else value = tonumber(value) local high = numArgDefault(args.high, 100) local low = numArgDefault(args.low, -100) if (isEmpty(args.low)) then if (pal ~= 'spectral' and pal ~= 'rdylgn' and pal ~= 'rdylbu' and (pal:len ~= 4 or (pal ~= 'rdgy' and pal ~= 'rdbu' and pal ~= 'puor' and pal ~= 'prgn' and pal ~= 'piyg' and pal ~= 'brbg'))) then low = 0 end end if (high
return p