nearest.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. const stripForm = ["flabebe", "floette", "florges", "vivillon", "basculin", "furfrou", "magearna"];
  2. const getSprite = pokemon => {
  3. pokemon = pokemon
  4. .replace("-alola", "-alolan")
  5. .replace("-galar", "-galarian")
  6. .replace("darmanitan-galarian", "darmanitan-galarian-standard");
  7. if (stripForm.find(s => pokemon.includes(s))) {
  8. pokemon = pokemon.replace(/-.*$/, "");
  9. }
  10. return `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`;
  11. }
  12. const titleCase = s => s.charAt(0).toUpperCase() + s.substr(1);
  13. const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
  14. const vectorMag = v => Math.sqrt(vectorDot(v, v));
  15. const pokemonLookup = new Fuse(database, { keys: [ "name" ] });
  16. // hex codes already include leading # in these functions
  17. // rgb values are [0, 255] in these functions
  18. const jab2hex = jab => d3.jab(...jab).formatHex();
  19. const rgb2hex = rgb => d3.rgb(...rgb).formatHex();
  20. const rgb2jab = rgb => {
  21. const { J, a, b } = d3.jab(d3.rgb(...rgb));
  22. return [ J, a, b ];
  23. }
  24. const hex2rgb = hex => {
  25. const { r, g, b } = d3.rgb(hex);
  26. return [ r, g, b ];
  27. };
  28. // scoring functions
  29. const getNormedScorer = (c, q) => {
  30. const factor = c / vectorMag(q);
  31. return yVec => factor * vectorDot(q, yVec) / vectorMag(yVec);
  32. };
  33. const getUnnormedScorer = (c, q) => yVec => c * vectorDot(q, yVec);
  34. // create a tile of a given hex color
  35. const createTile = hexColor => {
  36. const tile = document.createElement("div");
  37. tile.setAttribute("class", "color-tile");
  38. tile.setAttribute("style", `background-color: ${hexColor};`)
  39. tile.textContent = hexColor;
  40. return tile;
  41. }
  42. const createPokemon = ({ name, score, yRGB, yJAB }) => {
  43. const img = document.createElement("img");
  44. img.setAttribute("src", getSprite(name));
  45. const titleName = titleCase(name);
  46. const text = score ? `${titleName}: ${score.toFixed(3)}` : titleName
  47. const pkmn = document.createElement("div");
  48. pkmn.setAttribute("class", "pokemon");
  49. pkmn.appendChild(img);
  50. const textSpan = document.createElement("span");
  51. textSpan.textContent = text;
  52. textSpan.setAttribute("class", "pokemon_text");
  53. pkmn.appendChild(textSpan);
  54. pkmn.appendChild(createTile(rgb2hex(yRGB)));
  55. pkmn.appendChild(createTile(jab2hex(yJAB)));
  56. return pkmn;
  57. }
  58. let lastColorSearch = null;
  59. let lastPkmnSearch = null;
  60. const paramsChanged = (...args) => {
  61. const old = lastColorSearch;
  62. lastColorSearch = args;
  63. return old === null || old.filter((p, i) => p !== args[i]).length > 0
  64. }
  65. const renderVec = math => `\\vec{${math.charAt(0)}}${math.substr(1)}`;
  66. const renderNorm = vec => `\\frac{${vec}}{\\left|\\left|${vec}\\right|\\right|}`;
  67. const renderMath = (includeX, normQY) => {
  68. const xTerm = includeX ? "X\\left(P\\right)" : "";
  69. const qyMod = normQY ? c => renderNorm(renderVec(c)) : renderVec;
  70. return TeXZilla.toMathML(`${xTerm}-2${qyMod("q")}\\cdot ${qyMod("Y\\left(P\\right)")}`);
  71. }
  72. const onUpdate = (event) => {
  73. if (event) {
  74. event.preventDefault();
  75. }
  76. // Configuration Loading
  77. const includeX = document.getElementById("include-x")?.checked ?? false;
  78. const normQY = document.getElementById("norm-q-y")?.checked ?? false;
  79. const closeCoeff = document.getElementById("close-coeff")?.value ?? 2;
  80. const useRGB = document.getElementById("color-space")?.textContent === "RGB";
  81. const numPoke = document.getElementById("num-poke")?.value ?? 20;
  82. const pokemonName = document.getElementById("pokemon-name")?.value?.toLowerCase() ?? "";
  83. const targetColor = "#" + (document.getElementById("color-input")?.value?.replace("#", "") ?? "FFFFFF");
  84. const targetRGB = hex2rgb(targetColor);
  85. // Update display values
  86. // document.getElementById("x-term").textContent = includeX ? "X(P)" : "";
  87. // document.getElementById("c-value").textContent = closeCoeff;
  88. // document.getElementById("q-vec").innerHTML = normQY ? "<mover><m" : "q";
  89. // document.getElementById("y-vec").textContent = normQY ? "Ŷ(P)" : "Y(P)";
  90. // document.getElementById("close-coeff-display").innerHTML = closeCoeff;
  91. // document.getElementById("num-poke-display").textContent = numPoke;
  92. const objFnElem = document.getElementById("obj-fn");
  93. objFnElem.innerHTML = "";
  94. objFnElem.appendChild(renderMath(includeX, normQY));
  95. // determine metrics from configuration
  96. const targetInSpace = useRGB ? targetRGB : rgb2jab(targetRGB);
  97. const xSelector = includeX ? (useRGB ? ({ xRGB }) => xRGB : ({ xJAB }) => xJAB) : () => 0;
  98. const ySelector = useRGB ? ({ yRGB }) => yRGB : ({ yJAB }) => yJAB;
  99. const yScorer = (normQY ? getNormedScorer : getUnnormedScorer)(closeCoeff, targetInSpace);
  100. const totalScorer = info => xSelector(info) - yScorer(ySelector(info));
  101. const newParams = paramsChanged(includeX, normQY, closeCoeff, useRGB, numPoke, targetColor);
  102. if (targetColor.length === 7 && newParams) {
  103. // calculate luminance to determine if text should be dark or light
  104. const textColor = vectorDot(targetRGB, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd";
  105. document.querySelector("body").setAttribute("style", `background: ${targetColor}; color: ${textColor}`);
  106. const bestList = document.getElementById("best-list");
  107. bestList.innerHTML = ''; // do the lazy thing
  108. // actually score pokemon
  109. database
  110. .map(info => ({ ...info, score: totalScorer(info) }))
  111. .sort((a, b) => a.score - b.score)
  112. .slice(0, numPoke)
  113. .forEach(info => {
  114. const li = document.createElement("li");
  115. li.appendChild(createPokemon(info))
  116. bestList.appendChild(li);
  117. });
  118. }
  119. if (pokemonName.length > 0 && (lastPkmnSearch !== pokemonName || newParams)) {
  120. lastPkmnSearch = pokemonName;
  121. // lookup by pokemon too
  122. const searchList = document.getElementById("search-list");
  123. searchList.innerHTML = '';
  124. pokemonLookup
  125. .search(pokemonName, { limit: 15 })
  126. .map(({ item }) => ({ ...item, score: totalScorer(item) }))
  127. .forEach(item => {
  128. const li = document.createElement("li");
  129. li.appendChild(createPokemon(item))
  130. searchList.appendChild(li);
  131. });
  132. }
  133. };
  134. const onRandomColor = () => {
  135. document.getElementById("color-input").value = rgb2hex([Math.random(), Math.random(), Math.random()].map(c => c * 255));
  136. onUpdate();
  137. };
  138. const onToggleSpace = () => {
  139. const element = document.getElementById("color-space");
  140. const current = element?.textContent;
  141. element.textContent = current === "RGB" ? "CAM02-UCS" : "RGB";
  142. document.getElementById("space-toggle").textContent = `Swap to ${current}`
  143. onUpdate();
  144. };