Browse Source

Pivot to the new control setup

Kirk Trombley 2 years ago
parent
commit
c311b6b420
3 changed files with 198 additions and 239 deletions
  1. 167 90
      form.js
  2. 30 148
      index.html
  3. 1 1
      styles.css

+ 167 - 90
form.js

@@ -1,52 +1,27 @@
 const selectors = {
-  get sortControl() {
-    return document.forms.sortControl.elements;
-  },
-  get clusterControl() {
-    return document.forms.clusterControl.elements;
-  },
-  get colorSelect(){
+  get colorSelect() {
     return document.forms.colorSelect.elements;
   },
-  get resultsToDisplay() {
-    return selectors.sortControl.resultsToDisplay.value;
-  },
-  get colorSpace() {
-    return selectors.sortControl.colorSpace.value;
-  },
-  get sortMetric() {
-    return selectors.sortControl.sortMetric.value;
-  },
-  get sortOrder() {
-    return selectors.sortControl.sortOrder.checked ? "max" : "min";
-  },
-  get scaleFactor() {
-    return selectors.sortControl.rescaleFactor.value;
+  set colorText(hex) {
+    selectors.colorSelect.colorText.value = hex;
   },
-  get useClusters() {
-    return selectors.sortControl.useClusters.value;
+  set colorPicker(hex) {
+    selectors.colorSelect.colorPicker.value = hex;
   },
-  get clusterSortMetric() {
-    return selectors.clusterControl.sortMetric.value;
+
+  get sortControl() {
+    return document.forms.sortControl;
   },
-  get clusterSortOrder() {
-    return selectors.clusterControl.sortOrder.checked ? "max" : "min";
+  get resultsToDisplay() {
+    return selectors.sortControl.elements.resultsToDisplay.value;
   },
-  get clusterScaleFactor() {
-    return selectors.clusterControl.rescaleFactor.value;
+  get colorSpace() {
+    return selectors.sortControl.elements.colorSpace.value;
   },
+
   get prevColors() {
     return document.getElementById("prev-colors");
   },
-  get metricFormTemplate() {
-    return document.getElementById("metric-form-template").content;
-  },
-  get scaleFormTemplate() {
-    return document.getElementById("scale-form-template").content;
-  },
-  get sizeFactorFormTemplate() {
-    return document.getElementById("size-factor-template").content;
-  },
   get pokemonTemplate() {
     return document.getElementById("pkmn-template").content;
   },
@@ -56,24 +31,77 @@ const selectors = {
   get nameSearchResults() {
     return document.getElementById("name-results");
   },
+
   set background(hex) {
     document.querySelector(":root").style.setProperty("--background", hex);
   },
   set highlight(hex) {
     document.querySelector(":root").style.setProperty("--highlight", hex);
   },
-  set colorText(hex) {
-    selectors.colorSelect.colorText.value = hex;
+
+  get metricSelectTemplate() {
+    return document.getElementById("metric-select-template").content;
   },
-  set colorPicker(hex) {
-    selectors.colorSelect.colorPicker.value = hex;
+
+  get sortFunction() {
+    return document.forms.sortFunction;
+  },
+  get sortMetric() {
+    return selectors.sortFunction.elements.sortMetric.value;
+  },
+  get sortOrder() {
+    return selectors.sortFunction.elements.sortOrder.checked ? "max" : "min";
+  },
+  get sortUseBestCluster() {
+    return selectors.sortFunction.elements.useBestCluster.checked;
+  },
+  get sortUseWholeImage() {
+    return selectors.sortFunction.elements.useWholeImage.checked;
+  },
+  get sortClusterSize() {
+    return selectors.sortFunction.elements.clusterSize.checked;
+  },
+  get sortInverseClusterSize() {
+    return selectors.sortFunction.elements.invClusterSize.checked;
+  },
+  get sortTotalSize() {
+    return selectors.sortFunction.elements.totalSize.checked;
+  },
+  get sortInverseTotalSize() {
+    return selectors.sortFunction.elements.invTotalSize.checked;
+  },
+  set sortMetricSymbol(sym) {
+    selectors.sortFunction.elements.metricSymbolP.value = sym;
+    selectors.sortFunction.elements.metricSymbolB.value = sym;
+  },
+
+  get clusterFunction() {
+    return document.forms.clusterFunction;
+  },
+  get clusterSortMetric() {
+    return selectors.clusterFunction.elements.sortMetric.value;
+  },
+  get clusterSortOrder() {
+    return selectors.clusterFunction.elements.sortOrder.checked ? "max" : "min";
+  },
+  get clusterSortClusterSize() {
+    return selectors.clusterFunction.elements.clusterSize.checked;
+  },
+  get clusterSortInverseClusterSize() {
+    return selectors.clusterFunction.elements.invClusterSize.checked;
+  },
+  get clusterSortTotalSize() {
+    return selectors.clusterFunction.elements.totalSize.checked;
+  },
+  get clusterSortInverseTotalSize() {
+    return selectors.clusterFunction.elements.invTotalSize.checked;
+  },
+  set clusterMetricSymbol(sym) {
+    selectors.clusterFunction.elements.metricSymbol.value = sym;
   },
 };
 
 const onMetricChange = (elements, skipUpdates = false) => {
-  elements.sortOrderLabel.value = elements.sortOrder.checked
-    ? "Maximizing"
-    : "Minimizing";
   const kind = elements.metricKind.value;
   elements.whole.disabled = kind !== "whole";
   elements.mean.disabled = kind !== "mean";
@@ -81,6 +109,14 @@ const onMetricChange = (elements, skipUpdates = false) => {
   elements.sortMetric.value = elements[kind].value;
 
   if (!skipUpdates) {
+    // terrible hack
+    selectors.sortMetricSymbol = document
+      .querySelector(`option[value=${selectors.sortMetric}]`)
+      .textContent.at(-2);
+    selectors.clusterMetricSymbol = document
+      .querySelector(`option[value=${selectors.clusterSortMetric}]`)
+      .textContent.at(-2);
+
     updateSort();
   }
 };
@@ -128,23 +164,32 @@ const sortOrders = {
   min: (a, b) => a - b,
 };
 
-const scaleOptions = {
-  none: () => [1, 1, 1],
-  direct: (scores) => scores.clusters.map((c) => c.proportion),
-  inverse: (scores) => scores.clusters.map((c) => c.inverseProportion),
-  size: (scores) => [scores.total.size, scores.total.size, scores.total.size],
-  inverseSize: (scores) => [
-    scores.total.inverseSize,
-    scores.total.inverseSize,
-    scores.total.inverseSize,
-  ],
-};
-
-const cardinalityTerms = {
-  clusterSize: (scores, index) => scores.clusters[index].size,
-  invClusterSize: (scores, index) => scores.clusters[index].inverseSize,
-  totalSize: (scores) => scores.total.size,
-  invTotalSize: (scores) => scores.total.inverseSize,
+const getCardinalityFactorExtractor = (
+  clusterSize,
+  invClusterSize,
+  totalSize,
+  invTotalSize
+) => {
+  const extractors = [];
+  if (clusterSize) {
+    extractors.push((scores) => scores.clusters.map(({ size }) => size));
+  }
+  if (invClusterSize) {
+    extractors.push((scores) => scores.clusters.map(({ inverseSize }) => inverseSize));
+  }
+  if (totalSize) {
+    extractors.push((scores) => scores.clusters.map(() => scores.total.size));
+  }
+  if (invTotalSize) {
+    extractors.push((scores) => scores.clusters.map(() => scores.total.inverseSize));
+  }
+  return (scores) =>
+    extractors
+      .map((ext) => ext(scores))
+      .reduce(
+        (acc, xs) => acc.map((a, i) => a * xs[i]),
+        scores.clusters.map(() => 1)
+      );
 };
 
 const currentScores = {};
@@ -179,22 +224,55 @@ const updateScores = (rgb) => {
 const updateSort = () => {
   // update cluster rankings
   const clusterSortOrder = sortOrders[selectors.clusterSortOrder];
-  const clusterScaleOption = scaleOptions[selectors.clusterScaleFactor];
+  const getClusterCardinalityFactors = getCardinalityFactorExtractor(
+    selectors.clusterSortClusterSize,
+    selectors.clusterSortInverseClusterSize,
+    selectors.clusterSortTotalSize,
+    selectors.clusterSortInverseTotalSize
+  );
   pokemonData.forEach(({ name }) => {
     const { jab, rgb } = currentScores[name];
-    // multiply scale with the intended metric, and find the index of the best value
-    const forSpace = (clusters, scales) =>
+    // multiply scales with the intended metric, and find the index of the best value
+    const forSpace = (clusters, factors) =>
       clusters
-        .map((c, i) => [c[selectors.clusterSortMetric] * scales[i], i])
+        .map((c, i) => [c[selectors.clusterSortMetric] * factors[i], i])
         .reduce((a, b) => (clusterSortOrder(a[0], b[0]) > 0 ? b : a))[1];
     currentBestClusterIndices[name] = {
-      jab: forSpace(jab.clusters, clusterScaleOption(jab)),
-      rgb: forSpace(rgb.clusters, clusterScaleOption(rgb)),
+      jab: forSpace(jab.clusters, getClusterCardinalityFactors(jab)),
+      rgb: forSpace(rgb.clusters, getClusterCardinalityFactors(rgb)),
     };
   });
 
   // set up for actual sort
-  const scaleOption = scaleOptions[selectors.scaleFactor];
+  const getCardinalityFactors = getCardinalityFactorExtractor(
+    selectors.sortClusterSize,
+    selectors.sortInverseClusterSize,
+    selectors.sortTotalSize,
+    selectors.sortInverseTotalSize
+  );
+  const factors = [
+    (name) =>
+      getCardinalityFactors(currentScores[name][selectors.colorSpace])[
+        currentBestClusterIndices[name][selectors.colorSpace]
+      ],
+  ];
+  if (selectors.sortUseWholeImage) {
+    factors.push(
+      (name) => currentScores[name][selectors.colorSpace].total[selectors.sortMetric]
+    );
+  }
+  if (selectors.sortUseBestCluster) {
+    factors.push(
+      (name) =>
+        currentScores[name][selectors.colorSpace].clusters[
+          currentBestClusterIndices[name][selectors.colorSpace]
+        ][selectors.sortMetric]
+    );
+  }
+  pokemonData.forEach(({ name }) => {
+    currentSortValues[name] = factors.map((fn) => fn(name)).reduce((x, y) => x * y);
+  });
+  /*
   switch (selectors.useClusters) {
     case "off":
       pokemonData.forEach(({ name }) => {
@@ -220,6 +298,7 @@ const updateSort = () => {
       });
       break;
   }
+  */
 
   // update actual sorted data
   const sortOrder = sortOrders[selectors.sortOrder];
@@ -288,9 +367,14 @@ const makePokemonTile = (name) => {
     `;
   });
 
-  if (selectors.useClusters === "off") {
+  if (selectors.sortUseWholeImage) {
     clone.querySelector(".pkmn").classList.add("pkmn-total-selected");
-  } else {
+  }
+  if (
+    selectors.sortUseBestCluster ||
+    selectors.sortClusterSize ||
+    selectors.sortInverseClusterSize
+  ) {
     clone
       .querySelector(".pkmn")
       .classList.add(
@@ -332,25 +416,18 @@ const showResults = () => {
 };
 
 window.addEventListener("load", () => {
-  const metricTemplate = selectors.metricFormTemplate;
-
-  selectors.sortControl.metric.prepend(metricTemplate.cloneNode(true));
-  selectors.sortControl.metricKind.value = "whole";
-  selectors.sortControl.whole.value = "alpha";
-  onMetricChange(selectors.sortControl, true);
-
-  selectors.clusterControl.metric.appendChild(metricTemplate.cloneNode(true));
-  selectors.clusterControl.sortOrder.checked = true;
-  selectors.clusterControl.metricKind.value = "statistic";
-  selectors.clusterControl.statistic.value = "importance";
-  onMetricChange(selectors.clusterControl, true);
-
-  const scaleTemplate = selectors.scaleFormTemplate;
-  selectors.sortControl.rescaleSection.appendChild(scaleTemplate.cloneNode(true));
-  selectors.sortControl.rescaleFactor.value = "inverse";
-
-  selectors.clusterControl.rescaleSection.appendChild(scaleTemplate.cloneNode(true));
-  selectors.clusterControl.rescaleFactor.value = "none";
+  const metricSelect = selectors.metricSelectTemplate;
+
+  selectors.sortFunction.appendChild(metricSelect.cloneNode(true));
+  selectors.sortFunction.elements.metricKind.value = "whole";
+  selectors.sortFunction.elements.whole.value = "alpha";
+  onMetricChange(selectors.sortFunction.elements, true);
+
+  selectors.clusterFunction.appendChild(metricSelect.cloneNode(true));
+  selectors.clusterFunction.elements.sortOrder.checked = true;
+  selectors.clusterFunction.elements.metricKind.value = "statistic";
+  selectors.clusterFunction.elements.statistic.value = "importance";
+  onMetricChange(selectors.clusterFunction.elements, true);
 
   document.body.addEventListener("click", ({ target: { innerText }, detail }) => {
     if (detail === 2 && innerText?.includes("#")) {

+ 30 - 148
index.html

@@ -14,17 +14,9 @@
   <body>
     <noscript>Requires javascript</noscript>
 
-    <template id="metric-form-template">
-      <div>
-        <label>
-          <input type="checkbox" name="sortOrder" role="button" hidden />
-          <output name="sortOrderLabel"></output>
-          (click to toggle)
-        </label>
-      </div>
-
-      <fieldset>
-        <legend>Evaluation Function</legend>
+    <template id="metric-select-template">
+      <fieldset onchange="onMetricChange(event.target.form.elements)">
+        <legend>Metric</legend>
         <div>
           <label>
             <input type="radio" name="metricKind" value="whole" />
@@ -46,7 +38,7 @@
         <select name="whole" disabled>
           <option value="sigma">RMS Deviation (σ)</option>
           <option value="bigTheta">Cosine Difference (Θ)</option>
-          <option value="alpha">Geometric Difference (α)</option>
+          <option value="alpha" selected>Geometric Difference (α)</option>
         </select>
         <select name="mean" disabled>
           <option value="theta">Angular Difference (θ)</option>
@@ -67,67 +59,6 @@
         </select>
         <output name="sortMetric" hidden aria-hidden="true"></output>
       </fieldset>
-
-      <fieldset>
-        <legend>Cardinality Terms</legend>
-        <div>
-          <label>
-            <input type="checkbox" name="clusterSize" />
-            Cluster Size
-          </label>
-        </div>
-        <div>
-          <label>
-            <input type="checkbox" name="invClusterSize" />
-            Inverse Cluster Size
-          </label>
-        </div>
-        <div>
-          <label>
-            <input type="checkbox" name="totalSize" />
-            Image Size
-          </label>
-        </div>
-        <div>
-          <label>
-            <input type="checkbox" name="invTotalSize" />
-            Inverse Image Size
-          </label>
-        </div>
-      </fieldset>
-    </template>
-
-    <template id="scale-form-template">
-      <div>
-        <label>
-          <input type="radio" name="rescaleFactor" value="none" />
-          None
-        </label>
-      </div>
-      <div>
-        <label>
-          <input type="radio" name="rescaleFactor" value="direct" />
-          Cluster Size
-        </label>
-      </div>
-      <div>
-        <label>
-          <input type="radio" name="rescaleFactor" value="inverse" />
-          Inverse Cluster Size
-        </label>
-      </div>
-      <div>
-        <label>
-          <input type="radio" name="rescaleFactor" value="size" />
-          Image Size
-        </label>
-      </div>
-      <div>
-        <label>
-          <input type="radio" name="rescaleFactor" value="inverseSize" />
-          Inverse Image Size
-        </label>
-      </div>
     </template>
 
     <template id="pkmn-template">
@@ -143,7 +74,12 @@
       </div>
     </template>
 
-    <form action="javascript:void(0);" id="sortFunction" autocomplete="off">
+    <form
+      action="javascript:void(0);"
+      id="sortFunction"
+      autocomplete="off"
+      onchange="updateSort()"
+    >
       <div class="fn-group fn-group-outer">
         <label class="fn-minmax">
           <input type="checkbox" name="sortOrder" role="button" hidden />
@@ -158,12 +94,22 @@
           onchange="event.target.parentNode.classList.toggle('fn-part--disabled')"
         >
           <label class="fn-part">
-            <input type="checkbox" name="useBestCluster" checked />
+            <input type="checkbox" name="useWholeImage" checked />
             <output name="metricSymbolP">α</output>(P)
           </label>
           <label class="fn-part">
-            <input type="checkbox" name="useWholeImage" checked />
+            <input type="checkbox" name="totalSize" checked />
+            |P|
+          </label>
+          ⋅
+          <label class="fn-part fn-part--disabled">
+            <input type="checkbox" name="invTotalSize" />
+            |P|<sup>-1</sup>
+          </label>
+          ⋅
+          <label class="fn-part">
+            <input type="checkbox" name="useBestCluster" checked />
             <output name="metricSymbolB">α</output>(B)
           </label>
@@ -176,22 +122,18 @@
             <input type="checkbox" name="invClusterSize" checked />
             |B|<sup>-1</sup>
           </label>
-          ⋅
-          <label class="fn-part">
-            <input type="checkbox" name="totalSize" checked />
-            |P|
-          </label>
-          ⋅
-          <label class="fn-part fn-part--disabled">
-            <input type="checkbox" name="invTotalSize" />
-            |P|<sup>-1</sup>
-          </label>
         </div>
         <span class="fn-bracket">]</span>
       </div>
+      <!-- Template mount point -->
     </form>
 
-    <form action="javascript:void(0);" id="clusterFunction" autocomplete="off">
+    <form
+      action="javascript:void(0);"
+      id="clusterFunction"
+      autocomplete="off"
+      onchange="updateSort()"
+    >
       <div class="fn-group">
         <span>where B =&nbsp;</span>
         <label class="fn-minmax fn-minmax--wide">
@@ -232,6 +174,7 @@
         </div>
         <span class="fn-bracket">]</span>
       </div>
+      <!-- Template mount point -->
     </form>
 
     <form action="javascript:void(0);" id="colorSelect" autocomplete="off">
@@ -286,67 +229,6 @@
           sRGB
         </label>
       </div>
-      <div>Best Match</div>
-      <fieldset name="metric" onchange="onMetricChange(event.target.form.elements)">
-        <!-- template mount point -->
-        <legend>Sort By</legend>
-        <div>
-          <label>
-            <input type="checkbox" name="useBestCluster" />
-            Best Cluster
-          </label>
-        </div>
-        <div>
-          <label>
-            <input type="checkbox" name="useWholeImage" />
-            Whole Image
-          </label>
-        </div>
-      </fieldset>
-      <fieldset
-        onchange="
-          const elements = event.target.form.elements;
-          document.forms.sortControl.elements.rescaleSection.hidden = elements.useClusters.value === 'off';
-          document.forms.clusterControl.hidden = elements.useClusters.value === 'off';
-          onMetricChange(elements);
-        "
-      >
-        <legend>Comparison Value</legend>
-        <div>
-          <label>
-            <input type="radio" name="useClusters" value="off" />
-            Distance of Whole Image
-          </label>
-        </div>
-        <div>
-          <label>
-            <input type="radio" name="useClusters" value="on" />
-            Distance of Best Cluster
-          </label>
-        </div>
-        <div>
-          <label>
-            <input type="radio" name="useClusters" value="mult" checked />
-            Product of Distance Values
-          </label>
-        </div>
-      </fieldset>
-      <fieldset name="rescaleSection" onchange="updateSort()">
-        <legend>Scaled By</legend>
-        <!-- template mount point -->
-      </fieldset>
-    </form>
-
-    <form action="javascript:void(0);" id="clusterControl" autocomplete="off">
-      <div>Best Cluster</div>
-      <fieldset name="metric" onchange="onMetricChange(event.target.form.elements)">
-        <legend>Distance Metric</legend>
-        <!-- template mount point -->
-      </fieldset>
-      <fieldset name="rescaleSection" onchange="updateSort()">
-        <legend>Scaled By</legend>
-        <!-- template mount point -->
-      </fieldset>
     </form>
 
     <div id="color-results"></div>

+ 1 - 1
styles.css

@@ -71,9 +71,9 @@ select:disabled {
 .pkmn > :is(.pkmn-total, .pkmn-cls)::before {
   display: inline-block;
   content: "";
+  font-size: 80%;
   width: 1ch;
   margin-inline: 0.25ch;
-  margin-block-end: 0.125rem;
 }
 
 .pkmn-total-selected > .pkmn-total::before,