Module:Color contrast

From Helldivers Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:Color contrast/doc

local p = {}
local HTMLcolor = mw.loadData('Module:Color contrast/colors')

-- Helper function to convert sRGB component to linearized value
local function sRGB(v)
	return (v <= 0.03928) and (v / 12.92) or math.pow((v + 0.055) / 1.055, 2.4)
end

-- Calculate luminance from RGB values
local function rgbdec2lum(R, G, B)
	if R >= 0 and R < 256 and G >= 0 and G < 256 and B >= 0 and B < 256 then
		return 0.2126 * sRGB(R / 255) + 0.7152 * sRGB(G / 255) + 0.0722 * sRGB(B / 255)
	else
		return ''
	end
end

-- Convert HSL to luminance
local function hsl2lum(h, s, l)
	if h >= 0 and h < 360 and s >= 0 and s <= 1 and l >= 0 and l <= 1 then
		local c = (1 - math.abs(2 * l - 1)) * s
		local x = c * (1 - math.abs(h / 60 % 2 - 1))
		local m = l - c / 2

		local r, g, b = m, m, m
		if h < 60 then
			r = r + c; g = g + x
		elseif h < 120 then
			r = r + x; g = g + c
		elseif h < 180 then
			g = g + c; b = b + x
		elseif h < 240 then
			g = g + x; b = b + c
		elseif h < 300 then
			r = r + x; b = b + c
		else
			r = r + c; b = b + x
		end
		return rgbdec2lum(255 * r, 255 * g, 255 * b)
	else
		return ''
	end
end

-- Convert a color string to luminance
local function color2lum(c)
	if not c then return '' end

	-- Clean up color input
	c = c:gsub("&#35;", "#"):match('^%s*(.-)[%s;]*$'):lower()
	c = mw.text.unstripNoWiki(c)

	-- Try to look up color in predefined colors
	local L = HTMLcolor[c]
	if L then return L end

	-- HSL to luminance
	if mw.ustring.match(c, '^hsl%(%s*[0-9.]+%s*,%s*[0-9.]+%%%s*,%s*[0-9.]+%%%s*%)$') then
		local h, s, l = mw.ustring.match(c, '^hsl%(%s*([0-9.]+)%s*,%s*([0-9.]+)%%%s*,%s*([0-9.]+)%%%s*%)$')
		return hsl2lum(tonumber(h), tonumber(s) / 100, tonumber(l) / 100)
	end

	-- RGB to luminance
	if mw.ustring.match(c, '^rgb%(%s*[0-9]+%s*,%s*[0-9]+%s*,%s*[0-9]+%s*%)$') then
		local R, G, B = mw.ustring.match(c, '^rgb%(%s*([0-9]+)%s*,%s*([0-9]+)%s*,%s*([0-9]+)%s*%)$')
		return rgbdec2lum(tonumber(R), tonumber(G), tonumber(B))
	end

	-- RGB percentage to luminance
	if mw.ustring.match(c, '^rgb%(%s*[0-9.]+%%%s*,%s*[0-9.]+%%%s*,%s*[0-9.]+%%%s*%)$') then
		local R, G, B = mw.ustring.match(c, '^rgb%(%s*([0-9.]+)%%%s*,%s*([0-9.]+)%%%s*,%s*([0-9.]+)%%%s*%)$')
		return rgbdec2lum(255 * tonumber(R) / 100, 255 * tonumber(G) / 100, 255 * tonumber(B) / 100)
	end

	-- Hex color to luminance
	c = mw.ustring.match(c, '^[%s#]*([a-f0-9]*)[%s]*$')
	if c then
		local cs = mw.text.split(c, '')
		if #cs == 6 then
			local R = 16 * tonumber('0x' .. cs[1]) + tonumber('0x' .. cs[2])
			local G = 16 * tonumber('0x' .. cs[3]) + tonumber('0x' .. cs[4])
			local B = 16 * tonumber('0x' .. cs[5]) + tonumber('0x' .. cs[6])
			return rgbdec2lum(R, G, B)
		elseif #cs == 3 then
			local R = 16 * tonumber('0x' .. cs[1]) + tonumber('0x' .. cs[1])
			local G = 16 * tonumber('0x' .. cs[2]) + tonumber('0x' .. cs[2])
			local B = 16 * tonumber('0x' .. cs[3]) + tonumber('0x' .. cs[3])
			return rgbdec2lum(R, G, B)
		end
	end

	return ''
end

-- Calculate contrast ratio between two luminance values
local function contrast_ratio(lum1, lum2)
	if lum2 > lum1 then lum1, lum2 = lum2, lum1 end
	return (lum1 + 0.05) / (lum2 + 0.05)
end

-- Exported functions
function p._lum(color)
	return color2lum(color)
end

function p._ratio(args)
	local lum1 = color2lum(args[1])
	local lum2 = color2lum(args[2])
	return (lum1 ~= '' and lum2 ~= '') and contrast_ratio(lum1, lum2) or args['error'] or '?'
end

function p._greatercontrast(args)
	local bias = tonumber(args['bias'] or '0') or 0
	local css = args['css'] and args['css'] ~= ''
	local lum1 = color2lum(args[1] or '')
	local lum2 = color2lum(args[2] or '#FFFFFF')
	local lum3 = color2lum(args[3] or '#000000')

	local ratio1 = contrast_ratio(lum1, lum2)
	local ratio2 = contrast_ratio(lum1, lum3)

	if css then
		local c1 = args[1] or ''
		if mw.ustring.match(c1, '^[A-Fa-f0-9]+$') then c1 = '#' .. c1 end
		if mw.ustring.match(args[2], '^[A-Fa-f0-9]+$') then c2 = '#' .. args[2] end
		if mw.ustring.match(args[3], '^[A-Fa-f0-9]+$') then c3 = '#' .. args[3] end
		return 'background-color:' .. c1 .. '; color:' .. ((ratio1 + bias > ratio2) and args[2] or args[3]) .. ';'
	end

	return (ratio1 + bias > ratio2) and args[2] or args[3]
end

function p._styleratio(args)
	local style = (args[1] or ''):lower()
	local bg, fg = 'white', 'black'
	local lum_bg, lum_fg = 1, 0

	if args[2] then
		local lum = color2lum(args[2])
		if lum ~= '' then bg, lum_bg = args[2], lum end
	end
	if args[3] then
		local lum = color2lum(args[3])
		if lum ~= '' then fg, lum_fg = args[3], lum end
	end

	local slist = mw.text.split(mw.ustring.gsub(mw.ustring.gsub(style, '&#[Xx]23;', '#'), '&#35;', '#'), ';')
	for _, s in ipairs(slist) do
		local k, v = s:match('^[%s]*(.-):%s*(.-)%s*$')
		if k == 'background' or k == 'background-color' then
			local lum = color2lum(v)
			if lum ~= '' then bg, lum_bg = v, lum end
		elseif k == 'color' then
			local lum = color2lum(v)
			if lum ~= '' then fg, lum_fg = v, lum end
		end
	end

	return contrast_ratio(lum_bg, lum_fg)
end

-- Wrapper functions
function p.lum(frame)
	local color = frame.args[1] or frame:getParent().args[1]
	return p._lum(color)
end

function p.ratio(frame)
	local args = frame.args[1] and frame.args or frame:getParent().args
	return p._ratio(args)
end

function p.styleratio(frame)
	local args = frame.args[1] and frame.args or frame:getParent().args
	return p._styleratio(args)
end

function p.greatercontrast(frame)
	local args = frame.args[1] and frame.args or frame:getParent().args
	return p._greatercontrast(args)
end

return p