From d077bf6bccb78fba4e195485f392521a384fa130 Mon Sep 17 00:00:00 2001 From: lambda Date: Fri, 2 Jan 2026 01:38:47 +0300 Subject: [PATCH] Generate basic code --- colors.json | 3 ++ src/colors.json | 9 ++++ src/colors.ts | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ src/math.ts | 19 +++++++ src/preview.ts | 76 ++++++++++++++++++++++++++++ tsconfig.json | 21 ++++++++ 6 files changed, 260 insertions(+) create mode 100644 colors.json create mode 100644 src/colors.json create mode 100644 src/colors.ts create mode 100644 src/math.ts create mode 100644 src/preview.ts create mode 100644 tsconfig.json diff --git a/colors.json b/colors.json new file mode 100644 index 0000000..afb33f3 --- /dev/null +++ b/colors.json @@ -0,0 +1,3 @@ +{ + "base": "#2b2b2b" +} diff --git a/src/colors.json b/src/colors.json new file mode 100644 index 0000000..b3c1e60 --- /dev/null +++ b/src/colors.json @@ -0,0 +1,9 @@ +{ + "bg": "#0f0f10", + "fg": "#e6e6e6", + "accentRedHue": 0, + "accentGreenHue": 140, + "accentSaturation": 65, + "accentLightness": 60, + "shadeSteps": 4 +} diff --git a/src/colors.ts b/src/colors.ts new file mode 100644 index 0000000..6c04fda --- /dev/null +++ b/src/colors.ts @@ -0,0 +1,132 @@ +import { byteToRatio, round } from "./math"; + +type Color = { r: number; g: number; b: number }; +type HSL = { h: number; s: number; l: number }; +type RgbProcessingFunc = (r: number, g: number, b: number) => T; + +const hexToRgb = (hex: string) => { + const h = hex.replace("#", ""); + return [ + parseInt(h.slice(0, 2), 16), + parseInt(h.slice(2, 4), 16), + parseInt(h.slice(4, 6), 16), + ]; +}; + +const rgbToHex: RgbProcessingFunc = (r, g, b) => + "#" + [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join(""); + +const rgbToRatios: RgbProcessingFunc = (rIN, gIN, bIN) => ({ + r: byteToRatio(rIN), + g: byteToRatio(gIN), + b: byteToRatio(bIN), +}); + +const rgbToHsl: RgbProcessingFunc = (rIN, gIN, bIN) => { + const HUE_SEGMENTS = 6; + const DEGREES_IN_CIRCLE = 360; + const PERCENT_FACTOR = 100; + + const { r, g, b } = rgbToRatios(rIN, gIN, bIN); + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + + let h = 0; + let s = 0; + const l = (max + min) / 2; + + if (delta !== 0) { + // насыщенность (s) зависит от lightness (l) + s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min); + + // вычисляем секцию оттенка в диапазоне 0..HUE_SEGMENTS, затем переводим в градусы + const hueSection = + max === r + ? (g - b) / delta + (g < b ? HUE_SEGMENTS : 0) + : max === g + ? (b - r) / delta + 2 + : (r - g) / delta + 4; + + h = hueSection * (DEGREES_IN_CIRCLE / HUE_SEGMENTS); + if (!Number.isFinite(h) || Number.isNaN(h)) h = 0; + h = ((h % DEGREES_IN_CIRCLE) + DEGREES_IN_CIRCLE) % DEGREES_IN_CIRCLE; + } + + return { + h: round(h), + s: round(s * PERCENT_FACTOR), + l: round(l * PERCENT_FACTOR), + }; +}; + +export const hslToRgb = (h: number, s: number, l: number) => { + s /= 100; + l /= 100; + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + let r = 0, + g = 0, + b = 0; + if (h < 60) { + r = c; + g = x; + b = 0; + } else if (h < 120) { + r = x; + g = c; + b = 0; + } else if (h < 180) { + r = 0; + g = c; + b = x; + } else if (h < 240) { + r = 0; + g = x; + b = c; + } else if (h < 300) { + r = x; + g = 0; + b = c; + } else { + r = c; + g = 0; + b = x; + } + return [ + Math.round((r + m) * 255), + Math.round((g + m) * 255), + Math.round((b + m) * 255), + ]; +}; + +export const hslToHex = (h: number, s: number, l: number) => { + const [r, g, b] = hslToRgb(h, s, l); + return rgbToHex(r, g, b); +}; + +const hexToHsl = (hex: string) => { + const [r, g, b] = hexToRgb(hex); + return rgbToHsl(r, g, b); +}; + +export const clamp = (v: number, a = 0, b = 100) => Math.min(Math.max(v, a), b); + +// luminance & contrast +export const luminance = (hex: string) => { + const [r, g, b] = 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; +}; + +export const contrastRatio = (a: string, b: string) => { + const L1 = luminance(a), + L2 = luminance(b); + const [x, y] = L1 > L2 ? [L1, L2] : [L2, L1]; + return +((x + 0.05) / (y + 0.05)).toFixed(2); +}; + +export { hexToRgb, hexToHsl, rgbToHex, rgbToHsl }; diff --git a/src/math.ts b/src/math.ts new file mode 100644 index 0000000..7d0a407 --- /dev/null +++ b/src/math.ts @@ -0,0 +1,19 @@ +const BYTE_MAX = 255; +const ROUND_PRECISION = 2; // знаков после запятой в выходе + +/** + * Преобразует байтное значение (0–255) в нормализованное отношение в диапазоне 0.0–1.0. + * + * @returns Нормализованное значение 0.0–1.0. При x = NaN | Infinity возвращает 0. + */ +const byteToRatio = (x: number) => { + const n = x / BYTE_MAX; + return isNaN(n) || !isFinite(n) ? 0 : n; +}; + +const round = (v: number, digits = ROUND_PRECISION) => { + const factor = Math.pow(10, digits); + return Math.round(v * factor) / factor; +}; + +export { byteToRatio, round }; diff --git a/src/preview.ts b/src/preview.ts new file mode 100644 index 0000000..18da91e --- /dev/null +++ b/src/preview.ts @@ -0,0 +1,76 @@ +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]; + +const hexToRgbStr = (hex: string) => { + const [r, g, b] = hexToRgb(hex); + return `${r};${g};${b}`; +}; +const block = (hex: string, label?: string) => { + 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]}`); + }, +); +console.log(); +console.log( + `Contrast red/bg: ${contrastRatio(red, bg)} | green/bg: ${contrastRatio(green, bg)}\n`, +); +console.log( + `\x1b[38;2;${hexToRgbStr(green)}m✔ Success\x1b[0m \x1b[38;2;${hexToRgbStr(red)}m✖ Error\x1b[0m\n`, +); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4aac54a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM"], + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + "isolatedModules": true, + "noEmit": true, + "types": ["bun-types"] + }, + "include": ["src/**/*", "types/**/*"], + "exclude": ["node_modules", "dist"] +}