Module:Color temperature

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

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.