main.js 22 KB

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