Jump to content
Toggle menu
  • 8 articles
  • 75 files
  • 7 users
  • 37.8K edits
Neodyland Wiki
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:Color: Difference between revisions

From Neodyland Wiki
m Protected "Module:Color": High-risk template or module: 283 transclusions (more info) ([Edit=Require autoconfirmed or confirmed access] (indefinite))
m Handle rounding in corner cases
Line 51: Line 51:
elseif (channelMax == r) then
elseif (channelMax == r) then
h = 60 * ((g - b) / range)
h = 60 * ((g - b) / range)
if (h < 0) then
h = 360 + h
end
elseif (channelMax == g) then
elseif (channelMax == g) then
h = 60 * (2 + (b - r) / range)
h = 60 * (2 + (b - r) / range)
else
else
h = 60 * (4 + (r - g) / range)
h = 60 * (4 + (r - g) / range)
end
if (h < 0) then
h = 360 + h
end
end
L = channelMax + channelMin
L = channelMax + channelMin
Line 79: Line 79:
elseif (channelMax == r) then
elseif (channelMax == r) then
h = 60 * ((g - b) / range)
h = 60 * ((g - b) / range)
if (h < 0) then
h = 360 + h
end
elseif (channelMax == g) then
elseif (channelMax == g) then
h = 60 * (2 + (b - r) / range)
h = 60 * (2 + (b - r) / range)
else
else
h = 60 * (4 + (r - g) / range)
h = 60 * (4 + (r - g) / range)
end
if (h < 0) then
h = 360 + h
end
end
if (channelMax == 0) then
if (channelMax == 0) then
Line 150: Line 150:
end
end


local function formatAngle(value, p)
local function formatToPrecision(value, p)
local vs = string.format("%." .. p .. "f", value)
return string.format("%." .. p .. "f", value)
local zeros
end
 
local function getFractionalZeros(p)
if (p > 0) then
if (p > 0) then
zeros = "." .. string.rep("0", p)
return "." .. string.rep("0", p)
else
else
zeros = ""
return ""
end
end
if (vs == "360" .. zeros) then
vs = "0" .. zeros -- handle rounding to 360
end
return vs .. "°"
end
end


Line 181: Line 179:
local p = tonumber(args.precision) or 0
local p = tonumber(args.precision) or 0
local s = args.pctsign or "1"
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
if (fk == 100  .. getFractionalZeros(p)) then
local fZero = 0 .. getFractionalZeros(p)
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
if (s ~= "0") then
s = "%%"
return fc .. "%, " .. fm .. "%, " .. fy .. "%, " .. fk .. "%"
else
else
s = ""
return fc .. ", " .. fm .. ", " .. fy .. ", " .. fk
end
end
local rgb = hexToRgb(hex)
local cmyk = rgbToCmyk(rgb.r, rgb.g, rgb.b)
return string.format("%." .. p .. "f" .. s .. ", %." .. p .. "f" .. s .. ", %." .. p .. "f" .. s .. ", %." .. p .. "f" .. s, cmyk.c, cmyk.m, cmyk.y, cmyk.k)
else
else
return ""
return ""
Line 201: Line 210:
local rgb = hexToRgb(hex)
local rgb = hexToRgb(hex)
local hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
local hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
return formatAngle(hsl.h, p) .. string.format(", %." .. p .. "f%%, %." .. p .. "f%%", hsl.s, hsl.l)
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
else
return ""
return ""
Line 214: Line 241:
local rgb = hexToRgb(hex)
local rgb = hexToRgb(hex)
local hsv = rgbToHsv(rgb.r, rgb.g, rgb.b)
local hsv = rgbToHsv(rgb.r, rgb.g, rgb.b)
return formatAngle(hsv.h, p) .. string.format(", %." .. p .. "f%%, %." .. p .. "f%%", hsv.s, hsv.v)
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
else
return ""
return ""
Line 227: Line 272:
local rgb = hexToRgb(hex)
local rgb = hexToRgb(hex)
local LCh = srgbToCielchuvD65o2deg(rgb.r, rgb.g, rgb.b)
local LCh = srgbToCielchuvD65o2deg(rgb.r, rgb.g, rgb.b)
return string.format("%." .. p .. "f, %." .. p .. "f, ", LCh.L, LCh.C) .. formatAngle(LCh.h, p)
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
else
return ""
return ""

Revision as of 23:39, 14 January 2022

Module documentation[ view · edit · history · purge ]
This documentation is transcluded from Module:Color/doc.

This template is being used in the wrong namespace. Use {{Heavily used module}} instead! 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 to 0 (zero)
  • pctsign: set to 0 (zero) to suppress percent signs in the generated output
  • proportion: proportion of color2, defaults to 50
  • min: minimum value of proportion range, defaults to 0
  • max: maximum value of proportion range, defaults to 100
local p = {}

local function hexToRgb(color)
	cleanColor = color:gsub("&#35;", "#"):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 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
	channelMax = math.max(r, g, b)
	channelMin = math.min(r, g, b)
	range = channelMax - channelMin
	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
	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
	channelMax = math.max(r, g, b)
	channelMin = math.min(r, g, b)
	range = channelMax - channelMin
	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

-- c in [0, 255], condition tweaked for no discontinuity
-- http://entropymine.com/imageworsener/srgbformula/
local function inverseCompanding(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 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
	R = inverseCompanding(r)
	G = inverseCompanding(g)
	B = inverseCompanding(b)
	-- https://github.com/w3c/csswg-drafts/issues/5922
	X = 0.1804807884018343 * B + 0.357584339383878 * G + 0.41239079926595934 * R
	Y = 0.07219231536073371 * B + 0.21263900587151027 * R + 0.715168678767756 * G
	Z = 0.01933081871559182 * R + 0.11919477979462598 * G + 0.9505321522496607 * B
	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) 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)
			u = 13 * L * (4 * X / d - 0.19783000664283678994)
			v = 13 * L * (9 * Y / d - 0.46831999493879099801)
			h = math.atan2(v, u) * 57.2957795130823208768
			if (h < 0) then
				h = h + 360
			elseif (h == 0) then
				h = 0 -- ensure zero is positive
			end
			C = math.sqrt(u * u + v * v)
			if (C == 0) then
				C = 0
				h = 0
			end
		end
	end
	return { L = L, C = C, h = h }
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

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
		if (fk == 100  .. getFractionalZeros(p)) then
			local fZero = 0 .. getFractionalZeros(p)
			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

return p
Contents
Cookies help us deliver our services. By using our services, you agree to our use of cookies.