{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prism-lens",
  "type": "registry:component",
  "title": "Cauchy Dispersion Prism",
  "description": "A collimated beam refracted into 16 spectral bands by the genuine Cauchy relation n(λ) = A + B/λ², with symmetric-passage prism deviation. Ships with real optical glass constants.",
  "registryDependencies": [
    "https://chromologium.com/r/optical-engine.json"
  ],
  "files": [
    {
      "path": "components/optics/renderers/dispersion.ts",
      "content": "/* Design tokens & architecture sourced from The Chromologium (https://chromologium.com) */\n/**\n * The Chromologium — Cauchy Dispersion Renderer (LAB-07)\n *\n * Renders a collimated white beam through a prism and fans it into sixteen\n * spectral bands using the genuine Cauchy relation n(λ) = A + B/λ² and the\n * symmetric-passage deviation δ(λ) = 2·asin(n·sin(α/2)) − α. The band nearest\n * the live `--ds-accent` token's dominant wavelength is emphasized, so the\n * instrument decomposes the active movement's own chromatic focal point.\n */\n\nimport { readTokenSrgb } from \"@/lib/shaderTokens\";\nimport { createProgram, drawFullscreen, setUniforms } from \"../glUtils\";\nimport type { FrameInfo, OpticalRenderer, OpticalSurfaceState } from \"../types\";\n\nconst FRAGMENT = `#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 outColor;\n\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform vec2 u_pointer;      // normalized 0..1, y up\nuniform float u_pointerIn;\nuniform float u_cauchyA;\nuniform float u_cauchyB;     // nm^2 (scaled: B/1e4 passed as-is in nm^2)\nuniform float u_apex;        // radians\nuniform float u_accentLambda;\nuniform vec3 u_bg;\nuniform vec3 u_fg;\nuniform vec3 u_accent;\n\n// CIE-ish wavelength (nm) to linear-light RGB approximation.\nvec3 wavelengthToRgb(float l) {\n  vec3 c = vec3(0.0);\n  if (l < 440.0)      c = vec3((440.0 - l) / 60.0, 0.0, 1.0);\n  else if (l < 490.0) c = vec3(0.0, (l - 440.0) / 50.0, 1.0);\n  else if (l < 510.0) c = vec3(0.0, 1.0, (510.0 - l) / 20.0);\n  else if (l < 580.0) c = vec3((l - 510.0) / 70.0, 1.0, 0.0);\n  else if (l < 645.0) c = vec3(1.0, (645.0 - l) / 65.0, 0.0);\n  else                c = vec3(1.0, 0.0, 0.0);\n  // Visibility falloff at spectrum edges\n  float fade = 1.0;\n  if (l < 420.0) fade = 0.35 + 0.65 * (l - 380.0) / 40.0;\n  if (l > 660.0) fade = 0.35 + 0.65 * (700.0 - l) / 40.0;\n  return c * fade;\n}\n\nfloat segGlow(vec2 p, vec2 a, vec2 b, float sharp) {\n  vec2 pa = p - a;\n  vec2 ba = b - a;\n  float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n  float d = length(pa - ba * h);\n  return exp(-d * d * sharp);\n}\n\nfloat rayGlow(vec2 p, vec2 origin, vec2 dir, float sharp) {\n  vec2 po = p - origin;\n  float t = max(dot(po, dir), 0.0);\n  float d = length(po - dir * t);\n  // soften with distance for a physical falloff\n  return exp(-d * d * sharp) * exp(-t * 0.35);\n}\n\n// Signed distance to an apex-up equilateral-ish triangle centered at c.\nfloat triangleSdf(vec2 p, vec2 c, float r) {\n  p -= c;\n  const float k = 1.7320508;\n  p.x = abs(p.x) - r;\n  p.y = p.y + r / k - r * 0.35;\n  if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;\n  p.x -= clamp(p.x, -2.0 * r, 0.0);\n  return -length(p) * sign(p.y);\n}\n\nvoid main() {\n  float aspect = u_resolution.x / max(u_resolution.y, 1.0);\n  vec2 p = (v_uv - 0.5) * vec2(aspect, 1.0);\n\n  vec3 col = u_bg;\n\n  // Optical bench vignette\n  col *= 1.0 - 0.28 * dot(p, p);\n\n  // Prism body\n  vec2 prismC = vec2(-0.02, 0.03);\n  float prismR = 0.21;\n  float sd = triangleSdf(p, prismC, prismR);\n  float inside = smoothstep(0.006, -0.006, sd);\n  vec3 glass = mix(u_bg, u_accent, 0.14) + vec3(0.03);\n  col = mix(col, glass, inside * 0.85);\n  // Fresnel edge highlight\n  float edge = smoothstep(0.012, 0.0, abs(sd)) * 0.5;\n  col += vec3(1.0) * edge * 0.35;\n\n  // Incident white beam: from left edge toward the prism's left face,\n  // height steered by the pointer. White light is white in any movement.\n  float beamY = mix(0.05, (u_pointer.y - 0.5) * 0.6 + 0.03, u_pointerIn);\n  vec2 entry = vec2(prismC.x - prismR * 0.62, beamY);\n  float beam = segGlow(p, vec2(-aspect * 0.6, beamY), entry, 1500.0);\n  col += vec3(1.0) * beam * 1.1;\n\n  // Spectral fan out of the right face\n  vec2 exitP = vec2(prismC.x + prismR * 0.55, beamY * 0.55 + 0.01);\n  float halfApex = u_apex * 0.5;\n  vec3 fan = vec3(0.0);\n  for (int i = 0; i < 16; i++) {\n    float f = float(i) / 15.0;\n    float lambda = 400.0 + 300.0 * f;\n    float n = u_cauchyA + u_cauchyB / (lambda * lambda);\n    float s = clamp(n * sin(halfApex), -1.0, 1.0);\n    float dev = 2.0 * asin(s) - u_apex;      // physical deviation, radians\n    // project deviation into screen space: relative spreads stay\n    // proportional, the multiplier only widens the stage\n    float screenAng = (dev - 0.62) * 5.0 + 0.32;\n    vec2 dir = normalize(vec2(cos(screenAng), -sin(screenAng)));\n    float w = rayGlow(p, exitP, dir, 6000.0);\n    float emphasis = 1.0 + 2.2 * exp(-pow((lambda - u_accentLambda) / 16.0, 2.0))\n      * (0.75 + 0.25 * sin(u_time * 2.2));\n    fan += wavelengthToRgb(lambda) * w * emphasis;\n  }\n  col += fan * 0.30;\n\n  // Caustic pool where the fan lands\n  float pool = exp(-pow((p.y + 0.34) * 9.0, 2.0)) *\n    exp(-pow((p.x - 0.32) * 2.4, 2.0));\n  col += u_accent * pool * 0.10;\n\n  outColor = vec4(col, 1.0);\n}`;\n\ninterface DispersionStore {\n  program: WebGLProgram;\n  locations: Map<string, WebGLUniformLocation | null>;\n}\n\n/** Dominant-wavelength estimate (nm) for an sRGB triple, via HSV hue. */\nexport function dominantWavelength(rgb: [number, number, number]): number {\n  const [r, g, b] = rgb;\n  const max = Math.max(r, g, b);\n  const min = Math.min(r, g, b);\n  const d = max - min;\n  if (d < 1e-5) return 560; // achromatic — park mid-spectrum\n  let hue: number;\n  if (max === r) hue = ((g - b) / d + (g < b ? 6 : 0)) * 60;\n  else if (max === g) hue = ((b - r) / d + 2) * 60;\n  else hue = ((r - g) / d + 4) * 60;\n  // Piecewise hue → wavelength; extra-spectral purples clamp to violet.\n  const stops: [number, number][] = [\n    [0, 650],\n    [60, 580],\n    [120, 530],\n    [180, 490],\n    [240, 460],\n    [300, 430],\n    [360, 430],\n  ];\n  for (let i = 0; i < stops.length - 1; i++) {\n    const [h0, l0] = stops[i];\n    const [h1, l1] = stops[i + 1];\n    if (hue >= h0 && hue <= h1) {\n      const t = (hue - h0) / (h1 - h0);\n      return l0 + (l1 - l0) * t;\n    }\n  }\n  return 560;\n}\n\nexport function createDispersionRenderer(): OpticalRenderer {\n  return {\n    init(gl: WebGL2RenderingContext, surface: OpticalSurfaceState): void {\n      const program = createProgram(gl, FRAGMENT);\n      const store: DispersionStore = { program, locations: new Map() };\n      surface.store.set(\"dispersion\", store);\n    },\n\n    render(\n      gl: WebGL2RenderingContext,\n      surface: OpticalSurfaceState,\n      frame: FrameInfo,\n    ): void {\n      const store = surface.store.get(\"dispersion\") as DispersionStore;\n      gl.useProgram(store.program);\n      const accent = readTokenSrgb(\"--ds-accent\", [0.79, 0.65, 0.35]);\n      // The bench is a camera obscura: derive a dark chamber ground from the\n      // movement's primary surface so spectral rays stay legible in light\n      // movements while dark movements keep their native depth.\n      const bg = readTokenSrgb(\"--ds-bg-primary\", [0.06, 0.06, 0.08]);\n      const chamber: [number, number, number] = [\n        bg[0] * 0.13 + 0.02,\n        bg[1] * 0.13 + 0.02,\n        bg[2] * 0.13 + 0.025,\n      ];\n      const custom = surface.getUniforms?.() ?? {};\n      setUniforms(gl, store.program, store.locations, {\n        u_resolution: [surface.width, surface.height],\n        u_time: frame.t,\n        u_pointer: [\n          surface.pointer.x / Math.max(1, surface.cssWidth),\n          1 - surface.pointer.y / Math.max(1, surface.cssHeight),\n        ],\n        u_pointerIn: surface.pointer.inside ? 1 : 0,\n        u_accentLambda: dominantWavelength(accent),\n        u_bg: chamber,\n        u_fg: readTokenSrgb(\"--ds-fg-primary\", [0.92, 0.92, 0.94]),\n        u_accent: accent,\n        ...custom,\n      });\n      drawFullscreen(gl);\n    },\n\n    dispose(gl: WebGL2RenderingContext, surface: OpticalSurfaceState): void {\n      const store = surface.store.get(\"dispersion\") as\n        | DispersionStore\n        | undefined;\n      if (store) gl.deleteProgram(store.program);\n      surface.store.delete(\"dispersion\");\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/optics/renderers/dispersion.ts"
    }
  ],
  "meta": {
    "glasses": {
      "bk7": {
        "A": 1.5046,
        "B": 4200,
        "note": "Borosilicate crown"
      },
      "sf10": {
        "A": 1.728,
        "B": 13420,
        "note": "Dense flint"
      },
      "fused-silica": {
        "A": 1.458,
        "B": 3540,
        "note": "UV-grade quartz"
      },
      "bohemian-crystal": {
        "A": 1.64,
        "B": 10800,
        "note": "Lead crystal (Atlas CZE)"
      }
    },
    "citation": "Cauchy, A.-L. (1836). Mémoire sur la dispersion de la lumière. J. G. Calve."
  },
  "author": "The Chromologium",
  "homepage": "https://chromologium.com",
  "license": "MIT"
}
