|
@@ -1,7 +1,7 @@
|
|
|
// ---- Math and Utilities ----
|
|
|
// 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 vectorMag = v => Math.sqrt(vectorDot(v, v));
|
|
|
|
|
|
// Angle Math
|
|
|
const rad2deg = 180 / Math.PI;
|
|
@@ -12,14 +12,12 @@ const productLift =
|
|
|
(...factors) =>
|
|
|
(...args) =>
|
|
|
factors
|
|
|
- .filter((fn) => !!fn)
|
|
|
- .map((fn) => fn(...args))
|
|
|
+ .filter(fn => !!fn)
|
|
|
+ .map(fn => fn(...args))
|
|
|
.reduce((x, y) => x * y, 1);
|
|
|
-const mapValues = (obj, fn) =>
|
|
|
- Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, fn(value, key)]));
|
|
|
|
|
|
// Contrast + Shadow + Hover Colors
|
|
|
-const getContrastingTextColor = (hex) => {
|
|
|
+const getContrastingTextColor = hex => {
|
|
|
const { r, g, b } = d3.color(hex);
|
|
|
return vectorDot([r, g, b], [0.3, 0.6, 0.1]) >= 128
|
|
|
? "var(--color-dark)"
|
|
@@ -41,26 +39,30 @@ const calcImportance = (chroma, lightness, proportion) =>
|
|
|
Math.tanh(100 * (proportion - 0.8)); // penalty for being <50%
|
|
|
|
|
|
// Conversions
|
|
|
-const jab2hex = (jab) => d3.jab(...jab).formatHex();
|
|
|
-const rgb2hex = (rgb) => d3.rgb(...rgb).formatHex();
|
|
|
-const jab2hue = (jab) => d3.jch(d3.jab(...jab)).h || 0;
|
|
|
-const rgb2hue = (rgb) => d3.hsl(d3.rgb(...rgb)).h || 0;
|
|
|
+const jab2hex = jab => d3.jab(...jab).formatHex();
|
|
|
+const rgb2hex = rgb => d3.rgb(...rgb).formatHex();
|
|
|
+const jab2hue = jab => d3.jch(d3.jab(...jab)).h || 0;
|
|
|
+const rgb2hue = rgb => d3.hsl(d3.rgb(...rgb)).h || 0;
|
|
|
const jab2lit = ([j]) => j / 100;
|
|
|
-const rgb2lit = (rgb) => d3.hsl(d3.rgb(...rgb)).l || 0;
|
|
|
-const jab2chroma = (jab) => d3.jch(d3.jab(...jab)).C / 100;
|
|
|
-const rgb2chroma = (rgb) => d3.jch(d3.rgb(...rgb)).C / 100;
|
|
|
+const rgb2lit = rgb => d3.hsl(d3.rgb(...rgb)).l || 0;
|
|
|
+const jab2chroma = jab => d3.jch(d3.jab(...jab)).C / 100;
|
|
|
+const rgb2chroma = rgb => d3.jch(d3.rgb(...rgb)).C / 100;
|
|
|
|
|
|
// Pre-computation
|
|
|
const buildVectorData = (vector, toHue, toLightness, toChroma, toHex) => {
|
|
|
const sqMag = vectorDot(vector, vector);
|
|
|
const mag = Math.sqrt(sqMag);
|
|
|
- const unit = vector.map((c) => c / mag);
|
|
|
+ const unit = vector.map(c => c / mag);
|
|
|
const hue = toHue(vector);
|
|
|
const lightness = toLightness(vector);
|
|
|
const chroma = toChroma(vector);
|
|
|
const hex = toHex(vector);
|
|
|
return { vector, sqMag, mag, unit, hue, lightness, chroma, hex };
|
|
|
};
|
|
|
+const buildVectorDataJab = vector =>
|
|
|
+ buildVectorData(vector, jab2hue, jab2lit, jab2chroma, jab2hex);
|
|
|
+const buildVectorDataRgb = vector =>
|
|
|
+ buildVectorData(vector, rgb2hue, rgb2lit, rgb2chroma, rgb2hex);
|
|
|
|
|
|
const buildClusterData = (
|
|
|
size,
|
|
@@ -72,12 +74,9 @@ const buildClusterData = (
|
|
|
nu2,
|
|
|
nu3,
|
|
|
totalSize,
|
|
|
- toHue,
|
|
|
- toLightness,
|
|
|
- toChroma,
|
|
|
- toHex
|
|
|
+ buildVectorDataForSpace
|
|
|
) => {
|
|
|
- const mu = buildVectorData([mu1, mu2, mu3], toHue, toLightness, toChroma, toHex);
|
|
|
+ const mu = buildVectorDataForSpace([mu1, mu2, mu3]);
|
|
|
const nu = [nu1, nu2, nu3];
|
|
|
const muNuAngle = rad2deg * Math.acos(vectorDot(mu.unit, nu) / vectorMag(nu));
|
|
|
const proportion = size / totalSize;
|
|
@@ -98,98 +97,26 @@ const buildClusterData = (
|
|
|
const buildPokemonData = ([name, size, ...values]) => ({
|
|
|
name,
|
|
|
jab: {
|
|
|
- total: buildClusterData(
|
|
|
- size,
|
|
|
- ...values.slice(0, 7),
|
|
|
- size,
|
|
|
- jab2hue,
|
|
|
- jab2lit,
|
|
|
- jab2chroma,
|
|
|
- jab2hex
|
|
|
- ),
|
|
|
+ total: buildClusterData(size, ...values.slice(0, 7), size, buildVectorDataJab),
|
|
|
clusters: [
|
|
|
- buildClusterData(
|
|
|
- ...values.slice(7, 15),
|
|
|
- size,
|
|
|
- jab2hue,
|
|
|
- jab2lit,
|
|
|
- jab2chroma,
|
|
|
- jab2hex
|
|
|
- ),
|
|
|
- buildClusterData(
|
|
|
- ...values.slice(15, 23),
|
|
|
- size,
|
|
|
- jab2hue,
|
|
|
- jab2lit,
|
|
|
- jab2chroma,
|
|
|
- jab2hex
|
|
|
- ),
|
|
|
- buildClusterData(
|
|
|
- ...values.slice(23, 31),
|
|
|
- size,
|
|
|
- jab2hue,
|
|
|
- jab2lit,
|
|
|
- jab2chroma,
|
|
|
- jab2hex
|
|
|
- ),
|
|
|
- buildClusterData(
|
|
|
- ...values.slice(31, 39),
|
|
|
- size,
|
|
|
- jab2hue,
|
|
|
- jab2lit,
|
|
|
- jab2chroma,
|
|
|
- jab2hex
|
|
|
- ),
|
|
|
- ].filter((c) => c.size !== 0),
|
|
|
+ buildClusterData(...values.slice(7, 15), size, buildVectorDataJab),
|
|
|
+ buildClusterData(...values.slice(15, 23), size, buildVectorDataJab),
|
|
|
+ buildClusterData(...values.slice(23, 31), size, buildVectorDataJab),
|
|
|
+ buildClusterData(...values.slice(31, 39), size, buildVectorDataJab),
|
|
|
+ ].filter(c => c.size !== 0),
|
|
|
},
|
|
|
rgb: {
|
|
|
- total: buildClusterData(
|
|
|
- size,
|
|
|
- ...values.slice(39, 46),
|
|
|
- size,
|
|
|
- rgb2hue,
|
|
|
- rgb2lit,
|
|
|
- rgb2chroma,
|
|
|
- rgb2hex
|
|
|
- ),
|
|
|
+ total: buildClusterData(size, ...values.slice(39, 46), size, buildVectorDataRgb),
|
|
|
clusters: [
|
|
|
- buildClusterData(
|
|
|
- ...values.slice(46, 54),
|
|
|
- size,
|
|
|
- rgb2hue,
|
|
|
- rgb2lit,
|
|
|
- rgb2chroma,
|
|
|
- rgb2hex
|
|
|
- ),
|
|
|
- buildClusterData(
|
|
|
- ...values.slice(54, 62),
|
|
|
- size,
|
|
|
- rgb2hue,
|
|
|
- rgb2lit,
|
|
|
- rgb2chroma,
|
|
|
- rgb2hex
|
|
|
- ),
|
|
|
- buildClusterData(
|
|
|
- ...values.slice(62, 70),
|
|
|
- size,
|
|
|
- rgb2hue,
|
|
|
- rgb2lit,
|
|
|
- rgb2chroma,
|
|
|
- rgb2hex
|
|
|
- ),
|
|
|
- buildClusterData(
|
|
|
- ...values.slice(70, 78),
|
|
|
- size,
|
|
|
- rgb2hue,
|
|
|
- rgb2lit,
|
|
|
- rgb2chroma,
|
|
|
- rgb2hex
|
|
|
- ),
|
|
|
- ].filter((c) => c.size !== 0),
|
|
|
+ buildClusterData(...values.slice(46, 54), size, buildVectorDataRgb),
|
|
|
+ buildClusterData(...values.slice(54, 62), size, buildVectorDataRgb),
|
|
|
+ buildClusterData(...values.slice(62, 70), size, buildVectorDataRgb),
|
|
|
+ buildClusterData(...values.slice(70, 78), size, buildVectorDataRgb),
|
|
|
+ ].filter(c => c.size !== 0),
|
|
|
},
|
|
|
});
|
|
|
|
|
|
-const pokemonData = databaseV3.map((row) => buildPokemonData(row));
|
|
|
+const pokemonData = databaseV3.map(row => buildPokemonData(row));
|
|
|
|
|
|
const calcScores = (data, target) => {
|
|
|
const sigma = Math.sqrt(
|
|
@@ -227,6 +154,11 @@ const calcScores = (data, target) => {
|
|
|
};
|
|
|
};
|
|
|
|
|
|
+const sortOrders = {
|
|
|
+ max: (a, b) => b - a,
|
|
|
+ min: (a, b) => a - b,
|
|
|
+};
|
|
|
+
|
|
|
// ---- Styling ----
|
|
|
|
|
|
const rootStyle = document.querySelector(":root").style;
|
|
@@ -238,20 +170,193 @@ const setColorStyles = (style, hex) => {
|
|
|
style.setProperty("--shadow-component", highlight.includes("light") ? "255" : "0");
|
|
|
};
|
|
|
|
|
|
-// ---- List Render ----
|
|
|
+// ---- Pokemon Display ----
|
|
|
+
|
|
|
+// pulled out bc the render uses them
|
|
|
+const metricScores = {};
|
|
|
+const bestClusterIndices = {};
|
|
|
+const objectiveValues = {};
|
|
|
+
|
|
|
+const pokemonTileTemplate = document.getElementById("pkmn-tile-template").content;
|
|
|
+const pokemonDataTemplate = document.getElementById("pkmn-data-template").content;
|
|
|
+
|
|
|
+const loadTemplateWithBinds = content => {
|
|
|
+ const fragment = content.cloneNode(true);
|
|
|
+ const binds = Object.fromEntries(
|
|
|
+ Array.from(fragment.querySelectorAll("[bind-to]")).map(element => {
|
|
|
+ const name = element.getAttribute("bind-to");
|
|
|
+ element.removeAttribute("bind-to");
|
|
|
+ return [name, element];
|
|
|
+ })
|
|
|
+ );
|
|
|
+ return [fragment, binds];
|
|
|
+};
|
|
|
+
|
|
|
+const getSpriteName = (() => {
|
|
|
+ const stripForm = [
|
|
|
+ "flabebe",
|
|
|
+ "floette",
|
|
|
+ "florges",
|
|
|
+ "vivillon",
|
|
|
+ "basculin",
|
|
|
+ "furfrou",
|
|
|
+ "magearna",
|
|
|
+ "alcremie",
|
|
|
+ ];
|
|
|
+ return pokemon => {
|
|
|
+ pokemon = pokemon
|
|
|
+ .replace("-alola", "-alolan")
|
|
|
+ .replace("-galar", "-galarian")
|
|
|
+ .replace("-hisui", "-hisuian")
|
|
|
+ .replace("-paldea", "-paldean")
|
|
|
+ .replace("-paldeanfire", "-paldean-fire") // tauros
|
|
|
+ .replace("-paldeanwater", "-paldean-water") // tauros
|
|
|
+ .replace("-phony", "") // sinistea and polteageist
|
|
|
+ .replace("darmanitan-galarian", "darmanitan-galarian-standard")
|
|
|
+ .replace("chienpao", "chien-pao")
|
|
|
+ .replace("tinglu", "ting-lu")
|
|
|
+ .replace("wochien", "wo-chien")
|
|
|
+ .replace("chiyu", "chi-yu");
|
|
|
+ if (stripForm.find(s => pokemon.includes(s))) {
|
|
|
+ pokemon = pokemon.replace(/-.*$/, "");
|
|
|
+ }
|
|
|
+ return pokemon;
|
|
|
+ };
|
|
|
+})();
|
|
|
+
|
|
|
+const formatName = name =>
|
|
|
+ name
|
|
|
+ .split("-")
|
|
|
+ .map(part => part.charAt(0).toUpperCase() + part.substr(1))
|
|
|
+ .join(" ");
|
|
|
|
|
|
const renderPokemon = (list, target) => {
|
|
|
- target.innerHTML = "";
|
|
|
- // TODO
|
|
|
+ target.innerText = "";
|
|
|
+
|
|
|
+ const {
|
|
|
+ sortUseWholeImage,
|
|
|
+ sortUseBestCluster,
|
|
|
+ sortUseClusterSize,
|
|
|
+ sortUseInvClusterSize,
|
|
|
+ sortUseTotalSize,
|
|
|
+ sortUseInvTotalSize,
|
|
|
+ } = Object.fromEntries(new FormData(document.forms.colorCalculateForm).entries());
|
|
|
+
|
|
|
+ const enableTotalFlags = !!(
|
|
|
+ sortUseWholeImage ||
|
|
|
+ sortUseTotalSize ||
|
|
|
+ sortUseInvTotalSize
|
|
|
+ );
|
|
|
+ const enableClusterFlags = !!(
|
|
|
+ sortUseBestCluster ||
|
|
|
+ sortUseClusterSize ||
|
|
|
+ sortUseInvClusterSize
|
|
|
+ );
|
|
|
+
|
|
|
+ list.forEach(pkmnName => {
|
|
|
+ const [tile, { image, name, score, ...binds }] =
|
|
|
+ loadTemplateWithBinds(pokemonTileTemplate);
|
|
|
+
|
|
|
+ const spriteName = getSpriteName(pkmnName);
|
|
|
+ const imageErrHandler = () => {
|
|
|
+ image.removeEventListener("error", imageErrHandler);
|
|
|
+ image.src = `https://img.pokemondb.net/sprites/sword-shield/icon/${spriteName}.png`;
|
|
|
+ };
|
|
|
+ image.addEventListener("error", imageErrHandler);
|
|
|
+ image.src = `https://img.pokemondb.net/sprites/scarlet-violet/icon/${spriteName}.png`;
|
|
|
+
|
|
|
+ name.innerText = name.title = image.alt = formatName(pkmnName);
|
|
|
+
|
|
|
+ const colorSpace = document.forms.colorSortForm.elements.colorSpace.value;
|
|
|
+
|
|
|
+ score.innerText = objectiveValues[pkmnName][colorSpace].toFixed(2);
|
|
|
+
|
|
|
+ const { total, clusters } = metricScores[pkmnName][colorSpace];
|
|
|
+ [
|
|
|
+ [clusters[0], binds.cls1Btn, binds.cls1Data],
|
|
|
+ [clusters[1], binds.cls2Btn, binds.cls2Data],
|
|
|
+ [clusters[2], binds.cls3Btn, binds.cls3Data],
|
|
|
+ [clusters[3], binds.cls4Btn, binds.cls4Data],
|
|
|
+ [total, binds.totalBtn, binds.totalData],
|
|
|
+ ]
|
|
|
+ .filter(([data]) => !!data)
|
|
|
+ .forEach(([data, button, dataTile], index) => {
|
|
|
+ button.hidden = false;
|
|
|
+ button.innerText = data.muHex;
|
|
|
+ button.dataset.included =
|
|
|
+ enableClusterFlags && index === bestClusterIndices[pkmnName][colorSpace];
|
|
|
+ setColorStyles(button.style, data.muHex);
|
|
|
+ button.addEventListener("click", () => {
|
|
|
+ model.setTargetColor(data.muHex);
|
|
|
+ });
|
|
|
+
|
|
|
+ const [tooltip, tooltipBinds] = loadTemplateWithBinds(pokemonDataTemplate);
|
|
|
+ Object.entries(tooltipBinds).forEach(([metricName, target]) => {
|
|
|
+ target.innerText = data[metricName].toFixed(2).replace(".00", "");
|
|
|
+ });
|
|
|
+
|
|
|
+ dataTile.append(tooltip);
|
|
|
+ });
|
|
|
+
|
|
|
+ binds.totalBtn.dataset.included = enableTotalFlags;
|
|
|
+
|
|
|
+ target.append(tile);
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
-// ---- Shared State ----
|
|
|
+const colorSearchResultsTarget = document.getElementById("color-results");
|
|
|
+const nameSearchResultsTarget = document.getElementById("name-results");
|
|
|
|
|
|
-const state = {
|
|
|
- get targetColor() {
|
|
|
- return this._targetColor || "";
|
|
|
- },
|
|
|
- set targetColor(newColor) {
|
|
|
+// ---- Name Lookup ----
|
|
|
+
|
|
|
+const lookupLimit = 24;
|
|
|
+const pokemonLookup = new Fuse(pokemonData, { keys: ["name"] });
|
|
|
+const nameSearchResults = [];
|
|
|
+
|
|
|
+const renderNameSearchResults = () => {
|
|
|
+ renderPokemon(nameSearchResults, nameSearchResultsTarget);
|
|
|
+};
|
|
|
+
|
|
|
+document.forms.nameSearchForm.elements.input.addEventListener(
|
|
|
+ "input",
|
|
|
+ ({ target: { value } }) => {
|
|
|
+ nameSearchResults.splice(
|
|
|
+ 0,
|
|
|
+ Infinity,
|
|
|
+ ...pokemonLookup
|
|
|
+ .search(value, { limit: lookupLimit })
|
|
|
+ .map(({ item: { name } }) => name)
|
|
|
+ );
|
|
|
+ renderNameSearchResults();
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+document.forms.nameSearchForm.elements.clear.addEventListener("click", () => {
|
|
|
+ nameSearchResults.splice(0);
|
|
|
+ document.forms.nameSearchForm.elements.input.value = "";
|
|
|
+ renderNameSearchResults();
|
|
|
+});
|
|
|
+
|
|
|
+document.forms.nameSearchForm.elements.random.addEventListener("click", () => {
|
|
|
+ nameSearchResults.splice(
|
|
|
+ 0,
|
|
|
+ Infinity,
|
|
|
+ ...Array.from(
|
|
|
+ { length: lookupLimit },
|
|
|
+ () => pokemonData[Math.floor(Math.random() * pokemonData.length)].name
|
|
|
+ )
|
|
|
+ );
|
|
|
+ renderNameSearchResults();
|
|
|
+});
|
|
|
+
|
|
|
+// ---- Calculation Logic ----
|
|
|
+
|
|
|
+const model = new (class {
|
|
|
+ #targetColor = "";
|
|
|
+
|
|
|
+ ranked = [];
|
|
|
+
|
|
|
+ setTargetColor(newColor) {
|
|
|
const hex = `#${newColor?.replace("#", "")}`;
|
|
|
if (hex.length !== 7) {
|
|
|
return;
|
|
@@ -259,51 +364,126 @@ const state = {
|
|
|
|
|
|
setColorStyles(rootStyle, hex);
|
|
|
|
|
|
- const oldColor = this._targetColor;
|
|
|
- this._targetColor = hex;
|
|
|
+ const oldColor = this.#targetColor;
|
|
|
+ this.#targetColor = hex;
|
|
|
+
|
|
|
+ document.forms.targetColorForm.elements.colorText.value = hex;
|
|
|
+ document.forms.targetColorForm.elements.colorText.dataset.lastValid = hex;
|
|
|
+ document.forms.targetColorForm.elements.colorPicker.value = hex;
|
|
|
|
|
|
if (oldColor) {
|
|
|
const prevButton = document.createElement("button");
|
|
|
prevButton.innerText = oldColor;
|
|
|
prevButton.classList = "color-select";
|
|
|
setColorStyles(prevButton.style, oldColor);
|
|
|
- prevButton.addEventListener("click", () => (this.targetColor = oldColor));
|
|
|
+ prevButton.addEventListener("click", () => this.setTargetColor(oldColor));
|
|
|
document.getElementById("prevColors").prepend(prevButton);
|
|
|
}
|
|
|
|
|
|
- document.forms.targetColorForm.elements.colorText.value = hex;
|
|
|
- document.forms.targetColorForm.elements.colorPicker.value = hex;
|
|
|
+ const rgb = d3.rgb(hex);
|
|
|
+ const { J, a, b } = d3.jab(rgb);
|
|
|
+ const targetJab = buildVectorDataJab([J, a, b]);
|
|
|
+ const targetRgb = buildVectorDataRgb([rgb.r, rgb.g, rgb.b]);
|
|
|
+
|
|
|
+ pokemonData.forEach(({ name, jab, rgb }) => {
|
|
|
+ metricScores[name] = {
|
|
|
+ jab: {
|
|
|
+ total: calcScores(jab.total, targetJab),
|
|
|
+ clusters: jab.clusters.map(c => calcScores(c, targetJab)),
|
|
|
+ },
|
|
|
+ rgb: {
|
|
|
+ total: calcScores(rgb.total, targetRgb),
|
|
|
+ clusters: rgb.clusters.map(c => calcScores(c, targetRgb)),
|
|
|
+ },
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ this.calculateObjective();
|
|
|
+ }
|
|
|
|
|
|
- // TODO trigger recalc
|
|
|
- },
|
|
|
+ calculateObjective() {
|
|
|
+ const {
|
|
|
+ clusterUseClusterSize,
|
|
|
+ clusterUseInvClusterSize,
|
|
|
+ clusterUseTotalSize,
|
|
|
+ clusterUseInvTotalSize,
|
|
|
+ clusterSortOrder,
|
|
|
+ sortUseWholeImage,
|
|
|
+ sortUseBestCluster,
|
|
|
+ sortUseClusterSize,
|
|
|
+ sortUseInvClusterSize,
|
|
|
+ sortUseTotalSize,
|
|
|
+ sortUseInvTotalSize,
|
|
|
+ } = Object.fromEntries(new FormData(document.forms.colorCalculateForm).entries());
|
|
|
+
|
|
|
+ const clsMetric = document.forms.clusterMetricForm.elements.metric.value;
|
|
|
+ const getClusterScore = productLift(
|
|
|
+ cluster => cluster[clsMetric],
|
|
|
+ clusterUseClusterSize && (cluster => cluster.size),
|
|
|
+ clusterUseInvClusterSize && (cluster => cluster.inverseSize),
|
|
|
+ clusterUseTotalSize && ((_, total) => total.size),
|
|
|
+ clusterUseInvTotalSize && ((_, total) => total.inverseSize)
|
|
|
+ );
|
|
|
+
|
|
|
+ const clsSort = sortOrders[clusterSortOrder];
|
|
|
+ const getBestClusterIndex = ({ total, clusters }) =>
|
|
|
+ clusters
|
|
|
+ .map((c, i) => [getClusterScore(c, total), i])
|
|
|
+ .reduce((a, b) => (clsSort(a[0], b[0]) > 0 ? b : a))[1];
|
|
|
+
|
|
|
+ Object.entries(metricScores).forEach(([name, { jab, rgb }]) => {
|
|
|
+ bestClusterIndices[name] = {
|
|
|
+ jab: getBestClusterIndex(jab),
|
|
|
+ rgb: getBestClusterIndex(rgb),
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ const metric = document.forms.sortMetricForm.elements.metric.value;
|
|
|
+ const getSortScore = productLift(
|
|
|
+ sortUseWholeImage && (({ total }) => total[metric]),
|
|
|
+ sortUseBestCluster && (({ clusters }, i) => clusters[i][metric]),
|
|
|
+ sortUseClusterSize && (({ clusters }, i) => clusters[i].size),
|
|
|
+ sortUseInvClusterSize && (({ clusters }, i) => clusters[i].inverseSize),
|
|
|
+ sortUseTotalSize && (({ total }) => total.size),
|
|
|
+ sortUseInvTotalSize && (({ total }) => total.inverseSize)
|
|
|
+ );
|
|
|
+
|
|
|
+ Object.entries(metricScores).forEach(([name, { jab, rgb }]) => {
|
|
|
+ objectiveValues[name] = {
|
|
|
+ jab: getSortScore(jab, bestClusterIndices[name].jab),
|
|
|
+ rgb: getSortScore(rgb, bestClusterIndices[name].rgb),
|
|
|
+ };
|
|
|
+ });
|
|
|
|
|
|
- get colorSearchResults() {
|
|
|
- return this._colorSearchResults || [];
|
|
|
- },
|
|
|
- set colorSearchResults(results) {
|
|
|
- this._colorSearchResults = results;
|
|
|
- renderColorSearchResults();
|
|
|
- },
|
|
|
-
|
|
|
- get nameSearchResults() {
|
|
|
- return this._nameSearchResults || [];
|
|
|
- },
|
|
|
- set nameSearchResults(results) {
|
|
|
- this._nameSearchResults = results;
|
|
|
renderNameSearchResults();
|
|
|
- },
|
|
|
-};
|
|
|
+ this.rank();
|
|
|
+ }
|
|
|
|
|
|
-const colorSearchResultsTarget = document.getElementById("color-results");
|
|
|
-const nameSearchResultsTarget = document.getElementById("name-results");
|
|
|
+ rank() {
|
|
|
+ const { colorSpace, sortOrder } = Object.fromEntries(
|
|
|
+ new FormData(document.forms.colorSortForm).entries()
|
|
|
+ );
|
|
|
+ const compare = sortOrders[sortOrder];
|
|
|
+ const sortFn = (a, b) =>
|
|
|
+ compare(objectiveValues[a][colorSpace], objectiveValues[b][colorSpace]);
|
|
|
|
|
|
-function renderColorSearchResults() {
|
|
|
- renderPokemon(state.colorSearchResults, colorSearchResultsTarget);
|
|
|
-}
|
|
|
+ this.ranked = pokemonData
|
|
|
+ .map(({ name }) => name)
|
|
|
+ .sort((a, b) => sortFn(a, b) || a.localeCompare(b));
|
|
|
+
|
|
|
+ this.renderColorSearchResults();
|
|
|
+ }
|
|
|
|
|
|
-function renderNameSearchResults() {
|
|
|
- renderPokemon(state.nameSearchResults, nameSearchResultsTarget);
|
|
|
-}
|
|
|
+ renderColorSearchResults() {
|
|
|
+ renderPokemon(
|
|
|
+ this.ranked.slice(
|
|
|
+ 0,
|
|
|
+ parseInt(document.forms.colorDisplayForm.elements.resultsToDisplay.value)
|
|
|
+ ),
|
|
|
+ colorSearchResultsTarget
|
|
|
+ );
|
|
|
+ }
|
|
|
+})();
|
|
|
|
|
|
// ---- Form Controls ----
|
|
|
|
|
@@ -313,26 +493,112 @@ document.forms.targetColorForm.elements.colorText.addEventListener(
|
|
|
if (target.willValidate && !target.validity.valid) {
|
|
|
target.value = target.dataset.lastValid || "";
|
|
|
} else {
|
|
|
- state.targetColor = target.dataset.lastValid = target.value;
|
|
|
+ model.setTargetColor(target.value);
|
|
|
}
|
|
|
}
|
|
|
);
|
|
|
|
|
|
document.forms.targetColorForm.elements.colorPicker.addEventListener(
|
|
|
"change",
|
|
|
- ({ target }) => {
|
|
|
- state.targetColor = target.value;
|
|
|
- }
|
|
|
+ ({ target }) => model.setTargetColor(target.value)
|
|
|
);
|
|
|
|
|
|
const randomizeTargetColor = () =>
|
|
|
- (state.targetColor = d3
|
|
|
- .hsl(Math.random() * 360, Math.random(), Math.random())
|
|
|
- .formatHex());
|
|
|
+ model.setTargetColor(
|
|
|
+ d3.hsl(Math.random() * 360, Math.random(), Math.random()).formatHex()
|
|
|
+ );
|
|
|
|
|
|
document.forms.targetColorForm.elements.randomColor.addEventListener(
|
|
|
"click",
|
|
|
randomizeTargetColor
|
|
|
);
|
|
|
|
|
|
+document.forms.colorDisplayForm.elements.resultsToDisplay.addEventListener(
|
|
|
+ "input",
|
|
|
+ ({ target: { value } }) => {
|
|
|
+ document.forms.colorDisplayForm.elements.output.value = value;
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+document.forms.colorDisplayForm.elements.resultsToDisplay.addEventListener("change", () =>
|
|
|
+ model.renderColorSearchResults()
|
|
|
+);
|
|
|
+
|
|
|
+Array.from(document.forms.colorSortForm.elements).forEach(el =>
|
|
|
+ el.addEventListener("change", () => model.rank())
|
|
|
+);
|
|
|
+
|
|
|
+const clusterRankingTitle = document.getElementById("cls-title");
|
|
|
+const clusterMetricSection = document.getElementById("cls-metric-mount");
|
|
|
+const clusterFunctionSection = document.getElementById("cls-fn");
|
|
|
+
|
|
|
+Array.from(document.forms.colorCalculateForm.elements).forEach(el =>
|
|
|
+ el.addEventListener("change", () => {
|
|
|
+ const { sortUseBestCluster, sortUseClusterSize, sortUseInvClusterSize } =
|
|
|
+ Object.fromEntries(new FormData(document.forms.colorCalculateForm).entries());
|
|
|
+ clusterRankingTitle.dataset.faded =
|
|
|
+ clusterMetricSection.dataset.faded =
|
|
|
+ clusterFunctionSection.dataset.faded =
|
|
|
+ !(sortUseBestCluster || sortUseClusterSize || sortUseInvClusterSize);
|
|
|
+ model.calculateObjective();
|
|
|
+ })
|
|
|
+);
|
|
|
+
|
|
|
+// ---- Add Metric Selections ----
|
|
|
+
|
|
|
+const metricSelectTemplate = document.getElementById("metric-select-template").content;
|
|
|
+const sortMetricForm = metricSelectTemplate.cloneNode(true).firstElementChild;
|
|
|
+sortMetricForm.id = "sortMetricForm";
|
|
|
+const clusterMetricForm = metricSelectTemplate.cloneNode(true).firstElementChild;
|
|
|
+clusterMetricForm.id = "clusterMetricForm";
|
|
|
+document.getElementById("sort-metric-mount").append(sortMetricForm);
|
|
|
+document.getElementById("cls-metric-mount").append(clusterMetricForm);
|
|
|
+
|
|
|
+document.forms.sortMetricForm.elements.metricKind.value = "whole";
|
|
|
+document.forms.clusterMetricForm.elements.metricKind.value = "stat";
|
|
|
+
|
|
|
+const updateMetricSelects = form => {
|
|
|
+ const kind = form.elements.metricKind.value;
|
|
|
+ form.elements.whole.disabled = kind !== "whole";
|
|
|
+ form.elements.mean.disabled = kind !== "mean";
|
|
|
+ form.elements.stat.disabled = kind !== "stat";
|
|
|
+ form.elements.metric.value = form.elements[kind].value;
|
|
|
+};
|
|
|
+
|
|
|
+const getMetricSymbol = metricName =>
|
|
|
+ // terrible hack
|
|
|
+ document.querySelector(`option[value=${metricName}]`).textContent.at(-2);
|
|
|
+
|
|
|
+const onMetricChange = () => {
|
|
|
+ updateMetricSelects(document.forms.sortMetricForm);
|
|
|
+ updateMetricSelects(document.forms.clusterMetricForm);
|
|
|
+
|
|
|
+ document.forms.colorCalculateForm.elements.sortMetricSymbolP.value =
|
|
|
+ document.forms.colorCalculateForm.elements.sortMetricSymbolB.value = getMetricSymbol(
|
|
|
+ document.forms.sortMetricForm.elements[
|
|
|
+ document.forms.sortMetricForm.elements.metricKind.value
|
|
|
+ ].value
|
|
|
+ );
|
|
|
+
|
|
|
+ document.forms.colorCalculateForm.elements.clusterMetricSymbol.value = getMetricSymbol(
|
|
|
+ document.forms.clusterMetricForm.elements[
|
|
|
+ document.forms.clusterMetricForm.elements.metricKind.value
|
|
|
+ ].value
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+onMetricChange();
|
|
|
+
|
|
|
+document.forms.sortMetricForm.addEventListener("change", () => {
|
|
|
+ onMetricChange();
|
|
|
+ model.calculateObjective();
|
|
|
+});
|
|
|
+
|
|
|
+document.forms.clusterMetricForm.addEventListener("change", () => {
|
|
|
+ onMetricChange();
|
|
|
+ model.calculateObjective();
|
|
|
+});
|
|
|
+
|
|
|
+// ---- Pick Starting Color ----
|
|
|
+
|
|
|
randomizeTargetColor();
|