|
@@ -17,6 +17,8 @@ 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
|
|
@@ -33,11 +35,18 @@ const hex2rgb = hex => {
|
|
|
};
|
|
|
|
|
|
// scoring functions
|
|
|
-const getNormedScorer = (c, q) => {
|
|
|
- const factor = c / vectorMag(q);
|
|
|
- return yVec => factor * vectorDot(q, yVec) / vectorMag(yVec);
|
|
|
+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, q) => yVec => c * vectorDot(q, yVec);
|
|
|
+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 => {
|
|
@@ -48,22 +57,52 @@ const createTile = hexColor => {
|
|
|
return tile;
|
|
|
}
|
|
|
|
|
|
-const createPokemon = ({ name, score, yRGB, yJAB }) => {
|
|
|
- const img = document.createElement("img");
|
|
|
- img.setAttribute("src", getSprite(name));
|
|
|
-
|
|
|
+const renderPokemon = (
|
|
|
+ { name, scoreRGB = null, scoreJAB = null, yRGB, yJAB },
|
|
|
+ { rgbClass = "", jabClass = "" } = {},
|
|
|
+) => {
|
|
|
const titleName = titleCase(name);
|
|
|
- const text = score ? `${titleName}: ${score.toFixed(3)}` : titleName
|
|
|
-
|
|
|
+ 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");
|
|
|
- pkmn.appendChild(img);
|
|
|
- const textSpan = document.createElement("span");
|
|
|
- textSpan.textContent = text;
|
|
|
- textSpan.setAttribute("class", "pokemon_text");
|
|
|
- pkmn.appendChild(textSpan);
|
|
|
- pkmn.appendChild(createTile(rgb2hex(yRGB)));
|
|
|
- pkmn.appendChild(createTile(jab2hex(yJAB)));
|
|
|
+ pkmn.setAttribute("class", "pokemon_tile");
|
|
|
+ pkmn.innerHTML = `
|
|
|
+ <img src="${getSprite(name)}" />
|
|
|
+ <div class="pokemon_tile-info_panel">
|
|
|
+ <span class="pokemon_tile-pokemon_name">${titleName}</span>
|
|
|
+ <div class="pokemon_tile-results">
|
|
|
+ <div class="pokemon_tile-labels">
|
|
|
+ <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;
|
|
|
}
|
|
|
|
|
@@ -80,10 +119,21 @@ const renderVec = math => `\\vec{${math.charAt(0)}}${math.substr(1)}`;
|
|
|
|
|
|
const renderNorm = vec => `\\frac{${vec}}{\\left|\\left|${vec}\\right|\\right|}`;
|
|
|
|
|
|
-const renderMath = (includeX, normQY) => {
|
|
|
+const renderMath = (includeX, normQY, closeCoeff) => {
|
|
|
const xTerm = includeX ? "X\\left(P\\right)" : "";
|
|
|
const qyMod = normQY ? c => renderNorm(renderVec(c)) : renderVec;
|
|
|
- return TeXZilla.toMathML(`${xTerm}-2${qyMod("q")}\\cdot ${qyMod("Y\\left(P\\right)")}`);
|
|
|
+ return TeXZilla.toMathML(`${xTerm}-${closeCoeff}${qyMod("q")}\\cdot ${qyMod("Y\\left(P\\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) => {
|
|
@@ -98,60 +148,93 @@ const onUpdate = (event) => {
|
|
|
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);
|
|
|
-
|
|
|
- // Update display values
|
|
|
- // document.getElementById("x-term").textContent = includeX ? "X(P)" : "";
|
|
|
- // document.getElementById("c-value").textContent = closeCoeff;
|
|
|
- // document.getElementById("q-vec").innerHTML = normQY ? "<mover><m" : "q";
|
|
|
- // document.getElementById("y-vec").textContent = normQY ? "Ŷ(P)" : "Y(P)";
|
|
|
- // 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));
|
|
|
+ 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);
|
|
|
+ // TODO render q vectors somewhere
|
|
|
+
|
|
|
+ // 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));
|
|
|
|
|
|
- // determine metrics from configuration
|
|
|
- const targetInSpace = useRGB ? targetRGB : rgb2jab(targetRGB);
|
|
|
- const xSelector = includeX ? (useRGB ? ({ xRGB }) => xRGB : ({ xJAB }) => xJAB) : () => 0;
|
|
|
- const ySelector = useRGB ? ({ yRGB }) => yRGB : ({ yJAB }) => yJAB;
|
|
|
- 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
|
|
|
+ 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, { jabClass: "hide" }))
|
|
|
+ bestListRGB.appendChild(li);
|
|
|
+ });
|
|
|
|
|
|
- // 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);
|
|
|
- });
|
|
|
+ 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, { rgbClass: "hide" }))
|
|
|
+ bestListJAB.appendChild(li);
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- if (pokemonName.length > 0 && (lastPkmnSearch !== pokemonName || newParams)) {
|
|
|
+ // Lookup by name
|
|
|
+ if (pokemonName.length === 0) {
|
|
|
+ const searchList = document.getElementById("search-list");
|
|
|
+ searchList.innerHTML = '';
|
|
|
+ } else if (lastPkmnSearch !== pokemonName || newParams) {
|
|
|
+ // Update last search
|
|
|
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) }))
|
|
|
+ .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(createPokemon(item))
|
|
|
+ li.appendChild(renderPokemon(item))
|
|
|
searchList.appendChild(li);
|
|
|
});
|
|
|
}
|
|
@@ -161,11 +244,3 @@ const onRandomColor = () => {
|
|
|
document.getElementById("color-input").value = rgb2hex([Math.random(), Math.random(), Math.random()].map(c => c * 255));
|
|
|
onUpdate();
|
|
|
};
|
|
|
-
|
|
|
-const onToggleSpace = () => {
|
|
|
- const element = document.getElementById("color-space");
|
|
|
- const current = element?.textContent;
|
|
|
- element.textContent = current === "RGB" ? "CAM02-UCS" : "RGB";
|
|
|
- document.getElementById("space-toggle").textContent = `Swap to ${current}`
|
|
|
- onUpdate();
|
|
|
-};
|