UNPKG

@thi.ng/color

Version:

Array-based color types, CSS parsing, conversions, transformations, declarative theme generation, gradients, presets

120 lines (119 loc) 3.65 kB
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 };