parent
907724c772
commit
d077bf6bcc
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"base": "#2b2b2b"
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"bg": "#0f0f10",
|
||||||
|
"fg": "#e6e6e6",
|
||||||
|
"accentRedHue": 0,
|
||||||
|
"accentGreenHue": 140,
|
||||||
|
"accentSaturation": 65,
|
||||||
|
"accentLightness": 60,
|
||||||
|
"shadeSteps": 4
|
||||||
|
}
|
||||||
@ -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<T> = (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<string> = (r, g, b) =>
|
||||||
|
"#" + [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
||||||
|
|
||||||
|
const rgbToRatios: RgbProcessingFunc<Color> = (rIN, gIN, bIN) => ({
|
||||||
|
r: byteToRatio(rIN),
|
||||||
|
g: byteToRatio(gIN),
|
||||||
|
b: byteToRatio(bIN),
|
||||||
|
});
|
||||||
|
|
||||||
|
const rgbToHsl: RgbProcessingFunc<HSL> = (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 };
|
||||||
@ -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`,
|
||||||
|
);
|
||||||
@ -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"]
|
||||||
|
}
|
||||||
Loading…
Reference in new issue