Module:Color/sandbox
![]() | This is the module sandbox page for Module:Color (diff). See also the companion subpage for test cases (run). |
![]() | This module is rated as ready for general use. It has reached a mature form and is thought to be relatively bug-free and ready for use wherever appropriate. It is ready to mention on help pages and other Wikipedia resources as an option for new users to learn. To reduce server load and bad output, it should be improved by sandbox testing rather than repeated trial-and-error editing. |
![]() | This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is protected from editing. |
![]() | This Lua module is used on approximately 620 pages and changes may be widely noticed. Test changes in the module's /sandbox or /testcases subpages, or in your own module sandbox. Consider discussing changes on the talk page before implementing them. |
This module is used primarily by {{Infobox color}}, eliminating the need for external color converters and preventing mismatch between color coordinates.
Usage
To use this module, you may use one of the above listed templates or invoke the module directly. All functions that accept hexadecimal triplets also handle the shorthand three-digit format.
To convert a hexadecimal triplet to an RGB triplet as comma-separated values:
{{#invoke:Color|hexToRgbTriplet|color}}
To convert a hexadecimal triplet to the CMYK color model without a color profile (which is a very bad idea!):
{{#invoke:Color|hexToCmyk|color|precision=?|pctsign=?}}
To convert a hexadecimal triplet to HSL or HSV:
{{#invoke:Color|hexToHsl|color|precision=?}}
{{#invoke:Color|hexToHsv|color|precision=?}}
To convert a hexadecimal triplet to the perceptual CIELChuv color space:
{{#invoke:Color|hexToCielch|color|precision=?}}
To mix two colors in the more physically correct linear RGB space:
{{#invoke:Color|hexMix|color1|color2|proportion|min=?|max=?}}
To convert an RGB triplet to a hex code:
{{#invoke:Color|rgbTripletToHex|r|g|b}}
The following parameters are optional:
precision
: defaults to0
(zero)pctsign
: set to0
(zero) to suppress percent signs in the generated outputproportion
: proportion ofcolor2
, defaults to 50min
: minimum value of proportion range, defaults to 0max
: maximum value of proportion range, defaults to 100
local p = {}
local function isEmpty(v)
return v == nil or v == ''
end
local function isNotEmpty(v)
return v ~= nil and v ~= ''
end
local function argDefault(value, default)
if (value == nil or value == '') then
return default
else
return value
end
end
local function numArgDefault(value, default)
if (value == nil or value == '') then
return default
else
return tonumber(value)
end
end
local function isArgTrue(value)
return (value ~= nil and value ~= '' and value ~= '0')
end
local function isEmpty(v)
return v == nil or v == ''
end
local function isNotEmpty(v)
return v ~= nil and 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, 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 == 0) then
return palette[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 gradient(t, palette)
local colors = {
spectral = { '9E0142', 'D53E4F', 'F46D43', 'FDAE61', 'FEE08B', 'FFFFBF', 'E6F598', 'ABDDA4', '66C2A5', '3288BD', '5E4FA2' },
rdylbu = { 'A50026', 'D73027', 'F46D43', 'FDAE61', 'FEE090', 'FFFFBF', 'E0F3F8', 'ABD9E9', '74ADD1', '4575B4', '313695' },
rdylgn = { 'A50026', 'D73027', 'F46D43', 'FDAE61', 'FEE08B', 'FFFFBF', 'D9EF8B', 'A6D96A', '66BD63', '1A9850', '006837' },
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, colors[palette])
end
local function softSigmoid(x)
local ax = math.abs(x)
if (ax > 0.000000000000000111) then
return x / (1 + ax)
else
return x
end
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
divergentPalettes = {
["spectral"] = true,
["puor"] = true,
["brbg"] = true,
["prgn"] = true,
["piyg"] = true,
["rdbu"] = true,
["rdgy"] = true,
["rdylbu"] = true,
["rdylgn"] = true
}
function p.hexGradient(frame)
local args = frame.args or frame:getParent().args
local value = numArgDefault(args[1], 0)
local low = numArgDefault(args.low, 100)
local high = numArgDefault(args.high, -100)
local pal = argDefault(args.pal, 'spectral')
if (isEmpty(args.low) and divergentPalettes[pal:lower()] == nil) then
low = 0
end
local t
if (high == low) then
t = 0.5
elseif (isArgTrue(args.inv)) then
t = (high - t) / (high - low)
else
t = (t - low) / (high - low)
end
if (isArgTrue(args.comp)) then
t = 0.5 * softSigmoid(2 * t - 1) + 0.5
end
return gradient(t, pal)
end
return p