置顶公告:【置顶】关于临时开启评论区所有功能的公告(2022.10.22) | 【置顶】关于本站Widget恢复使用的公告
  • 你好~!欢迎来到萌娘百科镜像站!如需查看或编辑,请联系本站管理员注册账号。
  • 本镜像站和其他萌娘百科的镜像站无关,请注意分别。

Module:Hct/HctSolver

贴贴♀百科,万娘皆可贴的百科全书!转载请标注来源页面的网页链接,并声明引自贴贴百科。内容不可商用。
跳到导航 跳到搜索
Template-info.svg 模块文档  [创建] [刷新]
  1. --[[
  2. Copyright 2021 Google LLC
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]
  13. --[[
  14. This file has been modified. The original version is at
  15. https://github.com/material-foundation/material-color-utilities
  16. ]]
  17. local colorUtils = require('Module:Hct/ColorUtils');
  18. local mathUtils = require('Module:Hct/MathUtils');
  19. local Cam16 = require('Module:Hct/Cam16');
  20. --[[ A class that solves the HCT equation. ]]
  21. local HctSolver = {
  22. SCALED_DISCOUNT_FROM_LINRGB = {
  23. {0.001200833568784504, 0.002389694492170889, 0.0002795742885861124},
  24. {0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398},
  25. {0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076},
  26. };
  27. LINRGB_FROM_SCALED_DISCOUNT = {
  28. {1373.2198709594231, -1100.4251190754821, -7.278681089101213},
  29. {-271.815969077903, 559.6580465940733, -32.46047482791194},
  30. {1.9622899599665666, -57.173814538844006, 308.7233197812385},
  31. };
  32. Y_FROM_LINRGB = {0.2126, 0.7152, 0.0722};
  33. CRITICAL_PLANES = {
  34. 0.015176349177441876, 0.045529047532325624, 0.07588174588720938,
  35. 0.10623444424209313, 0.13658714259697685, 0.16693984095186062,
  36. 0.19729253930674434, 0.2276452376616281, 0.2579979360165119,
  37. 0.28835063437139563, 0.3188300904430532, 0.350925934958123,
  38. 0.3848314933096426, 0.42057480301049466, 0.458183274052838,
  39. 0.4976837250274023, 0.5391024159806381, 0.5824650784040898,
  40. 0.6277969426914107, 0.6751227633498623, 0.7244668422128921,
  41. 0.775853049866786, 0.829304845476233, 0.8848452951698498,
  42. 0.942497089126609, 1.0022825574869039, 1.0642236851973577,
  43. 1.1283421258858297, 1.1946592148522128, 1.2631959812511864,
  44. 1.3339731595349034, 1.407011200216447, 1.4823302800086415,
  45. 1.5599503113873272, 1.6398909516233677, 1.7221716113234105,
  46. 1.8068114625156377, 1.8938294463134073, 1.9832442801866852,
  47. 2.075074464868551, 2.1693382909216234, 2.2660538449872063,
  48. 2.36523901573795, 2.4669114995532007, 2.5710888059345764,
  49. 2.6777882626779785, 2.7870270208169257, 2.898822059350997,
  50. 3.0131901897720907, 3.1301480604002863, 3.2497121605402226,
  51. 3.3718988244681087, 3.4967242352587946, 3.624204428461639,
  52. 3.754355295633311, 3.887192587735158, 4.022731918402185,
  53. 4.160988767090289, 4.301978482107941, 4.445716283538092,
  54. 4.592217266055746, 4.741496401646282, 4.893568542229298,
  55. 5.048448422192488, 5.20615066083972, 5.3666897647573375,
  56. 5.5300801301023865, 5.696336044816294, 5.865471690767354,
  57. 6.037501145825082, 6.212438385869475, 6.390297286737924,
  58. 6.571091626112461, 6.7548350853498045, 6.941541251256611,
  59. 7.131223617812143, 7.323895587840543, 7.5195704746346665,
  60. 7.7182615035334345, 7.919981813454504, 8.124744458384042,
  61. 8.332562408825165, 8.543448553206703, 8.757415699253682,
  62. 8.974476575321063, 9.194643831691977, 9.417930041841839,
  63. 9.644347703669503, 9.873909240696694, 10.106627003236781,
  64. 10.342513269534024, 10.58158024687427, 10.8238400726681,
  65. 11.069304815507364, 11.317986476196008, 11.569896988756009,
  66. 11.825048221409341, 12.083451977536606, 12.345119996613247,
  67. 12.610063955123938, 12.878295467455942, 13.149826086772048,
  68. 13.42466730586372, 13.702830557985108, 13.984327217668513,
  69. 14.269168601521828, 14.55736596900856, 14.848930523210871,
  70. 15.143873411576273, 15.44220572664832, 15.743938506781891,
  71. 16.04908273684337, 16.35764934889634, 16.66964922287304,
  72. 16.985093187232053, 17.30399201960269, 17.62635644741625,
  73. 17.95219714852476, 18.281524751807332, 18.614349837764564,
  74. 18.95068293910138, 19.290534541298456, 19.633915083172692,
  75. 19.98083495742689, 20.331304511189067, 20.685334046541502,
  76. 21.042933821039977, 21.404114048223256, 21.76888489811322,
  77. 22.137256497705877, 22.50923893145328, 22.884842241736916,
  78. 23.264076429332462, 23.6469514538663, 24.033477234264016,
  79. 24.42366364919083, 24.817520537484558, 25.21505769858089,
  80. 25.61628489293138, 26.021211842414342, 26.429848230738664,
  81. 26.842203703840827, 27.258287870275353, 27.678110301598522,
  82. 28.10168053274597, 28.529008062403893, 28.96010235337422,
  83. 29.39497283293396, 29.83362889318845, 30.276079891419332,
  84. 30.722335150426627, 31.172403958865512, 31.62629557157785,
  85. 32.08401920991837, 32.54558406207592, 33.010999283389665,
  86. 33.4802739966603, 33.953417292456834, 34.430438229418264,
  87. 34.911345834551085, 35.39614910352207, 35.88485700094671,
  88. 36.37747846067349, 36.87402238606382, 37.37449765026789,
  89. 37.87891309649659, 38.38727753828926, 38.89959975977785,
  90. 39.41588851594697, 39.93615253289054, 40.460400508064545,
  91. 40.98864111053629, 41.520882981230194, 42.05713473317016,
  92. 42.597404951718396, 43.141702194811224, 43.6900349931913,
  93. 44.24241185063697, 44.798841244188324, 45.35933162437017,
  94. 45.92389141541209, 46.49252901546552, 47.065252796817916,
  95. 47.64207110610409, 48.22299226451468, 48.808024568002054,
  96. 49.3971762874833, 49.9904556690408, 50.587870934119984,
  97. 51.189430279724725, 51.79514187861014, 52.40501387947288,
  98. 53.0190544071392, 53.637271562750364, 54.259673423945976,
  99. 54.88626804504493, 55.517063457223934, 56.15206766869424,
  100. 56.79128866487574, 57.43473440856916, 58.08241284012621,
  101. 58.734331877617365, 59.39049941699807, 60.05092333227251,
  102. 60.715611475655585, 61.38457167773311, 62.057811747619894,
  103. 62.7353394731159, 63.417162620860914, 64.10328893648692,
  104. 64.79372614476921, 65.48848194977529, 66.18756403501224,
  105. 66.89098006357258, 67.59873767827808, 68.31084450182222,
  106. 69.02730813691093, 69.74813616640164, 70.47333615344107,
  107. 71.20291564160104, 71.93688215501312, 72.67524319850172,
  108. 73.41800625771542, 74.16517879925733, 74.9167682708136,
  109. 75.67278210128072, 76.43322770089146, 77.1981124613393,
  110. 77.96744375590167, 78.74122893956174, 79.51947534912904,
  111. 80.30219030335869, 81.08938110306934, 81.88105503125999,
  112. 82.67721935322541, 83.4778813166706, 84.28304815182372,
  113. 85.09272707154808, 85.90692527145302, 86.72564993000343,
  114. 87.54890820862819, 88.3767072518277, 89.2090541872801,
  115. 90.04595612594655, 90.88742016217518, 91.73345337380438,
  116. 92.58406282226491, 93.43925555268066, 94.29903859396902,
  117. 95.16341895893969, 96.03240364439274, 96.9059996312159,
  118. 97.78421388448044, 98.6670533535366, 99.55452497210776,
  119. };
  120. };
  121. --[[
  122. Sanitizes a small enough angle in radians.
  123. @param angle An angle in radians; must not deviate too much from 0.
  124. @return A coterminal angle between 0 and 2pi.
  125. ]]
  126. function HctSolver.sanitizeRadians(angle)
  127. return (angle + math.pi * 8) % (math.pi * 2);
  128. end
  129. --[[
  130. Delinearizes an RGB component, returning a floating-point number.
  131. @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
  132. @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
  133. ]]
  134. function HctSolver.trueDelinearized(rgbComponent)
  135. local normalized = rgbComponent / 100.0;
  136. local delinearized = 0.0;
  137. if normalized <= 0.0031308 then
  138. delinearized = normalized * 12.92;
  139. else
  140. delinearized = 1.055 * math.pow(normalized, 1.0 / 2.4) - 0.055;
  141. end
  142. return delinearized * 255.0;
  143. end
  144. function HctSolver.chromaticAdaptation(component)
  145. local af = math.pow(math.abs(component), 0.42);
  146. return mathUtils.signum(component) * 400.0 * af / (af + 27.13);
  147. end
  148. --[[
  149. Returns the hue of a linear RGB color in CAM16.
  150. @param linrgb The linear RGB coordinates of a color.
  151. @return The hue of the color in CAM16, in radians.
  152. ]]
  153. function HctSolver.hueOf(linrgb)
  154. local scaledDiscount =
  155. mathUtils.matrixMultiply(linrgb, HctSolver.SCALED_DISCOUNT_FROM_LINRGB);
  156. local rA = HctSolver.chromaticAdaptation(scaledDiscount[1]);
  157. local gA = HctSolver.chromaticAdaptation(scaledDiscount[2]);
  158. local bA = HctSolver.chromaticAdaptation(scaledDiscount[3]);
  159. -- redness-greenness
  160. local a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
  161. -- yellowness-blueness
  162. local b = (rA + gA - 2.0 * bA) / 9.0;
  163. return math.atan2(b, a);
  164. end
  165. function HctSolver.areInCyclicOrder(a, b, c)
  166. local deltaAB = HctSolver.sanitizeRadians(b - a);
  167. local deltaAC = HctSolver.sanitizeRadians(c - a);
  168. return deltaAB < deltaAC;
  169. end
  170. --[[
  171. Solves the lerp equation.
  172. @param source The starting number.
  173. @param mid The number in the middle.
  174. @param target The ending number.
  175. @return A number t such that lerp(source, target, t) = mid.
  176. ]]
  177. function HctSolver.intercept(source, mid, target)
  178. return (mid - source) / (target - source);
  179. end
  180. function HctSolver.lerpPoint(source, t, target)
  181. return {
  182. source[1] + (target[1] - source[1]) * t,
  183. source[2] + (target[2] - source[2]) * t,
  184. source[3] + (target[3] - source[3]) * t,
  185. };
  186. end
  187. --[[
  188. Intersects a segment with a plane.
  189. @param source The coordinates of point A.
  190. @param coordinate The R-, G-, or B-coordinate of the plane.
  191. @param target The coordinates of point B.
  192. @param axis The axis the plane is perpendicular with. (1: R, 2: G, 3: B)
  193. @return The intersection point of the segment AB with the plane
  194. R=coordinate, G=coordinate, or B=coordinate
  195. ]]
  196. function HctSolver.setCoordinate(source, coordinate, target, axis)
  197. local t = HctSolver.intercept(source[axis], coordinate, target[axis]);
  198. return HctSolver.lerpPoint(source, t, target);
  199. end
  200. function HctSolver.isBounded(x)
  201. return 0.0 <= x and x <= 100.0;
  202. end
  203. --[[
  204. Returns the nth possible vertex of the polygonal intersection.
  205. @param y The Y value of the plane.
  206. @param n The zero-based index of the point. 0 <= n <= 11.
  207. @return The nth possible vertex of the polygonal intersection of the y plane
  208. and the RGB cube, in linear RGB coordinates, if it exists. If this
  209. possible vertex lies outside of the cube, {-1.0, -1.0, -1.0} is returned.
  210. ]]
  211. function HctSolver.nthVertex(y, n)
  212. local kR = HctSolver.Y_FROM_LINRGB[1];
  213. local kG = HctSolver.Y_FROM_LINRGB[2];
  214. local kB = HctSolver.Y_FROM_LINRGB[3];
  215. local coordA = n % 4 <= 1 and 0.0 or 100.0;
  216. local coordB = n % 2 == 0 and 0.0 or 100.0;
  217. if n < 4 then
  218. local g = coordA;
  219. local b = coordB;
  220. local r = (y - g * kG - b * kB) / kR;
  221. if HctSolver.isBounded(r) then
  222. return {r, g, b};
  223. else
  224. return {-1.0, -1.0, -1.0};
  225. end
  226. elseif n < 8 then
  227. local b = coordA;
  228. local r = coordB;
  229. local g = (y - r * kR - b * kB) / kG;
  230. if HctSolver.isBounded(g) then
  231. return {r, g, b};
  232. else
  233. return {-1.0, -1.0, -1.0};
  234. end
  235. else
  236. local r = coordA;
  237. local g = coordB;
  238. local b = (y - r * kR - g * kG) / kB;
  239. if HctSolver.isBounded(b) then
  240. return {r, g, b};
  241. else
  242. return {-1.0, -1.0, -1.0};
  243. end
  244. end
  245. end
  246. --[[
  247. Finds the segment containing the desired color.
  248. @param y The Y value of the color.
  249. @param targetHue The hue of the color.
  250. @return Two sets of linear RGB coordinates, each corresponding to an endpoint
  251. of the segment containing the desired color.
  252. ]]
  253. function HctSolver.bisectToSegment(y, targetHue)
  254. local left = {-1.0, -1.0, -1.0};
  255. local right = left;
  256. local leftHue = 0.0;
  257. local rightHue = 0.0;
  258. local initialized = false;
  259. local uncut = true;
  260. for n = 0, 11 do
  261. local mid = HctSolver.nthVertex(y, n);
  262. if mid[1] >= 0 then
  263. local midHue = HctSolver.hueOf(mid);
  264. if initialized then
  265. if uncut or HctSolver.areInCyclicOrder(leftHue, midHue, rightHue) then
  266. uncut = false;
  267. if HctSolver.areInCyclicOrder(leftHue, targetHue, midHue) then
  268. right = mid;
  269. rightHue = midHue;
  270. else
  271. left = mid;
  272. leftHue = midHue;
  273. end
  274. end
  275. else
  276. left = mid;
  277. right = mid;
  278. leftHue = midHue;
  279. rightHue = midHue;
  280. initialized = true;
  281. end
  282. end
  283. end
  284. return left, right;
  285. end
  286. function HctSolver.midpoint(a, b)
  287. return {
  288. (a[1] + b[1]) / 2,
  289. (a[2] + b[2]) / 2,
  290. (a[3] + b[3]) / 2,
  291. };
  292. end
  293. function HctSolver.criticalPlaneBelow(x)
  294. return math.floor(x - 0.5);
  295. end
  296. function HctSolver.criticalPlaneAbove(x)
  297. return math.ceil(x - 0.5);
  298. end
  299. --[[
  300. Finds a color with the given Y and hue on the boundary of the
  301. cube.
  302. @param y The Y value of the color.
  303. @param targetHue The hue of the color.
  304. @return The desired color, in linear RGB coordinates.
  305. ]]
  306. function HctSolver.bisectToLimit(y, targetHue)
  307. local left, right = HctSolver.bisectToSegment(y, targetHue);
  308. local leftHue = HctSolver.hueOf(left);
  309. for axis = 1, 3 do
  310. if left[axis] ~= right[axis] then
  311. local lPlane = -1;
  312. local rPlane = 255;
  313. if left[axis] < right[axis] then
  314. lPlane = HctSolver.criticalPlaneBelow(
  315. HctSolver.trueDelinearized(left[axis]));
  316. rPlane = HctSolver.criticalPlaneAbove(
  317. HctSolver.trueDelinearized(right[axis]));
  318. else
  319. lPlane = HctSolver.criticalPlaneAbove(
  320. HctSolver.trueDelinearized(left[axis]));
  321. rPlane = HctSolver.criticalPlaneBelow(
  322. HctSolver.trueDelinearized(right[axis]));
  323. end
  324. for i = 1, 8 do
  325. if math.abs(rPlane - lPlane) <= 1 then
  326. break;
  327. end
  328. local mPlane = math.floor((lPlane + rPlane) / 2.0);
  329. local midPlaneCoordinate = HctSolver.CRITICAL_PLANES[mPlane + 1];
  330. local mid = HctSolver.setCoordinate(left, midPlaneCoordinate, right, axis);
  331. local midHue = HctSolver.hueOf(mid);
  332. if HctSolver.areInCyclicOrder(leftHue, targetHue, midHue) then
  333. right = mid;
  334. rPlane = mPlane;
  335. else
  336. left = mid;
  337. leftHue = midHue;
  338. lPlane = mPlane;
  339. end
  340. end
  341. end
  342. end
  343. return HctSolver.midpoint(left, right);
  344. end
  345. function HctSolver.inverseChromaticAdaptation(adapted)
  346. local adaptedAbs = math.abs(adapted);
  347. local base = math.max(0, 27.13 * adaptedAbs / (400.0 - adaptedAbs));
  348. return mathUtils.signum(adapted) * math.pow(base, 1.0 / 0.42);
  349. end
  350. --[[
  351. Finds a color with the given hue, chroma, and Y.
  352. @param hueRadians The desired hue in radians.
  353. @param chroma The desired chroma.
  354. @param y The desired Y.
  355. @return The desired color as a hexadecimal integer, if found; 0 otherwise.
  356. ]]
  357. function HctSolver.findResultByJ(hueRadians, chroma, y)
  358. -- Initial estimate of j.
  359. local j = math.sqrt(y) * 11.0;
  360. -- ===========================================================
  361. -- Operations inlined from Cam16 to avoid repeated calculation
  362. -- ===========================================================
  363. local viewingConditions = require('Module:Hct/ViewingConditions');
  364. local tInnerCoeff =
  365. 1 / math.pow(1.64 - math.pow(0.29, viewingConditions.n), 0.73);
  366. local eHue = 0.25 * (math.cos(hueRadians + 2.0) + 3.8);
  367. local p1 =
  368. eHue * (50000.0 / 13.0) * viewingConditions.nc * viewingConditions.ncb;
  369. local hSin = math.sin(hueRadians);
  370. local hCos = math.cos(hueRadians);
  371. for iterationRound = 1, 5 do
  372. -- ===========================================================
  373. -- Operations inlined from Cam16 to avoid repeated calculation
  374. -- ===========================================================
  375. local jNormalized = j / 100.0;
  376. local alpha =
  377. (chroma == 0.0 or j == 0.0) and 0.0 or chroma / math.sqrt(jNormalized);
  378. local t = math.pow(alpha * tInnerCoeff, 1.0 / 0.9);
  379. local ac = viewingConditions.aw *
  380. math.pow(jNormalized, 1.0 / viewingConditions.c / viewingConditions.z);
  381. local p2 = ac / viewingConditions.nbb;
  382. local gamma = 23.0 * (p2 + 0.305) * t /
  383. (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin);
  384. local a = gamma * hCos;
  385. local b = gamma * hSin;
  386. local rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
  387. local gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
  388. local bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
  389. local rCScaled = HctSolver.inverseChromaticAdaptation(rA);
  390. local gCScaled = HctSolver.inverseChromaticAdaptation(gA);
  391. local bCScaled = HctSolver.inverseChromaticAdaptation(bA);
  392. local linrgb = mathUtils.matrixMultiply(
  393. {rCScaled, gCScaled, bCScaled},
  394. HctSolver.LINRGB_FROM_SCALED_DISCOUNT
  395. );
  396. -- ===========================================================
  397. -- Operations inlined from Cam16 to avoid repeated calculation
  398. -- ===========================================================
  399. if linrgb[1] < 0 or linrgb[2] < 0 or linrgb[3] < 0 then
  400. return 0;
  401. end
  402. local kR = HctSolver.Y_FROM_LINRGB[1];
  403. local kG = HctSolver.Y_FROM_LINRGB[2];
  404. local kB = HctSolver.Y_FROM_LINRGB[3];
  405. local fnj = kR * linrgb[1] + kG * linrgb[2] + kB * linrgb[3];
  406. if fnj <= 0 then
  407. return 0;
  408. end
  409. if iterationRound == 5 or math.abs(fnj - y) < 0.002 then
  410. if linrgb[1] > 100.01 or linrgb[2] > 100.01 or linrgb[3] > 100.01 then
  411. return 0;
  412. end
  413. return colorUtils.argbFromLinrgb(linrgb);
  414. end
  415. -- Iterates with Newton method,
  416. -- Using 2 * fn(j) / j as the approximation of fn'(j)
  417. j = j - (fnj - y) * j / (2 * fnj);
  418. end
  419. return 0;
  420. end
  421. --[[
  422. Finds an sRGB color with the given hue, chroma, and L*, if possible.
  423. @param hueDegrees The desired hue, in degrees.
  424. @param chroma The desired chroma.
  425. @param lstar The desired L*.
  426. @return A hexadecimal representing the sRGB color. The color has sufficiently
  427. close hue, chroma, and L* to the desired values, if possible; otherwise,
  428. the hue and L* will be sufficiently close, and chroma will be maximized.
  429. ]]
  430. function HctSolver.solveToInt(hueDegrees, chroma, lstar)
  431. if chroma < 0.0001 or lstar < 0.0001 or lstar > 99.9999 then
  432. return colorUtils.argbFromLstar(lstar);
  433. end
  434. hueDegrees = mathUtils.sanitizeDegreesDouble(hueDegrees);
  435. local hueRadians = math.rad(hueDegrees);
  436. local y = colorUtils.yFromLstar(lstar);
  437. local exactAnswer = HctSolver.findResultByJ(hueRadians, chroma, y);
  438. if exactAnswer ~= 0 then
  439. return exactAnswer;
  440. end
  441. local linrgb = HctSolver.bisectToLimit(y, hueRadians);
  442. return colorUtils.argbFromLinrgb(linrgb);
  443. end
  444. --[[
  445. Finds an sRGB color with the given hue, chroma, and L*, if possible.
  446. @param hueDegrees The desired hue, in degrees.
  447. @param chroma The desired chroma.
  448. @param lstar The desired L*.
  449. @return An CAM16 object representing the sRGB color. The color has
  450. sufficiently close hue, chroma, and L* to the desired values, if possible;
  451. otherwise, the hue and L* will be sufficiently close, and chroma will be
  452. maximized.
  453. ]]
  454. function HctSolver.solveToCam(hueDegrees, chroma, lstar)
  455. return Cam16.fromInt(HctSolver.solveToInt(hueDegrees, chroma, lstar));
  456. end
  457. return HctSolver;