All files / src/components/Skeleton/utils color.ts

100% Statements 91/91
98.18% Branches 54/55
100% Functions 19/19
100% Lines 70/70

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145  10035x 22473x     178x 178x   171x   171x 171x 171x 171x   171x 3x 3x 3x 3x   168x 168x 168x 168x     171x       2392x 2392x 2392x   2391x 2392x 2391x 2391x   2391x 9548x 9x   2392x   2390x 7170x 7167x     2390x 2390x 2390x 7168x   2389x 2389x 2392x 2386x 2386x 2385x 2386x     2389x     2576x 2576x   2574x 2574x 2572x   2571x 2393x   1x     7656x   2552x 7656x     2552x       7656x       2549x 2549x                 3x 3x                 2549x                           2549x 2549x             2548x 2548x   2548x 2549x 2549x    
export type RGBA = { r: number; g: number; b: number; a: number };
const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
const clamp255 = (x: number) => Math.max(0, Math.min(255, Math.round(x)));
 
function parseHex(hex: string): RGBA | null {
  const h = hex.replace("#", "").trim();
  if (![3, 4, 6, 8].includes(h.length) || !/^[0-9a-f]+$/i.test(h)) return null;
 
  const expand = (s: string) => (s.length === 1 ? s + s : s);
 
  let r = 0,
    g = 0,
    b = 0,
    a = 255;
 
  if (h.length === 3 || h.length === 4) {
    r = parseInt(expand(h[0]!), 16);
    g = parseInt(expand(h[1]!), 16);
    b = parseInt(expand(h[2]!), 16);
    if (h.length === 4) a = parseInt(expand(h[3]!), 16);
  } else {
    r = parseInt(h.slice(0, 2), 16);
    g = parseInt(h.slice(2, 4), 16);
    b = parseInt(h.slice(4, 6), 16);
    if (h.length === 8) a = parseInt(h.slice(6, 8), 16);
  }
 
  return { r, g, b, a: a / 255 };
}
 
function parseRgb(input: string): RGBA | null {
  const s = input.trim().toLowerCase();
  const m = s.match(/^rgba?\((.*)\)$/);
  if (!m) return null;
 
  const body = m[1]!.trim();
  const slashParts = body.split("/").map((x) => x.trim());
  const left = slashParts[0]!;
  const alphaPart = slashParts[1];
 
  const comps = left.includes(",")
    ? left.split(",").map((x) => x.trim())
    : left.split(/\s+/).map((x) => x.trim());
 
  if (comps.length < 3) return null;
 
  const to255 = (v: string) => {
    if (v.endsWith("%")) return clamp255(parseFloat(v) * 2.55);
    return clamp255(parseFloat(v));
  };
 
  const r = to255(comps[0]!);
  const g = to255(comps[1]!);
  const b = to255(comps[2]!);
  if ([r, g, b].some((n) => Number.isNaN(n))) return null;
 
  let a = 1;
  const aStr = alphaPart ?? comps[3];
  if (aStr != null) {
    const t = aStr.trim();
    if (t.endsWith("%")) a = clamp01(parseFloat(t) / 100);
    else a = clamp01(parseFloat(t));
    if (Number.isNaN(a)) a = 1;
  }
 
  return { r, g, b, a };
}
export function parseColorToRgba(input: string): RGBA | null {
  const s = input.trim();
  if (!s) return null;
 
  const lower = s.toLowerCase();
  if (lower.startsWith("var(")) return null;
  if (lower === "currentcolor") return null;
 
  if (s.startsWith("#")) return parseHex(s);
  if (lower.startsWith("rgb")) return parseRgb(s);
 
  return null;
}
export function luminance({ r, g, b }: RGBA): number {
  const srgb = [r, g, b].map((v) => v / 255);
 
  const lin = srgb.map((c) =>
    c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
  );
 
  return 0.2126 * lin[0]! + 0.7152 * lin[1]! + 0.0722 * lin[2]!;
}
 
function blendChannel(c: number, target: number, t: number) {
  return clamp255(c + (target - c) * t);
}
 
export function lighten(rgba: RGBA, t = 0.45): RGBA {
  const k = clamp01(t);
  return {
    r: blendChannel(rgba.r, 255, k),
    g: blendChannel(rgba.g, 255, k),
    b: blendChannel(rgba.b, 255, k),
    a: rgba.a,
  };
}
 
export function darken(rgba: RGBA, t = 0.22): RGBA {
  const k = clamp01(t);
  return {
    r: blendChannel(rgba.r, 0, k),
    g: blendChannel(rgba.g, 0, k),
    b: blendChannel(rgba.b, 0, k),
    a: rgba.a,
  };
}
 
export function toRgbaString({ r, g, b, a }: RGBA): string {
  return `rgba(${clamp255(r)}, ${clamp255(g)}, ${clamp255(b)}, ${clamp01(a)})`;
}
 
export type AutoHighlightOptions = {
  lightThreshold?: number;
  lightenBy?: number;
  darkenBy?: number;
  alphaBoost?: number;
};
 
export function generateAutoHighlight(
  baseColor: string,
  opts: AutoHighlightOptions = {}
): string | null {
  const rgba = parseColorToRgba(baseColor);
  if (!rgba) return null;
 
  const {
    lightThreshold = 0.8,
    lightenBy = 0.5,
    darkenBy = 0.22,
    alphaBoost = 0.06,
  } = opts;
  const L = luminance(rgba);
  const hi =
    L >= lightThreshold ? darken(rgba, darkenBy) : lighten(rgba, lightenBy);
  hi.a = clamp01(Math.max(hi.a, rgba.a + alphaBoost));
  return toRgbaString(hi);
}