|
@@ -1,51 +1,71 @@
|
|
const getSprite = pokemon => `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`
|
|
const getSprite = pokemon => `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`
|
|
|
|
|
|
-const vectorMag = v => Math.sqrt(v.map(x => x * x).reduce((x, y) => x + y));
|
|
|
|
|
|
+const vectorDot = (u, v) => u.map((x, i) => x * v[i]).reduce((x, y) => x + y);
|
|
|
|
|
|
-const onColorChange = () => {
|
|
|
|
- const numPoke = document.getElementById("num-poke")?.value;
|
|
|
|
- document.getElementById("num-poke-display").innerHTML = numPoke;
|
|
|
|
|
|
+const vectorMag = v => Math.sqrt(vectorDot(v, v));
|
|
|
|
|
|
- const closeCoeff = document.getElementById("close-coeff")?.value;
|
|
|
|
- document.getElementById("close-coeff-display").innerHTML = closeCoeff;
|
|
|
|
|
|
+// hex codes already include leading # in these functions
|
|
|
|
+// rgb values are [0, 1] in these functions
|
|
|
|
+const hex2luv = $.colorspaces.converter("hex", "CIELUV");
|
|
|
|
+const luv2hex = $.colorspaces.converter("CIELUV", "hex");
|
|
|
|
+const rgb2luv = $.colorspaces.converter("sRGB", "CIELUV");
|
|
|
|
+const luv2rgb = $.colorspaces.converter("CIELUV", "sRGB");
|
|
|
|
+const rgb2hex = $.colorspaces.converter("sRGB", "hex");
|
|
|
|
+const hex2rgb = $.colorspaces.converter("hex", "sRGB");
|
|
|
|
+
|
|
|
|
+// scoring functions
|
|
|
|
+const getNormedScorer = (c, q) => {
|
|
|
|
+ const factor = c / vectorMag(q);
|
|
|
|
+ return yVec => factor * vectorDot(q, yVec) / vectorMag(yVec);
|
|
|
|
+};
|
|
|
|
+const getUnnormedScorer = (c, q) => yVec => c * vectorDot(q, yVec);
|
|
|
|
|
|
|
|
+const onUpdate = () => {
|
|
|
|
+ // Configuration Loading
|
|
const includeX = document.getElementById("include-x")?.checked ?? false;
|
|
const includeX = document.getElementById("include-x")?.checked ?? false;
|
|
const normQY = document.getElementById("norm-q-y")?.checked ?? false;
|
|
const normQY = document.getElementById("norm-q-y")?.checked ?? false;
|
|
|
|
+ const closeCoeff = document.getElementById("close-coeff")?.value ?? 2;
|
|
|
|
+ const useRGB = document.getElementById("color-space")?.textContent === "RGB";
|
|
|
|
+ const numPoke = document.getElementById("num-poke")?.value ?? 20;
|
|
|
|
+ const pokemonName = document.getElementById("pokemon-name")?.value ?? "";
|
|
|
|
+ const targetColor = "#" + (document.getElementById("color-input")?.value?.replace("#", "") ?? "FFFFFF");
|
|
|
|
+ const targetRGB = hex2rgb(targetColor).map(x => x * 255);
|
|
|
|
+
|
|
|
|
+ // Update display values
|
|
|
|
+ document.getElementById("x-term").textContent = includeX ? "X(P)" : "";
|
|
|
|
+ document.getElementById("c-value").textContent = closeCoeff;
|
|
|
|
+ document.getElementById("q-vec").textContent = normQY ? "q̂" : "q";
|
|
|
|
+ document.getElementById("y-vec").textContent = normQY ? "Ŷ(P)" : "Y(P)";
|
|
|
|
+ document.getElementById("close-coeff-display").innerHTML = closeCoeff;
|
|
|
|
+ document.getElementById("num-poke-display").textContent = numPoke;
|
|
|
|
+
|
|
|
|
+ // calculate luminance to determine if text should be dark or light
|
|
|
|
+ const textColor = vectorDot(targetRGB, [0.3, 0.6, 0.1]) >= 128 ? "#222" : "#ddd";
|
|
|
|
+ document.querySelector("body").setAttribute("style", `background: ${targetColor}; color: ${textColor}`);
|
|
|
|
|
|
const bestList = document.getElementById("best-list");
|
|
const bestList = document.getElementById("best-list");
|
|
bestList.innerHTML = ''; // do the lazy thing
|
|
bestList.innerHTML = ''; // do the lazy thing
|
|
|
|
|
|
- const color = document.getElementById("color-input")?.value;
|
|
|
|
- if (color.length !== 6) {
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
- document.querySelector("body").setAttribute("style", `background: #${color}`);
|
|
|
|
- const colorValues = [
|
|
|
|
- Number.parseInt(color.substring(0, 2), 16),
|
|
|
|
- Number.parseInt(color.substring(2, 4), 16),
|
|
|
|
- Number.parseInt(color.substring(4, 6), 16),
|
|
|
|
- ];
|
|
|
|
- const colorMag = vectorMag(colorValues);
|
|
|
|
|
|
+ // determine metrics from configuration
|
|
|
|
+ const targetInSpace = useRGB ? targetRGB : rgb2luv(targetRGB.map(x => x / 255));
|
|
|
|
+ const xSelector = includeX ? (useRGB ? ({ xRGB }) => xRGB : ({ xLUV }) => xLUV) : () => 0;
|
|
|
|
+ const ySelector = useRGB ? ({ yRGB }) => yRGB : ({ yLUV }) => yLUV;
|
|
|
|
+ const yScorer = (normQY ? getNormedScorer : getUnnormedScorer)(closeCoeff, targetInSpace);
|
|
|
|
|
|
- database.map(([name, x, ...y]) => {
|
|
|
|
- const xComp = includeX ? x : 0;
|
|
|
|
- const norm = normQY ? vectorMag(y) * colorMag : 1;
|
|
|
|
- return {
|
|
|
|
- name,
|
|
|
|
- score: xComp - closeCoeff * y.map((yc, i) => yc * colorValues[i] / norm).reduce((x, y) => x + y),
|
|
|
|
- avgColor: y,
|
|
|
|
- };
|
|
|
|
- }).sort((a, b) => a.score - b.score)
|
|
|
|
|
|
+ // actually score pokemon
|
|
|
|
+ database
|
|
|
|
+ .map(info => ({ ...info, score: xSelector(info) - yScorer(ySelector(info)) }))
|
|
|
|
+ .sort((a, b) => a.score - b.score)
|
|
.slice(0, numPoke)
|
|
.slice(0, numPoke)
|
|
- .forEach(({ name, avgColor}) => {
|
|
|
|
|
|
+ .forEach(({ name, score, yRGB }) => {
|
|
const li = document.createElement("li");
|
|
const li = document.createElement("li");
|
|
const img = document.createElement("img");
|
|
const img = document.createElement("img");
|
|
const tile = document.createElement("div");
|
|
const tile = document.createElement("div");
|
|
- const hexColor = avgColor.map(c => Math.round(c).toString(16).padStart(2, "0")).reduce((x, y) => x + y);
|
|
|
|
- tile.setAttribute("style", `width: 25px; height: 25px; background-color: #${hexColor}`)
|
|
|
|
|
|
+ const hexColor = rgb2hex(yRGB.map(x => x / 255));
|
|
|
|
+ tile.setAttribute("style", `width: 25px; height: 25px; background-color: ${hexColor}`)
|
|
img.setAttribute("src", getSprite(name));
|
|
img.setAttribute("src", getSprite(name));
|
|
li.appendChild(img)
|
|
li.appendChild(img)
|
|
- li.appendChild(document.createTextNode(name));
|
|
|
|
|
|
+ li.appendChild(document.createTextNode(`${name}: ${score.toFixed(3)}`));
|
|
li.appendChild(tile)
|
|
li.appendChild(tile)
|
|
li.setAttribute("style", "display: flex; flex-flow: row nowrap; justify-content: space-between; width: 320px")
|
|
li.setAttribute("style", "display: flex; flex-flow: row nowrap; justify-content: space-between; width: 320px")
|
|
bestList.appendChild(li);
|
|
bestList.appendChild(li);
|
|
@@ -53,27 +73,15 @@ const onColorChange = () => {
|
|
};
|
|
};
|
|
|
|
|
|
const onRandomColor = () => {
|
|
const onRandomColor = () => {
|
|
- const colorInput = document.getElementById("color-input");
|
|
|
|
- colorInput.value = hsvToRGB(Math.random(), 0.9, 0.9);
|
|
|
|
- onColorChange();
|
|
|
|
|
|
+ document.getElementById("color-input").value = rgb2hex([Math.random(), Math.random(), Math.random()]);
|
|
|
|
+ onUpdate();
|
|
};
|
|
};
|
|
|
|
|
|
-// mostly stolen from https://stackoverflow.com/a/17243070
|
|
|
|
-// adapted slightly
|
|
|
|
-const hsvToRGB = (h, s, v) => {
|
|
|
|
- var r, g, b, i, f, p, q, t;
|
|
|
|
- i = Math.floor(h * 6);
|
|
|
|
- f = h * 6 - i;
|
|
|
|
- p = v * (1 - s);
|
|
|
|
- q = v * (1 - f * s);
|
|
|
|
- t = v * (1 - (1 - f) * s);
|
|
|
|
- switch (i % 6) {
|
|
|
|
- case 0: r = v, g = t, b = p; break;
|
|
|
|
- case 1: r = q, g = v, b = p; break;
|
|
|
|
- case 2: r = p, g = v, b = t; break;
|
|
|
|
- case 3: r = p, g = q, b = v; break;
|
|
|
|
- case 4: r = t, g = p, b = v; break;
|
|
|
|
- case 5: r = v, g = p, b = q; break;
|
|
|
|
- }
|
|
|
|
- return [r, g, b].map(c => Math.round(c * 255).toString(16).padStart(2, "0")).reduce((x, y) => x + y);
|
|
|
|
|
|
+const onToggleSpace = () => {
|
|
|
|
+ const element = document.getElementById("color-space");
|
|
|
|
+ const current = element?.textContent;
|
|
|
|
+ element.textContent = current === "RGB" ? "CIELUV" : "RGB";
|
|
|
|
+ document.getElementById("space-toggle").textContent = `Swap to ${current}`
|
|
|
|
+ onUpdate();
|
|
}
|
|
}
|
|
|
|
+
|