|
@@ -1,6 +1,8 @@
|
|
|
// Selectors + DOM Manipulation
|
|
|
const getColorInputNode = () => document.getElementById("color-input");
|
|
|
const getMetricDropdownNode = () => document.getElementById("metric");
|
|
|
+const getMeanArgumentDropdownNode = () => document.getElementById("image-summary");
|
|
|
+const getClusterMeanWarning = () => document.getElementById("cluster-mean-warning");
|
|
|
const getIncludeXToggleNode = () => document.getElementById("include-x");
|
|
|
const getNormQYToggleNode = () => document.getElementById("norm-q-y");
|
|
|
const getCloseCoeffSliderNode = () => document.getElementById("close-coeff");
|
|
@@ -34,22 +36,47 @@ const vectorNorm = v => { const n = vectorMag(v); return [ n, v.map(c => c / n)
|
|
|
const angleDiff = (a, b) => { const raw = Math.abs(a - b); return raw < 180 ? raw : (360 - raw); };
|
|
|
const rad2deg = 180 / Math.PI;
|
|
|
|
|
|
-// Pre-Compute Y Data
|
|
|
-const pokemonColorData = database.map(data => {
|
|
|
- const yRGBColor = d3.rgb(...data.yRGB);
|
|
|
- const [ yJABNorm, yJABHat ] = vectorNorm(data.yJAB);
|
|
|
- const [ yRGBNorm, yRGBHat ] = vectorNorm(data.yRGB);
|
|
|
+// Conversions
|
|
|
+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;
|
|
|
|
|
|
+// Arg Compare
|
|
|
+const argComp = comp => ra => ra.map((x, i) => [x, i]).reduce((a, b) => comp(a[0], b[0]) > 0 ? b : a)[1];
|
|
|
+const argMin = argComp((a, b) => a - b);
|
|
|
+const argMax = argComp((a, b) => b - a);
|
|
|
+
|
|
|
+// Pre-Compute Data
|
|
|
+const computeVectorData = (vector, toHex, toHue) => {
|
|
|
+ const [ magnitude, unit ] = vectorNorm(vector);
|
|
|
return {
|
|
|
- ...data,
|
|
|
- yJABHex: d3.jab(...data.yJAB).formatHex(),
|
|
|
- yJABNorm, yJABHat,
|
|
|
- yRGBHex: yRGBColor.formatHex(),
|
|
|
- yRGBNorm, yRGBHat,
|
|
|
- yHueAngleJAB: rad2deg * Math.atan2(data.yJAB[2], data.yJAB[1]),
|
|
|
- yHueAngleRGB: d3.hsl(yRGBColor).h,
|
|
|
- }
|
|
|
+ vector,
|
|
|
+ magnitude,
|
|
|
+ magSq: magnitude * magnitude,
|
|
|
+ unit,
|
|
|
+ hex: toHex(vector),
|
|
|
+ hue: toHue(vector),
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const computeStats = (varFromZero, trueMeanVec, kMeanStruct, toHex, toHue) => ({
|
|
|
+ varFromZero,
|
|
|
+ trueMean: computeVectorData(trueMeanVec, toHex, toHue),
|
|
|
+ kMeans: kMeanStruct.slice(0, 3).map(z => computeVectorData(z, toHex, toHue)),
|
|
|
+ kWeights: kMeanStruct[3],
|
|
|
+ largestCluster: argMax(kMeanStruct[3]),
|
|
|
+ smallestCluster: argMin(kMeanStruct[3]),
|
|
|
});
|
|
|
+
|
|
|
+const pokemonColorData = database.map(({
|
|
|
+ name, xJAB, xRGB, yJAB, yRGB, zJAB, zRGB,
|
|
|
+}) => ({
|
|
|
+ name,
|
|
|
+ jabStats: computeStats(xJAB, yJAB, zJAB, jab2hex, jab2hue),
|
|
|
+ rgbStats: computeStats(xRGB, yRGB, zRGB, rgb2hex, rgb2hue),
|
|
|
+}));
|
|
|
+
|
|
|
const pokemonLookup = new Fuse(pokemonColorData, { keys: [ "name" ] });
|
|
|
|
|
|
// Color Calculations
|
|
@@ -65,27 +92,16 @@ const readColorInput = () => {
|
|
|
const rgb = d3.color(colorInput);
|
|
|
const { J, a, b } = d3.jab(rgb);
|
|
|
|
|
|
- const qJAB = [ J, a, b ];
|
|
|
- const qRGB = [ rgb.r, rgb.g, rgb.b ];
|
|
|
-
|
|
|
- const [ qJABNorm, qJABHat ] = vectorNorm(qJAB);
|
|
|
- const qJABNormSq = qJABNorm * qJABNorm;
|
|
|
-
|
|
|
- const [ qRGBNorm, qRGBHat ] = vectorNorm(qRGB);
|
|
|
- const qRGBNormSq = qRGBNorm * qRGBNorm;
|
|
|
-
|
|
|
return {
|
|
|
- qHex: rgb.formatHex(),
|
|
|
- qJAB, qJABHat, qJABNorm, qJABNormSq,
|
|
|
- qRGB, qRGBHat, qRGBNorm, qRGBNormSq,
|
|
|
- qHueAngleJAB: rad2deg * Math.atan2(b, a),
|
|
|
- qHueAngleRGB: d3.hsl(rgb).h,
|
|
|
+ jabData: computeVectorData([ J, a, b ], jab2hex, jab2hue),
|
|
|
+ rgbData: computeVectorData([ rgb.r, rgb.g, rgb.b ], rgb2hex, rgb2hue),
|
|
|
};
|
|
|
};
|
|
|
|
|
|
// State
|
|
|
const state = {
|
|
|
metric: null,
|
|
|
+ meanArgument: 0, // TODO
|
|
|
includeX: null,
|
|
|
normQY: null,
|
|
|
closeCoeff: null,
|
|
@@ -96,66 +112,62 @@ const state = {
|
|
|
};
|
|
|
|
|
|
// Metrics
|
|
|
-const scoringMetrics = [
|
|
|
- ({ xJAB, xRGB, yJAB, yRGB }) => [
|
|
|
- xJAB - 2 * vectorDot(yJAB, state.targetColor.qJAB),
|
|
|
- xRGB - 2 * vectorDot(yRGB, state.targetColor.qRGB),
|
|
|
- ],
|
|
|
- ({ yJABHat, yRGBHat }) => [
|
|
|
- -vectorDot(yJABHat, state.targetColor.qJABHat),
|
|
|
- -vectorDot(yRGBHat, state.targetColor.qRGBHat),
|
|
|
- ],
|
|
|
- ({ yJAB, yRGB }) => [
|
|
|
- vectorSqDist(state.targetColor.qJAB, yJAB),
|
|
|
- vectorSqDist(state.targetColor.qRGB, yRGB),
|
|
|
- ],
|
|
|
- ({ yHueAngleJAB, yHueAngleRGB }) => [
|
|
|
- angleDiff(state.targetColor.qHueAngleJAB, yHueAngleJAB),
|
|
|
- angleDiff(state.targetColor.qHueAngleRGB, yHueAngleRGB),
|
|
|
- ],
|
|
|
- // TODO - might want an alternative metric of subbing these Z's in for Y
|
|
|
- ({ zJAB, zRGB }) => [
|
|
|
- Math.min(...zJAB.map(z => vectorSqDist(z, state.targetColor.qJAB))),
|
|
|
- Math.min(...zRGB.map(z => vectorSqDist(z, state.targetColor.qRGB))),
|
|
|
- ],
|
|
|
- ({ zJAB, zRGB }) => [
|
|
|
- Math.max(...zJAB.map(z => vectorSqDist(z, state.targetColor.qJAB))),
|
|
|
- Math.max(...zRGB.map(z => vectorSqDist(z, state.targetColor.qRGB))),
|
|
|
- ],
|
|
|
- ({ xJAB, xRGB, yJAB, yRGB, yJABHat, yRGBHat }) => [
|
|
|
- (state.includeX ? xJAB : 0) - state.closeCoeff * vectorDot(
|
|
|
- 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 ? state.targetColor.qRGBHat : state.targetColor.qRGB
|
|
|
- ),
|
|
|
- ],
|
|
|
+const summarySelectors = [
|
|
|
+ // true mean
|
|
|
+ stats => stats.trueMean,
|
|
|
+ // largest cluster
|
|
|
+ stats => stats.kMeans[stats.largestCluster],
|
|
|
+ // smallest cluster
|
|
|
+ stats => stats.kMeans[stats.smallestCluster],
|
|
|
+ // best fit cluster
|
|
|
+ (stats, q) => stats.kMeans[argMin(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]))],
|
|
|
+ // worst fit cluster
|
|
|
+ (stats, q) => stats.kMeans[argMax(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]))],
|
|
|
+];
|
|
|
+
|
|
|
+const selectedSummary = (stats, q) => summarySelectors[state.meanArgument](stats, q);
|
|
|
+
|
|
|
+const metrics = [
|
|
|
+ // RMS
|
|
|
+ (stats, q) => stats.varFromZero - 2 * vectorDot(selectedSummary(stats, q).vector, q.vector),
|
|
|
+ // mean angle
|
|
|
+ (stats, q) => -vectorDot(selectedSummary(stats, q).unit, q.unit),
|
|
|
+ // mean dist
|
|
|
+ (stats, q) => vectorSqDist(selectedSummary(stats, q).vector, q.vector),
|
|
|
+ // hue angle
|
|
|
+ (stats, q) => angleDiff(selectedSummary(stats, q).hue, q.hue),
|
|
|
+ // custom
|
|
|
+ (stats, q) => (state.includeX ? stats.varFromZero : 0) - state.closeCoeff * vectorDot(
|
|
|
+ selectedSummary(stats, q)[state.normQY ? "unit" : "vector"],
|
|
|
+ state.normQY ? q.unit : q.vector,
|
|
|
+ ),
|
|
|
];
|
|
|
|
|
|
-const calcDisplayMetrics = ({
|
|
|
- xJAB, xRGB, yJAB, yRGB, yJABHat, yJABNorm, yRGBHat, yRGBNorm, yHueAngleJAB, yHueAngleRGB,
|
|
|
-}) => {
|
|
|
- // TODO - case on state.metric to avoid recalculation of subterms?
|
|
|
+const scorePokemon = pkmn => ({
|
|
|
+ jab: metrics[state.metric](pkmn.jabStats, state.targetColor.jabData),
|
|
|
+ rgb: metrics[state.metric](pkmn.rgbStats, state.targetColor.rgbData),
|
|
|
+});
|
|
|
|
|
|
- const cosAngleJAB = vectorDot(state.targetColor.qJABHat, yJABHat);
|
|
|
- const yTermJAB = cosAngleJAB * yJABNorm * state.targetColor.qJABNorm;
|
|
|
+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 cosAngleRGB = vectorDot(state.targetColor.qRGBHat, yRGBHat);
|
|
|
- const yTermRGB = cosAngleRGB * yRGBNorm * state.targetColor.qRGBNorm;
|
|
|
+ const cosAngleJAB = vectorDot(state.targetColor.jabData.unit, jabStats.trueMean.unit);
|
|
|
+ const yTermJAB = cosAngleJAB * jabStats.trueMean.magnitude * state.targetColor.jabData.magnitude;
|
|
|
|
|
|
- // TODO Z-dists?
|
|
|
+ const cosAngleRGB = vectorDot(state.targetColor.rgbData.unit, rgbStats.trueMean.unit);
|
|
|
+ const yTermRGB = cosAngleRGB * rgbStats.trueMean.magnitude * state.targetColor.rgbData.magnitude;
|
|
|
|
|
|
return {
|
|
|
- stdDevRGB: Math.sqrt(xRGB - 2 * yTermRGB + state.targetColor.qRGBNormSq),
|
|
|
- stdDevJAB: Math.sqrt(xJAB - 2 * yTermJAB + state.targetColor.qJABNormSq),
|
|
|
+ stdDevJAB: Math.sqrt(jabStats.varFromZero - 2 * yTermJAB + state.targetColor.jabData.magSq),
|
|
|
+ stdDevRGB: Math.sqrt(rgbStats.varFromZero - 2 * yTermRGB + state.targetColor.rgbData.magSq),
|
|
|
angleJAB: rad2deg * Math.acos(cosAngleJAB),
|
|
|
angleRGB: rad2deg * Math.acos(cosAngleRGB),
|
|
|
- meanDistJAB: vectorDist(state.targetColor.qJAB, yJAB),
|
|
|
- meanDistRGB: vectorDist(state.targetColor.qRGB, yRGB),
|
|
|
- hueAngleJAB: angleDiff(state.targetColor.qHueAngleJAB, yHueAngleJAB),
|
|
|
- hueAngleRGB: angleDiff(state.targetColor.qHueAngleRGB, yHueAngleRGB),
|
|
|
+ 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),
|
|
|
};
|
|
|
};
|
|
|
|
|
@@ -164,68 +176,79 @@ const renderQVec = (q, node, sub) => {
|
|
|
node.innerHTML = TeXZilla.toMathMLString(String.raw`\vec{q}_{\text{${sub}}} = \left(\text{${q.join(", ")}}\right)`);
|
|
|
};
|
|
|
|
|
|
+const mathArgBest = (mxn, arg) => `\\underset{${arg}}{\\arg\\${mxn}}`;
|
|
|
+
|
|
|
const mathDefinitions = {
|
|
|
- "x-definition": String.raw`
|
|
|
- X\left(P\right) = \frac{1}{\left|P\right|}\sum_{p\in P}{\left|\left|\vec{p}\right|\right|^2}
|
|
|
- `,
|
|
|
- "y-definition": String.raw`
|
|
|
- \vec{Y}\left(P\right) = \frac{1}{\left|P\right|}\sum_{p\in P}{\vec{p}}
|
|
|
- `,
|
|
|
- "v-perp-definition": String.raw`
|
|
|
- \vec{v}_{\perp} = \text{oproj}_{\left\{\vec{J}, \vec{L}\right\}}{\vec{v}}
|
|
|
+ "main-definition": String.raw`
|
|
|
+ \begin{aligned}
|
|
|
+ \vec{\mu}\left(P\right) &= \frac{1}{\left|P\right|}\sum_{p\in P}{\vec{p}} \\
|
|
|
+ I\left(P\right) &= \frac{1}{\left|P\right|}\sum_{p\in P}{\left|\left|\vec{p}\right|\right|^2} \\
|
|
|
+ \vec{x}_{\perp} &= \text{oproj}_{\left\{\vec{J}, \vec{L}\right\}}{\vec{x}} \\
|
|
|
+ \Delta{H}\left(P\right) &= \angle \left(\vec{q}_{\perp}, \vec{\mu}\left(P\right)_{\perp} \right)
|
|
|
+ \end{aligned}
|
|
|
`,
|
|
|
- "del-h-definition": String.raw`
|
|
|
- \Delta{H} = \angle \left(\vec{q}_{\perp}, \vec{Y}_{\perp}\left(P\right) \right)
|
|
|
+ "k-definition": String.raw`
|
|
|
+ \begin{aligned}
|
|
|
+ K_{\text{big}}\left(P\right) &= ${mathArgBest("max", "P_i")} \frac{\left|P_i\right|}{\left|P\right|} \\
|
|
|
+ K_{\text{small}}\left(P\right) &= ${mathArgBest("min", "P_i")} \frac{\left|P_i\right|}{\left|P\right|} \\
|
|
|
+ K_{\text{best}}\left(P\right) &= ${mathArgBest("min", "P_i")} \frac{\left|P\right|}{\left|P_i\right|} \left|\left| \vec{q} - \vec{\mu}\left(P_i\right) \right|\right| \\
|
|
|
+ K_{\text{worst}}\left(P\right) &= ${mathArgBest("max", "P_i")} \frac{\left|P\right|}{\left|P_i\right|} \left|\left| \vec{q} - \vec{\mu}\left(P_i\right) \right|\right|
|
|
|
+ \end{aligned}
|
|
|
`,
|
|
|
"cluster-definition": String.raw`
|
|
|
- \left\{P_1, P_2, P_3\right\} = \arg\min_{\left\{P_1, P_2, P_3\right\}} \sum_{i=1}^3 \sum_{p\inP_i} \left|\left| \vec{p} - \vec{Y}\left(P_i\right) \right|\right|^2
|
|
|
+ \begin{aligned}
|
|
|
+ \left\{P_1, P_2, P_3\right\} = ${mathArgBest("max", String.raw`\left\{P_1, P_2, P_3\right\}`)} \sum_{i=1}^3 \sum_{p\inP_i} \left|\left| \vec{p} - \vec{\mu}\left(P_i\right) \right|\right|^2
|
|
|
+ \end{aligned}
|
|
|
`,
|
|
|
- "z-best-definition": String.raw`
|
|
|
- \vec{Z}_{\text{best}}\left(P\right) = \vec{Y}\left(\arg\min_{P_i} \left|\left| \vec{q} - \vec{Y}\left(P_i\right) \right|\right| \right)
|
|
|
- `,
|
|
|
- "z-worst-definition": String.raw`
|
|
|
- \vec{Z}_{\text{worst}}\left(P\right) = \vec{Y}\left(\arg\max_{P_i} \left|\left| \vec{q} - \vec{Y}\left(P_i\right) \right|\right| \right)
|
|
|
+ "rms-definition": String.raw`
|
|
|
+ \text{RMS}_{P}\left(q\right) = \sqrt{E\left[\left|\left|\vec{q} - \vec{p}\right|\right|^2\right]} = \sqrt{\frac{1}{|P|}\sum_{p \in P}{\left|\left|\vec{p} - \vec{q}\right|\right|^2}}
|
|
|
`,
|
|
|
"result-definition": String.raw`
|
|
|
\left(
|
|
|
- \text{RMS}_P\left(q\right),
|
|
|
- \angle \left(\vec{q}, \vec{Y}\left(P\right)\right),
|
|
|
- \left|\left| \vec{q} - \vec{Y}\left(P\right) \right|\right|,
|
|
|
- \Delta{H}
|
|
|
+ \text{RMS}_P\left(q\right),
|
|
|
+ \angle \left(\vec{q}, \vec{\mu}\left(P\right)\right),
|
|
|
+ \left|\left| \vec{q} - \vec{\mu}\left(P\right) \right|\right|,
|
|
|
+ \Delta{H}\left(P\right)
|
|
|
\right)
|
|
|
`,
|
|
|
};
|
|
|
|
|
|
const metricText = [
|
|
|
- String.raw`\text{RMS}_{P}\left(q\right) ~ \arg\min_{P}\left[X\left(P\right) - 2\vec{q}\cdot \vec{Y}\left(P\right)\right]`,
|
|
|
- String.raw`\angle \left(\vec{q}, \vec{Y}\left(P\right)\right) ~ \arg\max_{P}\left[\cos\left(\angle \left(\vec{q}, \vec{Y}\left(P\right)\right)\right)\right]`,
|
|
|
- String.raw`\left|\left| \vec{q} - \vec{Y}\left(P\right) \right|\right| ~ \arg\min_{P}\left[\left|\left| \vec{q} - \vec{Y}\left(P\right) \right|\right|^2\right]`,
|
|
|
- String.raw`\Delta{H}`,
|
|
|
- String.raw`\left|\left| \vec{q} - \vec{Z}_{\text{best}}\left(P\right) \right|\right|`,
|
|
|
- String.raw`\left|\left| \vec{q} - \vec{Z}_{\text{worst}}\left(P\right) \right|\right|`,
|
|
|
-].map(s => TeXZilla.toMathML(s));
|
|
|
+ muArg => String.raw`${mathArgBest("min", "P")}\left[I\left(P\right) - 2\vec{q}\cdot \vec{\mu}\left(${muArg}\right)\right]`,
|
|
|
+ muArg => String.raw`${mathArgBest("max", "P")}\left[\cos\left(\angle \left(\vec{q}, \vec{\mu}\left(${muArg}\right)\right)\right)\right]`,
|
|
|
+ muArg => String.raw`${mathArgBest("min", "P")}\left[\left|\left| \vec{q} - \vec{\mu}\left(${muArg}\right) \right|\right|^2\right]`,
|
|
|
+ muArg => String.raw`${mathArgBest("min", "P")} \left[\angle \left(\vec{q}_{\perp}, \vec{\mu}\left(${muArg}\right)_{\perp} \right)\right]`,
|
|
|
+].map(s => muArg => TeXZilla.toMathML(s(muArg)));
|
|
|
+
|
|
|
+const muArgs = [
|
|
|
+ "P",
|
|
|
+ String.raw`K_{\text{big}}\left(P\right)`,
|
|
|
+ String.raw`K_{\text{small}}\left(P\right)`,
|
|
|
+ String.raw`K_{\text{best}}\left(P\right)`,
|
|
|
+ String.raw`K_{\text{worst}}\left(P\right)`,
|
|
|
+];
|
|
|
|
|
|
const renderVec = math => String.raw`\vec{${math.charAt(0)}}${math.substr(1)}`;
|
|
|
const renderNorm = vec => String.raw`\frac{${vec}}{\left|\left|${vec}\right|\right|}`;
|
|
|
const updateObjective = () => {
|
|
|
- let tex = metricText?.[state.metric];
|
|
|
+ const muArg = muArgs[state.meanArgument];
|
|
|
+ let tex = metricText?.[state.metric]?.(muArg);
|
|
|
if (!tex) {
|
|
|
const { includeX, normQY, closeCoeff } = state;
|
|
|
if (!includeX && closeCoeff === 0) {
|
|
|
tex = TeXZilla.toMathML(String.raw`\text{Empty Metric}`);
|
|
|
} else {
|
|
|
- const qyMod = normQY ? c => renderNorm(renderVec(c)) : renderVec;
|
|
|
+ const qyMod = normQY ? renderNorm : c => c;
|
|
|
tex = TeXZilla.toMathML(String.raw`
|
|
|
- \arg
|
|
|
- \m${includeX ? "in" : "ax"}_{P}
|
|
|
+ ${mathArgBest(includeX ? "min" : "max", "P")}
|
|
|
\left[
|
|
|
- ${includeX ? String.raw`X\left(P\right)` : ""}
|
|
|
+ ${includeX ? String.raw`I\left(P\right)` : ""}
|
|
|
${closeCoeff === 0 ? "" : String.raw`
|
|
|
${includeX ? "-" : ""}
|
|
|
${(includeX && closeCoeff !== 1) ? closeCoeff : ""}
|
|
|
- ${qyMod("q")}
|
|
|
+ ${qyMod("\\vec{q}")}
|
|
|
\cdot
|
|
|
- ${qyMod(String.raw`Y\left(P\right)`)}
|
|
|
+ ${qyMod(String.raw`\vec{\mu}\left(${muArg}\right)`)}
|
|
|
`}
|
|
|
\right]
|
|
|
`);
|
|
@@ -251,7 +274,7 @@ const getSprite = pokemon => {
|
|
|
};
|
|
|
|
|
|
const renderPokemon = (data, classes = {}) => {
|
|
|
- const { name, yJAB, yJABHex, yRGB, yRGBHex } = data;
|
|
|
+ const { name, jabStats, rgbStats, scores } = data;
|
|
|
const { labelClass = "", rgbClass = "", jabClass = "", tileClass = "" } = classes;
|
|
|
let { resultsClass = "" } = classes;
|
|
|
let displayMetrics = {};
|
|
@@ -264,14 +287,16 @@ const renderPokemon = (data, classes = {}) => {
|
|
|
const {
|
|
|
stdDevJAB = 0, stdDevRGB = 0,
|
|
|
angleJAB = 0, angleRGB = 0,
|
|
|
- meanDistJAB = 0, meanDistRGB,
|
|
|
+ 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(yRGB);
|
|
|
- const rgbVec = yRGB.map(c => c.toFixed()).join(", ");
|
|
|
- const jabVec = yJAB.map(c => c.toFixed(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}`);
|
|
@@ -280,7 +305,9 @@ const renderPokemon = (data, classes = {}) => {
|
|
|
<img src="${getSprite(name)}" />
|
|
|
</div>
|
|
|
<div class="pokemon_tile-info_panel">
|
|
|
- <span class="pokemon_tile-pokemon_name">${titleName}</span>
|
|
|
+ <span class="pokemon_tile-pokemon_name">
|
|
|
+ ${titleName} ${scores?.jab?.toFixed(2) ?? ""} ${scores?.rgb?.toFixed(2) ?? ""}
|
|
|
+ </span>
|
|
|
<div class="pokemon_tile-results">
|
|
|
<div class="pokemon_tile-labels ${labelClass}">
|
|
|
<span class="${jabClass}">Jab: </span>
|
|
@@ -295,11 +322,11 @@ const renderPokemon = (data, classes = {}) => {
|
|
|
</span>
|
|
|
</div>
|
|
|
<div class="pokemon_tile-hex_column">
|
|
|
- <div class="pokemon_tile-hex_color ${jabClass}" style="background-color: ${yJABHex}; color: ${textHex}">
|
|
|
- <span>${yJABHex}</span><span class="pokemon_tile-vector">(${jabVec})</span>
|
|
|
+ <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>
|
|
|
- <div class="pokemon_tile-hex_color ${rgbClass}" style="background-color: ${yRGBHex}; color: ${textHex}">
|
|
|
- <span>${yRGBHex}</span><span class="pokemon_tile-vector">(${rgbVec})</span>
|
|
|
+ <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>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -328,9 +355,8 @@ const rescore = () => {
|
|
|
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) }));
|
|
|
+ const scores = pokemonColorData.map(data => ({ ...data, scores: scorePokemon(data) }));
|
|
|
|
|
|
const jabList = getScoreListJABNode();
|
|
|
const appendJAB = getPokemonAppender(jabList);
|
|
@@ -339,14 +365,14 @@ const rescore = () => {
|
|
|
|
|
|
// extract best CIECAM02 results
|
|
|
const bestJAB = scores
|
|
|
- .sort((a, b) => a.scores[0] - b.scores[0])
|
|
|
+ .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" }));
|
|
|
|
|
|
// extract best RGB results
|
|
|
const bestRGB = scores
|
|
|
- .sort((a, b) => a.scores[1] - b.scores[1])
|
|
|
+ .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" }));
|
|
@@ -361,11 +387,11 @@ const onColorChanged = skipScore => {
|
|
|
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");
|
|
|
+ renderQVec(state.targetColor.jabData.vector.map(c => c.toFixed(2)), getQJABDisplay(), "Jab");
|
|
|
+ renderQVec(state.targetColor.rgbData.vector.map(c => c.toFixed()), getQRGBDisplay(), "RGB");
|
|
|
|
|
|
- const textColor = getContrastingTextColor(state.targetColor.qRGB);
|
|
|
- document.querySelector("body").setAttribute("style", `background: ${state.targetColor.qHex}; color: ${textColor}`);
|
|
|
+ const textColor = getContrastingTextColor(state.targetColor.rgbData.vector);
|
|
|
+ document.querySelector("body").setAttribute("style", `background: ${state.targetColor.rgbData.hex}; color: ${textColor}`);
|
|
|
state.targetColor
|
|
|
if (!skipScore) {
|
|
|
rescore();
|
|
@@ -392,13 +418,37 @@ const onCustomControlsChanged = skipScore => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+const checkClusterMeanWarning = () => {
|
|
|
+ const warning = getClusterMeanWarning();
|
|
|
+ const unhidden = warning.getAttribute("class").replaceAll("hide", "");
|
|
|
+ if (state.meanArgument !== 0 && state.metric !== 1 && state.metric !== 2) {
|
|
|
+ warning.setAttribute("class", unhidden);
|
|
|
+ } else {
|
|
|
+ warning.setAttribute("class", unhidden + " hide");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onMeanArgumentChanged = skipScore => {
|
|
|
+ const meanArgument = getMeanArgumentDropdownNode()?.selectedIndex ?? 0;
|
|
|
+ if (meanArgument === state.meanArgument) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ state.meanArgument = meanArgument;
|
|
|
+ checkClusterMeanWarning();
|
|
|
+ updateObjective();
|
|
|
+ if (!skipScore) {
|
|
|
+ rescore();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const onMetricChanged = skipScore => {
|
|
|
const metric = getMetricDropdownNode()?.selectedIndex ?? 0;
|
|
|
if (metric === state.metric) {
|
|
|
return;
|
|
|
}
|
|
|
state.metric = metric;
|
|
|
- if (state.metric === 6) { // Custom
|
|
|
+ checkClusterMeanWarning();
|
|
|
+ if (state.metric === 4) { // Custom
|
|
|
showCustomControls();
|
|
|
onCustomControlsChanged(skipScore); // triggers rescore
|
|
|
} else {
|
|
@@ -442,12 +492,13 @@ const onRandomPokemon = () => {
|
|
|
const onPageLoad = () => {
|
|
|
// render static explanations
|
|
|
Object.entries(mathDefinitions).forEach(([id, tex]) => {
|
|
|
- document.getElementById(id).appendChild(TeXZilla.toMathML(tex));
|
|
|
+ document.getElementById(id)?.appendChild(TeXZilla.toMathML(tex));
|
|
|
});
|
|
|
|
|
|
// fake some events but don't do any scoring
|
|
|
onColorChanged(true);
|
|
|
onMetricChanged(true);
|
|
|
+ onMeanArgumentChanged(true);
|
|
|
onLimitChanged(true);
|
|
|
// then do a rescore directly, which will do nothing unless old data was loaded
|
|
|
rescore();
|