// Selectors + DOM Manipulation const getColorInputNode = () => document.getElementById("color-input"); const getMetricDropdownNode = () => document.getElementById("metric"); const getIncludeXToggleNode = () => document.getElementById("include-x"); const getNormQYToggleNode = () => document.getElementById("norm-q-y"); const getCloseCoeffSliderNode = () => document.getElementById("close-coeff"); const getCloseCoeffDisplayNode = () => document.getElementById("close-coeff-display"); const getLimitSliderNode = () => document.getElementById("num-poke"); const getLimitDisplayNode = () => document.getElementById("num-poke-display"); const getNameInputNode = () => document.getElementById("pokemon-name"); const getScoreListJABNode = () => document.getElementById("best-list-jab"); const getScoreListRGBNode = () => document.getElementById("best-list-rgb"); const getSearchListNode = () => document.getElementById("search-list"); const getHideableControlNodes = () => document.querySelectorAll(".hideable_control"); 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() .forEach(n => n.setAttribute("class", "hideable_control hideable_control--hidden")); const showCustomControls = () => getHideableControlNodes() .forEach(n => n.setAttribute("class", "hideable_control")); // Vector Math const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y); const vectorSqMag = v => vectorDot(v, v); const vectorMag = v => Math.sqrt(vectorSqMag(v)); const vectorSqDist = (u, v) => vectorSqMag(u.map((x, i) => x - v[i])); const vectorDist = (u, v) => Math.sqrt(vectorSqDist(u, v)); 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 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); 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, } }); const pokemonLookup = new Fuse(pokemonColorData, { keys: [ "name" ] }); // Color Calculations const getContrastingTextColor = rgb => vectorDot(rgb, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd"; const readColorInput = () => { const colorInput = "#" + (getColorInputNode()?.value?.replace("#", "") ?? "FFFFFF"); if (colorInput.length !== 7) { return; } 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, }; }; // State const state = { metric: null, includeX: null, normQY: null, closeCoeff: null, numPoke: null, searchTerm: null, targetColor: null, searchResults: null, }; // 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), ], ({ 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 calcDisplayMetrics = ({ xJAB, xRGB, yJAB, yRGB, yJABHat, yJABNorm, yRGBHat, yRGBNorm, yHueAngleJAB, yHueAngleRGB, }) => { // TODO - case on state.metric to avoid recalculation of subterms? const cosAngleJAB = vectorDot(state.targetColor.qJABHat, yJABHat); const yTermJAB = cosAngleJAB * yJABNorm * state.targetColor.qJABNorm; const cosAngleRGB = vectorDot(state.targetColor.qRGBHat, yRGBHat); const yTermRGB = cosAngleRGB * yRGBNorm * state.targetColor.qRGBNorm; return { stdDevRGB: Math.sqrt(xRGB - 2 * yTermRGB + state.targetColor.qRGBNormSq), stdDevJAB: Math.sqrt(xJAB - 2 * yTermJAB + state.targetColor.qJABNormSq), 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), }; }; // Math Rendering const renderQVec = (q, node, sub) => { node.innerHTML = TeXZilla.toMathMLString(String.raw`\vec{q}_{\text{${sub}}} = \left(\text{${q.join(", ")}}\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), \left|\left| \vec{q} - \vec{Y}\left(P\right) \right|\right|, \Delta{H} \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} = \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; 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(tex); }; // Pokemon Rendering const stripForm = ["flabebe", "floette", "florges", "vivillon", "basculin", "furfrou", "magearna"]; const getSprite = pokemon => { pokemon = pokemon .replace("-alola", "-alolan") .replace("-galar", "-galarian") .replace("darmanitan-galarian", "darmanitan-galarian-standard"); if (stripForm.find(s => pokemon.includes(s))) { pokemon = pokemon.replace(/-.*$/, ""); } return `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`; }; const renderPokemon = (data, classes = {}) => { const { name, yJAB, yJABHex, yRGB, yRGBHex } = data; 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, meanDistJAB = 0, meanDistRGB, 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 pkmn = document.createElement("div"); pkmn.setAttribute("class", "pokemon_tile"); pkmn.innerHTML = `