From 4cf2780774ba58de44d421027254dfbb045e9a3b Mon Sep 17 00:00:00 2001 From: lambda Date: Sat, 3 Jan 2026 15:52:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=B0=D0=BB=D0=B8=D1=82=D1=80=D1=8B=20?= =?UTF-8?q?=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20=D1=84=D0=B0=D0=B9=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/colors.ts | 13 ++++---- src/pallette.ts | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ src/preview.ts | 76 +++++++++----------------------------------- 3 files changed, 104 insertions(+), 68 deletions(-) create mode 100644 src/pallette.ts diff --git a/src/colors.ts b/src/colors.ts index b266787..319851a 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -88,11 +88,11 @@ export const hslToRgb: ColorProcessingFunc = (h, s, l) => { [c, 0, x], // 300..360 ]; - const [rp, gp, bp] = table[sector]; + const [rp, gp, bp] = table[sector] as [number, number, number]; - const r = round(clamp(rp + m, 0, 1) * BYTE_MAX); - const g = round(clamp(gp + m, 0, 1) * BYTE_MAX); - const b = round(clamp(bp + m, 0, 1) * BYTE_MAX); + const r = Math.round(clamp(rp + m, 0, 1) * BYTE_MAX); + const g = Math.round(clamp(gp + m, 0, 1) * BYTE_MAX); + const b = Math.round(clamp(bp + m, 0, 1) * BYTE_MAX); return { r, g, b }; }; @@ -109,11 +109,12 @@ const hexToHsl = (hex: string) => { // luminance & contrast export const luminance = (hex: string) => { - const [r, g, b] = Array.from(Object.values(hexToRgb(hex))).map((v) => { + const [r, g, b] = Object.values(hexToRgb(hex)).map((v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); - return 0.2126 * r + 0.7152 * g + 0.0722 * b; + + return 0.2126 * (r ?? 0) + 0.7152 * (g ?? 0) + 0.0722 * (b ?? 0); }; export const contrastRatio = (a: string, b: string) => { diff --git a/src/pallette.ts b/src/pallette.ts new file mode 100644 index 0000000..acc0979 --- /dev/null +++ b/src/pallette.ts @@ -0,0 +1,83 @@ +import colors from "./colors.json"; +import { hslToHex, hexToHsl, contrastRatio } from "./colors"; +import { clamp } from "./math"; + +type Pallette = { + bg: string; + fg: string; + red: string; + green: string; + shade1: string; + shade2: string; + shade3: string; + shade4: string; +}; + +const MIN_CONTRAST = 3.0; +const FALLBACK_WHITE = "#ffffff"; + +const bg = colors.bg ?? "#0f0f10"; +const fg = colors.fg ?? "#e6e6e6"; +const redHue = colors.accentRedHue ?? 0; +const greenHue = colors.accentGreenHue ?? 140; +const sat = colors.accentSaturation ?? 65; +const light = colors.accentLightness ?? 60; +const steps = colors.shadeSteps ?? 4; + +// generate accents +// ensure reasonable contrast with bg; if too low, nudge lightness +const fixContrast = ( + ref: string, + hue: number, + sat: number, + lightness: number, +) => { + let cur = hslToHex(hue, sat, lightness); + let ctr = contrastRatio(cur, ref); + let l = lightness; + let tries = 0; + while (ctr < MIN_CONTRAST && tries < 10) { + l = clamp(l + 6, 0, 100); // make lighter + cur = hslToHex(hue, sat, l); + ctr = contrastRatio(cur, ref); + tries++; + } + return cur; +}; + +const red = fixContrast(bg, redHue, sat, light); +const green = fixContrast(bg, greenHue, sat, light); + +// generate 4 darker shades from fg towards bg by lightness interpolation +const { h: fgH, s: fgS, l: fgL } = hexToHsl(fg); +const { s: bgS, l: bgL } = hexToHsl(bg); +const shades: string[] = []; +for (let i = 1; i <= steps; i++) { + const t = i / (steps + 1); // fraction towards bg + const l = fgL + (bgL - fgL) * t; + const s = fgS + (bgS - fgS) * t; + const h = fgH; // keep fg hue + shades.push(hslToHex(h, s, l)); +} + +const pallette: Pallette = { + bg, + fg, + red, + green, + shade1: shades[0] ?? FALLBACK_WHITE, + shade2: shades[1] ?? FALLBACK_WHITE, + shade3: shades[2] ?? FALLBACK_WHITE, + shade4: shades[3] ?? FALLBACK_WHITE, +}; + +function* createPalletteGenerator() { + const keys = Object.keys(pallette) as (keyof Pallette)[]; + for (let key of keys) { + yield [key, pallette[key]]; + } +} + +const palletteGenerator = createPalletteGenerator(); + +export { pallette, palletteGenerator }; diff --git a/src/preview.ts b/src/preview.ts index 18da91e..83390a0 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -1,76 +1,28 @@ -import colors from "./colors.json"; -import { hexToRgb, hslToHex, hexToHsl, clamp, contrastRatio } from "./colors"; - -const bg = colors.bg ?? "#0f0f10"; -const fg = colors.fg ?? "#e6e6e6"; -const redHue = colors.accentRedHue ?? 0; -const greenHue = colors.accentGreenHue ?? 140; -const sat = colors.accentSaturation ?? 65; -const light = colors.accentLightness ?? 60; -const steps = colors.shadeSteps ?? 4; - -// generate accents -let red = hslToHex(redHue, sat, light); -let green = hslToHex(greenHue, sat, light); - -// ensure reasonable contrast with bg; if too low, nudge lightness -const MIN_CONTRAST = 3.0; -const fixContrast = ( - hex: string, - ref: string, - hue: number, - sat: number, - lightness: number, -) => { - let cur = hslToHex(hue, sat, lightness); - let ctr = contrastRatio(cur, ref); - let l = lightness; - let tries = 0; - while (ctr < MIN_CONTRAST && tries < 10) { - l = clamp(l + 6); // make lighter - cur = hslToHex(hue, sat, l); - ctr = contrastRatio(cur, ref); - tries++; - } - return cur; -}; -red = fixContrast(red, bg, redHue, sat, light); -green = fixContrast(green, bg, greenHue, sat, light); - -// generate 4 darker shades from fg towards bg by lightness interpolation -const { h: fgH, s: fgS, l: fgL } = hexToHsl(fg); -const { s: bgS, l: bgL } = hexToHsl(bg); -const shades: string[] = []; -for (let i = 1; i <= steps; i++) { - const t = i / (steps + 1); // fraction towards bg - const l = fgL + (bgL - fgL) * t; - const s = fgS + (bgS - fgS) * t; - const h = fgH; // keep fg hue - shades.push(hslToHex(h, s, l)); -} - -// final palette order: bg, fg, red, green, shade1..shade4 -const palette = [bg, fg, red, green, ...shades]; +import { hexToRgb, contrastRatio } from "./colors"; +import { pallette, palletteGenerator } from "./pallette"; const hexToRgbStr = (hex: string) => { - const [r, g, b] = hexToRgb(hex); + const { r, g, b } = hexToRgb(hex); return `${r};${g};${b}`; }; + const block = (hex: string, label?: string) => { - const [r, g, b] = hexToRgb(hex); + const { r, g, b } = hexToRgb(hex); process.stdout.write(`\x1b[48;2;${r};${g};${b}m ${label ?? hex} \x1b[0m\n`); }; console.log("\nGenerated 8-color palette:\n"); -["bg", "fg", "red", "green", "shade1", "shade2", "shade3", "shade4"].forEach( - (name, idx) => { - block(palette[idx], `${name} ${palette[idx]}`); - }, -); + +for (let [name, color] of palletteGenerator) { + block(color ?? "", `${name} ${color}`); +} + console.log(); + console.log( - `Contrast red/bg: ${contrastRatio(red, bg)} | green/bg: ${contrastRatio(green, bg)}\n`, + `Contrast red/bg: ${contrastRatio(pallette.red, pallette.bg)} | green/bg: ${contrastRatio(pallette.green, pallette.bg)}\n`, ); + console.log( - `\x1b[38;2;${hexToRgbStr(green)}m✔ Success\x1b[0m \x1b[38;2;${hexToRgbStr(red)}m✖ Error\x1b[0m\n`, + `\x1b[38;2;${hexToRgbStr(pallette.green)}m✔ Success\x1b[0m \x1b[38;2;${hexToRgbStr(pallette.red)}m✖ Error\x1b[0m\n`, );