Przeglądaj źródła

Add random pokemon button, avoid re-randoming on color change, clean up

Kirk Trombley 3 lat temu
rodzic
commit
2a49082e69
3 zmienionych plików z 60 dodań i 39 usunięć
  1. 1 2
      nearest.css
  2. 4 3
      nearest.html
  3. 55 34
      nearest.js

+ 1 - 2
nearest.css

@@ -148,7 +148,7 @@ body {
 
 .pokemon_tile-score_column {
     margin-left: 4px;
-    min-width: 10em;
+    min-width: 12em;
     display: flex;
     flex-flow: column nowrap;
 }
@@ -158,7 +158,6 @@ body {
 }
 
 .pokemon_tile-hex_column {
-    /* flex: 1 1 10%; */
     flex: 1;
     margin-left: 8px;
     display: flex;

+ 4 - 3
nearest.html

@@ -21,7 +21,7 @@
             <form class="panel config" onsubmit="event.preventDefault()">
                 <div class="container control">
                     <img src="https://img.pokemondb.net/sprites/sword-shield/icon/bulbasaur.png" />
-                    <button class="padded" type="button" onclick="onRandomColor()">Random color</button>
+                    <button class="padded" type="button" onclick="onRandomColor()">Random Color</button>
                     <input size="7" maxlength="7" id="color-input" oninput="onColorChanged()" />
                 </div>
 
@@ -57,7 +57,7 @@
                     <input type="range" min="1" max="100" value="10" oninput="onLimitChanged()" id="num-poke">
                 </div>
             </form>
-            
+
             <div class="panel math-section">
                 <div class="container center-aligned">
                     <div class="panel">
@@ -86,8 +86,9 @@
             </div>
 
             <div class="panel bypkmn">
-                <form class="container control" onsubmit="event.preventDefault(); onSearchChanged()">
+                <form class="container control" onsubmit="event.preventDefault()">
                     <label for="pokemon-name">Search By Pokemon</label>
+                    <button class="padded" type="button" onclick="onRandomPokemon()">Random Pokemon</button>
                     <input id="pokemon-name" size="15" oninput="onSearchChanged()">
                 </form>
                 <ul id="search-list" class="pkmn-list"></ul>

+ 55 - 34
nearest.js

@@ -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)}&deg;, ${chromaAngle.toFixed(2)}&deg;)
           </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();
 };