|
@@ -67,7 +67,7 @@ const readColorInput = () => {
|
|
|
const qRGB = [ rgb.r, rgb.g, rgb.b ];
|
|
|
|
|
|
const [ qJABNorm, qJABHat ] = vectorNorm(qJAB);
|
|
|
- const qJABNormSq = qJABNorm * qJABNorm;
|
|
|
+ const qJABNormSq = qJABNorm * qJABNorm;
|
|
|
const [ _, qChromaHat ] = vectorNorm(qJAB.slice(1));
|
|
|
|
|
|
const [ qRGBNorm, qRGBHat ] = vectorNorm(qRGB);
|
|
@@ -90,15 +90,16 @@ const state = {
|
|
|
numPoke: null,
|
|
|
searchTerm: null,
|
|
|
targetColor: null,
|
|
|
+ searchResults: null,
|
|
|
};
|
|
|
|
|
|
// Metrics
|
|
|
const scoringMetrics = [
|
|
|
- ({ xJAB, xRGB, yJAB, yRGB }) => [
|
|
|
+ ({ xJAB, xRGB, yJAB, yRGB }) => [
|
|
|
xJAB - 2 * vectorDot(yJAB, state.targetColor.qJAB),
|
|
|
xRGB - 2 * vectorDot(yRGB, state.targetColor.qRGB),
|
|
|
],
|
|
|
- ({ yJABHat, yRGBHat }) => [
|
|
|
+ ({ yJABHat, yRGBHat }) => [
|
|
|
-vectorDot(yJABHat, state.targetColor.qJABHat),
|
|
|
-vectorDot(yRGBHat, state.targetColor.qRGBHat),
|
|
|
],
|
|
@@ -106,13 +107,13 @@ const scoringMetrics = [
|
|
|
acosDeg(vectorDot(state.targetColor.qChromaHat, yChromaHat)),
|
|
|
angleDiff(state.targetColor.qHueAngle, yHueAngle),
|
|
|
],
|
|
|
- ({ xJAB, xRGB, yJAB, yRGB, yJABHat, yRGBHat }) => [
|
|
|
+ ({ xJAB, xRGB, yJAB, yRGB, yJABHat, yRGBHat }) => [
|
|
|
(state.includeX ? xJAB : 0) - state.closeCoeff * vectorDot(
|
|
|
- state.normQY ? yJABHat : yJAB,
|
|
|
+ state.normQY ? yJABHat : yJAB,
|
|
|
state.normQY ? state.targetColor.qJABHat : state.targetColor.qJAB
|
|
|
),
|
|
|
(state.includeX ? xRGB : 0) - state.closeCoeff * vectorDot(
|
|
|
- state.normQY ? yRGBHat : yRGB,
|
|
|
+ state.normQY ? yRGBHat : yRGB,
|
|
|
state.normQY ? state.targetColor.qRGBHat : state.targetColor.qRGB
|
|
|
),
|
|
|
]
|
|
@@ -132,8 +133,8 @@ const calcDisplayMetrics = ({
|
|
|
return {
|
|
|
stdDevRGB: Math.sqrt(xRGB - 2 * yTermRGB + state.targetColor.qRGBNormSq),
|
|
|
stdDevJAB: Math.sqrt(xJAB - 2 * yTermJAB + state.targetColor.qJABNormSq),
|
|
|
- angleJAB: acosDeg(cosAngleJAB),
|
|
|
- angleRGB: acosDeg(cosAngleRGB),
|
|
|
+ angleJAB: acosDeg(cosAngleJAB),
|
|
|
+ angleRGB: acosDeg(cosAngleRGB),
|
|
|
chromaAngle: acosDeg(vectorDot(state.targetColor.qChromaHat, yChromaHat)),
|
|
|
hueAngle: angleDiff(state.targetColor.qHueAngle, yHueAngle),
|
|
|
};
|
|
@@ -147,9 +148,9 @@ const renderQVec = (q, node, sub) => {
|
|
|
const renderVec = math => `\\vec{${math.charAt(0)}}${math.substr(1)}`;
|
|
|
const renderNorm = vec => `\\frac{${vec}}{\\left|\\left|${vec}\\right|\\right|}`;
|
|
|
const metricText = [
|
|
|
- "\\text{RMS}_{P} ~ \\arg\\min_{P}\\left[X\\left(P\\right) - 2\\vec{q}\\cdot \\vec{Y}\\left(P\\right)\\right]",
|
|
|
- `\\angle \\left(\\vec{q}, \\vec{Y}\\left(P\\right)\\right) ~ \\arg\\min_{P}\\left[-${renderNorm(renderVec("q"))}\\cdot ${renderNorm(renderVec("Y\\left(P\\right)"))}\\right]`,
|
|
|
- "\\angle \\left(\\vec{q}_{\\perp}, \\vec{Y}\\left(P\\right)_{\\perp} \\right)",
|
|
|
+ "\\text{RMS}_{P} ~ \\arg\\min_{P}\\left[X\\left(P\\right) - 2\\vec{q}\\cdot \\vec{Y}\\left(P\\right)\\right]",
|
|
|
+ `\\angle \\left(\\vec{q}, \\vec{Y}\\left(P\\right)\\right) ~ \\arg\\min_{P}\\left[-${renderNorm(renderVec("q"))}\\cdot ${renderNorm(renderVec("Y\\left(P\\right)"))}\\right]`,
|
|
|
+ "\\angle \\left(\\vec{q}_{\\perp}, \\vec{Y}\\left(P\\right)_{\\perp} \\right)",
|
|
|
];
|
|
|
const updateObjective = () => {
|
|
|
let tex = metricText?.[state.metric];
|
|
@@ -180,8 +181,20 @@ const getSprite = pokemon => {
|
|
|
|
|
|
const renderPokemon = (data, classes = {}) => {
|
|
|
const { name, yJAB, yJABHex, yRGB, yRGBHex } = data;
|
|
|
- const { stdDevJAB, stdDevRGB, angleJAB, angleRGB, chromaAngle, hueAngle } = calcDisplayMetrics(data)
|
|
|
- const { labelClass = "", rgbClass = "", jabClass = "", scoreClass = "" } = classes;
|
|
|
+ const { labelClass = "", rgbClass = "", jabClass = "" } = classes;
|
|
|
+ let { resultsClass = "" } = classes;
|
|
|
+ let displayMetrics = {};
|
|
|
+ if (!state.targetColor) {
|
|
|
+ // no color selected need to skip scores
|
|
|
+ resultsClass = "hide";
|
|
|
+ } else {
|
|
|
+ displayMetrics = calcDisplayMetrics(data);
|
|
|
+ }
|
|
|
+ const {
|
|
|
+ stdDevJAB = 0, stdDevRGB = 0,
|
|
|
+ angleJAB = 0, angleRGB = 0,
|
|
|
+ chromaAngle = 0, hueAngle = 0,
|
|
|
+ } = displayMetrics;
|
|
|
|
|
|
const titleName = name.charAt(0).toUpperCase() + name.substr(1);
|
|
|
const textHex = getContrastingTextColor(yRGB);
|
|
@@ -201,7 +214,7 @@ const renderPokemon = (data, classes = {}) => {
|
|
|
<span class="${jabClass}">Jab: </span>
|
|
|
<span class="${rgbClass}">RGB: </span>
|
|
|
</div>
|
|
|
- <div class="pokemon_tile-score_column ${scoreClass}">
|
|
|
+ <div class="pokemon_tile-score_column ${resultsClass}">
|
|
|
<span class="pokemon_tile-no_flex ${jabClass}">
|
|
|
(${stdDevJAB.toFixed(2)}, ${angleJAB.toFixed(2)}°, ${chromaAngle.toFixed(2)}°)
|
|
|
</span>
|
|
@@ -229,22 +242,20 @@ const getPokemonAppender = targetList => (pokemonData, classes) => {
|
|
|
targetList.appendChild(li);
|
|
|
};
|
|
|
|
|
|
-// Search
|
|
|
-const search = () => {
|
|
|
+// Update Search Results
|
|
|
+const renderSearch = () => {
|
|
|
const resultsNode = getSearchListNode();
|
|
|
const append = getPokemonAppender(resultsNode);
|
|
|
- let found;
|
|
|
- if (state.searchTerm.trim().toLowerCase() === "!random") {
|
|
|
- found = Array.from({ length: 10 }, () => pokemonColorData[Math.floor(Math.random() * pokemonColorData.length)]);
|
|
|
- } else {
|
|
|
- found = pokemonLookup.search(state.searchTerm, { limit: 10 }).map(({ item }) => item);
|
|
|
- }
|
|
|
clearNodeContents(resultsNode);
|
|
|
- found.forEach(item => append(item));
|
|
|
+ state.searchResults?.forEach(pkmn => append(pkmn));
|
|
|
};
|
|
|
|
|
|
// Scoring
|
|
|
const rescore = () => {
|
|
|
+ if (!state.targetColor) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const metricFn = scoringMetrics[state.metric ?? 0];
|
|
|
// TODO might like to save this somewhere instead of recomputing when limit changes
|
|
|
const scores = pokemonColorData.map(data => ({ ...data, scores: metricFn(data) }));
|
|
@@ -269,15 +280,15 @@ const rescore = () => {
|
|
|
bestRGB.forEach(data => appendRGB(data, { labelClass: "hide", jabClass: "hide" }));
|
|
|
|
|
|
// update the rendered search results as well
|
|
|
- search();
|
|
|
+ renderSearch();
|
|
|
};
|
|
|
|
|
|
// Listeners
|
|
|
-const onColorChanged = skipScore => {
|
|
|
+const onColorChanged = skipScore => {
|
|
|
const readColor = readColorInput();
|
|
|
if (readColor) {
|
|
|
state.targetColor = readColor;
|
|
|
-
|
|
|
+
|
|
|
renderQVec(state.targetColor.qJAB.map(c => c.toFixed(2)), getQJABDisplay(), "Jab");
|
|
|
renderQVec(state.targetColor.qRGB.map(c => c.toFixed()), getQRGBDisplay(), "RGB");
|
|
|
|
|
@@ -338,21 +349,31 @@ const onLimitChanged = skipScore => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const onSearchChanged = skipSearch => {
|
|
|
+const onSearchChanged = () => {
|
|
|
state.searchTerm = getNameInputNode()?.value?.toLowerCase() ?? "";
|
|
|
if (state.searchTerm.length === 0) {
|
|
|
- clearNodeContents(getSearchListNode());
|
|
|
- } else if (!skipSearch) {
|
|
|
- search();
|
|
|
+ state.searchResults = [];
|
|
|
+ } else {
|
|
|
+ state.searchResults = pokemonLookup
|
|
|
+ .search(state.searchTerm, { limit: 10 })
|
|
|
+ .map(({ item }) => item);
|
|
|
}
|
|
|
+ renderSearch();
|
|
|
+};
|
|
|
+
|
|
|
+const onRandomPokemon = () => {
|
|
|
+ getNameInputNode().value = "";
|
|
|
+ state.searchResults = Array.from({ length: 10 }, () => pokemonColorData[Math.floor(Math.random() * pokemonColorData.length)]);
|
|
|
+ renderSearch();
|
|
|
};
|
|
|
|
|
|
-const onPageLoad = () => {
|
|
|
- // fake some events but don't do any searching or scoring
|
|
|
- onSearchChanged(true);
|
|
|
+const onPageLoad = () => {
|
|
|
+ // fake some events but don't do any scoring
|
|
|
onColorChanged(true);
|
|
|
onMetricChanged(true);
|
|
|
onLimitChanged(true);
|
|
|
- // then do a rescore directly, which will itself trigger a search
|
|
|
+ // then do a rescore directly, which will do nothing unless old data was loaded
|
|
|
rescore();
|
|
|
+ // finally render search in case rescore didn't
|
|
|
+ onSearchChanged();
|
|
|
};
|