@thi.ng/color
Version:
Array-based color types, CSS parsing, conversions, transformations, declarative theme generation, gradients, presets
120 lines (119 loc) • 3.65 kB
JavaScript
import { rotateRight } from "@thi.ng/binary/rotate";
import { interleave4_12_24, interleave4_16_32 } from "@thi.ng/binary/splat";
import { isString } from "@thi.ng/checks/is-string";
import { assert } from "@thi.ng/errors/assert";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
import { unsupported } from "@thi.ng/errors/unsupported";
import { TAU } from "@thi.ng/math/api";
import { clamp01 } from "@thi.ng/math/interval";
import { fract } from "@thi.ng/math/prec";
import { ParsedColor } from "../api.js";
import { INV8BIT } from "../api/constants.js";
import { CSS_NAMES } from "../api/names.js";
import { CSS_SYSTEM_COLORS } from "../api/system.js";
import { intArgb32Srgb } from "../int/int-srgb.js";
const parseCss = (src) => {
src = (isString(src) ? src : src.deref()).toLowerCase();
const named = CSS_NAMES[src] || CSS_SYSTEM_COLORS[src];
if (named || src[0] === "#")
return new ParsedColor(
"srgb",
intArgb32Srgb([], parseHex(named || src))
);
const parts = src.split(/[(),/ ]+/);
const [mode, a, b, c, d] = parts;
assert(parts.length === 5 || parts.length === 6, `invalid color: ${src}`);
switch (mode) {
case "rgb":
case "rgba":
return new ParsedColor("srgb", [
__numOrPercent(a, 1, INV8BIT, true),
__numOrPercent(b, 1, INV8BIT, true),
__numOrPercent(c, 1, INV8BIT, true),
__alpha(d)
]);
case "hsl":
case "hsla":
return new ParsedColor("hsl", [
__hue(a),
__percent(b),
__percent(c),
__alpha(d)
]);
case "lab":
return new ParsedColor("lab50", [
__numOrPercent(a),
__numOrPercent(b, 1.25),
__numOrPercent(c, 1.25),
__alpha(d)
]);
case "lch":
return new ParsedColor(mode, [
__numOrPercent(a),
__numOrPercent(b, 1.5),
__hue(c),
__alpha(d)
]);
case "oklab":
return new ParsedColor(mode, [
__numOrPercent(a, 1, 1),
__numOrPercent(b, 0.4, 1),
__numOrPercent(c, 0.4, 1),
__alpha(d)
]);
case "oklch":
return new ParsedColor(mode, [
__numOrPercent(a, 1, 1),
__numOrPercent(b, 0.4, 1),
__hue(c),
__alpha(d)
]);
default:
unsupported(`color mode: ${mode}`);
}
};
const HUE_NORMS = {
rad: TAU,
grad: 400,
turn: 1,
deg: 360
};
const __hue = (x) => {
const match = /^(-?[0-9.]+)(deg|rad|grad|turn)?$/.exec(x);
assert(!!match, `expected hue, got: ${x}`);
return fract(parseFloat(match[1]) / (HUE_NORMS[match[2]] || 360));
};
const __alpha = (x) => x ? __numOrPercent(x, 1, 1, true) : 1;
const __percent = (x, clamp = true) => {
assert(/^([0-9.]+)%$/.test(x), `expected percentage, got: ${x}`);
const res = parseFloat(x) / 100;
return clamp ? clamp01(res) : res;
};
const __numOrPercent = (x, scalePerc = 1, scale = 0.01, clamp = false) => {
assert(/^-?[0-9.]+%?$/.test(x), `expected number or percentage, got: ${x}`);
const res = parseFloat(x) * (x.endsWith("%") ? 0.01 * scalePerc : scale);
return clamp ? clamp01(res) : res;
};
const parseHex = (src) => {
const match = /^#?([0-9a-f]{3,8})$/i.exec(src);
if (match) {
const hex = match[1];
const val = parseInt(hex, 16);
switch (hex.length) {
case 3:
return (interleave4_12_24(val) | 4278190080) >>> 0;
case 4:
return rotateRight(interleave4_16_32(val), 8);
case 6:
return (val | 4278190080) >>> 0;
case 8:
return rotateRight(val, 8);
default:
}
}
return illegalArgs(`invalid hex color: "${src}"`);
};
export {
parseCss,
parseHex
};