本WIKI于2020.07.21由往事仇心创建,2021.12.25正式建组“空桑档案馆”,编辑权限逐步开放,建议各位少主收藏。
目前正在搭建基础框架与美工优化,欢迎翻阅已开放区域,并提出宝贵建议。
“空桑档案馆”搭建组持续招募ing,期待更多能人异士参与食物语WIKI建设。
反馈留言  ·  编辑教程  ·  收藏方法  ·  

全站通知:

模块:Hct/HctSolver

来自食物语-档案馆WIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索

此模块的文档可以在模块:Hct/HctSolver/doc创建

--[[
  Copyright 2021 Google LLC
 
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
 
      http://www.apache.org/licenses/LICENSE-2.0
 
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
]]
 
--[[
  This file has been modified. The original version is at
      https://github.com/material-foundation/material-color-utilities
]]

local colorUtils = require('Module:Hct/ColorUtils');
local mathUtils = require('Module:Hct/MathUtils');
local Cam16 = require('Module:Hct/Cam16');


--[[ A class that solves the HCT equation. ]]
local HctSolver = {
	SCALED_DISCOUNT_FROM_LINRGB = {
		{0.001200833568784504, 0.002389694492170889, 0.0002795742885861124},
		{0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398},
		{0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076},
	};
	
	LINRGB_FROM_SCALED_DISCOUNT = {
		{1373.2198709594231, -1100.4251190754821, -7.278681089101213},
		{-271.815969077903, 559.6580465940733, -32.46047482791194},
		{1.9622899599665666, -57.173814538844006, 308.7233197812385},
	};
	
	Y_FROM_LINRGB = {0.2126, 0.7152, 0.0722};

	CRITICAL_PLANES = {
		0.015176349177441876, 0.045529047532325624, 0.07588174588720938,
		0.10623444424209313,  0.13658714259697685,  0.16693984095186062,
		0.19729253930674434,  0.2276452376616281,   0.2579979360165119,
		0.28835063437139563,  0.3188300904430532,   0.350925934958123,
		0.3848314933096426,   0.42057480301049466,  0.458183274052838,
		0.4976837250274023,   0.5391024159806381,   0.5824650784040898,
		0.6277969426914107,   0.6751227633498623,   0.7244668422128921,
		0.775853049866786,    0.829304845476233,    0.8848452951698498,
		0.942497089126609,    1.0022825574869039,   1.0642236851973577,
		1.1283421258858297,   1.1946592148522128,   1.2631959812511864,
		1.3339731595349034,   1.407011200216447,    1.4823302800086415,
		1.5599503113873272,   1.6398909516233677,   1.7221716113234105,
		1.8068114625156377,   1.8938294463134073,   1.9832442801866852,
		2.075074464868551,    2.1693382909216234,   2.2660538449872063,
		2.36523901573795,     2.4669114995532007,   2.5710888059345764,
		2.6777882626779785,   2.7870270208169257,   2.898822059350997,
		3.0131901897720907,   3.1301480604002863,   3.2497121605402226,
		3.3718988244681087,   3.4967242352587946,   3.624204428461639,
		3.754355295633311,    3.887192587735158,    4.022731918402185,
		4.160988767090289,    4.301978482107941,    4.445716283538092,
		4.592217266055746,    4.741496401646282,    4.893568542229298,
		5.048448422192488,    5.20615066083972,     5.3666897647573375,
		5.5300801301023865,   5.696336044816294,    5.865471690767354,
		6.037501145825082,    6.212438385869475,    6.390297286737924,
		6.571091626112461,    6.7548350853498045,   6.941541251256611,
		7.131223617812143,    7.323895587840543,    7.5195704746346665,
		7.7182615035334345,   7.919981813454504,    8.124744458384042,
		8.332562408825165,    8.543448553206703,    8.757415699253682,
		8.974476575321063,    9.194643831691977,    9.417930041841839,
		9.644347703669503,    9.873909240696694,    10.106627003236781,
		10.342513269534024,   10.58158024687427,    10.8238400726681,
		11.069304815507364,   11.317986476196008,   11.569896988756009,
		11.825048221409341,   12.083451977536606,   12.345119996613247,
		12.610063955123938,   12.878295467455942,   13.149826086772048,
		13.42466730586372,    13.702830557985108,   13.984327217668513,
		14.269168601521828,   14.55736596900856,    14.848930523210871,
		15.143873411576273,   15.44220572664832,    15.743938506781891,
		16.04908273684337,    16.35764934889634,    16.66964922287304,
		16.985093187232053,   17.30399201960269,    17.62635644741625,
		17.95219714852476,    18.281524751807332,   18.614349837764564,
		18.95068293910138,    19.290534541298456,   19.633915083172692,
		19.98083495742689,    20.331304511189067,   20.685334046541502,
		21.042933821039977,   21.404114048223256,   21.76888489811322,
		22.137256497705877,   22.50923893145328,    22.884842241736916,
		23.264076429332462,   23.6469514538663,     24.033477234264016,
		24.42366364919083,    24.817520537484558,   25.21505769858089,
		25.61628489293138,    26.021211842414342,   26.429848230738664,
		26.842203703840827,   27.258287870275353,   27.678110301598522,
		28.10168053274597,    28.529008062403893,   28.96010235337422,
		29.39497283293396,    29.83362889318845,    30.276079891419332,
		30.722335150426627,   31.172403958865512,   31.62629557157785,
		32.08401920991837,    32.54558406207592,    33.010999283389665,
		33.4802739966603,     33.953417292456834,   34.430438229418264,
		34.911345834551085,   35.39614910352207,    35.88485700094671,
		36.37747846067349,    36.87402238606382,    37.37449765026789,
		37.87891309649659,    38.38727753828926,    38.89959975977785,
		39.41588851594697,    39.93615253289054,    40.460400508064545,
		40.98864111053629,    41.520882981230194,   42.05713473317016,
		42.597404951718396,   43.141702194811224,   43.6900349931913,
		44.24241185063697,    44.798841244188324,   45.35933162437017,
		45.92389141541209,    46.49252901546552,    47.065252796817916,
		47.64207110610409,    48.22299226451468,    48.808024568002054,
		49.3971762874833,     49.9904556690408,     50.587870934119984,
		51.189430279724725,   51.79514187861014,    52.40501387947288,
		53.0190544071392,     53.637271562750364,   54.259673423945976,
		54.88626804504493,    55.517063457223934,   56.15206766869424,
		56.79128866487574,    57.43473440856916,    58.08241284012621,
		58.734331877617365,   59.39049941699807,    60.05092333227251,
		60.715611475655585,   61.38457167773311,    62.057811747619894,
		62.7353394731159,     63.417162620860914,   64.10328893648692,
		64.79372614476921,    65.48848194977529,    66.18756403501224,
		66.89098006357258,    67.59873767827808,    68.31084450182222,
		69.02730813691093,    69.74813616640164,    70.47333615344107,
		71.20291564160104,    71.93688215501312,    72.67524319850172,
		73.41800625771542,    74.16517879925733,    74.9167682708136,
		75.67278210128072,    76.43322770089146,    77.1981124613393,
		77.96744375590167,    78.74122893956174,    79.51947534912904,
		80.30219030335869,    81.08938110306934,    81.88105503125999,
		82.67721935322541,    83.4778813166706,     84.28304815182372,
		85.09272707154808,    85.90692527145302,    86.72564993000343,
		87.54890820862819,    88.3767072518277,     89.2090541872801,
		90.04595612594655,    90.88742016217518,    91.73345337380438,
		92.58406282226491,    93.43925555268066,    94.29903859396902,
		95.16341895893969,    96.03240364439274,    96.9059996312159,
		97.78421388448044,    98.6670533535366,     99.55452497210776,
	};
};

--[[
  Sanitizes a small enough angle in radians.

  @param angle An angle in radians; must not deviate too much from 0.
  @return A coterminal angle between 0 and 2pi.
]]
function HctSolver.sanitizeRadians(angle)
	return (angle + math.pi * 8) % (math.pi * 2);
end

--[[
  Delinearizes an RGB component, returning a floating-point number.

  @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
  @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
]]
function HctSolver.trueDelinearized(rgbComponent)
	local normalized = rgbComponent / 100.0;
	local delinearized = 0.0;
	if normalized <= 0.0031308 then
		delinearized = normalized * 12.92;
	else
		delinearized = 1.055 * math.pow(normalized, 1.0 / 2.4) - 0.055;
	end
	return delinearized * 255.0;
end

function HctSolver.chromaticAdaptation(component)
	local af = math.pow(math.abs(component), 0.42);
	return mathUtils.signum(component) * 400.0 * af / (af + 27.13);
end

--[[
  Returns the hue of a linear RGB color in CAM16.

  @param linrgb The linear RGB coordinates of a color.
  @return The hue of the color in CAM16, in radians.
]]
function HctSolver.hueOf(linrgb)
	local scaledDiscount =
		mathUtils.matrixMultiply(linrgb, HctSolver.SCALED_DISCOUNT_FROM_LINRGB);
	local rA = HctSolver.chromaticAdaptation(scaledDiscount[1]);
	local gA = HctSolver.chromaticAdaptation(scaledDiscount[2]);
	local bA = HctSolver.chromaticAdaptation(scaledDiscount[3]);
	-- redness-greenness
	local a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
	-- yellowness-blueness
	local b = (rA + gA - 2.0 * bA) / 9.0;
	return math.atan2(b, a);
end

function HctSolver.areInCyclicOrder(a, b, c)
	local deltaAB = HctSolver.sanitizeRadians(b - a);
	local deltaAC = HctSolver.sanitizeRadians(c - a);
	return deltaAB < deltaAC;
end

--[[
  Solves the lerp equation.

  @param source The starting number.
  @param mid The number in the middle.
  @param target The ending number.
  @return A number t such that lerp(source, target, t) = mid.
]]
function HctSolver.intercept(source, mid, target)
	return (mid - source) / (target - source);
end

function HctSolver.lerpPoint(source, t, target)
	return {
		source[1] + (target[1] - source[1]) * t,
		source[2] + (target[2] - source[2]) * t,
		source[3] + (target[3] - source[3]) * t,
	};
end

--[[
  Intersects a segment with a plane.

  @param source The coordinates of point A.
  @param coordinate The R-, G-, or B-coordinate of the plane.
  @param target The coordinates of point B.
  @param axis The axis the plane is perpendicular with. (1: R, 2: G, 3: B)
  @return The intersection point of the segment AB with the plane
      R=coordinate, G=coordinate, or B=coordinate
]]
function HctSolver.setCoordinate(source, coordinate, target, axis)
	local t = HctSolver.intercept(source[axis], coordinate, target[axis]);
	return HctSolver.lerpPoint(source, t, target);
end

function HctSolver.isBounded(x)
	return 0.0 <= x and x <= 100.0;
end

--[[
  Returns the nth possible vertex of the polygonal intersection.

  @param y The Y value of the plane.
  @param n The zero-based index of the point. 0 <= n <= 11.
  @return The nth possible vertex of the polygonal intersection of the y plane
      and the RGB cube, in linear RGB coordinates, if it exists. If this
      possible vertex lies outside of the cube, {-1.0, -1.0, -1.0} is returned.
]]
function HctSolver.nthVertex(y, n)
	local kR = HctSolver.Y_FROM_LINRGB[1];
	local kG = HctSolver.Y_FROM_LINRGB[2];
	local kB = HctSolver.Y_FROM_LINRGB[3];
	local coordA = n % 4 <= 1 and 0.0 or 100.0;
	local coordB = n % 2 == 0 and 0.0 or 100.0;
	if n < 4 then
		local g = coordA;
		local b = coordB;
		local r = (y - g * kG - b * kB) / kR;
		if HctSolver.isBounded(r) then
			return {r, g, b};
		else
			return {-1.0, -1.0, -1.0};
		end
	elseif n < 8 then
		local b = coordA;
		local r = coordB;
		local g = (y - r * kR - b * kB) / kG;
		if HctSolver.isBounded(g) then
			return {r, g, b};
		else
			return {-1.0, -1.0, -1.0};
		end
	else
		local r = coordA;
		local g = coordB;
		local b = (y - r * kR - g * kG) / kB;
		if HctSolver.isBounded(b) then
			return {r, g, b};
		else
			return {-1.0, -1.0, -1.0};
		end
	end
end

--[[
  Finds the segment containing the desired color.

  @param y The Y value of the color.
  @param targetHue The hue of the color.
  @return Two sets of linear RGB coordinates, each corresponding to an endpoint
      of the segment containing the desired color.
]]
function HctSolver.bisectToSegment(y, targetHue)
	local left = {-1.0, -1.0, -1.0};
	local right = left;
	local leftHue = 0.0;
	local rightHue = 0.0;
	local initialized = false;
	local uncut = true;
	for n = 0, 11 do
		local mid = HctSolver.nthVertex(y, n);
		if mid[1] >= 0 then
			local midHue = HctSolver.hueOf(mid);
			if initialized then
				if uncut or HctSolver.areInCyclicOrder(leftHue, midHue, rightHue) then
					uncut = false;
					if HctSolver.areInCyclicOrder(leftHue, targetHue, midHue) then
						right = mid;
						rightHue = midHue;
					else
						left = mid;
						leftHue = midHue;
					end
				end
			else
				left = mid;
				right = mid;
				leftHue = midHue;
				rightHue = midHue;
				initialized = true;
			end
		end
	end
	return left, right;
end

function HctSolver.midpoint(a, b)
	return {
		(a[1] + b[1]) / 2,
		(a[2] + b[2]) / 2,
		(a[3] + b[3]) / 2,
	};
end

function HctSolver.criticalPlaneBelow(x)
	return math.floor(x - 0.5);
end

function HctSolver.criticalPlaneAbove(x)
	return math.ceil(x - 0.5);
end

--[[
  Finds a color with the given Y and hue on the boundary of the
  cube.

  @param y The Y value of the color.
  @param targetHue The hue of the color.
  @return The desired color, in linear RGB coordinates.
]]
function HctSolver.bisectToLimit(y, targetHue)
	local left, right = HctSolver.bisectToSegment(y, targetHue);
	local leftHue = HctSolver.hueOf(left);
	for axis = 1, 3 do
		if left[axis] ~= right[axis] then
			local lPlane = -1;
			local rPlane = 255;
			if left[axis] < right[axis] then
				lPlane = HctSolver.criticalPlaneBelow(
					HctSolver.trueDelinearized(left[axis]));
				rPlane = HctSolver.criticalPlaneAbove(
					HctSolver.trueDelinearized(right[axis]));
			else
				lPlane = HctSolver.criticalPlaneAbove(
					HctSolver.trueDelinearized(left[axis]));
				rPlane = HctSolver.criticalPlaneBelow(
					HctSolver.trueDelinearized(right[axis]));
			end
			for i = 1, 8 do
				if math.abs(rPlane - lPlane) <= 1 then
					break;
				end
				local mPlane = math.floor((lPlane + rPlane) / 2.0);
				local midPlaneCoordinate = HctSolver.CRITICAL_PLANES[mPlane + 1];
				local mid = HctSolver.setCoordinate(left, midPlaneCoordinate, right, axis);
				local midHue = HctSolver.hueOf(mid);
				if HctSolver.areInCyclicOrder(leftHue, targetHue, midHue) then
					right = mid;
					rPlane = mPlane;
				else
					left = mid;
					leftHue = midHue;
					lPlane = mPlane;
				end
			end
		end
	end
	return HctSolver.midpoint(left, right);
end

function HctSolver.inverseChromaticAdaptation(adapted)
	local adaptedAbs = math.abs(adapted);
	local base = math.max(0, 27.13 * adaptedAbs / (400.0 - adaptedAbs));
	return mathUtils.signum(adapted) * math.pow(base, 1.0 / 0.42);
end

--[[
  Finds a color with the given hue, chroma, and Y.

  @param hueRadians The desired hue in radians.
  @param chroma The desired chroma.
  @param y The desired Y.
  @return The desired color as a hexadecimal integer, if found; 0 otherwise.
]]
function HctSolver.findResultByJ(hueRadians, chroma, y)
	-- Initial estimate of j.
	local j = math.sqrt(y) * 11.0;
	-- ===========================================================
	-- Operations inlined from Cam16 to avoid repeated calculation
	-- ===========================================================
	local viewingConditions = require('Module:Hct/ViewingConditions');
	local tInnerCoeff =
		1 / math.pow(1.64 - math.pow(0.29, viewingConditions.n), 0.73);
	local eHue = 0.25 * (math.cos(hueRadians + 2.0) + 3.8);
	local p1 =
		eHue * (50000.0 / 13.0) * viewingConditions.nc * viewingConditions.ncb;
	local hSin = math.sin(hueRadians);
	local hCos = math.cos(hueRadians);
	for iterationRound = 1, 5 do
		-- ===========================================================
		-- Operations inlined from Cam16 to avoid repeated calculation
		-- ===========================================================
		local jNormalized = j / 100.0;
		local alpha =
			(chroma == 0.0 or j == 0.0) and 0.0 or chroma / math.sqrt(jNormalized);
		local t = math.pow(alpha * tInnerCoeff, 1.0 / 0.9);
		local ac = viewingConditions.aw *
			math.pow(jNormalized, 1.0 / viewingConditions.c / viewingConditions.z);
		local p2 = ac / viewingConditions.nbb;
		local gamma = 23.0 * (p2 + 0.305) * t /
				(23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin);
		local a = gamma * hCos;
		local b = gamma * hSin;
		local rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
		local gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
		local bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
		local rCScaled = HctSolver.inverseChromaticAdaptation(rA);
		local gCScaled = HctSolver.inverseChromaticAdaptation(gA);
		local bCScaled = HctSolver.inverseChromaticAdaptation(bA);
		local linrgb = mathUtils.matrixMultiply(
			{rCScaled, gCScaled, bCScaled},
			HctSolver.LINRGB_FROM_SCALED_DISCOUNT
		);
		-- ===========================================================
		-- Operations inlined from Cam16 to avoid repeated calculation
		-- ===========================================================
		if linrgb[1] < 0 or linrgb[2] < 0 or linrgb[3] < 0 then
			return 0;
		end
		local kR = HctSolver.Y_FROM_LINRGB[1];
		local kG = HctSolver.Y_FROM_LINRGB[2];
		local kB = HctSolver.Y_FROM_LINRGB[3];
		local fnj = kR * linrgb[1] + kG * linrgb[2] + kB * linrgb[3];
		if fnj <= 0 then
			return 0;
		end
		if iterationRound == 5 or math.abs(fnj - y) < 0.002 then
			if linrgb[1] > 100.01 or linrgb[2] > 100.01 or linrgb[3] > 100.01 then
				return 0;
			end
			return colorUtils.argbFromLinrgb(linrgb);
		end
		-- Iterates with Newton method,
		-- Using 2 * fn(j) / j as the approximation of fn'(j)
		j = j - (fnj - y) * j / (2 * fnj);
	end
	return 0;
end

--[[
  Finds an sRGB color with the given hue, chroma, and L*, if possible.

  @param hueDegrees The desired hue, in degrees.
  @param chroma The desired chroma.
  @param lstar The desired L*.
  @return A hexadecimal representing the sRGB color. The color has sufficiently
      close hue, chroma, and L* to the desired values, if possible; otherwise,
      the hue and L* will be sufficiently close, and chroma will be maximized.
]]
function HctSolver.solveToInt(hueDegrees, chroma, lstar)
	if chroma < 0.0001 or lstar < 0.0001 or lstar > 99.9999 then
		return colorUtils.argbFromLstar(lstar);
	end
	hueDegrees = mathUtils.sanitizeDegreesDouble(hueDegrees);
	local hueRadians = hueDegrees / 180 * math.pi;
	local y = colorUtils.yFromLstar(lstar);
	local exactAnswer = HctSolver.findResultByJ(hueRadians, chroma, y);
	if exactAnswer ~= 0 then
		return exactAnswer;
	end
	local linrgb = HctSolver.bisectToLimit(y, hueRadians);
	return colorUtils.argbFromLinrgb(linrgb);
end

--[[
  Finds an sRGB color with the given hue, chroma, and L*, if possible.

  @param hueDegrees The desired hue, in degrees.
  @param chroma The desired chroma.
  @param lstar The desired L*.
  @return An CAM16 object representing the sRGB color. The color has
      sufficiently close hue, chroma, and L* to the desired values, if possible;
      otherwise, the hue and L* will be sufficiently close, and chroma will be
      maximized.
]]
function HctSolver.solveToCam(hueDegrees, chroma, lstar)
	return Cam16.fromInt(HctSolver.solveToInt(hueDegrees, chroma, lstar));
end

return HctSolver;