Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

Jump to content

Module:Color temperature

From Wikipedia, the free encyclopedia

-- Module:Color temperature

local p = {}

local function blackbody_xy(T)
	-- Formulas from https://github.com/crosscountrycoder/color-converter/blob/main/README.md#polynomial_fitpy-and-polynomial_testpy
	-- T must be greater than or equal to 800 (this is enforced elsewhere in the code)
	local x, y

	if T < 1661 then
		x = 0.12790668 + 1311.3574 / T - 1335109.8 / T^2 + 6.9429482e8 / T^3 - 1.4571589e11 / T^4
		y = 0.35946988 + 477.95393 / T - 1062716.0 / T^2 + 7.5671541e8 / T^3 - 1.8700243e11 / T^4

	elseif T < 4328 then
		x = 0.21594484 + 460.83273 / T + 1497568.7 / T^2 - 3.3177634e9 / T^3 + 1.9306612e12 / T^4
		y = 0.15480888 + 1394.6527 / T - 2259297.6 / T^2 + 8.4916936e8 / T^3 + 3.1769991e11 / T^4

	else
		x = 0.23994527 + 246.81976 / T + 1707275.0 / T^2 - 5.4391967e8 / T^3 - 5.0736771e12 / T^4
		y = 0.23417818 + 359.64997 / T + 2585341.6 / T^2 - 8.1436004e9 / T^3 + 4.5501218e12 / T^4
	end

	return x, y
end

local function xy_to_XYZ(x, y)
	if y == 0 then
		return 0, 0, 0
	end

	local Y = 1
	local X = x / y
	local Z = (1 - x - y) / y

	return X, Y, Z
end

local function XYZ_to_linear_sRGB(X, Y, Z)
	local r =  3.2404542 * X - 1.5371385 * Y - 0.4985314 * Z
	local g = -0.9692660 * X + 1.8760108 * Y + 0.0415560 * Z
	local b =  0.0556434 * X - 0.2040259 * Y + 1.0572252 * Z

	return r, g, b
end

local function normalize_rgb(r, g, b)
	-- First normalize so the brightest linear RGB channel is 1.
	local max_channel = math.max(r, g, b)

	if max_channel <= 0 then
		return 0, 0, 0
	end

	r = r / max_channel
	g = g / max_channel
	b = b / max_channel

	-- Then, if needed, compress toward D65 white.
	--
	-- In linear sRGB, D65 white is (1, 1, 1). This transformation moves
	-- the color along the line toward white until the lowest channel reaches 0,
	-- while keeping the highest channel at 1.
	--
	-- Example:
	--   (1.0, 0.25, -0.5) -> (1.0, 0.5, 0.0)
	local min_channel = math.min(r, g, b)

	if min_channel < 0 then
		local denominator = 1 - min_channel

		r = (r - min_channel) / denominator
		g = (g - min_channel) / denominator
		b = (b - min_channel) / denominator
	end

	return r, g, b
end

local function linear_to_srgb(c)
	if c <= 0.0031308 then
		return 12.92 * c
	end

	return 1.055 * c^(1 / 2.4) - 0.055
end

local function to_8bit(c)
	return math.floor(255 * linear_to_srgb(c) + 0.5)
end

local function rgb_to_hex(r, g, b)
	return string.format("%02x%02x%02x", r, g, b)
end

local function temperature_to_rgb(T)
	local x, y = blackbody_xy(T)
	local X, Y, Z = xy_to_XYZ(x, y)
	local r, g, b = XYZ_to_linear_sRGB(X, Y, Z)

	r, g, b = normalize_rgb(r, g, b)

	return to_8bit(r), to_8bit(g), to_8bit(b)
end

local function get_temperature(frame)
	local parent = frame:getParent()
	local raw = frame.args[1]

	if (not raw or raw == "") and parent then
		raw = parent.args[1]
	end

	return tonumber(raw), raw
end

local function get_arg(frame, name)
	local parent = frame:getParent()
	local value = frame.args[name]

	if (not value or value == "") and parent then
		value = parent.args[name]
	end

	return value
end

function p.hex(frame)
	local T, raw = get_temperature(frame)

	if not T then
		return ""
	end

	if T < 800 then
		return "Error: Temperature must be at least 800 K"
	end

	local r, g, b = temperature_to_rgb(T)
	return rgb_to_hex(r, g, b)
end

function p.rgb(frame)
	local T, raw = get_temperature(frame)

	if not T then
		return ""
	end

	if T < 800 then
		return "Error: Temperature must be at least 800 K"
	end

	local r, g, b = temperature_to_rgb(T)
	return string.format("%d, %d, %d", r, g, b)
end

function p.xy(frame)
	local T, raw = get_temperature(frame)

	if not T then
		return ""
	end

	if T < 800 then
		return "Error: Temperature must be at least 800 K"
	end

	local x, y = blackbody_xy(T)
	return string.format("%.8f, %.8f", x, y)
end

function p.swatch(frame)
	local T, raw = get_temperature(frame)

	if not T then
		return ""
	end

	if T < 800 then
		return "Error: Temperature must be at least 800 K"
	end

	local text = get_arg(frame, "text")
	local r, g, b = temperature_to_rgb(T)
	local hex = rgb_to_hex(r, g, b)

	if not text or text == "" then
		text = mw.language.getContentLanguage():formatNum(T) .. " K"
	end

	return string.format(
		'<span class="mw-no-invert" style="background:gray;padding:1px 1px 1px 0;color:white;border:solid 2px gray;margin:0 0.1em;text-align:center;vertical-align:middle;display:inline;font-family:sans-serif"><span style="padding:1px;margin:0 3px 0 0;background:#%s">&nbsp;&nbsp;&nbsp;&nbsp;</span><small>%s</small></span>',
		hex,
		text
	)
end

return p