01 color
color2rgba
ts
type RGBA = { r: number; g: number; b: number; a?: number }
const FALLBACK_RGB: RGBA = { r: 0, g: 212, b: 255 }
let parserEl: HTMLElement | null = null
function getParserEl(): HTMLElement | null {
if (typeof document === "undefined") return null
if (!parserEl) {
parserEl = document.createElement("span")
parserEl.style.position = "absolute"
parserEl.style.visibility = "hidden"
parserEl.style.pointerEvents = "none"
document.body.appendChild(parserEl)
}
return parserEl
}
function clampAlpha(a: number): number {
if (Number.isNaN(a)) return 1
return Math.min(1, Math.max(0, a))
}
function toRgba({ r, g, b }: RGBA, alpha: number): string {
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
// -------- hex --------
export function parseHex(hex: string): RGBA | null {
const clean = hex.replace(/[^0-9a-fA-F]/g, "")
if (clean.length === 3) {
return {
r: parseInt((clean[0] ?? "0") + (clean[0] ?? "0"), 16),
g: parseInt((clean[1] ?? "0") + (clean[1] ?? "0"), 16),
b: parseInt((clean[2] ?? "0") + (clean[2] ?? "0"), 16),
}
}
if (clean.length === 6) {
return {
r: parseInt(clean.slice(0, 2), 16),
g: parseInt(clean.slice(2, 4), 16),
b: parseInt(clean.slice(4, 6), 16),
}
}
if (clean.length === 8) {
return {
r: parseInt(clean.slice(0, 2), 16),
g: parseInt(clean.slice(2, 4), 16),
b: parseInt(clean.slice(4, 6), 16),
a: parseInt(clean.slice(6, 8), 16) / 255,
}
}
return null
}
// -------- rgb --------
export function parseRgb(input: string): RGBA | null {
const match = input.match(
/rgba?\s*\(\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/
)
if (!match) return null
const parseChannel = (v: string) =>
v.endsWith("%")
? Math.round((parseFloat(v) / 100) * 255)
: parseInt(v, 10)
const r = Math.min(255, Math.max(0, parseChannel(match[1] ?? "0")))
const g = Math.min(255, Math.max(0, parseChannel(match[2] ?? "0")))
const b = Math.min(255, Math.max(0, parseChannel(match[3] ?? "0")))
const result: RGBA = { r, g, b }
const alphaStr = match[4]
if (alphaStr) {
result.a = alphaStr.endsWith("%")
? parseFloat(alphaStr) / 100
: parseFloat(alphaStr)
result.a = clampAlpha(result.a)
}
return result
}
// -------- browser fallback --------
function parseByBrowser(input: string): RGBA | null {
const el = getParserEl()
if (!el) return null
el.style.color = input
const resolved = getComputedStyle(el).color
const m = resolved.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/)
if (!m || !m[1] || !m[2] || !m[3]) return null
const result: RGBA = {
r: parseInt(m[1], 10),
g: parseInt(m[2], 10),
b: parseInt(m[3], 10),
}
if (m[4]) result.a = clampAlpha(parseFloat(m[4]))
return result
}
// -------- main --------
export function color2rgba(color: string, opacity?: number): string {
if (!color || typeof color !== "string") {
return toRgba(FALLBACK_RGB, opacity === undefined ? 1 : clampAlpha(opacity))
}
const input = color.trim()
if (!input) {
return toRgba(FALLBACK_RGB, opacity === undefined ? 1 : clampAlpha(opacity))
}
let rgb: RGBA | null = null
if (input.startsWith("#")) {
rgb = parseHex(input)
}
if (!rgb && input.startsWith("rgb")) {
rgb = parseRgb(input)
}
if (!rgb) {
rgb = parseByBrowser(input)
}
const resolved = rgb ?? FALLBACK_RGB
const alpha =
opacity !== undefined ? clampAlpha(opacity) : clampAlpha(resolved.a ?? 1)
return toRgba(resolved, alpha)
}