// 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 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 vectorMag = v => Math.sqrt(vectorDot(v, 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 acosDeg = v => Math.acos(v) * 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)); return { ...data, yJABHex: d3.jab(...data.yJAB).formatHex(), yJABNorm, yJABHat, yRGBHex: yRGBColor.formatHex(), yRGBNorm, yRGBHat, yChromaHat, yHueAngle: 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 [ _, 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, }; }; // 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), ], ({ yChromaHat, yHueAngle }) => [ acosDeg(vectorDot(state.targetColor.qChromaHat, yChromaHat)), angleDiff(state.targetColor.qHueAngle, yHueAngle), ], ({ 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, yJABHat, yJABNorm, yRGBHat, yRGBNorm, yChromaHat, yHueAngle, }) => { // 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: acosDeg(cosAngleJAB), angleRGB: acosDeg(cosAngleRGB), chromaAngle: acosDeg(vectorDot(state.targetColor.qChromaHat, yChromaHat)), hueAngle: angleDiff(state.targetColor.qHueAngle, yHueAngle), }; }; // Math Rendering const renderQVec = (q, node, sub) => { node.innerHTML = TeXZilla.toMathMLString(`\\vec{q}_{\\text{${sub}}} = \\left(${q.join(", ")}\\right)`); }; 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)", ]; 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\\min_{P}\\left[${xTerm}-${closeCoeff === 1 ? "" : closeCoeff}${qyMod("q")}\\cdot ${qyMod("Y\\left(P\\right)")}\\right]`; } const objFnNode = getObjFnDisplay(); clearNodeContents(objFnNode); objFnNode.appendChild(TeXZilla.toMathML(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, chromaAngle = 0, hueAngle = 0, } = displayMetrics; const titleName = name.charAt(0).toUpperCase() + name.substr(1); 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 = `