nearest.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. const getSprite = pokemon => `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`
  2. const titleCase = s => s.charAt(0).toUpperCase() + s.substr(1);
  3. const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
  4. const vectorMag = v => Math.sqrt(vectorDot(v, v));
  5. const pokemonLookup = new Fuse(database, { keys: [ "name" ] });
  6. // hex codes already include leading # in these functions
  7. // rgb values are [0, 1] in these functions
  8. const luv2hex = $.colorspaces.converter("CIELUV", "hex");
  9. const rgb2luv = $.colorspaces.converter("sRGB", "CIELUV");
  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. // create a tile of a given hex color
  19. const createTile = hexColor => {
  20. const tile = document.createElement("div");
  21. tile.setAttribute("class", "color-tile");
  22. tile.setAttribute("style", `background-color: ${hexColor};`)
  23. tile.textContent = hexColor;
  24. return tile;
  25. }
  26. const createPokemon = ({ name, score, yRGB, yLUV }) => {
  27. const img = document.createElement("img");
  28. img.setAttribute("src", getSprite(name));
  29. const titleName = titleCase(name);
  30. const text = score ? `${titleName}: ${score.toFixed(3)}` : titleName
  31. const pkmn = document.createElement("div");
  32. pkmn.setAttribute("class", "pokemon");
  33. pkmn.appendChild(img);
  34. const textSpan = document.createElement("span");
  35. textSpan.textContent = text;
  36. textSpan.setAttribute("class", "pokemon_text");
  37. pkmn.appendChild(textSpan);
  38. pkmn.appendChild(createTile(rgb2hex(yRGB.map(x => x / 255))));
  39. pkmn.appendChild(createTile(luv2hex(yLUV)));
  40. return pkmn;
  41. }
  42. let lastColorSearch = null;
  43. let lastPkmnSearch = null;
  44. const paramsChanged = (...args) => {
  45. const old = lastColorSearch;
  46. lastColorSearch = args;
  47. return old === null || old.filter((p, i) => p !== args[i]).length > 0
  48. }
  49. const onUpdate = () => {
  50. // Configuration Loading
  51. const includeX = document.getElementById("include-x")?.checked ?? false;
  52. const normQY = document.getElementById("norm-q-y")?.checked ?? false;
  53. const closeCoeff = document.getElementById("close-coeff")?.value ?? 2;
  54. const useRGB = document.getElementById("color-space")?.textContent === "RGB";
  55. const numPoke = document.getElementById("num-poke")?.value ?? 20;
  56. const pokemonName = document.getElementById("pokemon-name")?.value?.toLowerCase() ?? "";
  57. const targetColor = "#" + (document.getElementById("color-input")?.value?.replace("#", "") ?? "FFFFFF");
  58. const targetRGB = hex2rgb(targetColor).map(x => x * 255);
  59. // Update display values
  60. document.getElementById("x-term").textContent = includeX ? "X(P)" : "";
  61. document.getElementById("c-value").textContent = closeCoeff;
  62. document.getElementById("q-vec").textContent = normQY ? "q̂" : "q";
  63. document.getElementById("y-vec").textContent = normQY ? "Ŷ(P)" : "Y(P)";
  64. document.getElementById("close-coeff-display").innerHTML = closeCoeff;
  65. document.getElementById("num-poke-display").textContent = numPoke;
  66. // determine metrics from configuration
  67. const targetInSpace = useRGB ? targetRGB : rgb2luv(targetRGB.map(x => x / 255));
  68. const xSelector = includeX ? (useRGB ? ({ xRGB }) => xRGB : ({ xLUV }) => xLUV) : () => 0;
  69. const ySelector = useRGB ? ({ yRGB }) => yRGB : ({ yLUV }) => yLUV;
  70. const yScorer = (normQY ? getNormedScorer : getUnnormedScorer)(closeCoeff, targetInSpace);
  71. const totalScorer = info => xSelector(info) - yScorer(ySelector(info));
  72. const newParams = paramsChanged(includeX, normQY, closeCoeff, useRGB, numPoke, targetColor);
  73. if (targetColor.length === 7 && newParams) {
  74. // calculate luminance to determine if text should be dark or light
  75. const textColor = vectorDot(targetRGB, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd";
  76. document.querySelector("body").setAttribute("style", `background: ${targetColor}; color: ${textColor}`);
  77. const bestList = document.getElementById("best-list");
  78. bestList.innerHTML = ''; // do the lazy thing
  79. // actually score pokemon
  80. database
  81. .map(info => ({ ...info, score: totalScorer(info) }))
  82. .sort((a, b) => a.score - b.score)
  83. .slice(0, numPoke)
  84. .forEach(info => {
  85. const li = document.createElement("li");
  86. li.appendChild(createPokemon(info))
  87. bestList.appendChild(li);
  88. });
  89. }
  90. if (pokemonName.length > 0 && (lastPkmnSearch !== pokemonName || newParams)) {
  91. lastPkmnSearch = pokemonName;
  92. // lookup by pokemon too
  93. const searchList = document.getElementById("search-list");
  94. searchList.innerHTML = '';
  95. pokemonLookup.search(pokemonName, { limit: 10 })
  96. .map(({ item }) => ({ ...item, score: totalScorer(item) }))
  97. .forEach(item => {
  98. const li = document.createElement("li");
  99. li.appendChild(createPokemon(item))
  100. searchList.appendChild(li);
  101. });
  102. }
  103. };
  104. const onRandomColor = () => {
  105. document.getElementById("color-input").value = rgb2hex([Math.random(), Math.random(), Math.random()]);
  106. onUpdate();
  107. };
  108. const onToggleSpace = () => {
  109. const element = document.getElementById("color-space");
  110. const current = element?.textContent;
  111. element.textContent = current === "RGB" ? "CIELUV" : "RGB";
  112. document.getElementById("space-toggle").textContent = `Swap to ${current}`
  113. onUpdate();
  114. };