|
@@ -13,6 +13,7 @@ const getLimitDisplayNode = () => document.getElementById("num-poke-display");
|
|
|
const getNameInputNode = () => document.getElementById("pokemon-name");
|
|
|
const getScoreListJABNode = () => document.getElementById("best-list-jab");
|
|
|
const getScoreListRGBNode = () => document.getElementById("best-list-rgb");
|
|
|
+const getSearchSpaceDisplayNode = () => document.getElementById("search-space-display");
|
|
|
const getSearchListNode = () => document.getElementById("search-list");
|
|
|
const getHideableControlNodes = () => document.querySelectorAll(".hideable_control");
|
|
|
const getQJABDisplay = () => document.getElementById("q-vec-jab");
|
|
@@ -42,6 +43,10 @@ const jab2hex = jab => d3.jab(...jab).formatHex();
|
|
|
const rgb2hex = rgb => d3.rgb(...rgb).formatHex();
|
|
|
const jab2hue = ([, a, b]) => rad2deg * Math.atan2(b, a);
|
|
|
const rgb2hue = rgb => d3.hsl(d3.rgb(...rgb)).h || 0;
|
|
|
+const hex2rgb = hex => {
|
|
|
+ const { r, g, b } = d3.color(hex);
|
|
|
+ return [r, g, b];
|
|
|
+};
|
|
|
|
|
|
// Arg Compare
|
|
|
const argComp = comp => ra => ra.map((x, i) => [x, i]).reduce((a, b) => comp(a[0], b[0]) > 0 ? b : a)[1];
|
|
@@ -109,6 +114,7 @@ const state = {
|
|
|
closeCoeff: null,
|
|
|
numPoke: null,
|
|
|
searchTerm: null,
|
|
|
+ searchSpace: null,
|
|
|
targetColor: null,
|
|
|
searchResults: null,
|
|
|
};
|
|
@@ -144,7 +150,7 @@ const metrics = [
|
|
|
// mean angle
|
|
|
(stats, q) => -vectorDot(selectedSummary(stats, q)[0].unit, q.unit),
|
|
|
// mean dist
|
|
|
- (stats, q) => {
|
|
|
+ (stats, q) => {
|
|
|
// TODO I know there's some way to avoid recalculation here but I'm just too lazy right now
|
|
|
const [data, scale] = selectedSummary(stats, q);
|
|
|
return vectorSqDist(data.vector, q.vector) / (state.includeScaleInDist ? scale : 1);
|
|
@@ -153,7 +159,7 @@ const metrics = [
|
|
|
(stats, q) => angleDiff(selectedSummary(stats, q)[0].hue, q.hue),
|
|
|
// custom
|
|
|
(stats, q) => (state.includeX ? stats.inertia : 0) - state.closeCoeff * vectorDot(
|
|
|
- selectedSummary(stats, q)[0][state.normQY ? "unit" : "vector"],
|
|
|
+ selectedSummary(stats, q)[0][state.normQY ? "unit" : "vector"],
|
|
|
state.normQY ? q.unit : q.vector,
|
|
|
),
|
|
|
];
|
|
@@ -163,28 +169,11 @@ const scorePokemon = pkmn => ({
|
|
|
rgb: metrics[state.metric](pkmn.rgbStats, state.targetColor.rgbData),
|
|
|
});
|
|
|
|
|
|
-const calcDisplayMetrics = ({ jabStats, rgbStats }) => {
|
|
|
- // TODO - case on metric and meanArgument to avoid recalculation
|
|
|
- // TODO - is there ever any value to computing these around the selected summary instead?
|
|
|
- // obviously that has no mathematical value, and screws up the sqrts, but maybe?
|
|
|
-
|
|
|
- const cosAngleJAB = vectorDot(state.targetColor.jabData.unit, jabStats.trueMean.unit);
|
|
|
- const yTermJAB = cosAngleJAB * jabStats.trueMean.magnitude * state.targetColor.jabData.magnitude;
|
|
|
-
|
|
|
- const cosAngleRGB = vectorDot(state.targetColor.rgbData.unit, rgbStats.trueMean.unit);
|
|
|
- const yTermRGB = cosAngleRGB * rgbStats.trueMean.magnitude * state.targetColor.rgbData.magnitude;
|
|
|
-
|
|
|
- return {
|
|
|
- stdDevJAB: Math.sqrt(jabStats.inertia - 2 * yTermJAB + state.targetColor.jabData.magSq),
|
|
|
- stdDevRGB: Math.sqrt(rgbStats.inertia - 2 * yTermRGB + state.targetColor.rgbData.magSq),
|
|
|
- angleJAB: rad2deg * Math.acos(cosAngleJAB),
|
|
|
- angleRGB: rad2deg * Math.acos(cosAngleRGB),
|
|
|
- meanDistJAB: vectorDist(state.targetColor.jabData.vector, jabStats.trueMean.vector),
|
|
|
- meanDistRGB: vectorDist(state.targetColor.rgbData.vector, rgbStats.trueMean.vector),
|
|
|
- hueAngleJAB: angleDiff(state.targetColor.jabData.hue, jabStats.trueMean.hue),
|
|
|
- hueAngleRGB: angleDiff(state.targetColor.rgbData.hue, rgbStats.trueMean.hue),
|
|
|
- };
|
|
|
-};
|
|
|
+const calcDisplayMetrics = (meanData, q) => ({
|
|
|
+ theta: rad2deg * Math.acos(vectorDot(q.unit, meanData.unit)),
|
|
|
+ delta: vectorDist(q.vector, meanData.vector),
|
|
|
+ phi: angleDiff(q.hue, meanData.hue),
|
|
|
+});
|
|
|
|
|
|
// Math Rendering
|
|
|
const renderQVec = (q, node, sub) => {
|
|
@@ -231,10 +220,10 @@ const metricText = [
|
|
|
].map(s => muArg => TeXZilla.toMathML(s(muArg)));
|
|
|
|
|
|
const muArgs = [
|
|
|
- "P",
|
|
|
- String.raw`M\left(P\right)`,
|
|
|
- String.raw`m\left(P\right)`,
|
|
|
- String.raw`\alpha\left(P\right)`,
|
|
|
+ "P",
|
|
|
+ String.raw`M\left(P\right)`,
|
|
|
+ String.raw`m\left(P\right)`,
|
|
|
+ String.raw`\alpha\left(P\right)`,
|
|
|
String.raw`\omega\left(P\right)`,
|
|
|
];
|
|
|
|
|
@@ -283,80 +272,120 @@ const getSprite = pokemon => {
|
|
|
return `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`;
|
|
|
};
|
|
|
|
|
|
-const renderPokemon = (data, classes = {}) => {
|
|
|
- const { name, jabStats, rgbStats, scores } = data;
|
|
|
- const { labelClass = "", rgbClass = "", jabClass = "", tileClass = "" } = classes;
|
|
|
- let { resultsClass = "" } = classes;
|
|
|
- let displayMetrics = {};
|
|
|
- if (!state.targetColor) {
|
|
|
- // no color selected need to skip scores
|
|
|
- resultsClass = "hide";
|
|
|
+// TODO make the M m alpha omega labels more visible
|
|
|
+const renderCluster = ({
|
|
|
+ index, big, small, best, worst, pi, theta, delta, phi, hex, vector,
|
|
|
+}) => `
|
|
|
+ <div
|
|
|
+ class="pkmn_tile-cluster"
|
|
|
+ style="grid-area: k${index + 1}; color: ${getContrastingTextColor(hex2rgb(hex))}; background-color: ${hex};"
|
|
|
+ >
|
|
|
+ <div class="pkmn_tile-cluster-top_label" style="grid-area: bigm;">${index === big ? "M" : ""}</div>
|
|
|
+ <div class="pkmn_tile-cluster-top_label" style="grid-area: litm;">${index === small ? "m" : ""}</div>
|
|
|
+ <div class="pkmn_tile-cluster-top_label" style="grid-area: alp;">${index === best ? "α" : ""}</div>
|
|
|
+ <div class="pkmn_tile-cluster-top_label " style="grid-area: omg;">${index === worst ? "ω" : ""}</div>
|
|
|
+ <div class="pkmn_tile-cluster-stat_label" style="grid-area: mu;">μ =</div>
|
|
|
+ <div class="pkmn_tile-cluster-stat_label" style="grid-area: pi;">π =</div>
|
|
|
+ <div class="pkmn_tile-cluster-stat_label" style="grid-area: th;">θ =</div>
|
|
|
+ <div class="pkmn_tile-cluster-stat_label" style="grid-area: dl;">δ =</div>
|
|
|
+ <div class="pkmn_tile-cluster-stat_label" style="grid-area: ph;">ϕ =</div>
|
|
|
+ <div style="grid-area: mux">${hex}</div>
|
|
|
+ <div style="grid-area: muv; justify-self: center;">(${vector})</div>
|
|
|
+ <div style="grid-area: piv">${(pi * 100).toFixed(1)}%</div>
|
|
|
+ <div style="grid-area: thv">${theta.toFixed(2)}°</div>
|
|
|
+ <div style="grid-area: dlv">${delta.toFixed(2)}</div>
|
|
|
+ <div style="grid-area: phv">${phi.toFixed(2)}°</div>
|
|
|
+ </div>
|
|
|
+`;
|
|
|
+
|
|
|
+const getPokemonRenderer = targetList => (name, stats, q, score, vectorDecimals, idPostfix) => {
|
|
|
+ let sigma, metrics, kMeanInfo, kMeanResults;
|
|
|
+ if (q) {
|
|
|
+ sigma = Math.sqrt(stats.inertia - 2 * vectorDot(stats.trueMean.vector, q.vector) + q.magSq)
|
|
|
+ metrics = calcDisplayMetrics(stats.trueMean, q)
|
|
|
+ kMeanInfo = {
|
|
|
+ big: stats.largestCluster,
|
|
|
+ small: stats.smallestCluster,
|
|
|
+ best: getBestKMean(stats, q),
|
|
|
+ worst: getWorstKMean(stats, q), // TODO yeah yeah this is a recalc whatever
|
|
|
+ };
|
|
|
+ kMeanResults = stats.kMeans.map(k => calcDisplayMetrics(k, q));
|
|
|
} else {
|
|
|
- displayMetrics = calcDisplayMetrics(data);
|
|
|
+ // no target color, just do all zeros
|
|
|
+ sigma = 0;
|
|
|
+ metrics = { theta: 0, delta: 0, phi: 0 };
|
|
|
+ kMeanInfo = { big: 0, small: 0, best: 0, worst: 0 };
|
|
|
+ kMeanResults = [ metrics, metrics, metrics ];
|
|
|
}
|
|
|
- const {
|
|
|
- stdDevJAB = 0, stdDevRGB = 0,
|
|
|
- angleJAB = 0, angleRGB = 0,
|
|
|
- meanDistJAB = 0, meanDistRGB = 0,
|
|
|
- hueAngleJAB = 0, hueAngleRGB = 0,
|
|
|
- } = displayMetrics;
|
|
|
-
|
|
|
- const titleName = name.split("-").map(part => part.charAt(0).toUpperCase() + part.substr(1)).join(" ");
|
|
|
- const textHex = getContrastingTextColor(rgbStats.trueMean.vector);
|
|
|
- const rgbVec = rgbStats.trueMean.vector.map(c => c.toFixed()).join(", ");
|
|
|
- const jabVec = jabStats.trueMean.vector.map(c => c.toFixed(1)).join(", ");
|
|
|
-
|
|
|
- // TODO Z dists, Z colors
|
|
|
-
|
|
|
- const pkmn = document.createElement("div");
|
|
|
- pkmn.setAttribute("class", `pokemon_tile ${tileClass}`);
|
|
|
- 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} ${scores?.jab?.toFixed(2) ?? ""} ${scores?.rgb?.toFixed(2) ?? ""}
|
|
|
+ const clusterToggleId = `reveal_clusters-${name}-${idPostfix}`;
|
|
|
+
|
|
|
+ const li = document.createElement("li");
|
|
|
+ li.innerHTML = `
|
|
|
+ <div class="pkmn_tile">
|
|
|
+ <img class="pkmn_tile-img" src="${getSprite(name)}" />
|
|
|
+ <span class="pkmn_tile-name">
|
|
|
+ ${name.split("-").map(part => part.charAt(0).toUpperCase() + part.substr(1)).join(" ")}
|
|
|
</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 ${resultsClass}">
|
|
|
- <span class="${jabClass}">
|
|
|
- (${stdDevJAB.toFixed(2)}, ${angleJAB.toFixed(2)}°, ${meanDistJAB.toFixed(2)}, ${hueAngleJAB.toFixed(2)}°)
|
|
|
- </span>
|
|
|
- <span class="${rgbClass}">
|
|
|
- (${stdDevRGB.toFixed(2)}, ${angleRGB.toFixed(2)}°, ${meanDistRGB.toFixed(2)}, ${hueAngleRGB.toFixed(2)}°)
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div class="pokemon_tile-hex_column">
|
|
|
- <div class="pokemon_tile-hex_color ${jabClass}" style="background-color: ${jabStats.trueMean.hex}; color: ${textHex}">
|
|
|
- <span>${jabStats.trueMean.hex}</span><span class="pokemon_tile-vector">(${jabVec})</span>
|
|
|
+ <div class="pkmn_tile-fn">
|
|
|
+ ${score.toFixed(3)}
|
|
|
+ </div>
|
|
|
+ <input type="checkbox" id="${clusterToggleId}" class="pkmn_tile-reveal_clusters" role="button">
|
|
|
+ <label class="pkmn_tile-reveal_clusters_label" for="${clusterToggleId}">
|
|
|
+ <div class="pkmn_tile-reveal_clusters_label--closed">►</div>
|
|
|
+ <div class="pkmn_tile-reveal_clusters_label--open">▼</div>
|
|
|
+ </label>
|
|
|
+ <div
|
|
|
+ class="pkmn_tile-true_mean"
|
|
|
+ style="color: ${getContrastingTextColor(hex2rgb(stats.trueMean.hex))}; background-color: ${stats.trueMean.hex};"
|
|
|
+ >
|
|
|
+ <div class="pkmn_tile-true_mean-value">
|
|
|
+ <div class="pkmn_tile-true_mean-mu_label">μ =</div>
|
|
|
+ <div class="pkmn_tile-true_mean-mu_hex">${stats.trueMean.hex}</div>
|
|
|
+ <div class="pkmn_tile-true_mean-mu_vec">
|
|
|
+ (${stats.trueMean.vector.map(c => c.toFixed(vectorDecimals)).join(", ")})
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-inertia">
|
|
|
+ 𝖨 = ${stats.inertia.toFixed(2)}
|
|
|
+ </div>
|
|
|
+ <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-stat-sigma">
|
|
|
+ σ = ${sigma.toFixed(2)}
|
|
|
+ </div>
|
|
|
+ <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-stat-theta">
|
|
|
+ θ = ${metrics.theta.toFixed(2)}°
|
|
|
+ </div>
|
|
|
+ <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-stat-delta">
|
|
|
+ δ = ${metrics.delta.toFixed(2)}
|
|
|
</div>
|
|
|
- <div class="pokemon_tile-hex_color ${rgbClass}" style="background-color: ${rgbStats.trueMean.hex}; color: ${textHex}">
|
|
|
- <span>${rgbStats.trueMean.hex}</span><span class="pokemon_tile-vector">(${rgbVec})</span>
|
|
|
+ <div class="pkmn_tile-true_mean-stat pkmn_tile-true_mean-stat-phi">
|
|
|
+ ϕ = ${metrics.phi.toFixed(2)}°
|
|
|
</div>
|
|
|
- </div>
|
|
|
</div>
|
|
|
+ ${stats.kMeans.map((data, index) => renderCluster({
|
|
|
+ index,
|
|
|
+ ...kMeanInfo,
|
|
|
+ vectorDecimals,
|
|
|
+ pi: stats.kWeights[index],
|
|
|
+ ...kMeanResults[index],
|
|
|
+ hex: data.hex,
|
|
|
+ vector: data.vector.map(c => c.toFixed(vectorDecimals)).join(", "),
|
|
|
+ })).join("\n")}
|
|
|
</div>
|
|
|
`;
|
|
|
- return pkmn;
|
|
|
-};
|
|
|
-
|
|
|
-const getPokemonAppender = targetList => (pokemonData, classes) => {
|
|
|
- const li = document.createElement("li");
|
|
|
- li.appendChild(renderPokemon(pokemonData, classes));
|
|
|
targetList.appendChild(li);
|
|
|
};
|
|
|
|
|
|
// Update Search Results
|
|
|
const renderSearch = () => {
|
|
|
const resultsNode = getSearchListNode();
|
|
|
- const append = getPokemonAppender(resultsNode);
|
|
|
+ const append = getPokemonRenderer(resultsNode);
|
|
|
clearNodeContents(resultsNode);
|
|
|
- state.searchResults?.forEach(pkmn => append(pkmn));
|
|
|
+ const argMapper = state.searchSpace === "RGB"
|
|
|
+ ? pkmn => [pkmn.rgbStats, state.targetColor?.rgbData, state.targetColor ? scorePokemon(pkmn).rgb : 0, 2]
|
|
|
+ : pkmn => [pkmn.jabStats, state.targetColor?.jabData, state.targetColor ? scorePokemon(pkmn).jab : 0, 2]
|
|
|
+ state.searchResults?.forEach(pkmn => append(
|
|
|
+ pkmn.name, ...argMapper(pkmn), "search"
|
|
|
+ ));
|
|
|
};
|
|
|
|
|
|
// Scoring
|
|
@@ -369,23 +398,27 @@ const rescore = () => {
|
|
|
const scores = pokemonColorData.map(data => ({ ...data, scores: scorePokemon(data) }));
|
|
|
|
|
|
const jabList = getScoreListJABNode();
|
|
|
- const appendJAB = getPokemonAppender(jabList);
|
|
|
+ const appendJAB = getPokemonRenderer(jabList);
|
|
|
const rgbList = getScoreListRGBNode();
|
|
|
- const appendRGB = getPokemonAppender(rgbList);
|
|
|
+ const appendRGB = getPokemonRenderer(rgbList);
|
|
|
|
|
|
// extract best CIECAM02 results
|
|
|
const bestJAB = scores
|
|
|
.sort((a, b) => a.scores.jab - b.scores.jab)
|
|
|
.slice(0, state.numPoke);
|
|
|
clearNodeContents(jabList);
|
|
|
- bestJAB.forEach(data => appendJAB(data, { labelClass: "hide", rgbClass: "hide", tileClass: "pokemon_tile--smaller" }));
|
|
|
+ bestJAB.forEach(data => appendJAB(
|
|
|
+ data.name, data.jabStats, state.targetColor.jabData, data.scores.jab, 2, "jab"
|
|
|
+ ));
|
|
|
|
|
|
// extract best RGB results
|
|
|
const bestRGB = scores
|
|
|
.sort((a, b) => a.scores.rgb - b.scores.rgb)
|
|
|
.slice(0, state.numPoke);
|
|
|
clearNodeContents(rgbList);
|
|
|
- bestRGB.forEach(data => appendRGB(data, { labelClass: "hide", jabClass: "hide", tileClass: "pokemon_tile--smaller" }));
|
|
|
+ bestRGB.forEach(data => appendRGB(
|
|
|
+ data.name, data.rgbStats, state.targetColor.rgbData, data.scores.rgb, 2, "rgb"
|
|
|
+ ));
|
|
|
|
|
|
// update the rendered search results as well
|
|
|
renderSearch();
|
|
@@ -515,6 +548,13 @@ const onSearchChanged = () => {
|
|
|
renderSearch();
|
|
|
};
|
|
|
|
|
|
+const onSearchSpaceChanged = () => {
|
|
|
+ const old = state.searchSpace ?? "Jab";
|
|
|
+ state.searchSpace = old === "RGB" ? "Jab" : "RGB";
|
|
|
+ getSearchSpaceDisplayNode().textContent = old;
|
|
|
+ renderSearch();
|
|
|
+};
|
|
|
+
|
|
|
const onRandomPokemon = () => {
|
|
|
getNameInputNode().value = "";
|
|
|
state.searchResults = Array.from({ length: 10 }, () => pokemonColorData[Math.floor(Math.random() * pokemonColorData.length)]);
|