|
@@ -15,6 +15,9 @@ const getHideableControlNodes = () => document.querySelectorAll(".hideable_contr
|
|
|
const getQJABDisplay = () => document.getElementById("q-vec-jab");
|
|
|
const getQRGBDisplay = () => document.getElementById("q-vec-rgb");
|
|
|
const getObjFnDisplay = () => document.getElementById("obj-fn");
|
|
|
+const getXDefinitionDisplay = () => document.getElementById("x-definition");
|
|
|
+const getYDefinitionDisplay = () => document.getElementById("y-definition");
|
|
|
+const getResultDefinitionDisplay = () => document.getElementById("result-definition");
|
|
|
|
|
|
const clearNodeContents = node => { node.innerHTML = ""; };
|
|
|
const hideCustomControls = () => getHideableControlNodes()
|
|
@@ -29,14 +32,13 @@ const vectorNorm = v => { const n = vectorMag(v); return [ n, v.map(c => c / n)
|
|
|
|
|
|
// Angle Math
|
|
|
const angleDiff = (a, b) => { const raw = Math.abs(a - b); return raw < 180 ? raw : (360 - raw); };
|
|
|
-const acosDeg = v => Math.acos(v) * 180 / Math.PI;
|
|
|
+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);
|
|
|
- const [ , yChromaHat ] = vectorNorm(data.yJAB.slice(1));
|
|
|
+ const [ yRGBNorm, yRGBHat ] = vectorNorm(data.yRGB);
|
|
|
|
|
|
return {
|
|
|
...data,
|
|
@@ -44,8 +46,8 @@ const pokemonColorData = database.map(data => {
|
|
|
yJABNorm, yJABHat,
|
|
|
yRGBHex: yRGBColor.formatHex(),
|
|
|
yRGBNorm, yRGBHat,
|
|
|
- yChromaHat,
|
|
|
- yHueAngle: d3.hsl(yRGBColor).h,
|
|
|
+ yHueAngleJAB: rad2deg * Math.atan2(data.yJAB[2], data.yJAB[1]),
|
|
|
+ yHueAngleRGB: d3.hsl(yRGBColor).h,
|
|
|
}
|
|
|
});
|
|
|
const pokemonLookup = new Fuse(pokemonColorData, { keys: [ "name" ] });
|
|
@@ -68,16 +70,16 @@ const readColorInput = () => {
|
|
|
|
|
|
const [ qJABNorm, qJABHat ] = vectorNorm(qJAB);
|
|
|
const qJABNormSq = qJABNorm * qJABNorm;
|
|
|
- const [ , qChromaHat ] = vectorNorm(qJAB.slice(1));
|
|
|
|
|
|
const [ qRGBNorm, qRGBHat ] = vectorNorm(qRGB);
|
|
|
const qRGBNormSq = qRGBNorm * qRGBNorm;
|
|
|
- const qHueAngle = d3.hsl(rgb).h;
|
|
|
|
|
|
return {
|
|
|
qHex: rgb.formatHex(),
|
|
|
- qJAB, qJABHat, qJABNorm, qJABNormSq, qChromaHat,
|
|
|
- qRGB, qRGBHat, qRGBNorm, qRGBNormSq, qHueAngle,
|
|
|
+ qJAB, qJABHat, qJABNorm, qJABNormSq,
|
|
|
+ qRGB, qRGBHat, qRGBNorm, qRGBNormSq,
|
|
|
+ qHueAngleJAB: rad2deg * Math.atan2(b, a),
|
|
|
+ qHueAngleRGB: d3.hsl(rgb).h,
|
|
|
};
|
|
|
};
|
|
|
|
|
@@ -103,9 +105,9 @@ const scoringMetrics = [
|
|
|
-vectorDot(yJABHat, state.targetColor.qJABHat),
|
|
|
-vectorDot(yRGBHat, state.targetColor.qRGBHat),
|
|
|
],
|
|
|
- ({ yChromaHat, yHueAngle }) => [
|
|
|
- acosDeg(vectorDot(state.targetColor.qChromaHat, yChromaHat)),
|
|
|
- angleDiff(state.targetColor.qHueAngle, yHueAngle),
|
|
|
+ ({ yHueAngleJAB, yHueAngleRGB }) => [
|
|
|
+ angleDiff(state.targetColor.qHueAngleJAB, yHueAngleJAB),
|
|
|
+ angleDiff(state.targetColor.qHueAngleRGB, yHueAngleRGB),
|
|
|
],
|
|
|
({ xJAB, xRGB, yJAB, yRGB, yJABHat, yRGBHat }) => [
|
|
|
(state.includeX ? xJAB : 0) - state.closeCoeff * vectorDot(
|
|
@@ -120,7 +122,7 @@ const scoringMetrics = [
|
|
|
];
|
|
|
|
|
|
const calcDisplayMetrics = ({
|
|
|
- xJAB, xRGB, yJABHat, yJABNorm, yRGBHat, yRGBNorm, yChromaHat, yHueAngle,
|
|
|
+ xJAB, xRGB, yJABHat, yJABNorm, yRGBHat, yRGBNorm, yHueAngleJAB, yHueAngleRGB,
|
|
|
}) => {
|
|
|
// TODO - case on state.metric to avoid recalculation of subterms?
|
|
|
|
|
@@ -133,36 +135,66 @@ 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),
|
|
|
- chromaAngle: acosDeg(vectorDot(state.targetColor.qChromaHat, yChromaHat)),
|
|
|
- hueAngle: angleDiff(state.targetColor.qHueAngle, yHueAngle),
|
|
|
+ angleJAB: rad2deg * Math.acos(cosAngleJAB),
|
|
|
+ angleRGB: rad2deg * Math.acos(cosAngleRGB),
|
|
|
+ hueAngleJAB: angleDiff(state.targetColor.qHueAngleJAB, yHueAngleJAB),
|
|
|
+ hueAngleRGB: angleDiff(state.targetColor.qHueAngleRGB, yHueAngleRGB),
|
|
|
};
|
|
|
};
|
|
|
|
|
|
// Math Rendering
|
|
|
const renderQVec = (q, node, sub) => {
|
|
|
- node.innerHTML = TeXZilla.toMathMLString(`\\vec{q}_{\\text{${sub}}} = \\left(\\text{${q.join(", ")}}\\right)`);
|
|
|
+ node.innerHTML = TeXZilla.toMathMLString(String.raw`\vec{q}_{\text{${sub}}} = \left(\text{${q.join(", ")}}\right)`);
|
|
|
};
|
|
|
|
|
|
-const renderVec = math => `\\vec{${math.charAt(0)}}${math.substr(1)}`;
|
|
|
-const renderNorm = vec => `\\frac{${vec}}{\\left|\\left|${vec}\\right|\\right|}`;
|
|
|
+const xDefinition = TeXZilla.toMathML(String.raw`
|
|
|
+ X\left(P\right) = \frac{1}{\left|P\right|}\sum_{p\inP}{\left|\left|\vec{p}\right|\right|^2}
|
|
|
+`);
|
|
|
+const yDefinition = TeXZilla.toMathML(String.raw`
|
|
|
+ \vec{Y}\left(P\right) = \frac{1}{\left|P\right|}\sum_{p\inP}{\vec{p}}
|
|
|
+`);
|
|
|
+const resultDefinition = TeXZilla.toMathML(String.raw`
|
|
|
+ \left(
|
|
|
+ \text{RMS}_P\left(q\right),
|
|
|
+ \angle \left(\vec{q}, \vec{Y}\left(P\right)\right),
|
|
|
+ \Delta{H}
|
|
|
+ \right)
|
|
|
+`);
|
|
|
const metricText = [
|
|
|
- "\\text{RMS}_{P}\\left(q\\right) ~ \\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\\max_{P}\\left[\\cos\\left(\\angle \\left(\\vec{q}, \\vec{Y}\\left(P\\right)\\right)\\right)\\right]`,
|
|
|
- "\\angle \\left(\\vec{q}_{\\perp}, \\vec{Y}\\left(P\\right)_{\\perp} \\right), \\vec{v}_{\\perp} = \\text{oproj}_{\\left\\{\\vec{J}, \\vec{L}\\right\\}}{\\vec{v}}",
|
|
|
-];
|
|
|
+ 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`\Delta{H} = \angle \left(\vec{q}_{\perp}, \vec{Y}_{\perp}\left(P\right) \right), \vec{v}_{\perp} = \text{oproj}_{\left\{\vec{J}, \vec{L}\right\}}{\vec{v}}`,
|
|
|
+].map(s => TeXZilla.toMathML(s));
|
|
|
+
|
|
|
+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];
|
|
|
if (!tex) {
|
|
|
const { includeX, normQY, closeCoeff } = state;
|
|
|
- const xTerm = includeX ? "X\\left(P\\right)" : "";
|
|
|
- const qyMod = normQY ? c => renderNorm(renderVec(c)) : renderVec;
|
|
|
- tex = `\\arg\\${includeX ? "min" : "max"}_{P}\\left[${xTerm}${includeX ? "-" : ""}${closeCoeff === 1 ? "" : closeCoeff}${qyMod("q")}\\cdot ${qyMod("Y\\left(P\\right)")}\\right]`;
|
|
|
+ if (!includeX && closeCoeff === 0) {
|
|
|
+ tex = TeXZilla.toMathML(String.raw`\text{Empty Metric}`);
|
|
|
+ } else {
|
|
|
+ const qyMod = normQY ? c => renderNorm(renderVec(c)) : renderVec;
|
|
|
+ tex = TeXZilla.toMathML(String.raw`
|
|
|
+ \arg
|
|
|
+ \m${includeX ? "in" : "ax"}_{P}
|
|
|
+ \left[
|
|
|
+ ${includeX ? String.raw`X\left(P\right)` : ""}
|
|
|
+ ${closeCoeff === 0 ? "" : String.raw`
|
|
|
+ ${includeX ? "-" : ""}
|
|
|
+ ${(includeX && closeCoeff !== 1) ? closeCoeff : ""}
|
|
|
+ ${qyMod("q")}
|
|
|
+ \cdot
|
|
|
+ ${qyMod(String.raw`Y\left(P\right)`)}
|
|
|
+ `}
|
|
|
+ \right]
|
|
|
+ `);
|
|
|
+ }
|
|
|
}
|
|
|
const objFnNode = getObjFnDisplay();
|
|
|
clearNodeContents(objFnNode);
|
|
|
- objFnNode.appendChild(TeXZilla.toMathML(tex));
|
|
|
+ objFnNode.appendChild(tex);
|
|
|
};
|
|
|
|
|
|
// Pokemon Rendering
|
|
@@ -193,7 +225,7 @@ const renderPokemon = (data, classes = {}) => {
|
|
|
const {
|
|
|
stdDevJAB = 0, stdDevRGB = 0,
|
|
|
angleJAB = 0, angleRGB = 0,
|
|
|
- chromaAngle = 0, hueAngle = 0,
|
|
|
+ hueAngleJAB = 0, hueAngleRGB = 0,
|
|
|
} = displayMetrics;
|
|
|
|
|
|
const titleName = name.split("-").map(part => part.charAt(0).toUpperCase() + part.substr(1)).join(" ");
|
|
@@ -216,10 +248,10 @@ const renderPokemon = (data, classes = {}) => {
|
|
|
</div>
|
|
|
<div class="pokemon_tile-score_column ${resultsClass}">
|
|
|
<span class="pokemon_tile-no_flex ${jabClass}">
|
|
|
- (${stdDevJAB.toFixed(2)}, ${angleJAB.toFixed(2)}°, ${chromaAngle.toFixed(2)}°)
|
|
|
+ (${stdDevJAB.toFixed(2)}, ${angleJAB.toFixed(2)}°, ${hueAngleJAB.toFixed(2)}°)
|
|
|
</span>
|
|
|
<span class="pokemon_tile-no_flex ${rgbClass}">
|
|
|
- (${stdDevRGB.toFixed(2)}, ${angleRGB.toFixed(2)}°, ${hueAngle.toFixed(2)}°)
|
|
|
+ (${stdDevRGB.toFixed(2)}, ${angleRGB.toFixed(2)}°, ${hueAngleRGB.toFixed(2)}°)
|
|
|
</span>
|
|
|
</div>
|
|
|
<div class="pokemon_tile-hex_column">
|
|
@@ -368,6 +400,11 @@ const onRandomPokemon = () => {
|
|
|
};
|
|
|
|
|
|
const onPageLoad = () => {
|
|
|
+ // render static explanations
|
|
|
+ getXDefinitionDisplay().appendChild(xDefinition);
|
|
|
+ getYDefinitionDisplay().appendChild(yDefinition);
|
|
|
+ getResultDefinitionDisplay().appendChild(resultDefinition);
|
|
|
+
|
|
|
// fake some events but don't do any scoring
|
|
|
onColorChanged(true);
|
|
|
onMetricChanged(true);
|