123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- 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 getContrastingTextColor = rgb => vectorDot(rgb, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd"
- const pokemonLookup = new Fuse(database, { keys: [ "name" ] });
- // hex codes already include leading # in these functions
- // rgb values are [0, 255] in these functions
- const jab2hex = jab => d3.jab(...jab).formatHex();
- const rgb2hex = rgb => d3.rgb(...rgb).formatHex();
- const rgb2jab = rgb => {
- const { J, a, b } = d3.jab(d3.rgb(...rgb));
- return [ J, a, b ];
- }
- const hex2rgb = hex => {
- const { r, g, b } = d3.rgb(hex);
- return [ r, g, b ];
- };
- // scoring functions
- const getNormedScorer = (c, qRGB, qJAB) => {
- const fRGB = c / vectorMag(qRGB);
- const fJAB = c / vectorMag(qJAB);
- return ({ yRGB, yJAB }) => ({
- scoreRGB: fRGB * vectorDot(qRGB, yRGB) / vectorMag(yRGB),
- scoreJAB: fJAB * vectorDot(qJAB, yJAB) / vectorMag(yJAB),
- });
- };
- const getUnnormedScorer = (c, qRGB, qJAB) => ({ yRGB, yJAB }) => ({
- scoreRGB: c * vectorDot(qRGB, yRGB),
- scoreJAB: c * vectorDot(qJAB, yJAB),
- });
- // 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 renderPokemon = (
- { name, scoreRGB = null, scoreJAB = null, yRGB, yJAB },
- { labelClass = "", rgbClass = "", jabClass = "" } = {},
- ) => {
- const titleName = titleCase(name);
- const rgbHex = rgb2hex(yRGB);
- const jabHex = jab2hex(yJAB);
- const textHex = getContrastingTextColor(yRGB);
- const rgbVec = yRGB.map(c => c.toFixed()).join(", ")
- const jabVec = yJAB.map(c => c.toFixed(2)).join(", ")
- const scoreClass = scoreRGB === null || scoreJAB === null ? "hide" : "";
-
- const pkmn = document.createElement("div");
- pkmn.setAttribute("class", "pokemon_tile");
- pkmn.innerHTML = `
- <div class="pokemon_tile-image-wrapper">
- <img src="${getSprite(name)}" />
- </div>
- <div class="pokemon_tile-info_panel">
- <span class="pokemon_tile-pokemon_name">${titleName}</span>
- <div class="pokemon_tile-results">
- <div class="pokemon_tile-labels ${labelClass}">
- <span class="${jabClass}">Jab: </span>
- <span class="${rgbClass}">RGB: </span>
- </div>
- <div class="pokemon_tile-score_column ${scoreClass}">
- <span class="pokemon_tile-no_flex ${jabClass}">
- ${scoreJAB?.toFixed(2)}
- </span>
- <span class="pokemon_tile-no_flex ${rgbClass}">
- ${scoreRGB?.toFixed(2)}
- </span>
- </div>
- <div class="pokemon_tile-hex_column">
- <div class="pokemon_tile-hex_color ${jabClass}" style="background-color: ${jabHex}; color: ${textHex}">
- ${jabHex}
- </div>
- <div class="pokemon_tile-hex_color ${rgbClass}" style="background-color: ${rgbHex}; color: ${textHex}">
- ${rgbHex}
- </div>
- </div>
- <div class="pokemon_tile-vector_column">
- <span class="pokemon_tile-no_flex ${rgbClass}">(${rgbVec})</span>
- <span class="pokemon_tile-no_flex ${jabClass}">(${jabVec})</span>
- </div>
- </div>
- </div>
- `;
- 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 renderVec = math => `\\vec{${math.charAt(0)}}${math.substr(1)}`;
- const renderNorm = vec => `\\frac{${vec}}{\\left|\\left|${vec}\\right|\\right|}`;
- const renderMath = (includeX, normQY, closeCoeff) => {
- const xTerm = includeX ? "X\\left(P\\right)" : "";
- const qyMod = normQY ? c => renderNorm(renderVec(c)) : renderVec;
- return TeXZilla.toMathML(`\\arg\\min_{P}\\left[${xTerm}-${closeCoeff}${qyMod("q")}\\cdot ${qyMod("Y\\left(P\\right)")}\\right]`);
- }
- const renderQVec = (q, id, sub) => {
- document.getElementById(id).innerHTML = TeXZilla.toMathMLString(`\\vec{q}_{\\text{${sub}}} = \\left(${q.join(", ")}\\right)`);
- }
- const changePageColors = color => {
- // calculate luminance to determine if text should be dark or light
- const textColor = getContrastingTextColor([color.r, color.g, color.b]);
- document.querySelector("body").setAttribute("style", `background: ${color.formatHex()}; color: ${textColor}`);
- }
- const readColor = rgb => {
- const { J, a, b } = d3.jab(rgb);
- return [[rgb.r, rgb.g, rgb.b], [J, a, b]];
- }
- 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 colorInput = "#" + (document.getElementById("color-input")?.value?.replace("#", "") ?? "FFFFFF");
- // Check if parameters have changed
- const newParams = paramsChanged(includeX, normQY, closeCoeff, useRGB, numPoke, colorInput);
- if (newParams) {
- // Update display values
- document.getElementById("close-coeff-display").innerHTML = closeCoeff;
- document.getElementById("num-poke-display").textContent = numPoke;
- const objFnElem = document.getElementById("obj-fn");
- objFnElem.innerHTML = "";
- objFnElem.appendChild(renderMath(includeX, normQY, closeCoeff));
- }
- // Only modified if current color is valid
- let totalScorer = info => info;
- // Lookup by color
- if (colorInput.length === 7) {
- // Convert input color
- const targetColor = d3.color(colorInput);
- const [ targetRGB, targetJAB ] = readColor(targetColor);
- // Update the color display
- changePageColors(targetColor);
- renderQVec(targetRGB.map(c => c.toFixed()), "q-vec-rgb", "RGB");
- renderQVec(targetJAB.map(c => c.toFixed(2)), "q-vec-jab", "Jab");
- // Determine metrics from configuration
- const xSelector = includeX ? ({ xRGB, xJAB }) => [ xRGB, xJAB ] : () => [ 0, 0 ];
- const yScorer = (normQY ? getNormedScorer : getUnnormedScorer)(closeCoeff, targetRGB, targetJAB);
-
- // Set the scoring function
- totalScorer = info => {
- const [ xRGB, xJAB ] = xSelector(info);
- const { scoreRGB, scoreJAB } = yScorer(info);
- return {
- ...info,
- scoreRGB: xRGB - scoreRGB,
- scoreJAB: xJAB - scoreJAB,
- }
- };
- // Rescore Pokemon and update lists if config has changed
- if (newParams) {
- const scored = database.map(info => totalScorer(info));
-
- const bestListRGB = document.getElementById("best-list-rgb");
- bestListRGB.innerHTML = '';
- scored
- .sort((a, b) => a.scoreRGB - b.scoreRGB)
- .slice(0, numPoke)
- .forEach(info => {
- const li = document.createElement("li");
- li.appendChild(renderPokemon(info, { labelClass: "hide", jabClass: "hide" }))
- bestListRGB.appendChild(li);
- });
-
- const bestListJAB = document.getElementById("best-list-jab");
- bestListJAB.innerHTML = '';
- scored
- .sort((a, b) => a.scoreJAB - b.scoreJAB)
- .slice(0, numPoke)
- .forEach(info => {
- const li = document.createElement("li");
- li.appendChild(renderPokemon(info, { labelClass: "hide", rgbClass: "hide" }))
- bestListJAB.appendChild(li);
- });
- }
- }
- // Lookup by name
- const searchList = document.getElementById("search-list");
- searchList.innerHTML = '';
- if (pokemonName.length > 0 && (lastPkmnSearch !== pokemonName || newParams)) {
- pokemonLookup
- .search(pokemonName, { limit: 10 })
- // If scoring is impossible, totalScorer will just be identity
- .map(({ item }) => totalScorer(item))
- .forEach(item => {
- const li = document.createElement("li");
- li.appendChild(renderPokemon(item))
- searchList.appendChild(li);
- });
- }
- lastPkmnSearch = pokemonName;
- };
- const onRandomColor = () => {
- document.getElementById("color-input").value = rgb2hex([Math.random(), Math.random(), Math.random()].map(c => c * 255));
- onUpdate();
- };
|