nearest.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. const getSprite = pokemon => `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`
  2. const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
  3. const vectorMag = v => Math.sqrt(vectorDot(v, v));
  4. // hex codes already include leading # in these functions
  5. // rgb values are [0, 1] in these functions
  6. const hex2luv = $.colorspaces.converter("hex", "CIELUV");
  7. const luv2hex = $.colorspaces.converter("CIELUV", "hex");
  8. const rgb2luv = $.colorspaces.converter("sRGB", "CIELUV");
  9. const luv2rgb = $.colorspaces.converter("CIELUV", "sRGB");
  10. const rgb2hex = $.colorspaces.converter("sRGB", "hex");
  11. const hex2rgb = $.colorspaces.converter("hex", "sRGB");
  12. // scoring functions
  13. const getNormedScorer = (c, q) => {
  14. const factor = c / vectorMag(q);
  15. return yVec => factor * vectorDot(q, yVec) / vectorMag(yVec);
  16. };
  17. const getUnnormedScorer = (c, q) => yVec => c * vectorDot(q, yVec);
  18. const onUpdate = () => {
  19. // Configuration Loading
  20. const includeX = document.getElementById("include-x")?.checked ?? false;
  21. const normQY = document.getElementById("norm-q-y")?.checked ?? false;
  22. const closeCoeff = document.getElementById("close-coeff")?.value ?? 2;
  23. const useRGB = document.getElementById("color-space")?.textContent === "RGB";
  24. const numPoke = document.getElementById("num-poke")?.value ?? 20;
  25. const pokemonName = document.getElementById("pokemon-name")?.value ?? "";
  26. const targetColor = "#" + (document.getElementById("color-input")?.value?.replace("#", "") ?? "FFFFFF");
  27. const targetRGB = hex2rgb(targetColor).map(x => x * 255);
  28. // Update display values
  29. document.getElementById("x-term").textContent = includeX ? "X(P)" : "";
  30. document.getElementById("c-value").textContent = closeCoeff;
  31. document.getElementById("q-vec").textContent = normQY ? "q̂" : "q";
  32. document.getElementById("y-vec").textContent = normQY ? "Ŷ(P)" : "Y(P)";
  33. document.getElementById("close-coeff-display").innerHTML = closeCoeff;
  34. document.getElementById("num-poke-display").textContent = numPoke;
  35. // calculate luminance to determine if text should be dark or light
  36. const textColor = vectorDot(targetRGB, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd";
  37. document.querySelector("body").setAttribute("style", `background: ${targetColor}; color: ${textColor}`);
  38. const bestList = document.getElementById("best-list");
  39. bestList.innerHTML = ''; // do the lazy thing
  40. // determine metrics from configuration
  41. const targetInSpace = useRGB ? targetRGB : rgb2luv(targetRGB.map(x => x / 255));
  42. const xSelector = includeX ? (useRGB ? ({ xRGB }) => xRGB : ({ xLUV }) => xLUV) : () => 0;
  43. const ySelector = useRGB ? ({ yRGB }) => yRGB : ({ yLUV }) => yLUV;
  44. const yScorer = (normQY ? getNormedScorer : getUnnormedScorer)(closeCoeff, targetInSpace);
  45. // actually score pokemon
  46. database
  47. .map(info => ({ ...info, score: xSelector(info) - yScorer(ySelector(info)) }))
  48. .sort((a, b) => a.score - b.score)
  49. .slice(0, numPoke)
  50. .forEach(({ name, score, yRGB }) => {
  51. const li = document.createElement("li");
  52. const img = document.createElement("img");
  53. const tile = document.createElement("div");
  54. const hexColor = rgb2hex(yRGB.map(x => x / 255));
  55. tile.setAttribute("style", `width: 25px; height: 25px; background-color: ${hexColor}`)
  56. img.setAttribute("src", getSprite(name));
  57. li.appendChild(img)
  58. li.appendChild(document.createTextNode(`${name}: ${score.toFixed(3)}`));
  59. li.appendChild(tile)
  60. li.setAttribute("style", "display: flex; flex-flow: row nowrap; justify-content: space-between; width: 320px")
  61. bestList.appendChild(li);
  62. });
  63. };
  64. const onRandomColor = () => {
  65. document.getElementById("color-input").value = rgb2hex([Math.random(), Math.random(), Math.random()]);
  66. onUpdate();
  67. };
  68. const onToggleSpace = () => {
  69. const element = document.getElementById("color-space");
  70. const current = element?.textContent;
  71. element.textContent = current === "RGB" ? "CIELUV" : "RGB";
  72. document.getElementById("space-toggle").textContent = `Swap to ${current}`
  73. onUpdate();
  74. }