123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- // ---- 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));
- // Angle Math
- const rad2deg = 180 / Math.PI;
- // Misc
- const clamp = (mn, v, mx) => Math.min(Math.max(v, mn), mx);
- const productLift =
- (...factors) =>
- (...args) =>
- factors
- .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 { r, g, b } = d3.color(hex);
- return vectorDot([r, g, b], [0.3, 0.6, 0.1]) >= 128
- ? "var(--color-dark)"
- : "var(--color-light)";
- };
- // "Visual Importance"
- const calcImportance = (chroma, lightness, proportion) =>
- chroma +
- Math.tanh(100 * (chroma - 0.25)) + // penalty for being <25%
- Math.tanh(100 * (chroma - 0.4)) + // penalty for being <40%
- lightness +
- Math.tanh(100 * (lightness - 0.5)) + // penalty for being <50%
- proportion +
- Math.tanh(100 * (proportion - 0.05)) + // penalty for being <5%
- Math.tanh(100 * (proportion - 0.1)) + // penalty for being <15%
- Math.tanh(100 * (proportion - 0.15)) + // penalty for being <15%
- Math.tanh(100 * (proportion - 0.25)) + // penalty for being <25%
- 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 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;
- // 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 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 buildClusterData = (
- size,
- inertia,
- mu1,
- mu2,
- mu3,
- nu1,
- nu2,
- nu3,
- totalSize,
- toHue,
- toLightness,
- toChroma,
- toHex
- ) => {
- const mu = buildVectorData([mu1, mu2, mu3], toHue, toLightness, toChroma, toHex);
- const nu = [nu1, nu2, nu3];
- const muNuAngle = rad2deg * Math.acos(vectorDot(mu.unit, nu) / vectorMag(nu));
- const proportion = size / totalSize;
- const importance = calcImportance(mu.chroma, mu.lightness, proportion);
- return {
- size,
- inverseSize: 1 / size,
- inertia,
- mu,
- nu,
- muNuAngle,
- proportion,
- inverseProportion: 1 / proportion,
- importance,
- };
- };
- const buildPokemonData = ([name, size, ...values]) => ({
- name,
- jab: {
- total: buildClusterData(
- size,
- ...values.slice(0, 7),
- size,
- jab2hue,
- jab2lit,
- jab2chroma,
- jab2hex
- ),
- 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),
- },
- rgb: {
- total: buildClusterData(
- size,
- ...values.slice(39, 46),
- size,
- rgb2hue,
- rgb2lit,
- rgb2chroma,
- rgb2hex
- ),
- 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),
- },
- });
- const pokemonData = databaseV3.map((row) => buildPokemonData(row));
- const calcScores = (data, target) => {
- const sigma = Math.sqrt(
- data.inertia - 2 * vectorDot(data.mu.vector, target.vector) + target.sqMag
- );
- const bigTheta = 1 - vectorDot(data.nu, target.unit);
- const rawPhi = Math.abs(data.mu.hue - target.hue);
- return {
- sigma,
- bigTheta,
- alpha: sigma * Math.pow(bigTheta, target.chroma + target.lightness),
- theta: rad2deg * Math.acos(vectorDot(data.mu.unit, target.unit)),
- phi: Math.min(rawPhi, 360 - rawPhi),
- delta: vectorMag(data.mu.vector.map((x, i) => x - target.vector[i])),
- manhattan: data.mu.vector
- .map((x, i) => Math.abs(x - target.vector[i]))
- .reduce((x, y) => x + y),
- ch: Math.max(...data.mu.vector.map((x, i) => Math.abs(x - target.vector[i]))),
- lightnessDiff: Math.abs(data.mu.lightness - target.lightness),
- inertia: data.inertia,
- variance: data.inertia - data.mu.sqMag,
- muNuAngle: data.muNuAngle,
- size: data.size,
- lightness: data.mu.lightness,
- chroma: data.mu.chroma,
- importance: data.importance,
- inverseSize: data.inverseSize,
- proportion: data.proportion,
- inverseProportion: data.inverseProportion,
- muHex: data.mu.hex,
- };
- };
- // ---- Styling ----
- const rootStyle = document.querySelector(":root").style;
- const setColorStyles = (style, hex) => {
- const highlight = getContrastingTextColor(hex);
- style.setProperty("--highlight", highlight);
- style.setProperty("--background", hex);
- style.setProperty("--shadow-component", highlight.includes("light") ? "255" : "0");
- };
- // ---- List Render ----
- const renderPokemon = (list, target) => {
- target.innerHTML = "";
- // TODO
- };
- // ---- Shared State ----
- const state = {
- get targetColor() {
- return this._targetColor || "";
- },
- set targetColor(newColor) {
- const hex = `#${newColor?.replace("#", "")}`;
- if (hex.length !== 7) {
- return;
- }
- setColorStyles(rootStyle, hex);
- const oldColor = this._targetColor;
- this._targetColor = 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));
- document.getElementById("prevColors").prepend(prevButton);
- }
- document.forms.targetColorForm.elements.colorText.value = hex;
- document.forms.targetColorForm.elements.colorPicker.value = hex;
- // TODO trigger recalc
- },
- get colorSearchResults() {
- return this._colorSearchResults || [];
- },
- set colorSearchResults(results) {
- this._colorSearchResults = results;
- renderColorSearchResults();
- },
-
- get nameSearchResults() {
- return this._nameSearchResults || [];
- },
- set nameSearchResults(results) {
- this._nameSearchResults = results;
- renderNameSearchResults();
- },
- };
- const colorSearchResultsTarget = document.getElementById("color-results");
- const nameSearchResultsTarget = document.getElementById("name-results");
- function renderColorSearchResults() {
- renderPokemon(state.colorSearchResults, colorSearchResultsTarget);
- }
- function renderNameSearchResults() {
- renderPokemon(state.nameSearchResults, nameSearchResultsTarget);
- }
- // ---- Form Controls ----
- document.forms.targetColorForm.elements.colorText.addEventListener(
- "input",
- ({ target }) => {
- if (target.willValidate && !target.validity.valid) {
- target.value = target.dataset.lastValid || "";
- } else {
- state.targetColor = target.dataset.lastValid = target.value;
- }
- }
- );
- document.forms.targetColorForm.elements.colorPicker.addEventListener(
- "change",
- ({ target }) => {
- state.targetColor = target.value;
- }
- );
- const randomizeTargetColor = () =>
- (state.targetColor = d3
- .hsl(Math.random() * 360, Math.random(), Math.random())
- .formatHex());
- document.forms.targetColorForm.elements.randomColor.addEventListener(
- "click",
- randomizeTargetColor
- );
- randomizeTargetColor();
|