const stripForm = ["flabebe", "floette", "florges", "vivillon", "basculin", "furfrou", "magearna"]; const getSprite = pokemon => { pokemon = pokemon .replace("-alola", "-alolan") .replace("-galar", "-galarian") .replace("darmanitan-galarian", "darmanitan-galarian-standard"); if (stripForm.find(s => pokemon.includes(s))) { pokemon = pokemon.replace(/-.*$/, ""); } return `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 luv2hex = $.colorspaces.converter("CIELUV", "hex"); const rgb2luv = $.colorspaces.converter("sRGB", "CIELUV"); const rgb2hex = $.colorspaces.converter("sRGB", "hex"); const hex2rgb = $.colorspaces.converter("hex", "sRGB"); // scoring functions const getNormedScorer = (c, q) => { const factor = c / vectorMag(q); return yVec => factor * vectorDot(q, yVec) / vectorMag(yVec); }; 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 = (event) => { if (event) { event.preventDefault(); } // Configuration Loading const includeX = document.getElementById("include-x")?.checked ?? false; const normQY = document.getElementById("norm-q-y")?.checked ?? false; 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?.toLowerCase() ?? ""; const targetColor = "#" + (document.getElementById("color-input")?.value?.replace("#", "") ?? "FFFFFF"); const targetRGB = hex2rgb(targetColor).map(x => x * 255); // Update display values document.getElementById("x-term").textContent = includeX ? "X(P)" : ""; document.getElementById("c-value").textContent = closeCoeff; document.getElementById("q-vec").textContent = normQY ? "q̂" : "q"; document.getElementById("y-vec").textContent = normQY ? "Ŷ(P)" : "Y(P)"; document.getElementById("close-coeff-display").innerHTML = closeCoeff; document.getElementById("num-poke-display").textContent = numPoke; // 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); const totalScorer = info => xSelector(info) - yScorer(ySelector(info)); const newParams = paramsChanged(includeX, normQY, closeCoeff, useRGB, numPoke, targetColor); if (targetColor.length === 7 && newParams) { // 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 // actually score pokemon database .map(info => ({ ...info, score: totalScorer(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 || newParams)) { lastPkmnSearch = pokemonName; // lookup by pokemon too const searchList = document.getElementById("search-list"); searchList.innerHTML = ''; pokemonLookup .search(pokemonName, { limit: 15 }) .map(({ item }) => ({ ...item, score: totalScorer(item) })) .forEach(item => { const li = document.createElement("li"); li.appendChild(createPokemon(item)) searchList.appendChild(li); }); } }; const onRandomColor = () => { document.getElementById("color-input").value = rgb2hex([Math.random(), Math.random(), Math.random()]); onUpdate(); }; const onToggleSpace = () => { const element = document.getElementById("color-space"); const current = element?.textContent; element.textContent = current === "RGB" ? "CIELUV" : "RGB"; document.getElementById("space-toggle").textContent = `Swap to ${current}` onUpdate(); };