{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "frontispiece-plate",
  "type": "registry:component",
  "title": "Chromatic Loupe & Chrono-Degradation",
  "description": "A hoverable brass loupe with per-channel refraction over any image, plus an aging scrubber driving iron-gall fade, cellulose foxing, edge yellowing and craquelure.",
  "registryDependencies": [
    "https://chromologium.com/r/optical-engine.json"
  ],
  "files": [
    {
      "path": "components/optics/renderers/frontispiece.ts",
      "content": "/* Design tokens & architecture sourced from The Chromologium (https://chromologium.com) */\n/**\n * The Chromologium — Frontispiece Plate Renderer (Phase 4)\n *\n * Two instruments on one archival plate texture:\n *\n * 1. Chromatic loupe — a hovering optical lens that magnifies the plate and\n *    disperses its edges with per-channel refraction scaled by a Cauchy-style\n *    wavelength offset (red bends least, blue most).\n * 2. Chrono-degradation — an age uniform drives iron-gall fade, cellulose\n *    foxing blotches, edge yellowing, craquelure, and vignette, so the\n *    scrubber walks the plate between accession-era wear and modern\n *    archival restoration. The aging is applied to the reproduction plate,\n *    never to the live document text.\n *\n * The plate image is same-origin (public/monographs/…), so the texture\n * upload never taints the shared context.\n */\n\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 sampler2D u_tex;\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform vec2 u_pointer;      // uv, y up\nuniform float u_pointerIn;\nuniform float u_age;         // 0 = restored, 1 = century of neglect\nuniform float u_lensOn;\n\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  return mix(\n    mix(hash(i), hash(i + vec2(1, 0)), f.x),\n    mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x),\n    f.y\n  );\n}\nfloat fbm(vec2 p) {\n  float v = 0.0;\n  float a = 0.5;\n  for (int i = 0; i < 4; i++) {\n    v += a * noise(p);\n    p = p * 2.17 + 13.1;\n    a *= 0.5;\n  }\n  return v;\n}\n\nvec3 samplePlate(vec2 uv) {\n  return texture(u_tex, vec2(uv.x, 1.0 - uv.y)).rgb;\n}\n\nvoid main() {\n  float aspect = u_resolution.x / max(u_resolution.y, 1.0);\n  vec2 uv = v_uv;\n\n  // ---- Chromatic loupe -------------------------------------------------\n  vec3 col;\n  vec2 d = (uv - u_pointer) * vec2(aspect, 1.0);\n  float dist = length(d);\n  float lensR = 0.17;\n  float inLens = u_lensOn * u_pointerIn * smoothstep(lensR, lensR * 0.92, dist);\n\n  if (inLens > 0.001) {\n    // Barrel magnification inside the loupe\n    float k = dist / lensR;\n    vec2 mag = u_pointer + (uv - u_pointer) * mix(0.62, 1.0, k * k);\n    // Per-channel refraction: blue deviates most (n_blue > n_red)\n    vec2 off = (uv - u_pointer) * (1.0 - k) * 0.045;\n    float r = samplePlate(mag + off * 0.4).r;\n    float g = samplePlate(mag).g;\n    float b = samplePlate(mag - off * 0.7).b;\n    vec3 lensCol = vec3(r, g, b);\n    col = mix(samplePlate(uv), lensCol, inLens);\n  } else {\n    col = samplePlate(uv);\n  }\n\n  // Loupe rim: polished brass ring with a specular glint\n  float rim = u_lensOn * u_pointerIn *\n    smoothstep(0.012, 0.004, abs(dist - lensR));\n  vec3 brass = vec3(0.72, 0.58, 0.30);\n  float glint = 0.6 + 0.4 * sin(atan(d.y, d.x) * 2.0 + 0.8);\n  col = mix(col, brass * glint, rim * 0.85);\n\n  // ---- Chrono-degradation ----------------------------------------------\n  float age = clamp(u_age, 0.0, 1.0);\n\n  // Iron-gall fade: contrast decays, blacks lift warm\n  vec3 faded = mix(col, vec3(dot(col, vec3(0.299, 0.587, 0.114))), 0.35 * age);\n  faded = mix(faded, faded * vec3(1.04, 0.97, 0.86) + vec3(0.06, 0.04, 0.01), age * 0.5);\n\n  // Cellulose foxing: rust blotches emerge where fbm exceeds a falling threshold\n  float fox = fbm(uv * vec2(aspect, 1.0) * 7.0);\n  float foxing = smoothstep(1.0 - age * 0.42, 1.02 - age * 0.42, fox);\n  faded = mix(faded, vec3(0.52, 0.36, 0.20), foxing * 0.5 * age);\n\n  // Edge yellowing\n  vec2 e = abs(uv - 0.5) * 2.0;\n  float edge = smoothstep(0.55, 1.0, max(e.x, e.y));\n  faded = mix(faded, faded * vec3(0.92, 0.82, 0.58), edge * age * 0.55);\n\n  // Craquelure: thin ridged-noise fissures\n  float crack = abs(fbm(uv * vec2(aspect, 1.0) * 16.0 + 5.0) - 0.5);\n  float fissure = smoothstep(0.028, 0.0, crack) * smoothstep(0.35, 0.75, age);\n  faded = mix(faded, faded * 0.55, fissure * 0.6);\n\n  // Vignette of handling\n  float vig = smoothstep(0.5, 1.15, length((uv - 0.5) * vec2(aspect, 1.0)) * 1.5);\n  faded = mix(faded, faded * 0.72, vig * age * 0.7);\n\n  outColor = vec4(faded, 1.0);\n}`;\n\ninterface FrontispieceStore {\n  program: WebGLProgram;\n  locations: Map<string, WebGLUniformLocation | null>;\n  texture: WebGLTexture;\n  textureReady: boolean;\n}\n\nexport function createFrontispieceRenderer(imageUrl: string): OpticalRenderer {\n  return {\n    init(gl: WebGL2RenderingContext, surface: OpticalSurfaceState): void {\n      const program = createProgram(gl, FRAGMENT);\n      const texture = gl.createTexture();\n      if (!texture) throw new Error(\"optics: createTexture failed\");\n      gl.bindTexture(gl.TEXTURE_2D, texture);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n      // 1×1 vellum placeholder until the plate loads\n      gl.texImage2D(\n        gl.TEXTURE_2D,\n        0,\n        gl.RGBA,\n        1,\n        1,\n        0,\n        gl.RGBA,\n        gl.UNSIGNED_BYTE,\n        new Uint8Array([244, 240, 231, 255]),\n      );\n\n      const store: FrontispieceStore = {\n        program,\n        locations: new Map(),\n        texture,\n        textureReady: false,\n      };\n      surface.store.set(\"frontispiece\", store);\n\n      const image = new Image();\n      image.onload = () => {\n        // The surface may have been disposed while the plate downloaded\n        if (surface.store.get(\"frontispiece\") !== store) return;\n        gl.bindTexture(gl.TEXTURE_2D, store.texture);\n        gl.texImage2D(\n          gl.TEXTURE_2D,\n          0,\n          gl.RGBA,\n          gl.RGBA,\n          gl.UNSIGNED_BYTE,\n          image,\n        );\n        store.textureReady = true;\n      };\n      image.src = imageUrl;\n    },\n\n    render(\n      gl: WebGL2RenderingContext,\n      surface: OpticalSurfaceState,\n      frame: FrameInfo,\n    ): void {\n      const store = surface.store.get(\"frontispiece\") as FrontispieceStore;\n      // Until the plate texture arrives, leave the canvas transparent so the\n      // underlying <img> (the static state) keeps showing through.\n      if (!store.textureReady) return;\n      gl.useProgram(store.program);\n      gl.activeTexture(gl.TEXTURE0);\n      gl.bindTexture(gl.TEXTURE_2D, store.texture);\n      gl.uniform1i(gl.getUniformLocation(store.program, \"u_tex\"), 0);\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_lensOn: 1,\n        u_age: 0,\n        ...custom,\n      });\n      drawFullscreen(gl);\n    },\n\n    dispose(gl: WebGL2RenderingContext, surface: OpticalSurfaceState): void {\n      const store = surface.store.get(\"frontispiece\") as\n        | FrontispieceStore\n        | undefined;\n      if (!store) return;\n      gl.deleteProgram(store.program);\n      gl.deleteTexture(store.texture);\n      surface.store.delete(\"frontispiece\");\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/optics/renderers/frontispiece.ts"
    },
    {
      "path": "components/FrontispiecePlate.tsx",
      "content": "/* Design tokens & architecture sourced from The Chromologium (https://chromologium.com) */\n\"use client\";\n\n/**\n * The Chromologium — Frontispiece Plate Instrument\n *\n * Wraps a monograph's archival frontispiece image with two GPU instruments:\n * a hoverable chromatic loupe, and a chrono-degradation scrubber that walks\n * the reproduction plate between period wear and modern archival\n * restoration. The plain <img> beneath is always the static state — under\n * reduced-motion or the static tier nothing changes at all.\n */\n\nimport { useMemo, useState } from \"react\";\nimport { useOpticalSurface } from \"./optics/useOpticalSurface\";\nimport { createFrontispieceRenderer } from \"./optics/renderers/frontispiece\";\n\nexport interface FrontispiecePlateProps {\n  imageUrl: string;\n  alt: string;\n  imgClassName?: string;\n  /** Plate creation year label, e.g. \"1666\" or \"c. 1831\". */\n  year: string;\n}\n\nexport function FrontispiecePlate({\n  imageUrl,\n  alt,\n  imgClassName,\n  year,\n}: FrontispiecePlateProps) {\n  // 0 = fully restored, 1 = a century of neglect. Default: light period wear.\n  const [age, setAge] = useState(0.3);\n\n  const createRenderer = useMemo(\n    () => () => createFrontispieceRenderer(imageUrl),\n    [imageUrl],\n  );\n\n  const { hostRef, canvasRef, ready, active } = useOpticalSurface({\n    createRenderer,\n    getUniforms: () => ({ u_age: age }),\n    interactive: true,\n  });\n\n  return (\n    <figure className=\"frontispiece-instrument\">\n      <div ref={hostRef} className=\"frontispiece-instrument-host optical-host\">\n        <img src={imageUrl} alt={alt} className={imgClassName} />\n        <canvas\n          ref={canvasRef}\n          className={\n            ready ? \"optical-surface-canvas is-ready\" : \"optical-surface-canvas\"\n          }\n          aria-hidden\n        />\n      </div>\n      {active && (\n        <div className=\"frontispiece-chrono-row\">\n          <span className=\"frontispiece-chrono-label\">{year}</span>\n          <input\n            type=\"range\"\n            className=\"frontispiece-chrono-slider\"\n            min={0}\n            max={1}\n            step={0.01}\n            // Slider right = restoration; invert to the age uniform\n            value={1 - age}\n            onChange={(e) => setAge(1 - parseFloat(e.target.value))}\n            aria-label=\"Chrono-degradation scrubber — drag right to restore the plate, left to age it\"\n          />\n          <span className=\"frontispiece-chrono-label\">2026 RESTORATION</span>\n        </div>\n      )}\n    </figure>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/FrontispiecePlate.tsx"
    }
  ],
  "author": "The Chromologium",
  "homepage": "https://chromologium.com",
  "license": "MIT"
}
