{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "color-vision",
  "title": "色觉计算",
  "description": "零依赖色觉模拟、Lab 与 CIEDE2000 色差计算。",
  "registryDependencies": [
    "https://yiui.dev/r/color.json"
  ],
  "files": [
    {
      "path": "src/registry/lib/color-vision.ts",
      "content": "import { parseHex, rgbToHex } from \"@/lib/color\"\n\ntype ColorVision = \"normal\" | \"protanopia\" | \"deuteranopia\" | \"tritanopia\"\ntype Lab = { l: number; a: number; b: number }\ntype ColorConflict = {\n  a: number\n  b: number\n  vision: ColorVision\n  deltaE: number\n}\n// Machado、Oliveira、Fernandes 2009，作者发布表的 severity 1.0：\n// https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html\nconst matrices = {\n  protanopia: [\n    [0.152286, 1.052583, -0.204868],\n    [0.114503, 0.786281, 0.099216],\n    [-0.003882, -0.048116, 1.051998],\n  ],\n  deuteranopia: [\n    [0.367322, 0.860646, -0.227968],\n    [0.280085, 0.672501, 0.047413],\n    [-0.01182, 0.04294, 0.968881],\n  ],\n  tritanopia: [\n    [1.255528, -0.076749, -0.178779],\n    [-0.078411, 0.930809, 0.147602],\n    [0.004733, 0.691367, 0.3039],\n  ],\n} as const\nfunction linear(v: number) {\n  v /= 255\n  return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4\n}\nfunction channels(hex: string) {\n  const rgb = parseHex(hex)\n  if (!rgb) throw new TypeError(\"无效的 HEX 颜色\")\n  return [linear(rgb.r), linear(rgb.g), linear(rgb.b)] as const\n}\n/** 模拟结果保留线性 RGB 浮点值：色差直接用它计算，取整只发生在输出 HEX 时 */\nfunction simulateLinear(hex: string, type: Exclude<ColorVision, \"normal\">) {\n  const rgb = channels(hex)\n  const transform = (row: readonly [number, number, number]) =>\n    Math.max(\n      0,\n      Math.min(\n        1,\n        row.reduce((sum, coefficient, i) => sum + coefficient * rgb[i]!, 0),\n      ),\n    )\n  const matrix = matrices[type]\n  return [\n    transform(matrix[0]),\n    transform(matrix[1]),\n    transform(matrix[2]),\n  ] as const\n}\nfunction simulateColorVision(\n  hex: string,\n  type: Exclude<ColorVision, \"normal\">,\n) {\n  const [r, g, b] = simulateLinear(hex, type)\n  const encode = (v: number) =>\n    255 * (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055)\n  return rgbToHex({ r: encode(r), g: encode(g), b: encode(b), a: 1 })\n}\nfunction linearToLab([r, g, b]: readonly [number, number, number]): Lab {\n  const f = (v: number) =>\n    v > (6 / 29) ** 3 ? Math.cbrt(v) : v / (3 * (6 / 29) ** 2) + 4 / 29\n  const x = f((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047)\n  const y = f(0.2126729 * r + 0.7151522 * g + 0.072175 * b)\n  const z = f((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / 1.08883)\n  return { l: 116 * y - 16, a: 500 * (x - y), b: 200 * (y - z) }\n}\nfunction hexToLab(hex: string): Lab {\n  return linearToLab(channels(hex))\n}\nfunction deltaE2000(p: Lab, q: Lab) {\n  const rad = Math.PI / 180,\n    cos = (v: number) => Math.cos(v * rad),\n    sin = (v: number) => Math.sin(v * rad)\n  const c = (Math.hypot(p.a, p.b) + Math.hypot(q.a, q.b)) / 2\n  const g = 0.5 * (1 - Math.sqrt(c ** 7 / (c ** 7 + 25 ** 7)))\n  const a1 = (1 + g) * p.a,\n    a2 = (1 + g) * q.a\n  const c1 = Math.hypot(a1, p.b),\n    c2 = Math.hypot(a2, q.b)\n  const hue = (a: number, b: number) =>\n    a === 0 && b === 0 ? 0 : (Math.atan2(b, a) / rad + 360) % 360\n  const h1 = hue(a1, p.b),\n    h2 = hue(a2, q.b)\n  const dl = q.l - p.l,\n    dc = c2 - c1\n  let dh = h2 - h1\n  if (c1 * c2 === 0) dh = 0\n  else if (dh > 180) dh -= 360\n  else if (dh < -180) dh += 360\n  const dH = 2 * Math.sqrt(c1 * c2) * sin(dh / 2)\n  const L = (p.l + q.l) / 2,\n    C = (c1 + c2) / 2\n  const H =\n    c1 * c2 === 0\n      ? h1 + h2\n      : Math.abs(h1 - h2) <= 180\n        ? (h1 + h2) / 2\n        : (h1 + h2 + (h1 + h2 < 360 ? 360 : -360)) / 2\n  const T =\n    1 -\n    0.17 * cos(H - 30) +\n    0.24 * cos(2 * H) +\n    0.32 * cos(3 * H + 6) -\n    0.2 * cos(4 * H - 63)\n  const sl = 1 + (0.015 * (L - 50) ** 2) / Math.sqrt(20 + (L - 50) ** 2),\n    sc = 1 + 0.045 * C,\n    sh = 1 + 0.015 * C * T\n  const rt =\n    -2 *\n    Math.sqrt(C ** 7 / (C ** 7 + 25 ** 7)) *\n    sin(60 * Math.exp(-(((H - 275) / 25) ** 2)))\n  return Math.sqrt(\n    (dl / sl) ** 2 +\n      (dc / sc) ** 2 +\n      (dH / sh) ** 2 +\n      rt * (dc / sc) * (dH / sh),\n  )\n}\nfunction findColorConflicts(\n  colors: string[],\n  {\n    threshold = 10,\n    visions = [\"normal\", \"protanopia\", \"deuteranopia\", \"tritanopia\"],\n  }: { threshold?: number; visions?: ColorVision[] } = {},\n) {\n  const result: ColorConflict[] = []\n  for (const vision of visions) {\n    const labs = colors.map((c) =>\n      linearToLab(\n        vision === \"normal\" ? channels(c) : simulateLinear(c, vision),\n      ),\n    )\n    for (let a = 0; a < labs.length; a++)\n      for (let b = a + 1; b < labs.length; b++) {\n        const deltaE = deltaE2000(labs[a]!, labs[b]!)\n        if (deltaE < threshold) result.push({ a, b, vision, deltaE })\n      }\n  }\n  return result\n}\nexport { simulateColorVision, hexToLab, deltaE2000, findColorConflicts }\nexport type { ColorConflict, ColorVision, Lab }\n",
      "type": "registry:ui",
      "target": "lib/color-vision.ts"
    }
  ],
  "type": "registry:lib"
}
