Files
yaak/packages/theme/src/yaakColor.ts
2026-05-07 15:50:10 -07:00

150 lines
3.9 KiB
TypeScript

import parseColor from "parse-color";
export class YaakColor {
private readonly appearance: "dark" | "light" = "light";
private hue = 0;
private saturation = 0;
private lightness = 0;
private alpha = 1;
constructor(cssColor: string, appearance: "dark" | "light" = "light") {
try {
this.set(cssColor);
this.appearance = appearance;
} catch (err) {
console.log("Failed to parse CSS color", cssColor, err);
}
}
static transparent(): YaakColor {
return new YaakColor("rgb(0,0,0)", "light").translucify(1);
}
static white(): YaakColor {
return new YaakColor("rgb(0,0,0)", "light").lower(1);
}
static black(): YaakColor {
return new YaakColor("rgb(0,0,0)", "light").lift(1);
}
set(cssColor: string): YaakColor {
let fixedCssColor = cssColor;
if (cssColor.startsWith("#") && cssColor.length === 9) {
const [r, g, b, a] = hexToRgba(cssColor);
fixedCssColor = `rgba(${r},${g},${b},${a})`;
}
const { hsla } = parseColor(fixedCssColor);
this.hue = hsla[0];
this.saturation = hsla[1];
this.lightness = hsla[2];
this.alpha = hsla[3] ?? 1;
return this;
}
clone(): YaakColor {
return new YaakColor(this.css(), this.appearance);
}
lower(mod: number): YaakColor {
return this.appearance === "dark" ? this._darken(mod) : this._lighten(mod);
}
lift(mod: number): YaakColor {
return this.appearance === "dark" ? this._lighten(mod) : this._darken(mod);
}
minLightness(n: number): YaakColor {
const color = this.clone();
if (color.lightness < n) {
color.lightness = n;
}
return color;
}
isDark(): boolean {
return this.lightness < 50;
}
translucify(mod: number): YaakColor {
const color = this.clone();
color.alpha = color.alpha - color.alpha * mod;
return color;
}
opacify(mod: number): YaakColor {
const color = this.clone();
color.alpha = this.alpha + (100 - this.alpha) * mod;
return color;
}
desaturate(mod: number): YaakColor {
const color = this.clone();
color.saturation = color.saturation - color.saturation * mod;
return color;
}
saturate(mod: number): YaakColor {
const color = this.clone();
color.saturation = this.saturation + (100 - this.saturation) * mod;
return color;
}
lighterThan(color: YaakColor): boolean {
return this.lightness > color.lightness;
}
css(): string {
const [r, g, b] = parseColor(`hsl(${this.hue},${this.saturation}%,${this.lightness}%)`).rgb;
return rgbaToHex(r, g, b, this.alpha);
}
hexNoAlpha(): string {
const [r, g, b] = parseColor(`hsl(${this.hue},${this.saturation}%,${this.lightness}%)`).rgb;
return rgbaToHexNoAlpha(r, g, b);
}
private _lighten(mod: number): YaakColor {
const color = this.clone();
color.lightness = this.lightness + (100 - this.lightness) * mod;
return color;
}
private _darken(mod: number): YaakColor {
const color = this.clone();
color.lightness = this.lightness - this.lightness * mod;
return color;
}
}
function rgbaToHex(r: number, g: number, b: number, a: number): string {
const toHex = (n: number): string => {
const hex = Number(Math.round(n)).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
return `#${[toHex(r), toHex(g), toHex(b), toHex(a * 255)].join("").toUpperCase()}`;
}
function rgbaToHexNoAlpha(r: number, g: number, b: number): string {
const toHex = (n: number): string => {
const hex = Number(Math.round(n)).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
return `#${[toHex(r), toHex(g), toHex(b)].join("").toUpperCase()}`;
}
function hexToRgba(hex: string): [number, number, number, number] {
const fromHex = (value: string): number => {
if (value === "") return 255;
return Number(`0x${value}`);
};
const r = fromHex(hex.slice(1, 3));
const g = fromHex(hex.slice(3, 5));
const b = fromHex(hex.slice(5, 7));
const a = fromHex(hex.slice(7, 9));
return [r, g, b, a / 255];
}