123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- const selectors = {
- get colorSelect() {
- return document.forms.colorSelect.elements;
- },
- set colorText(hex) {
- selectors.colorSelect.colorText.value = hex;
- },
- set colorPicker(hex) {
- selectors.colorSelect.colorPicker.value = hex;
- },
- get sortControl() {
- return document.forms.sortControl;
- },
- get resultsToDisplay() {
- return selectors.sortControl.elements.resultsToDisplay.value;
- },
- get colorSpace() {
- return selectors.sortControl.elements.colorSpace.value;
- },
- get prevColors() {
- return document.getElementById("prev-colors");
- },
- get pokemonTemplate() {
- return document.getElementById("pkmn-template").content;
- },
- get colorSearchResults() {
- return document.getElementById("color-results");
- },
- get nameSearchResults() {
- return document.getElementById("name-results");
- },
- set background(hex) {
- document.querySelector(":root").style.setProperty("--background", hex);
- },
- set highlight(hex) {
- document.querySelector(":root").style.setProperty("--highlight", hex);
- },
- get metricSelectTemplate() {
- return document.getElementById("metric-select-template").content;
- },
- get sortFunction() {
- return document.forms.sortFunction;
- },
- get sortMetric() {
- return selectors.sortFunction.elements.sortMetric.value;
- },
- get sortOrder() {
- return selectors.sortFunction.elements.sortOrder.checked ? "max" : "min";
- },
- get sortUseBestCluster() {
- return selectors.sortFunction.elements.useBestCluster.checked;
- },
- get sortUseWholeImage() {
- return selectors.sortFunction.elements.useWholeImage.checked;
- },
- get sortClusterSize() {
- return selectors.sortFunction.elements.clusterSize.checked;
- },
- get sortInverseClusterSize() {
- return selectors.sortFunction.elements.invClusterSize.checked;
- },
- get sortTotalSize() {
- return selectors.sortFunction.elements.totalSize.checked;
- },
- get sortInverseTotalSize() {
- return selectors.sortFunction.elements.invTotalSize.checked;
- },
- set sortMetricSymbol(sym) {
- selectors.sortFunction.elements.metricSymbolP.value = sym;
- selectors.sortFunction.elements.metricSymbolB.value = sym;
- },
- get clusterFunction() {
- return document.forms.clusterFunction;
- },
- get clusterSortMetric() {
- return selectors.clusterFunction.elements.sortMetric.value;
- },
- get clusterSortOrder() {
- return selectors.clusterFunction.elements.sortOrder.checked ? "max" : "min";
- },
- get clusterSortClusterSize() {
- return selectors.clusterFunction.elements.clusterSize.checked;
- },
- get clusterSortInverseClusterSize() {
- return selectors.clusterFunction.elements.invClusterSize.checked;
- },
- get clusterSortTotalSize() {
- return selectors.clusterFunction.elements.totalSize.checked;
- },
- get clusterSortInverseTotalSize() {
- return selectors.clusterFunction.elements.invTotalSize.checked;
- },
- set clusterMetricSymbol(sym) {
- selectors.clusterFunction.elements.metricSymbol.value = sym;
- },
- };
- const onMetricChange = (elements, skipUpdates = false) => {
- const kind = elements.metricKind.value;
- elements.whole.disabled = kind !== "whole";
- elements.mean.disabled = kind !== "mean";
- elements.statistic.disabled = kind !== "statistic";
- elements.sortMetric.value = elements[kind].value;
- if (!skipUpdates) {
- // terrible hack
- selectors.sortMetricSymbol = document
- .querySelector(`option[value=${selectors.sortMetric}]`)
- .textContent.at(-2);
- selectors.clusterMetricSymbol = document
- .querySelector(`option[value=${selectors.clusterSortMetric}]`)
- .textContent.at(-2);
- updateSort();
- }
- };
- const onColorChange = (inputValue, skipUpdates = false) => {
- const colorInput = "#" + (inputValue?.replace("#", "") ?? "FFFFFF");
- if (colorInput.length !== 7) {
- return;
- }
- const rgb = d3.color(colorInput);
- if (!rgb) {
- return;
- }
- const hex = rgb.formatHex();
- selectors.colorText = hex;
- selectors.colorPicker = hex;
- const contrast = getContrastingTextColor(hex);
- const newColor = document.createElement("div");
- newColor.innerHTML = hex;
- newColor.style = `
- color: ${contrast};
- background-color: ${hex};
- `;
- selectors.prevColors.prepend(newColor);
- selectors.background = hex;
- selectors.highlight = contrast;
- if (!skipUpdates) {
- updateScores(rgb);
- updateSort();
- }
- };
- const randomColor = () =>
- d3.hsl(Math.random() * 360, Math.random(), Math.random()).formatHex();
- const sortOrders = {
- max: (a, b) => b - a,
- min: (a, b) => a - b,
- };
- const getCardinalityFactorExtractor = (
- clusterSize,
- invClusterSize,
- totalSize,
- invTotalSize
- ) => {
- const extractors = [];
- if (clusterSize) {
- extractors.push((scores) => scores.clusters.map(({ size }) => size));
- }
- if (invClusterSize) {
- extractors.push((scores) => scores.clusters.map(({ inverseSize }) => inverseSize));
- }
- if (totalSize) {
- extractors.push((scores) => scores.clusters.map(() => scores.total.size));
- }
- if (invTotalSize) {
- extractors.push((scores) => scores.clusters.map(() => scores.total.inverseSize));
- }
- return (scores) =>
- extractors
- .map((ext) => ext(scores))
- .reduce(
- (acc, xs) => acc.map((a, i) => a * xs[i]),
- scores.clusters.map(() => 1)
- );
- };
- const currentScores = {};
- const currentBestClusterIndices = {};
- const currentSortValues = {};
- let sortedData = [];
- const updateScores = (rgb) => {
- const { J, a, b } = d3.jab(rgb);
- const targetJab = buildVectorData([J, a, b], jab2hue, jab2lit, jab2chroma, jab2hex);
- const targetRgb = buildVectorData(
- [rgb.r, rgb.g, rgb.b],
- rgb2hue,
- rgb2lit,
- rgb2chroma,
- rgb2hex
- );
- pokemonData.forEach(({ name, jab, rgb }) => {
- currentScores[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)),
- },
- };
- });
- };
- const updateSort = () => {
- // update cluster rankings
- const clusterSortOrder = sortOrders[selectors.clusterSortOrder];
- const getClusterCardinalityFactors = getCardinalityFactorExtractor(
- selectors.clusterSortClusterSize,
- selectors.clusterSortInverseClusterSize,
- selectors.clusterSortTotalSize,
- selectors.clusterSortInverseTotalSize
- );
- pokemonData.forEach(({ name }) => {
- const { jab, rgb } = currentScores[name];
- // multiply scales with the intended metric, and find the index of the best value
- const forSpace = (clusters, factors) =>
- clusters
- .map((c, i) => [c[selectors.clusterSortMetric] * factors[i], i])
- .reduce((a, b) => (clusterSortOrder(a[0], b[0]) > 0 ? b : a))[1];
- currentBestClusterIndices[name] = {
- jab: forSpace(jab.clusters, getClusterCardinalityFactors(jab)),
- rgb: forSpace(rgb.clusters, getClusterCardinalityFactors(rgb)),
- };
- });
- // set up for actual sort
- const getCardinalityFactors = getCardinalityFactorExtractor(
- selectors.sortClusterSize,
- selectors.sortInverseClusterSize,
- selectors.sortTotalSize,
- selectors.sortInverseTotalSize
- );
- const factors = [
- (name) =>
- getCardinalityFactors(currentScores[name][selectors.colorSpace])[
- currentBestClusterIndices[name][selectors.colorSpace]
- ],
- ];
- if (selectors.sortUseWholeImage) {
- factors.push(
- (name) => currentScores[name][selectors.colorSpace].total[selectors.sortMetric]
- );
- }
- if (selectors.sortUseBestCluster) {
- factors.push(
- (name) =>
- currentScores[name][selectors.colorSpace].clusters[
- currentBestClusterIndices[name][selectors.colorSpace]
- ][selectors.sortMetric]
- );
- }
- pokemonData.forEach(({ name }) => {
- currentSortValues[name] = factors.map((fn) => fn(name)).reduce((x, y) => x * y);
- });
- /*
- switch (selectors.useClusters) {
- case "off":
- pokemonData.forEach(({ name }) => {
- currentSortValues[name] =
- currentScores[name][selectors.colorSpace].total[selectors.sortMetric];
- });
- break;
- case "on":
- pokemonData.forEach(({ name }) => {
- const index = currentBestClusterIndices[name][selectors.colorSpace];
- currentSortValues[name] =
- scaleOption(currentScores[name][selectors.colorSpace])[index] *
- currentScores[name][selectors.colorSpace].clusters[index][selectors.sortMetric];
- });
- break;
- case "mult":
- pokemonData.forEach(({ name }) => {
- const index = currentBestClusterIndices[name][selectors.colorSpace];
- currentSortValues[name] =
- currentScores[name][selectors.colorSpace].total[selectors.sortMetric] *
- scaleOption(currentScores[name][selectors.colorSpace])[index] *
- currentScores[name][selectors.colorSpace].clusters[index][selectors.sortMetric];
- });
- break;
- }
- */
- // update actual sorted data
- const sortOrder = sortOrders[selectors.sortOrder];
- sortedData = pokemonData
- .map(({ name }) => name)
- .sort((a, b) => sortOrder(currentSortValues[a], currentSortValues[b]));
- // and desplay results
- showResults();
- };
- const getSprite = (() => {
- const stripForm = [
- "flabebe",
- "floette",
- "florges",
- "vivillon",
- "basculin",
- "furfrou",
- "magearna",
- "alcremie",
- ];
- return (pokemon) => {
- pokemon = pokemon
- .replace("-alola", "-alolan")
- .replace("-galar", "-galarian")
- .replace("-phony", "") // sinistea and polteageist
- .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 makePokemonTile = (name) => {
- const clone = selectors.pokemonTemplate.cloneNode(true);
- const img = clone.querySelector("img");
- img.src = getSprite(name);
- img.alt = name;
- clone.querySelector(".pkmn-name").innerText = name
- .split("-")
- .map((part) => part.charAt(0).toUpperCase() + part.substr(1))
- .join(" ");
- clone.querySelector(".pkmn-score").innerText = currentSortValues[name].toFixed(3);
- const { total, clusters } = currentScores[name][selectors.colorSpace];
- const mu = clone.querySelector(".pkmn-total");
- mu.innerText = total.muHex;
- mu.style = `
- color: ${getContrastingTextColor(total.muHex)};
- background-color: ${total.muHex};
- `;
- clusters.forEach((cls, i) => {
- const clsDiv = clone.querySelector(`.pkmn-cls${i + 1}`);
- clsDiv.firstChild.innerText = (cls.proportion * 100).toFixed(2) + "%";
- clsDiv.lastChild.innerText = cls.muHex;
- clsDiv.style = `
- color: ${getContrastingTextColor(cls.muHex)};
- background-color: ${cls.muHex};
- `;
- });
- if (selectors.sortUseWholeImage) {
- clone.querySelector(".pkmn").classList.add("pkmn-total-selected");
- }
- if (
- selectors.sortUseBestCluster ||
- selectors.sortClusterSize ||
- selectors.sortInverseClusterSize
- ) {
- clone
- .querySelector(".pkmn")
- .classList.add(
- `pkmn-cls${currentBestClusterIndices[name][selectors.colorSpace] + 1}-selected`
- );
- }
- return clone;
- };
- const pokemonLookup = new Fuse(pokemonData, { keys: ["name"] });
- let currentNameSearchResults = [];
- const searchByName = (newSearch) => {
- currentNameSearchResults = pokemonLookup
- .search(newSearch, { limit: 10 })
- .map(({ item: { name } }) => name);
- showResults();
- };
- const randomPokemon = () => {
- currentNameSearchResults = Array.from(
- { length: 10 },
- () => pokemonData[Math.floor(Math.random() * pokemonData.length)].name
- );
- showResults();
- };
- const showResults = () => {
- selectors.colorSearchResults.innerHTML = "";
- sortedData.slice(0, selectors.resultsToDisplay).forEach((name) => {
- selectors.colorSearchResults.appendChild(makePokemonTile(name));
- });
- selectors.nameSearchResults.innerHTML = "";
- currentNameSearchResults.forEach((name) => {
- selectors.nameSearchResults.appendChild(makePokemonTile(name));
- });
- };
- window.addEventListener("load", () => {
- const metricSelect = selectors.metricSelectTemplate;
- selectors.sortFunction.appendChild(metricSelect.cloneNode(true));
- selectors.sortFunction.elements.metricKind.value = "whole";
- selectors.sortFunction.elements.whole.value = "alpha";
- onMetricChange(selectors.sortFunction.elements, true);
- selectors.clusterFunction.appendChild(metricSelect.cloneNode(true));
- selectors.clusterFunction.elements.sortOrder.checked = true;
- selectors.clusterFunction.elements.metricKind.value = "statistic";
- selectors.clusterFunction.elements.statistic.value = "importance";
- onMetricChange(selectors.clusterFunction.elements, true);
- document.body.addEventListener("click", ({ target: { innerText }, detail }) => {
- if (detail === 2 && innerText?.includes("#")) {
- const clickedHex = innerText?.match(/.*(#[0-9a-fA-F]{6}).*/)?.[1] ?? "";
- if (clickedHex) {
- onColorChange(clickedHex);
- }
- }
- });
- onColorChange(randomColor());
- });
|