You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

116 lines
3.0 KiB

import type { ColorValue, HSVColor } from './types';
import { hexToRgb, rgbToHsv, hslToHsv } from './convert';
const HEX_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isFiniteNumberInRange(value: unknown, min: number, max: number): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max;
}
function isRgbColor(value: unknown): boolean {
if (!isRecord(value)) return false;
return (
isFiniteNumberInRange(value.r, 0, 255) &&
isFiniteNumberInRange(value.g, 0, 255) &&
isFiniteNumberInRange(value.b, 0, 255) &&
isFiniteNumberInRange(value.a, 0, 1)
);
}
function isHslColor(value: unknown): boolean {
if (!isRecord(value)) return false;
return (
isFiniteNumberInRange(value.h, 0, 360) &&
isFiniteNumberInRange(value.s, 0, 100) &&
isFiniteNumberInRange(value.l, 0, 100) &&
isFiniteNumberInRange(value.a, 0, 1)
);
}
function isHsvColor(value: unknown): boolean {
if (!isRecord(value)) return false;
return (
isFiniteNumberInRange(value.h, 0, 360) &&
isFiniteNumberInRange(value.s, 0, 100) &&
isFiniteNumberInRange(value.v, 0, 100) &&
isFiniteNumberInRange(value.a, 0, 1)
);
}
/**
* Parses any supported CSS color string to internal HSV.
* Supports: #hex, rgb(), rgba(), hsl(), hsla()
* Returns null if the string cannot be parsed.
*/
export function parseColor(value: string): HSVColor | null {
const str = value.trim().toLowerCase();
const numeric = String.raw`(?:\d+(?:\.\d+)?|\.\d+)`;
// HEX
if (str.startsWith('#')) {
const rgb = hexToRgb(str);
return rgb ? rgbToHsv(rgb) : null;
}
// rgb() / rgba()
const rgbMatch = str.match(
new RegExp(
String.raw`^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*(${numeric}))?\s*\)$`
)
);
if (rgbMatch) {
const [, r, g, b, a] = rgbMatch;
const rgb = {
r: parseInt(r!, 10),
g: parseInt(g!, 10),
b: parseInt(b!, 10),
a: a !== undefined ? parseFloat(a) : 1
};
return isRgbColor(rgb) ? rgbToHsv(rgb) : null;
}
// hsl() / hsla()
const hslMatch = str.match(
new RegExp(
String.raw`^hsla?\(\s*(${numeric})\s*,\s*(${numeric})%\s*,\s*(${numeric})%(?:\s*,\s*(${numeric}))?\s*\)$`
)
);
if (hslMatch) {
const [, h, s, l, a] = hslMatch;
const hsl = {
h: parseFloat(h!),
s: parseFloat(s!),
l: parseFloat(l!),
a: a !== undefined ? parseFloat(a) : 1
};
return isHslColor(hsl) ? hslToHsv(hsl) : null;
}
return null;
}
/**
* Returns true if the string is a parseable CSS color.
*/
export function isValidColor(value: string): boolean {
return parseColor(value) !== null;
}
/**
* Type guard for the normalized `ColorValue` shape used across Active UI layers.
*/
export function isColorValue(value: unknown): value is ColorValue {
if (!isRecord(value)) return false;
return (
typeof value.hex === 'string' &&
HEX_RE.test(value.hex) &&
isRgbColor(value.rgb) &&
isHslColor(value.hsl) &&
isHsvColor(value.hsv)
);
}

Powered by TurnKey Linux.