main.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. // ---- Math and Utilities ----
  2. // Vector Math
  3. const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
  4. const unitVector = v => {
  5. const mag = Math.hypot(...v);
  6. return v.map(c => c / mag);
  7. };
  8. // Angle Math
  9. const rad2deg = 180 / Math.PI;
  10. const twoPi = 2 * Math.PI;
  11. // Color Conversion
  12. const hex2rgb = hex => {
  13. hex = hex.replace("#", "");
  14. const red = hex.substr(0, 2);
  15. const grn = hex.substr(2, 2);
  16. const blu = hex.substr(4, 2);
  17. return [red, grn, blu].map(c => parseInt(c, 16));
  18. };
  19. // calculated from analyze.py
  20. RGB_TO_LMS = [
  21. [0.4121965, 0.53627432, 0.05143268],
  22. [0.2119195, 0.68071831, 0.10738379],
  23. [0.08834911, 0.28185414, 0.63018663],
  24. ];
  25. LMS_TO_OKLAB = [
  26. [0.2104542553, 0.793617785, -0.0040720468],
  27. [1.9779984951, -2.428592205, 0.4505937099],
  28. [0.0259040371, 0.7827717662, -0.808675766],
  29. ];
  30. const hex2oklab = hex => {
  31. const lrgb = hex2rgb(hex).map(c => {
  32. const v = c / 255;
  33. return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
  34. });
  35. const lms = RGB_TO_LMS.map(row => Math.cbrt(vectorDot(row, lrgb)));
  36. return LMS_TO_OKLAB.map(row => vectorDot(row, lms));
  37. };
  38. // Misc
  39. const clamp = (min, value, max) => Math.min(Math.max(value, min), max);
  40. const productLift =
  41. (...factors) =>
  42. (...args) =>
  43. factors
  44. .filter(fn => !!fn)
  45. .map(fn => fn(...args))
  46. .reduce((x, y) => x * y, 1);
  47. // Pre-computation
  48. const getColorData = hex => {
  49. const lab = hex2oklab(hex);
  50. return {
  51. hex,
  52. vector: lab,
  53. unit: unitVector(lab),
  54. chroma: Math.hypot(lab[1], lab[2]),
  55. hue: (Math.atan2(lab[2], lab[1]) + twoPi) % twoPi,
  56. };
  57. };
  58. const pokemonData = database.map(({ total, clusters, ...rest }) => ({
  59. total: {
  60. ...total,
  61. stddevTotal: Math.hypot(...total.stddev),
  62. stddevL: total.stddev[0],
  63. stddevA: total.stddev[1],
  64. stddevB: total.stddev[2],
  65. chromaMean: total.chroma[0],
  66. chromaDev: total.chroma[1],
  67. unitCentroid: unitVector(total.centroid),
  68. proportionDisplay: null,
  69. proportion: 1,
  70. inverseProportion: 1,
  71. },
  72. clusters: clusters
  73. .sort((a, b) => b.size - a.size)
  74. .map(c => ({
  75. ...c,
  76. stddevTotal: Math.hypot(...c.stddev),
  77. stddevL: c.stddev[0],
  78. stddevA: c.stddev[1],
  79. stddevB: c.stddev[2],
  80. chromaMean: c.chroma[0],
  81. chromaDev: c.chroma[1],
  82. unitCentroid: unitVector(c.centroid),
  83. proportion: c.size / total.size,
  84. inverseProportion: total.size / c.size,
  85. }))
  86. .map(c => ({
  87. ...c,
  88. proportionDisplay: (100 * c.proportion).toFixed(2),
  89. })),
  90. ...rest,
  91. }));
  92. const pokemonLookup = new Fuse(pokemonData, { keys: ["name"] });
  93. const calcScores = (data, target) => {
  94. const { centroid, unitCentroid, tilt, stddev, chromaMean, chromaDev, hue } = data;
  95. const deltaVec = centroid.map((c, i) => c - target.vector[i]);
  96. const psiVec = stddev.map((s, i) => Math.hypot(s, deltaVec[i]));
  97. const psi = Math.hypot(...psiVec);
  98. const theta = Math.acos(Math.min(1, vectorDot(tilt, target.unit))) * rad2deg;
  99. const deltaC = chromaMean - target.chroma;
  100. const deltaH = Math.abs(hue - target.hue) * rad2deg;
  101. return {
  102. ...data,
  103. arcDiff: psi * theta,
  104. psi,
  105. psiVec,
  106. psiL: psiVec[0],
  107. psiA: psiVec[1],
  108. psiB: psiVec[2],
  109. psiC: Math.hypot(chromaDev, deltaC),
  110. theta,
  111. delta: Math.hypot(...deltaVec),
  112. deltaVec,
  113. deltaL: Math.abs(deltaVec[0]),
  114. deltaA: Math.abs(deltaVec[1]),
  115. deltaB: Math.abs(deltaVec[2]),
  116. deltaC: Math.abs(deltaC),
  117. deltaH: Math.min(deltaH, 360 - deltaH),
  118. angDiff: Math.acos(vectorDot(unitCentroid, target.unit)) * rad2deg,
  119. hue: hue * rad2deg,
  120. };
  121. };
  122. const sortOrders = {
  123. max: (a, b) => b - a,
  124. min: (a, b) => a - b,
  125. };
  126. // who needs a framework?
  127. const makeTemplate = (id, definition = () => ({})) => {
  128. const content = document.getElementById(id).content;
  129. return (...args) => {
  130. const fragment = content.cloneNode(true);
  131. const binds = Object.fromEntries(
  132. Array.from(fragment.querySelectorAll("[bind-to]")).map(element => {
  133. const name = element.getAttribute("bind-to");
  134. element.removeAttribute("bind-to");
  135. return [name, element];
  136. })
  137. );
  138. Object.entries(definition(...args))
  139. .map(([name, settings]) => [binds[name], settings])
  140. .filter(([bind]) => !!bind)
  141. .forEach(([bind, settings]) =>
  142. Object.entries(settings).forEach(([setting, value]) => {
  143. if (setting.startsWith("@")) {
  144. bind.addEventListener(setting.slice(1), value);
  145. } else if (setting.startsWith("--")) {
  146. bind.style.setProperty(setting, value);
  147. } else if (setting === "dataset") {
  148. Object.entries(value).forEach(([key, data]) => (bind.dataset[key] = data));
  149. } else if (setting === "append") {
  150. if (Array.isArray(value)) {
  151. bind.append(...value);
  152. } else {
  153. bind.append(value);
  154. }
  155. } else {
  156. bind[setting] = value;
  157. }
  158. })
  159. );
  160. return [fragment, binds];
  161. };
  162. };
  163. // ---- Selectors ----
  164. const rootStyle = document.querySelector(":root").style;
  165. const colorSearchResultsTarget = document.getElementById("color-results");
  166. const nameSearchResultsTarget = document.getElementById("name-results");
  167. const prevColorsSidebar = document.getElementById("prevColors");
  168. const clusterRankingTitle = document.getElementById("cls-title");
  169. const clusterMetricSection = document.getElementById("cls-metric-mount");
  170. const clusterFunctionSection = document.getElementById("cls-fn");
  171. const colorCalculateForm = document.forms.colorCalculateForm;
  172. const colorSortForm = document.forms.colorSortForm;
  173. const targetColorElements = document.forms.targetColorForm.elements;
  174. const colorDisplayElements = document.forms.colorDisplayForm.elements;
  175. const filterElements = document.forms.filterControl.elements;
  176. const nameSearchFormElements = document.forms.nameSearchForm.elements;
  177. // ---- Add Metric Selects ----
  178. const createMetricSelect = makeTemplate("metric-select-template");
  179. const [{ firstElementChild: sortMetricForm }] = createMetricSelect();
  180. const [{ firstElementChild: clusterMetricForm }] = createMetricSelect();
  181. document.getElementById("sort-metric-mount").append(sortMetricForm);
  182. sortMetricForm.elements.metricKind.value = "compare";
  183. document.getElementById("cls-metric-mount").append(clusterMetricForm);
  184. clusterMetricForm.elements.metricKind.value = "stat";
  185. const updateMetricSelects = form => {
  186. const kind = form.elements.metricKind.value;
  187. form.elements.compare.disabled = kind !== "compare";
  188. form.elements.stat.disabled = kind !== "stat";
  189. form.elements.metric.value = form.elements[kind].value;
  190. };
  191. // bit of a hack, but lets us control this all from the template
  192. const metricSymbols = Object.fromEntries(
  193. Array.from(document.querySelectorAll("option")).map(el => [
  194. el.value,
  195. el.textContent.split("(")[1].split(")")[0],
  196. ])
  197. );
  198. const updateMetricDisplays = () => {
  199. updateMetricSelects(sortMetricForm);
  200. updateMetricSelects(clusterMetricForm);
  201. colorCalculateForm.elements.sortMetricSymbolP.value =
  202. colorCalculateForm.elements.sortMetricSymbolB.value =
  203. metricSymbols[
  204. sortMetricForm.elements[sortMetricForm.elements.metricKind.value].value
  205. ];
  206. colorCalculateForm.elements.clusterMetricSymbol.value =
  207. metricSymbols[
  208. clusterMetricForm.elements[clusterMetricForm.elements.metricKind.value].value
  209. ];
  210. };
  211. // ---- Styling ----
  212. const getColorStyles = hex => {
  213. const rgb = hex2rgb(hex);
  214. const lum = vectorDot(rgb, [0.3, 0.6, 0.1]);
  215. const highlight = lum >= 128 ? "var(--color-dark)" : "var(--color-light)";
  216. return {
  217. "--highlight": highlight,
  218. "--background": hex,
  219. "--shadow-component": lum >= 128 ? "0" : "255",
  220. };
  221. };
  222. const setColorStyles = (style, hex) =>
  223. Object.entries(getColorStyles(hex)).forEach(([prop, value]) =>
  224. style.setProperty(prop, value)
  225. );
  226. // ---- Pokemon Display ----
  227. // pulled out bc the render uses them
  228. const metricScores = {};
  229. const bestClusterIndices = {};
  230. const objectiveValues = {};
  231. const createPokemonTooltip = makeTemplate("pkmn-data-template", data =>
  232. Object.fromEntries(
  233. Object.entries(data).map(([metric, value]) => [
  234. metric,
  235. {
  236. innerText: Array.isArray(value)
  237. ? value.map(v => v.toFixed(2)).join(", ")
  238. : value?.toFixed?.(3)?.replace(".000", ""),
  239. },
  240. ])
  241. )
  242. );
  243. const createPokemonTile = makeTemplate(
  244. "pkmn-tile-template",
  245. ({ name, species, color }, enableTotalFlags, enableClusterFlags) => {
  246. const formattedName = name
  247. .split("-")
  248. .map(part => part.charAt(0).toUpperCase() + part.substr(1))
  249. .join(" ");
  250. let spriteName = name
  251. .toLowerCase()
  252. .replace("'", "") // farfetchd line
  253. .replace("-gmax", "-gigantamax")
  254. .replace("-alola", "-alolan")
  255. .replace("-galar", "-galarian")
  256. .replace("-hisui", "-hisuian")
  257. .replace("-paldea", "-paldean")
  258. .replace("-paldean-combat", "-paldean") // tauros
  259. .replace("-paldean-blaze", "-paldean-fire") // tauros
  260. .replace("-paldean-aqua", "-paldean-water") // tauros
  261. .replace("-phony", "") // sinistea and polteageist
  262. .replace(". ", "-") // mr mime + rime
  263. .replace(": ", "-") // type: null
  264. .replaceAll("é", "e") // flabébé
  265. .replace("darmanitan-galarian", "darmanitan-galarian-standard")
  266. .replace("hippopotas-m", "hippopotas")
  267. .replace("hippowdon-m", "hippowdon")
  268. .replace("unfezant-m", "unfezant")
  269. .replace("frillish-m", "frillish")
  270. .replace("jellicent-m", "jellicent")
  271. .replace("pyroar-m", "pyroar")
  272. .replace("pyroar-f", "pyroar-female")
  273. .replace("indeedee-f", "indeedee-female")
  274. .replace("basculegion-f", "basculegion-female")
  275. .replace("tapu ", "tapu-")
  276. .replace("urshifu-gigantamax", "urshifu-gigantamax-single-strike")
  277. .replace("urshifu-rapid-strike-gigantamax", "urshifu-gigantamax-rapid-strike")
  278. .replace("calyrex-shadow", "calyrex-shadow-rider")
  279. .replace("calyrex-ice", "calyrex-ice-rider")
  280. .replace("maushold-four", "maushold-family4")
  281. .replace("chienpao", "chien-pao")
  282. .replace("tinglu", "ting-lu")
  283. .replace("wochien", "wo-chien")
  284. .replace("chiyu", "chi-yu")
  285. .replace(" ", "-");
  286. if (
  287. [
  288. "flabebe",
  289. "floette",
  290. "florges",
  291. "vivillon",
  292. "basculin",
  293. "furfrou",
  294. "magearna",
  295. "alcremie",
  296. "minior",
  297. ].find(s => spriteName.includes(s))
  298. ) {
  299. spriteName = spriteName.replace(/-.*$/, "");
  300. }
  301. const imageErrorHandler2 = ({ target }) => {
  302. target.removeEventListener("error", imageErrorHandler2);
  303. target.src = `https://img.pokemondb.net/sprites/scarlet-violet/icon/${spriteName}.png`;
  304. };
  305. const imageErrorHandler1 = ({ target }) => {
  306. target.removeEventListener("error", imageErrorHandler1);
  307. target.addEventListener("error", imageErrorHandler2);
  308. target.src = `https://img.pokemondb.net/sprites/sword-shield/icon/${spriteName}.png`;
  309. };
  310. const image = {
  311. alt: formattedName,
  312. src: `https://img.pokemondb.net/sprites/sword-shield/normal/${spriteName}.png`,
  313. "@error": imageErrorHandler1,
  314. };
  315. const linkName = species.replace(/[':.]/, "").replace(" ", "-");
  316. const link = {
  317. href: `https://pokemondb.net/pokedex/${linkName}`,
  318. };
  319. const score = {
  320. innerText: objectiveValues[name].toFixed(2),
  321. };
  322. const { total, clusters } = metricScores[name];
  323. const buttonBinds = [
  324. [clusters[0], "cls1Btn", "cls1Data", "infoHover1"],
  325. [clusters[1], "cls2Btn", "cls2Data", "infoHover2"],
  326. [clusters[2], "cls3Btn", "cls3Data", "infoHover3"],
  327. [clusters[3], "cls4Btn", "cls4Data", "infoHover4"],
  328. [total, "totalBtn", "totalData", "infoHover"],
  329. ]
  330. .filter(([data]) => !!data)
  331. .map(([data, button, tooltip, infoHover], index) => ({
  332. [button]: {
  333. dataset: {
  334. included: enableClusterFlags && index === bestClusterIndices[name],
  335. },
  336. hidden: false,
  337. innerText:
  338. data.hex + (data.proportionDisplay ? ` - ${data.proportionDisplay}%` : ""),
  339. "@click"() {
  340. model.setTargetColor(data.hex);
  341. },
  342. ...getColorStyles(data.hex),
  343. },
  344. [tooltip]: {
  345. append: createPokemonTooltip(data)[0],
  346. },
  347. [infoHover]: {
  348. "@mouseenter"({ target }) {
  349. target.closest(".pkmn-tile").dataset.info = `show-${infoHover}`;
  350. },
  351. "@mouseleave"({ target }) {
  352. target.closest(".pkmn-tile").dataset.info = "hide";
  353. },
  354. },
  355. }))
  356. .reduce((a, b) => ({ ...a, ...b }), {});
  357. buttonBinds.totalBtn.dataset.included = enableTotalFlags;
  358. return {
  359. name: {
  360. innerText: formattedName,
  361. title: formattedName,
  362. },
  363. image,
  364. color: { innerText: color },
  365. link,
  366. score,
  367. tileRoot: { dataset: { info: "hide" } },
  368. ...buttonBinds,
  369. };
  370. }
  371. );
  372. const renderPokemon = (list, target) => {
  373. target.innerText = "";
  374. const {
  375. sortUseWholeImage,
  376. sortUseBestCluster,
  377. sortUseClusterProportion,
  378. sortUseInvClusterProportion,
  379. } = Object.fromEntries(new FormData(colorCalculateForm).entries());
  380. const enableClusterFlags = !!(
  381. sortUseBestCluster ||
  382. sortUseClusterProportion ||
  383. sortUseInvClusterProportion
  384. );
  385. target.append(
  386. ...list.map(
  387. pkmn => createPokemonTile(pkmn, !!sortUseWholeImage, enableClusterFlags)[0]
  388. )
  389. );
  390. };
  391. // ---- Calculation Logic ----
  392. const model = {
  393. setTargetColor(newColor) {
  394. const hex = `#${newColor?.replace("#", "")}`;
  395. if (hex.length !== 7) {
  396. return;
  397. }
  398. setColorStyles(rootStyle, hex);
  399. const oldColor = this.targetColor;
  400. this.targetColor = hex;
  401. targetColorElements.colorText.value = hex;
  402. targetColorElements.colorText.dataset.lastValid = hex;
  403. targetColorElements.colorPicker.value = hex;
  404. if (oldColor) {
  405. const prevButton = document.createElement("button");
  406. prevButton.innerText = oldColor;
  407. prevButton.classList = "color-select";
  408. setColorStyles(prevButton.style, oldColor);
  409. prevButton.addEventListener("click", () => this.setTargetColor(oldColor));
  410. prevColorsSidebar.prepend(prevButton);
  411. }
  412. const targetData = getColorData(hex);
  413. targetColorElements.info.value = `
  414. (${(targetData.vector[0] * 100).toFixed(2)}%,
  415. ${targetData.chroma.toFixed(4)},
  416. ${(targetData.hue * rad2deg).toFixed(1)}°)
  417. `;
  418. pokemonData.forEach(({ name, total, clusters }) => {
  419. metricScores[name] = {
  420. total: calcScores(total, targetData),
  421. clusters: clusters.map(c => calcScores(c, targetData)),
  422. };
  423. });
  424. this.calculateObjective();
  425. },
  426. calculateObjective() {
  427. const {
  428. clusterUseClusterSize,
  429. clusterUseInvClusterSize,
  430. clusterSortOrder,
  431. sortUseWholeImage,
  432. sortUseBestCluster,
  433. sortUseClusterProportion,
  434. sortUseInvClusterProportion,
  435. } = Object.fromEntries(new FormData(colorCalculateForm).entries());
  436. const clsMetric = clusterMetricForm.elements.metric.value;
  437. const getClusterScore = productLift(
  438. cluster => cluster[clsMetric],
  439. clusterUseClusterSize && (cluster => cluster.size),
  440. clusterUseInvClusterSize && (cluster => cluster.inverseSize)
  441. );
  442. const clsSort = sortOrders[clusterSortOrder];
  443. const getBestClusterIndex = ({ total, clusters }) =>
  444. clusters
  445. .map((c, i) => [getClusterScore(c, total), i])
  446. .reduce((a, b) => (clsSort(a[0], b[0]) > 0 ? b : a))[1];
  447. Object.entries(metricScores).forEach(([name, scores]) => {
  448. bestClusterIndices[name] = getBestClusterIndex(scores);
  449. });
  450. const metric = sortMetricForm.elements.metric.value;
  451. const getSortScoreClusterFactors = productLift(
  452. sortUseBestCluster && (({ clusters }, i) => clusters[i][metric]),
  453. sortUseClusterProportion && (({ clusters }, i) => clusters[i].proportion),
  454. sortUseInvClusterProportion && (({ clusters }, i) => clusters[i].inverseProportion)
  455. );
  456. const usingCluster =
  457. sortUseBestCluster || sortUseClusterProportion || sortUseInvClusterProportion;
  458. const getSortScore = (scores, i) =>
  459. (sortUseWholeImage ? scores.total[metric] : 0) +
  460. (usingCluster ? getSortScoreClusterFactors(scores, i) : 0);
  461. Object.entries(metricScores).forEach(([name, scores]) => {
  462. objectiveValues[name] = getSortScore(scores, bestClusterIndices[name]);
  463. });
  464. this.renderNameSearchResults();
  465. this.rank();
  466. },
  467. rank() {
  468. const { sortOrder } = Object.fromEntries(new FormData(colorSortForm).entries());
  469. const compare = sortOrders[sortOrder];
  470. const sortFn = (a, b) => compare(objectiveValues[a], objectiveValues[b]);
  471. this.ranked = pokemonData
  472. .slice(0)
  473. .sort((a, b) => sortFn(a.name, b.name) || a.name.localeCompare(b.name));
  474. this.renderColorSearchResults();
  475. },
  476. setNameSearchResults(newNameResults) {
  477. this.nameSearchResults = newNameResults;
  478. this.renderNameSearchResults();
  479. },
  480. renderNameSearchResults() {
  481. renderPokemon(this.nameSearchResults ?? [], nameSearchResultsTarget);
  482. },
  483. renderColorSearchResults() {
  484. const count = parseInt(colorDisplayElements.resultsToDisplay.value);
  485. const dexNums = new Set();
  486. const toRender = [];
  487. for (const pkmn of this.ranked) {
  488. if (toRender.length >= count) {
  489. break;
  490. }
  491. if (!filterElements.allowNoStartForms.checked && pkmn.traits.includes("nostart")) {
  492. continue;
  493. }
  494. if (!filterElements.allowNFE.checked && pkmn.traits.includes("nfe")) {
  495. continue;
  496. }
  497. if (!filterElements.allowRepeatDexNum.checked) {
  498. if (dexNums.has(pkmn.num)) {
  499. continue;
  500. }
  501. dexNums.add(pkmn.num);
  502. }
  503. toRender.push(pkmn);
  504. }
  505. renderPokemon(toRender, colorSearchResultsTarget);
  506. },
  507. };
  508. // ---- Form Controls ----
  509. nameSearchFormElements.input.addEventListener("input", ({ target: { value } }) => {
  510. model.setNameSearchResults(
  511. pokemonLookup.search(value, { limit: 20 }).map(({ item }) => item)
  512. );
  513. });
  514. nameSearchFormElements.all.addEventListener("click", () => {
  515. nameSearchFormElements.input.value = "";
  516. model.setNameSearchResults(pokemonData.slice(0));
  517. });
  518. nameSearchFormElements.clear.addEventListener("click", () => {
  519. nameSearchFormElements.input.value = "";
  520. model.setNameSearchResults([]);
  521. });
  522. nameSearchFormElements.random.addEventListener("click", () => {
  523. model.setNameSearchResults(
  524. Array.from(
  525. { length: 20 },
  526. () => pokemonData[Math.floor(Math.random() * pokemonData.length)]
  527. )
  528. );
  529. });
  530. targetColorElements.colorText.addEventListener("input", ({ target }) => {
  531. if (target.willValidate && !target.validity.valid) {
  532. target.value = target.dataset.lastValid || "";
  533. } else {
  534. model.setTargetColor(target.value);
  535. }
  536. });
  537. targetColorElements.colorPicker.addEventListener("change", ({ target }) =>
  538. model.setTargetColor(target.value)
  539. );
  540. const randomizeTargetColor = () =>
  541. model.setTargetColor(
  542. [Math.random(), Math.random(), Math.random()]
  543. .map(component =>
  544. Math.floor(component * 256)
  545. .toString(16)
  546. .padStart(2, "0")
  547. )
  548. .reduce((x, y) => x + y)
  549. );
  550. targetColorElements.randomColor.addEventListener("click", randomizeTargetColor);
  551. colorDisplayElements.resultsToDisplay.addEventListener(
  552. "input",
  553. ({ target: { value } }) => {
  554. colorDisplayElements.output.value = value;
  555. }
  556. );
  557. colorDisplayElements.resultsToDisplay.addEventListener("change", () =>
  558. model.renderColorSearchResults()
  559. );
  560. Array.from(filterElements).forEach(el =>
  561. el.addEventListener("change", () => model.renderColorSearchResults())
  562. );
  563. Array.from(colorSortForm.elements).forEach(el =>
  564. el.addEventListener("change", () => model.rank())
  565. );
  566. Array.from(colorCalculateForm.elements).forEach(el =>
  567. el.addEventListener("change", () => {
  568. const { sortUseBestCluster, sortUseClusterProportion, sortUseInvClusterProportion } =
  569. Object.fromEntries(new FormData(colorCalculateForm).entries());
  570. clusterRankingTitle.dataset.faded =
  571. clusterMetricSection.dataset.faded =
  572. clusterFunctionSection.dataset.faded =
  573. !(sortUseBestCluster || sortUseClusterProportion || sortUseInvClusterProportion);
  574. model.calculateObjective();
  575. })
  576. );
  577. sortMetricForm.addEventListener("change", () => {
  578. updateMetricDisplays();
  579. model.calculateObjective();
  580. });
  581. clusterMetricForm.addEventListener("change", () => {
  582. updateMetricDisplays();
  583. model.calculateObjective();
  584. });
  585. // ---- Initial Setup ----
  586. updateMetricDisplays();
  587. randomizeTargetColor();