|
@@ -1,9 +1,13 @@
|
|
|
const getSprite = pokemon => `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`
|
|
|
|
|
|
+const titleCase = s => s.charAt(0).toUpperCase() + s.substr(1);
|
|
|
+
|
|
|
const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
|
|
|
|
|
|
const vectorMag = v => Math.sqrt(vectorDot(v, v));
|
|
|
|
|
|
+const pokemonLookup = new Fuse(database, { keys: [ "name" ] });
|
|
|
+
|
|
|
// hex codes already include leading # in these functions
|
|
|
// rgb values are [0, 1] in these functions
|
|
|
const hex2luv = $.colorspaces.converter("hex", "CIELUV");
|
|
@@ -20,6 +24,43 @@ const getNormedScorer = (c, q) => {
|
|
|
};
|
|
|
const getUnnormedScorer = (c, q) => yVec => c * vectorDot(q, yVec);
|
|
|
|
|
|
+// create a tile of a given hex color
|
|
|
+const createTile = hexColor => {
|
|
|
+ const tile = document.createElement("div");
|
|
|
+ tile.setAttribute("class", "color-tile");
|
|
|
+ tile.setAttribute("style", `background-color: ${hexColor};`)
|
|
|
+ tile.textContent = hexColor;
|
|
|
+ return tile;
|
|
|
+}
|
|
|
+
|
|
|
+const createPokemon = ({ name, score, yRGB, yLUV }) => {
|
|
|
+ const img = document.createElement("img");
|
|
|
+ img.setAttribute("src", getSprite(name));
|
|
|
+
|
|
|
+ const titleName = titleCase(name);
|
|
|
+ const text = score ? `${titleName}: ${score.toFixed(3)}` : titleName
|
|
|
+
|
|
|
+ const pkmn = document.createElement("div");
|
|
|
+ pkmn.setAttribute("class", "pokemon");
|
|
|
+ pkmn.appendChild(img);
|
|
|
+ const textSpan = document.createElement("span");
|
|
|
+ textSpan.textContent = text;
|
|
|
+ textSpan.setAttribute("class", "pokemon_text");
|
|
|
+ pkmn.appendChild(textSpan);
|
|
|
+ pkmn.appendChild(createTile(rgb2hex(yRGB.map(x => x / 255))));
|
|
|
+ pkmn.appendChild(createTile(luv2hex(yLUV)));
|
|
|
+ return pkmn;
|
|
|
+}
|
|
|
+
|
|
|
+let lastColorSearch = null;
|
|
|
+let lastPkmnSearch = null;
|
|
|
+
|
|
|
+const paramsChanged = (...args) => {
|
|
|
+ const old = lastColorSearch;
|
|
|
+ lastColorSearch = args;
|
|
|
+ return old === null || old.filter((p, i) => p !== args[i]).length > 0
|
|
|
+}
|
|
|
+
|
|
|
const onUpdate = () => {
|
|
|
// Configuration Loading
|
|
|
const includeX = document.getElementById("include-x")?.checked ?? false;
|
|
@@ -27,7 +68,7 @@ const onUpdate = () => {
|
|
|
const closeCoeff = document.getElementById("close-coeff")?.value ?? 2;
|
|
|
const useRGB = document.getElementById("color-space")?.textContent === "RGB";
|
|
|
const numPoke = document.getElementById("num-poke")?.value ?? 20;
|
|
|
- const pokemonName = document.getElementById("pokemon-name")?.value ?? "";
|
|
|
+ const pokemonName = document.getElementById("pokemon-name")?.value?.toLowerCase() ?? "";
|
|
|
const targetColor = "#" + (document.getElementById("color-input")?.value?.replace("#", "") ?? "FFFFFF");
|
|
|
const targetRGB = hex2rgb(targetColor).map(x => x * 255);
|
|
|
|
|
@@ -39,37 +80,44 @@ const onUpdate = () => {
|
|
|
document.getElementById("close-coeff-display").innerHTML = closeCoeff;
|
|
|
document.getElementById("num-poke-display").textContent = numPoke;
|
|
|
|
|
|
- // calculate luminance to determine if text should be dark or light
|
|
|
- const textColor = vectorDot(targetRGB, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd";
|
|
|
- document.querySelector("body").setAttribute("style", `background: ${targetColor}; color: ${textColor}`);
|
|
|
-
|
|
|
- const bestList = document.getElementById("best-list");
|
|
|
- bestList.innerHTML = ''; // do the lazy thing
|
|
|
-
|
|
|
- // determine metrics from configuration
|
|
|
- const targetInSpace = useRGB ? targetRGB : rgb2luv(targetRGB.map(x => x / 255));
|
|
|
- const xSelector = includeX ? (useRGB ? ({ xRGB }) => xRGB : ({ xLUV }) => xLUV) : () => 0;
|
|
|
- const ySelector = useRGB ? ({ yRGB }) => yRGB : ({ yLUV }) => yLUV;
|
|
|
- const yScorer = (normQY ? getNormedScorer : getUnnormedScorer)(closeCoeff, targetInSpace);
|
|
|
-
|
|
|
- // actually score pokemon
|
|
|
- database
|
|
|
- .map(info => ({ ...info, score: xSelector(info) - yScorer(ySelector(info)) }))
|
|
|
- .sort((a, b) => a.score - b.score)
|
|
|
- .slice(0, numPoke)
|
|
|
- .forEach(({ name, score, yRGB }) => {
|
|
|
- const li = document.createElement("li");
|
|
|
- const img = document.createElement("img");
|
|
|
- const tile = document.createElement("div");
|
|
|
- const hexColor = rgb2hex(yRGB.map(x => x / 255));
|
|
|
- tile.setAttribute("style", `width: 25px; height: 25px; background-color: ${hexColor}`)
|
|
|
- img.setAttribute("src", getSprite(name));
|
|
|
- li.appendChild(img)
|
|
|
- li.appendChild(document.createTextNode(`${name}: ${score.toFixed(3)}`));
|
|
|
- li.appendChild(tile)
|
|
|
- li.setAttribute("style", "display: flex; flex-flow: row nowrap; justify-content: space-between; width: 320px")
|
|
|
- bestList.appendChild(li);
|
|
|
- });
|
|
|
+ if (targetColor.length === 7 && paramsChanged(includeX, normQY, closeCoeff, useRGB, numPoke, targetColor)) {
|
|
|
+ // calculate luminance to determine if text should be dark or light
|
|
|
+ const textColor = vectorDot(targetRGB, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd";
|
|
|
+ document.querySelector("body").setAttribute("style", `background: ${targetColor}; color: ${textColor}`);
|
|
|
+
|
|
|
+ const bestList = document.getElementById("best-list");
|
|
|
+ bestList.innerHTML = ''; // do the lazy thing
|
|
|
+
|
|
|
+ // determine metrics from configuration
|
|
|
+ const targetInSpace = useRGB ? targetRGB : rgb2luv(targetRGB.map(x => x / 255));
|
|
|
+ const xSelector = includeX ? (useRGB ? ({ xRGB }) => xRGB : ({ xLUV }) => xLUV) : () => 0;
|
|
|
+ const ySelector = useRGB ? ({ yRGB }) => yRGB : ({ yLUV }) => yLUV;
|
|
|
+ const yScorer = (normQY ? getNormedScorer : getUnnormedScorer)(closeCoeff, targetInSpace);
|
|
|
+
|
|
|
+ // actually score pokemon
|
|
|
+ database
|
|
|
+ .map(info => ({ ...info, score: xSelector(info) - yScorer(ySelector(info)) }))
|
|
|
+ .sort((a, b) => a.score - b.score)
|
|
|
+ .slice(0, numPoke)
|
|
|
+ .forEach(info => {
|
|
|
+ const li = document.createElement("li");
|
|
|
+ li.appendChild(createPokemon(info))
|
|
|
+ bestList.appendChild(li);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (pokemonName.length > 0 && lastPkmnSearch !== pokemonName) {
|
|
|
+ lastPkmnSearch = pokemonName;
|
|
|
+ // lookup by pokemon too
|
|
|
+ const searchList = document.getElementById("search-list");
|
|
|
+ searchList.innerHTML = '';
|
|
|
+ pokemonLookup.search(pokemonName, { limit: 10 })
|
|
|
+ .forEach(({ item }) => {
|
|
|
+ const li = document.createElement("li");
|
|
|
+ li.appendChild(createPokemon(item))
|
|
|
+ searchList.appendChild(li);
|
|
|
+ });
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
const onRandomColor = () => {
|
|
@@ -83,5 +131,4 @@ const onToggleSpace = () => {
|
|
|
element.textContent = current === "RGB" ? "CIELUV" : "RGB";
|
|
|
document.getElementById("space-toggle").textContent = `Swap to ${current}`
|
|
|
onUpdate();
|
|
|
-}
|
|
|
-
|
|
|
+};
|