Difference between revisions of "Module:Color/sandbox"
Jump to navigation
Jump to search
Mediawiki>Ftrebien m |
m (1 revision imported) |
Latest revision as of 17:59, 21 January 2022
Documentation for this module may be created at Module:Color/sandbox/doc
local p = {}
local function isempty(v)
return v == nil or v == ''
end
local function hexToRgb(color)
local cleanColor = color:gsub("#", "#"):match('^[%s#]*(.-)[%s;]*$')
if (#cleanColor == 6) then
return {
r = tonumber(string.sub(cleanColor, 1, 2), 16),
g = tonumber(string.sub(cleanColor, 3, 4), 16),
b = tonumber(string.sub(cleanColor, 5, 6), 16)
}
elseif (#cleanColor == 3) then
return {
r = 17 * tonumber(string.sub(cleanColor, 1, 1), 16),
g = 17 * tonumber(string.sub(cleanColor, 2, 2), 16),
b = 17 * tonumber(string.sub(cleanColor, 3, 3), 16)
}
end
error("Invalid hexadecimal color " .. cleanColor, 1)
end
local function round(v)
if (v < 0) then
return math.ceil(v - 0.5)
else
return math.floor(v + 0.5)
end
end
local function rgbToHex(r, g, b)
return string.format("%02X%02X%02X", round(r), round(g), round(b))
end
local function rgbToCmyk(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")
end
local c = 1 - r / 255
local m = 1 - g / 255
local y = 1 - b / 255
local k = math.min(c, m, y)
if (k == 1) then
c = 0
m = 0
y = 0
else
local d = 1 - k
c = (c - k) / d
m = (m - k) / d
y = (y - k) / d
end
return { c = c * 100, m = m * 100, y = y * 100, k = k * 100 }
end
local function rgbToHsl(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")
end
local channelMax = math.max(r, g, b)
local channelMin = math.min(r, g, b)
local range = channelMax - channelMin
local h, s
if (range == 0) then
h = 0
elseif (channelMax == r) then
h = 60 * ((g - b) / range)
if (h < 0) then
h = 360 + h
end
elseif (channelMax == g) then
h = 60 * (2 + (b - r) / range)
else
h = 60 * (4 + (r - g) / range)
end
local L = channelMax + channelMin
if (L == 0 or L == 510) then
s = 0
else
s = 100 * range / math.min(L, 510 - L)
end
return { h = h, s = s, l = L * 50 / 255 }
end
local function rgbToHsv(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")
end
local channelMax = math.max(r, g, b)
local channelMin = math.min(r, g, b)
local range = channelMax - channelMin
local h, s
if (range == 0) then
h = 0
elseif (channelMax == r) then
h = 60 * ((g - b) / range)
if (h < 0) then
h = 360 + h
end
elseif (channelMax == g) then
h = 60 * (2 + (b - r) / range)
else
h = 60 * (4 + (r - g) / range)
end
if (channelMax == 0) then
s = 0
else
s = 100 * range / channelMax
end
return { h = h, s = s, v = channelMax * 100 / 255 }
end
local function hsvToRgb(h, s, v)
if (s > 100 or v > 100 or s < 0 or v < 0) then
error("Color level out of bounds")
end
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 == 0) then
return { r = vs, g = t, b = p }
elseif (hi == 1) then
return { r = q, g = vs, b = p }
else
return { r = p, g = vs, b = t }
end
else
if (hi == 3) then
return { r = p, g = q, b = vs }
elseif (hi == 4) then
return { r = t, g = p, b = vs }
else
return { r = vs, g = p, b = q }
end
end
end
-- 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
end
end
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
end
end
local function srgbToCielchuvD65o2deg(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")
end
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 == g and g == b) or L == 0) then
C = 0
h = 0
else
d = X + 3 * Z + 15 * Y
if (d == 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
h = 0 -- ensure zero is positive
end
C = math.sqrt(us * us + vs * vs) * 13 * L
if (C == 0) then
C = 0
h = 0
end
end
end
return { L = L, C = C, h = h }
end
local function srgbMix(t, r0, g0, b0, r1, g1, b1)
if (t > 1 or t < 0) then
error("Interpolation parameter out of bounds")
end
if (r0 > 255 or g0 > 255 or b0 > 255 or r1 > 255 or g1 > 255 or b1 > 255 or r0 < 0 or g0 < 0 or b0 < 0 or r1 < 0 or g1 < 0 or b1 < 0) then
error("Color level out of bounds")
end
local tc = 1 - t
return {
r = toNonLinear(tc * toLinear(r0) + t * toLinear(r1)),
g = toNonLinear(tc * toLinear(g0) + t * toLinear(g1)),
b = toNonLinear(tc * toLinear(b0) + t * toLinear(b1))
}
end
-- functions for generating gradients, inspired by OKLCH but not needing gamut mapping
local 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 + d
end
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 + d
end
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
end
end
local function adjustLightness(L, r, g, b)
if (L >= 100) then
return { r = 255, g = 255, b = 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 { r = toNonLinear(R), g = toNonLinear(G), b = toNonLinear(B) }
end
local function interpolateHue(t, r0, g0, b0, r1, g1, b1, direction)
local c0 = rgbToHsv(r0, g0, b0)
local c1 = rgbToHsv(r1, g1, b1)
if (c0.s == 0) then
c0.h = c1.h
if (c0.v == 0) then
c0.s = c1.s
end
end
if (c1.s == 0) then
c1.h = c0.h
if (c1.v == 0) then
c1.s = c0.s
end
end
local hn0 = c0.h / 360
local hn1 = c1.h / 360
if (direction == 0) then
local dhn = hn1 - hn0
if (dhn > 0.5) then
dhn = dhn - math.ceil(dhn - 0.5)
elseif (dhn < -0.5) then
dhn = dhn - math.floor(dhn + 0.5)
end
if (dhn >= 0) then
hn0 = hn0 - math.floor(hn0)
hn1 = hn0 + dhn
else
hn1 = hn1 - math.floor(hn1)
hn0 = hn1 - dhn
end
elseif (direction > 0) then
hn1 = 1 - math.ceil(hn1 - hn0) - math.floor(hn0) + hn1
hn0 = hn0 - math.floor(hn0)
else
hn0 = 1 - math.ceil(hn0 - hn1) - math.floor(hn1) + hn0
hn1 = hn1 - math.floor(hn1)
end
if (t < 0) then
t = 0
elseif (t > 1) then
t = 1
end
local tc = 1 - t
local ha = tc * adjustHueToCielch(360 * hn0) + t * adjustHueToCielch(360 * hn1)
local c = hsvToRgb(unadjustHueFromCielch(ha), tc * c0.s + t * c1.s, tc * c0.v + t * c1.v)
local L0 = getLightness(r0, g0, b0)
local L1 = getLightness(r1, g1, b1)
local Lc = tc * L0 + t * L1
return adjustLightness(Lc, c.r, c.g, c.b)
end
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 ""
end
end
local function polyMix(t, pal)
if (t <= 0) then
return pal[1]
elseif (t >= 1) then
return pal[#pal]
end
local n, f = math.modf(t * (#pal - 1))
if (f == 0) then
return pal[n + 1]
else
local c1 = hexToRgb(pal[n + 1])
local c2 = hexToRgb(pal[n + 2])
local c = srgbMix(f, c1.r, c1.g, c1.b, c2.r, c2.g, c2.b)
return rgbToHex(c.r, c.g, c.b)
end
end
local function sequentialGradient(t, name)
local colors = {
YlGnBu = { 'FFFFD9', 'EDF8B1', 'C7E9B4', '7FCDBB', '41B6C4', '1D91C0', '225EA8', '253494', '081D58' },
YlOrRd = { 'FFFFCC', 'FFEDA0', 'FED976', 'FEB24C', 'FD8D3C', 'FC4E2A', 'E31A1C', 'BD0026', '800026' },
PuBuGn = { 'FFF7FB', 'ECE2F0', 'D0D1E6', 'A6BDDB', '67A9CF', '3690C0', '02818A', '016C59', '014636' }
}
return polyMix(t / 100, colors[name or 'PuBuGn'])
end
local function divergentGradient(t, name)
local colors = {
RdYlBu = { 'A50026', 'D73027', 'F46D43', 'FDAE61', 'FEE090', 'FFFFBF', 'E0F3F8', 'ABD9E9', '74ADD1', '4575B4', '313695' },
RdYlGn = { 'A50026', 'D73027', 'F46D43', 'FDAE61', 'FEE08B', 'FFFFBF', 'D9EF8B', 'A6D96A', '66BD63', '1A9850', '006837' },
Spectral = { '9E0142', 'D53E4F', 'F46D43', 'FDAE61', 'FEE08B', 'FFFFBF', 'E6F598', 'ABDDA4', '66C2A5', '3288BD', '5E4FA2' }
}
return polyMix((t + 100) / 200, colors[name or 'RdYlGn'])
end
function p.hexToRgbTriplet(frame)
local args = frame.args or frame:getParent().args
local hex = args[1]
if (hex) then
local rgb = hexToRgb(hex)
return rgb.r .. ', ' .. rgb.g .. ', ' .. rgb.b
else
return ""
end
end
function p.hexToCmyk(frame)
local args = frame.args or frame:getParent().args
local hex = args[1]
if (hex) then
local p = tonumber(args.precision) or 0
local s = args.pctsign or "1"
local rgb = hexToRgb(hex)
local cmyk = rgbToCmyk(rgb.r, rgb.g, rgb.b)
local fk = formatToPrecision(cmyk.k, p)
local fc, fm, fy
local fracZeros = getFractionalZeros(p)
if (fk == 100 .. fracZeros) then
local fZero = 0 .. fracZeros
fc = fZero
fm = fZero
fy = fZero
else
fc = formatToPrecision(cmyk.c, p)
fm = formatToPrecision(cmyk.m, p)
fy = formatToPrecision(cmyk.y, p)
end
if (s ~= "0") then
return fc .. "%, " .. fm .. "%, " .. fy .. "%, " .. fk .. "%"
else
return fc .. ", " .. fm .. ", " .. fy .. ", " .. fk
end
else
return ""
end
end
function p.hexToHsl(frame)
local args = frame.args or frame:getParent().args
local hex = args[1]
if (hex) then
local p = tonumber(args.precision) or 0
local rgb = hexToRgb(hex)
local hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
local fl = formatToPrecision(hsl.l, p)
local fs, fh
local fracZeros = getFractionalZeros(p)
local fZero = 0 .. fracZeros
if (fl == fZero or fl == 100 .. fracZeros) then
fs = fZero
fh = fZero
else
fs = formatToPrecision(hsl.s, p)
if (fs == fZero) then
fh = fZero
else
fh = formatToPrecision(hsl.h, p)
if (fh == 360 .. fracZeros) then
fh = fZero -- handle rounding to 360
end
end
end
return fh .. "°, " .. fs .. "%, " .. fl .. "%"
else
return ""
end
end
function p.hexToHsv(frame)
local args = frame.args or frame:getParent().args
local hex = args[1]
if (hex) then
local p = tonumber(args.precision) or 0
local rgb = hexToRgb(hex)
local hsv = rgbToHsv(rgb.r, rgb.g, rgb.b)
local fv = formatToPrecision(hsv.v, p)
local fs, fh
local fracZeros = getFractionalZeros(p)
local fZero = 0 .. fracZeros
if (fv == fZero) then
fh = fZero
fs = fZero
else
fs = formatToPrecision(hsv.s, p)
if (fs == fZero) then
fh = fZero
else
fh = formatToPrecision(hsv.h, p)
if (fh == 360 .. fracZeros) then
fh = fZero -- handle rounding to 360
end
end
end
return fh .. "°, " .. fs .. "%, " .. fv .. "%"
else
return ""
end
end
function p.hexToCielch(frame)
local args = frame.args or frame:getParent().args
local hex = args[1]
if (hex) then
local p = tonumber(args.precision) or 0
local rgb = hexToRgb(hex)
local LCh = srgbToCielchuvD65o2deg(rgb.r, rgb.g, rgb.b)
local fL = formatToPrecision(LCh.L, p)
local fC, fh
local fracZeros = getFractionalZeros(p)
local fZero = 0 .. fracZeros
if (fL == fZero or fL == 100 .. fracZeros) then
fC = fZero
fh = fZero
else
fC = formatToPrecision(LCh.C, p)
if (fC == fZero) then
fh = fZero
else
fh = formatToPrecision(LCh.h, p)
if (fh == 360 .. fracZeros) then
fh = fZero -- handle rounding to 360
end
end
end
return fL .. ", " .. fC .. ", " .. fh .. "°"
else
return ""
end
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 min = tonumber(args.min) or 0
local max = tonumber(args.max) or 100
if (min >= max) then
error("Minimum proportion greater than or equal to maximum")
elseif (t < min) then
t = 0
elseif (t > max) then
t = 1
else
t = (t - min) / (max - min)
end
end
local rgb0 = hexToRgb(hex0)
local rgb1 = hexToRgb(hex1)
local rgb = srgbMix(t, rgb0.r, rgb0.g, rgb0.b, rgb1.r, rgb1.g, rgb1.b)
return rgbToHex(rgb.r, rgb.g, rgb.b)
end
function p.hexInterpolate(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 min = tonumber(args.min) or 0
local max = tonumber(args.max) or 100
if (min >= max) then
error("Minimum proportion greater than or equal to maximum")
elseif (t < min) then
t = 0
elseif (t > max) then
t = 1
else
t = (t - min) / (max - min)
end
end
local direction = tonumber(args.direction) or 0
local rgb0 = hexToRgb(hex0)
local rgb1 = hexToRgb(hex1)
local rgb = interpolateHue(t, rgb0.r, rgb0.g, rgb0.b, rgb1.r, rgb1.g, rgb1.b, direction)
return rgbToHex(rgb.r, rgb.g, rgb.b)
end
function p.hexSequentialGradient(frame)
local args = frame.args or frame:getParent().args
local t = args[1]
local name = args.name
local inv = args.inv
local comp = args.comp
if (isempty(t)) then
return ""
end
t = tonumber(t)
if (not isempty(inv) and inv ~= "0") then
t = 100 - t
end
if (not isempty(comp) and comp ~= "0") then
if (comp == "2") then
t = 100 * math.tanh(2 * t - 100)
else
t = 100 * math.tanh(t / 100)
end
end
return sequentialGradient(t, name)
end
function p.hexDivergentGradient(frame)
local args = frame.args or frame:getParent().args
local t = args[1]
local name = args.name
local inv = args.inv
local comp = args.comp
if (isempty(t)) then
return ""
end
t = tonumber(t)
if (not isempty(inv) and inv ~= "0") then
t = -t
end
if (not isempty(comp) and comp ~= "0") then
t = 100 * math.tanh(t / 100)
end
return divergentGradient(t, name)
end
return p