Module:Color Explained

-- Introduction: https://colorspace.r-forge.r-project.org/articles/color_spaces.html

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 elseif (#cleanColor

3) then return 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) endend

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 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 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 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 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) 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 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 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 "" endend

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 "" endend

function p.rgbTripletToHex(frame) local args = frame.args or frame:getParent.args local r = tonumber(args[1]) local g = tonumber(args[2]) local b = tonumber(args [3]) if (isempty(r) or isempty(g) or isempty(b)) then return "" else return rgbToHex(r,g,b) endend

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 "" endend

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 "" endend

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 "" endend

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 "" endend

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

return p