UNPKG

rybitten

Version:

A color space conversion library for transforming between RGB and RYB colors.

607 lines (541 loc) 17.4 kB
import "./demo.css"; import { rybHsl2rgb, ryb2rgb } from "./main"; import { ColorCoords, ColorCube, cubes } from "./cubes"; let currentCube: ColorCube = cubes.get("itten-normalized")!.cube; const logCube = (cube: ColorCube) => { console.log("Customized RYB_CUBE"); console.log(cube.map((row) => row.map((it) => it * 255 + "/255"))); }; const formatCSS = (rgb: ColorCoords): string => { return `rgb(${Math.round(rgb[0] * 255)} ${Math.round(rgb[1] * 255)} ${Math.round(rgb[2] * 255)})`; }; /* function sRGBtoLin(colorChannel: number): number { // Send this function a decimal sRGB gamma encoded color value // between 0.0 and 1.0, and it returns a linearized value. if ( colorChannel <= 0.04045 ) { return colorChannel / 12.92; } else { return Math.pow((( colorChannel + 0.055)/1.055),2.4); } } function rgbToLuminance(rgb: ColorCoords): number { // Convert RGB to luminance (Y component) // https://en.wikipedia.org/wiki/Relative_luminance const [r, g, b] = rgb.map(sRGBtoLin); return 0.2126 * r + 0.7152 * g + 0.0722 * b; } function YtoLstar(Y: number): number { // Send this function a luminance value between 0.0 and 1.0, // and it returns L* which is "perceptual lightness" if ( Y <= (216/24389)) { // The CIE standard states 0.008856 but 216/24389 is the intent for 0.008856451679036 return Y * (24389/27); // The CIE standard states 903.3, but 24389/27 is the intent, making 903.296296296296296 } return Math.pow(Y,(1/3)) * 116 - 16; } function rgbToLstar(rgb: ColorCoords): number { return YtoLstar(rgbToLuminance(rgb)); }*/ const hexToRgb = (hex: string): ColorCoords => { const bigint = parseInt(hex.slice(1), 16); const r = (bigint >> 16) & 255; const g = (bigint >> 8) & 255; const b = bigint & 255; return [r / 255, g / 255, b / 255]; }; const rgbToHex = (rgb: ColorCoords): string => { return `#${rgb .map((c) => Math.round(c * 255) .toString(16) .padStart(2, "0"), ) .join("")}`; }; const getColorsHSL = ( amount = 12, s = 1, l = 0.5, hFn = (h: number): number => h, oldScool = false, ) => new Array(amount).fill(0).map((_, i) => { const h = oldScool ? hFn(1 - i / amount) * 360 + 120 : hFn((i + 1) / amount) * 360; return formatCSS( rybHsl2rgb([h, s, l], { cube: currentCube, }), ); }); const romanNumerals = [ "Zero", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX", "XXI", "XXII", "XXIII", "XXIV", "XXV", "XXVI", "XXVII", "XXVIII", "XXIX", "XXX", ]; const createRamps = async (amount = 18, stepsPerRamp = 9) => { const ramps = new Array(amount - 1).fill(0).map((_, i) => { const h = i / (amount - 1); const steps = new Array(stepsPerRamp).fill(0).map((_, j) => { const l = (j + 1) / (stepsPerRamp + 1); return rgbToHex( rybHsl2rgb([h * 360, 1, 1 - l], { cube: currentCube, }), ); }); return steps; }); // add grey ramp ramps.push( new Array(stepsPerRamp).fill(0).map((_, j) => { const l = (j + 1) / (stepsPerRamp + 1); return rgbToHex( ryb2rgb([l, l, l], { cube: currentCube, }), ); }), ); const allHexes = ramps.flat().map((hex) => { // remove the first char (#) return hex.slice(1); }); const names = await fetch( `https://api.color.pizza/v1/?values=${allHexes.join()}&list=bestOf&noduplicates=true`, { method: "GET", }, ) .then((res) => res.json()) .then((data) => { return data.colors; }); const namesForHexes = allHexes.reduce( (acc: { [key: string]: string }, hex: string) => { const prefixedHex = `#${hex}`; const color = names.find( (color: any) => color.requestedHex === prefixedHex, ); acc[prefixedHex] = color?.name || "unknown"; return acc; }, {}, ); const $wrapper = document.querySelector("[data-ramps]") as HTMLElement; $wrapper.innerHTML = ramps .map((ramp, i) => { const activeIndex = 1 + Math.floor(Math.random() * (ramp.length - 2)); return `<div class="ramp"> <h2>${romanNumerals[i + 1]}</h2> ${ramp .map((hex, j) => { const activeClass = j === activeIndex ? "ramp__step--active" : ""; return `<div class="ramp__step ${activeClass}" style="--c: ${hex}; --rnd: ${-1 + Math.random() * 2}; --rnd2: ${Math.random()};"> <div class="ramp__inner"> <div class="ramp__label"> <span>${namesForHexes[hex]}</span> <span>${hex}</span> </div> </div> </div>`; }) .join("")} </div>`; }) .join(""); }; // generate an array containing each color gradient as // an array const colorStairs = (floors = 4, firstFloorSteps = 10): string[][] => [ getColorsHSL(firstFloorSteps, 1, 0.5), ...new Array(floors - 1) .fill("") .map((_, i) => getColorsHSL( firstFloorSteps * 2 * (i + 1), 1, 0.5 + ((i + 1) / floors) * 0.5, ), ), ]; const colorStairArrToGradient = (starisArr: string[][]): string => { //const gradients = starisArr.reverse(); const gradients = starisArr; return gradients.reduce( (prevGrad, colors, i) => (prevGrad += (prevGrad ? "," : "") + `linear-gradient(90deg, ${ !i ? colors.join() : colors .map( (c, i) => `${c} ${(i / (colors.length - 1)) * 100}% ${((i + 1) / (colors.length - 1)) * 100}%`, ) .join() })`), "", ); }; function generateColorSection(stairs: string[][], gradients: string): void { document.documentElement.style.setProperty("--g", gradients); document.documentElement.style.setProperty("--gs", `${stairs.length}`); document.documentElement.style.setProperty( "--gp", stairs .map((_, i) => `0% calc(${i * (100 / (stairs.length - 1))}% - 1px)`) .join(), ); } const colorsToHardStopGradients = (cls: string[]): string => cls .map( (c, i) => `${c} ${(i / cls.length) * 100}% ${((i + 1) / cls.length) * 100}%`, ) .join(); const $w = document.querySelector('[data-c="w"]') as HTMLInputElement; const $r = document.querySelector('[data-c="r"]') as HTMLInputElement; const $y = document.querySelector('[data-c="y"]') as HTMLInputElement; const $o = document.querySelector('[data-c="o"]') as HTMLInputElement; const $b = document.querySelector('[data-c="b"]') as HTMLInputElement; const $v = document.querySelector('[data-c="v"]') as HTMLInputElement; const $g = document.querySelector('[data-c="g"]') as HTMLInputElement; const $black = document.querySelector('[data-c="0"]') as HTMLInputElement; const $w2 = document.querySelector('[data-c2="w"]') as HTMLInputElement; const $r2 = document.querySelector('[data-c2="r"]') as HTMLInputElement; const $y2 = document.querySelector('[data-c2="y"]') as HTMLInputElement; const $o2 = document.querySelector('[data-c2="o"]') as HTMLInputElement; const $b2 = document.querySelector('[data-c2="b"]') as HTMLInputElement; const $v2 = document.querySelector('[data-c2="v"]') as HTMLInputElement; const $g2 = document.querySelector('[data-c2="g"]') as HTMLInputElement; const $black2 = document.querySelector('[data-c2="0"]') as HTMLInputElement; $w.parentElement!.style.setProperty("--c", `var(--white)`); $r.parentElement!.style.setProperty("--c", `var(--red)`); $y.parentElement!.style.setProperty("--c", `var(--yellow)`); $o.parentElement!.style.setProperty("--c", `var(--orange)`); $b.parentElement!.style.setProperty("--c", `var(--blue)`); $v.parentElement!.style.setProperty("--c", `var(--pink)`); $g.parentElement!.style.setProperty("--c", `var(--green)`); $black.parentElement!.style.setProperty("--c", `var(--black)`); $w2.parentElement!.style.setProperty("--c", `var(--white)`); $r2.parentElement!.style.setProperty("--c", `var(--red)`); $y2.parentElement!.style.setProperty("--c", `var(--yellow)`); $o2.parentElement!.style.setProperty("--c", `var(--orange)`); $b2.parentElement!.style.setProperty("--c", `var(--blue)`); $v2.parentElement!.style.setProperty("--c", `var(--pink)`); $g2.parentElement!.style.setProperty("--c", `var(--green)`); $black2.parentElement!.style.setProperty("--c", `var(--black)`); const lightnessSteps = 9; let timer: number | null = null; // color swatch const $swatchRGB = document.querySelector("[data-rgb]") as HTMLElement; const $swatchRYB = document.querySelector("[data-ryb]") as HTMLElement; const $swatchRGBinput = $swatchRGB.querySelector("input") as HTMLInputElement; let swatchTimer: number | null = null; function setSwatchColor(rgb: ColorCoords, bypassTimer = false) { const hex = rgbToHex(rgb); const ryb = ryb2rgb(rgb, { cube: currentCube }); const hexRYB = rgbToHex(ryb); const cssRGB = formatCSS(rgb); const cssRYB = formatCSS(ryb); $swatchRGB.style.setProperty("--c", cssRGB); $swatchRGBinput.value = hex; $swatchRYB.style.setProperty("--c", cssRYB); const [r, g, b] = rgb.map((c) => Math.round(c * 255)); const [r2, y2, b2] = ryb.map((c) => Math.round(c * 255)); $swatchRGB.querySelector(".swatch__value")!.textContent = `${r} ${g} ${b}`; $swatchRYB.querySelector(".swatch__value")!.textContent = `${r2} ${y2} ${b2}`; swatchTimer && clearTimeout(swatchTimer); const time = bypassTimer ? 0 : 1000; swatchTimer = setTimeout(() => { // get both names fetch(`https://api.color.pizza/v1/?values=${hex.slice(1)}&list=bestOf`, { method: "GET", }) .then((res) => res.json()) .then((data) => { $swatchRGB.querySelector(".swatch__name")!.textContent = data.colors[0].name; }); fetch(`https://api.color.pizza/v1/?values=${hexRYB.slice(1)}&list=bestOf`, { method: "GET", }) .then((res) => res.json()) .then((data) => { $swatchRYB.querySelector(".swatch__name")!.textContent = data.colors[0].name; }); }, time); } // generate a well saturated color let swatchColorHSL = [ Math.random() * 360, 0.2 + Math.random() * 0.8, 0.85 + Math.random() * 0.1, ] as ColorCoords; $swatchRGBinput.addEventListener("input", (e) => { const $target = e.target; if (!($target instanceof HTMLInputElement)) { return; } const value = $target.value; setSwatchColor(hexToRgb(value)); }); setSwatchColor(rybHsl2rgb(swatchColorHSL), true); const repaint = () => { setSwatchColor(hexToRgb($swatchRGBinput.value)); const colors = getColorsHSL(36, 1, 0.5, (h) => h, false); const colorsHSL = new Array(36).fill(0).map((_, index) => { const rybAngle = index * 10; return `hsl(${rybAngle}, 100%, 50%)`; }); document.documentElement.style.setProperty( "--gradientHSLHard", colorsToHardStopGradients(colorsHSL), ); document.documentElement.style.setProperty("--gradientHSL", colorsHSL.join()); document.documentElement.style.setProperty( "--gradientHard", colorsToHardStopGradients(colors), ); document.documentElement.style.setProperty("--gradient", colors.join()); /* document.documentElement.style.setProperty( "--gradient", colors.map(c => { const [r, g, b] = c.match(/\d+/g)!.map(Number); const lstar = rgbToLstar([r/255, g/255, b/255]); console.log(lstar) return `rgb(${Math.floor(lstar * 2.5)} ${Math.floor(lstar * 2.5)} ${Math.floor(lstar * 2.5)})`; }).join(), );*/ for (let i = 0; i <= lightnessSteps; i++) { const colors = getColorsHSL(36, 0.4, 0.2 + (i / lightnessSteps) * 0.7); document.documentElement.style.setProperty( `--gradient-l${i}`, colors.join(), ); } for (let i = 0; i < 36; i++) { const color = formatCSS( rybHsl2rgb([((i + 1) / 36) * 360, 1, 0.5], { cube: currentCube, }), ); document.documentElement.style.setProperty(`--color-${i + 1}`, color); } document.documentElement.style.setProperty( "--stops-3", colorsToHardStopGradients(getColorsHSL(3, 1, 0.5, (h) => h, true)), ); document.documentElement.style.setProperty( "--stops-6", colorsToHardStopGradients( getColorsHSL(6, 1, 0.5, (h) => h, true).filter((_, i) => i % 2), ), ); document.documentElement.style.setProperty( "--stops-12", colorsToHardStopGradients(getColorsHSL(12, 1, 0.5, (h) => h, true)), ); document.documentElement.style.setProperty( "--stops-24", colorsToHardStopGradients(getColorsHSL(24, 1, 0.5, (h) => h, true)), ); document.documentElement.style.setProperty( "--stops-48", colorsToHardStopGradients(getColorsHSL(48, 1, 0.5, (h) => h, true)), ); document.documentElement.style.setProperty( "--white", formatCSS(currentCube[0]), ); document.documentElement.style.setProperty( "--black", formatCSS(currentCube[7]), ); document.documentElement.style.setProperty( "--red", formatCSS(currentCube[1]), ); document.documentElement.style.setProperty( "--yellow", formatCSS(currentCube[2]), ); document.documentElement.style.setProperty( "--orange", formatCSS(currentCube[3]), ); document.documentElement.style.setProperty( "--blue", formatCSS(currentCube[4]), ); document.documentElement.style.setProperty( "--pink", formatCSS(currentCube[5]), ); document.documentElement.style.setProperty( "--green", formatCSS(currentCube[6]), ); $w.value = rgbToHex(currentCube[0]); $r.value = rgbToHex(currentCube[1]); $y.value = rgbToHex(currentCube[2]); $o.value = rgbToHex(currentCube[3]); $b.value = rgbToHex(currentCube[4]); $v.value = rgbToHex(currentCube[5]); $g.value = rgbToHex(currentCube[6]); $black.value = rgbToHex(currentCube[7]); $w2.value = rgbToHex(currentCube[0]); $r2.value = rgbToHex(currentCube[1]); $y2.value = rgbToHex(currentCube[2]); $o2.value = rgbToHex(currentCube[3]); $b2.value = rgbToHex(currentCube[4]); $v2.value = rgbToHex(currentCube[5]); $g2.value = rgbToHex(currentCube[6]); $black2.value = rgbToHex(currentCube[7]); logCube(currentCube); const stairs = colorStairs(4, 10); const gradient = colorStairArrToGradient(stairs); generateColorSection(stairs, gradient); clearTimeout(timer!); timer = setTimeout(() => { createRamps(); }, 1000); }; repaint(); const els = [$w, $r, $y, $o, $b, $v, $g, $black]; const els2 = [$w2, $r2, $y2, $o2, $b2, $v2, $g2, $black2]; document.querySelector("[data-edges]")?.addEventListener("input", (e) => { const $target = e.target; if (!($target instanceof HTMLInputElement)) { return; } const value = $target.value; const $tarInEls = els.find(($el) => $el.isEqualNode($target)); if (!$tarInEls) { return; } const index = els.indexOf($tarInEls); if (index > -1) { currentCube[index] = hexToRgb(value); repaint(); } // mirror the color change on els2 const $tarInEls2 = els2[index]; $tarInEls2.value = value; }); document.querySelector("[data-edges2]")?.addEventListener("input", (e) => { const $target = e.target; if (!($target instanceof HTMLInputElement)) { return; } const value = $target.value; const $tarInEls = els2.find(($el) => $el.isEqualNode($target)); if (!$tarInEls) { return; } const index = els2.indexOf($tarInEls); if (index > -1) { currentCube[index] = hexToRgb(value); repaint(); } // mirror the color change on els const $tarInEls1 = els[index]; $tarInEls1.value = value; }); /* const $select = document.createElement("select"); // create an option for each of the cubes for (const [key, obj] of cubes) { const $option = document.createElement("option"); $option.value = key; $option.textContent = obj.title; $select.appendChild($option); } $select.classList.add("select"); document.querySelector("body")!.appendChild($select); $select.addEventListener("change", (e) => { const $target = e.target; if (!($target instanceof HTMLSelectElement)) { return; } const value = $target.value; const cube = cubes.get(value); if (cube) { currentCube = cube.cube; repaint(); } });*/ const $presetsList = document.querySelector("[data-presets]") as HTMLElement; for (const [key, obj] of cubes) { const $li = document.createElement("li") as HTMLElement; const $label = document.createElement("label") as HTMLLabelElement; const $input = document.createElement("input") as HTMLInputElement; $input.type = "radio"; $input.name = "preset"; $input.value = key; $label.appendChild($input); const div = ` <div> <h3>${obj.title} <span>${obj.year}</span></h3> <strong>${obj.author}</strong> </div> `; $label.innerHTML += div; $li.appendChild($label); $presetsList.appendChild($li); } ( $presetsList.querySelector( "input[value='itten-normalized']", ) as HTMLInputElement ).checked = true; $presetsList.addEventListener( "change", (e) => { const $target = e.target; if (!($target instanceof HTMLInputElement)) { return; } const value = $target.value; const cube = cubes.get(value); if (cube) { currentCube = cube.cube; repaint(); } }, true, );