nearest.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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 getContrastingTextColor = rgb => vectorDot(rgb, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd"
  16. const pokemonLookup = new Fuse(database, { keys: [ "name" ] });
  17. // hex codes already include leading # in these functions
  18. // rgb values are [0, 255] in these functions
  19. const jab2hex = jab => d3.jab(...jab).formatHex();
  20. const rgb2hex = rgb => d3.rgb(...rgb).formatHex();
  21. const rgb2jab = rgb => {
  22. const { J, a, b } = d3.jab(d3.rgb(...rgb));
  23. return [ J, a, b ];
  24. }
  25. const hex2rgb = hex => {
  26. const { r, g, b } = d3.rgb(hex);
  27. return [ r, g, b ];
  28. };
  29. // scoring functions
  30. const getNormedScorer = (c, qRGB, qJAB) => {
  31. const fRGB = c / vectorMag(qRGB);
  32. const fJAB = c / vectorMag(qJAB);
  33. return ({ yRGB, yJAB }) => ({
  34. scoreRGB: fRGB * vectorDot(qRGB, yRGB) / vectorMag(yRGB),
  35. scoreJAB: fJAB * vectorDot(qJAB, yJAB) / vectorMag(yJAB),
  36. });
  37. };
  38. const getUnnormedScorer = (c, qRGB, qJAB) => ({ yRGB, yJAB }) => ({
  39. scoreRGB: c * vectorDot(qRGB, yRGB),
  40. scoreJAB: c * vectorDot(qJAB, yJAB),
  41. });
  42. // create a tile of a given hex color
  43. const createTile = hexColor => {
  44. const tile = document.createElement("div");
  45. tile.setAttribute("class", "color-tile");
  46. tile.setAttribute("style", `background-color: ${hexColor};`)
  47. tile.textContent = hexColor;
  48. return tile;
  49. }
  50. const renderPokemon = (
  51. { name, scoreRGB = null, scoreJAB = null, yRGB, yJAB },
  52. { labelClass = "", rgbClass = "", jabClass = "" } = {},
  53. ) => {
  54. const titleName = titleCase(name);
  55. const rgbHex = rgb2hex(yRGB);
  56. const jabHex = jab2hex(yJAB);
  57. const textHex = getContrastingTextColor(yRGB);
  58. const rgbVec = yRGB.map(c => c.toFixed()).join(", ")
  59. const jabVec = yJAB.map(c => c.toFixed(2)).join(", ")
  60. const scoreClass = scoreRGB === null || scoreJAB === null ? "hide" : "";
  61. const pkmn = document.createElement("div");
  62. pkmn.setAttribute("class", "pokemon_tile");
  63. pkmn.innerHTML = `
  64. <div class="pokemon_tile-image-wrapper">
  65. <img src="${getSprite(name)}" />
  66. </div>
  67. <div class="pokemon_tile-info_panel">
  68. <span class="pokemon_tile-pokemon_name">${titleName}</span>
  69. <div class="pokemon_tile-results">
  70. <div class="pokemon_tile-labels ${labelClass}">
  71. <span class="${jabClass}">Jab: </span>
  72. <span class="${rgbClass}">RGB: </span>
  73. </div>
  74. <div class="pokemon_tile-score_column ${scoreClass}">
  75. <span class="pokemon_tile-no_flex ${jabClass}">
  76. ${scoreJAB?.toFixed(2)}
  77. </span>
  78. <span class="pokemon_tile-no_flex ${rgbClass}">
  79. ${scoreRGB?.toFixed(2)}
  80. </span>
  81. </div>
  82. <div class="pokemon_tile-hex_column">
  83. <div class="pokemon_tile-hex_color ${jabClass}" style="background-color: ${jabHex}; color: ${textHex}">
  84. ${jabHex}
  85. </div>
  86. <div class="pokemon_tile-hex_color ${rgbClass}" style="background-color: ${rgbHex}; color: ${textHex}">
  87. ${rgbHex}
  88. </div>
  89. </div>
  90. <div class="pokemon_tile-vector_column">
  91. <span class="pokemon_tile-no_flex ${rgbClass}">(${rgbVec})</span>
  92. <span class="pokemon_tile-no_flex ${jabClass}">(${jabVec})</span>
  93. </div>
  94. </div>
  95. </div>
  96. `;
  97. return pkmn;
  98. }
  99. let lastColorSearch = null;
  100. let lastPkmnSearch = null;
  101. const paramsChanged = (...args) => {
  102. const old = lastColorSearch;
  103. lastColorSearch = args;
  104. return old === null || old.filter((p, i) => p !== args[i]).length > 0
  105. }
  106. const renderVec = math => `\\vec{${math.charAt(0)}}${math.substr(1)}`;
  107. const renderNorm = vec => `\\frac{${vec}}{\\left|\\left|${vec}\\right|\\right|}`;
  108. const renderMath = (includeX, normQY, closeCoeff) => {
  109. const xTerm = includeX ? "X\\left(P\\right)" : "";
  110. const qyMod = normQY ? c => renderNorm(renderVec(c)) : renderVec;
  111. return TeXZilla.toMathML(`\\arg\\min_{P}\\left[${xTerm}-${closeCoeff}${qyMod("q")}\\cdot ${qyMod("Y\\left(P\\right)")}\\right]`);
  112. }
  113. const renderQVec = (q, id, sub) => {
  114. document.getElementById(id).innerHTML = TeXZilla.toMathMLString(`\\vec{q}_{\\text{${sub}}} = \\left(${q.join(", ")}\\right)`);
  115. }
  116. const changePageColors = color => {
  117. // calculate luminance to determine if text should be dark or light
  118. const textColor = getContrastingTextColor([color.r, color.g, color.b]);
  119. document.querySelector("body").setAttribute("style", `background: ${color.formatHex()}; color: ${textColor}`);
  120. }
  121. const readColor = rgb => {
  122. const { J, a, b } = d3.jab(rgb);
  123. return [[rgb.r, rgb.g, rgb.b], [J, a, b]];
  124. }
  125. const onUpdate = (event) => {
  126. if (event) {
  127. event.preventDefault();
  128. }
  129. // Configuration Loading
  130. const includeX = document.getElementById("include-x")?.checked ?? false;
  131. const normQY = document.getElementById("norm-q-y")?.checked ?? false;
  132. const closeCoeff = document.getElementById("close-coeff")?.value ?? 2;
  133. const useRGB = document.getElementById("color-space")?.textContent === "RGB";
  134. const numPoke = document.getElementById("num-poke")?.value ?? 20;
  135. const pokemonName = document.getElementById("pokemon-name")?.value?.toLowerCase() ?? "";
  136. const colorInput = "#" + (document.getElementById("color-input")?.value?.replace("#", "") ?? "FFFFFF");
  137. // Clear pokemon search
  138. if (pokemonName.length === 0) {
  139. const searchList = document.getElementById("search-list");
  140. searchList.innerHTML = '';
  141. }
  142. // Check if parameters have changed
  143. const newParams = paramsChanged(includeX, normQY, closeCoeff, useRGB, numPoke, colorInput);
  144. if (newParams) {
  145. // Update display values
  146. document.getElementById("close-coeff-display").innerHTML = closeCoeff;
  147. document.getElementById("num-poke-display").textContent = numPoke;
  148. const objFnElem = document.getElementById("obj-fn");
  149. objFnElem.innerHTML = "";
  150. objFnElem.appendChild(renderMath(includeX, normQY, closeCoeff));
  151. }
  152. // Only modified if current color is valid
  153. let totalScorer = info => info;
  154. // Lookup by color
  155. if (colorInput.length === 7) {
  156. // Convert input color
  157. const targetColor = d3.color(colorInput);
  158. const [ targetRGB, targetJAB ] = readColor(targetColor);
  159. // Update the color display
  160. changePageColors(targetColor);
  161. renderQVec(targetRGB.map(c => c.toFixed()), "q-vec-rgb", "RGB");
  162. renderQVec(targetJAB.map(c => c.toFixed(2)), "q-vec-jab", "Jab");
  163. // Determine metrics from configuration
  164. const xSelector = includeX ? ({ xRGB, xJAB }) => [ xRGB, xJAB ] : () => [ 0, 0 ];
  165. const yScorer = (normQY ? getNormedScorer : getUnnormedScorer)(closeCoeff, targetRGB, targetJAB);
  166. // Set the scoring function
  167. totalScorer = info => {
  168. const [ xRGB, xJAB ] = xSelector(info);
  169. const { scoreRGB, scoreJAB } = yScorer(info);
  170. return {
  171. ...info,
  172. scoreRGB: xRGB - scoreRGB,
  173. scoreJAB: xJAB - scoreJAB,
  174. }
  175. };
  176. // Rescore Pokemon and update lists if config has changed
  177. if (newParams) {
  178. const scored = database.map(info => totalScorer(info));
  179. const bestListRGB = document.getElementById("best-list-rgb");
  180. bestListRGB.innerHTML = '';
  181. scored
  182. .sort((a, b) => a.scoreRGB - b.scoreRGB)
  183. .slice(0, numPoke)
  184. .forEach(info => {
  185. const li = document.createElement("li");
  186. li.appendChild(renderPokemon(info, { labelClass: "hide", jabClass: "hide" }))
  187. bestListRGB.appendChild(li);
  188. });
  189. const bestListJAB = document.getElementById("best-list-jab");
  190. bestListJAB.innerHTML = '';
  191. scored
  192. .sort((a, b) => a.scoreJAB - b.scoreJAB)
  193. .slice(0, numPoke)
  194. .forEach(info => {
  195. const li = document.createElement("li");
  196. li.appendChild(renderPokemon(info, { labelClass: "hide", rgbClass: "hide" }))
  197. bestListJAB.appendChild(li);
  198. });
  199. }
  200. }
  201. // Lookup by name
  202. if (lastPkmnSearch !== pokemonName || newParams) {
  203. const searchList = document.getElementById("search-list");
  204. searchList.innerHTML = '';
  205. pokemonLookup
  206. .search(pokemonName, { limit: 10 })
  207. // If scoring is impossible, totalScorer will just be identity
  208. .map(({ item }) => totalScorer(item))
  209. .forEach(item => {
  210. const li = document.createElement("li");
  211. li.appendChild(renderPokemon(item))
  212. searchList.appendChild(li);
  213. });
  214. }
  215. lastPkmnSearch = pokemonName;
  216. };
  217. const onRandomColor = () => {
  218. document.getElementById("color-input").value = rgb2hex([Math.random(), Math.random(), Math.random()].map(c => c * 255));
  219. onUpdate();
  220. };