shadimage
morphiq
componentprism-glasssource: ok · refreshed

Prism Glass

A GPU-accelerated refractive lens for images and rendered textures. Spectral dispersion, uniform frost and a directional rim light, from an SDF-defined shape — no 3D model required. It refracts the texture you give it, not the DOM behind it.

npx shadcn add "https://morphiq.prathampatel.design/r/prism-glass.json"
free

Visual style

glass-blur3d

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · heavythree
webglthree@react-three/fiber@react-three/drei

Files5 · source cached 10/8/2026, 3:25:46 AM

packages/prism-glass/src/index.js
/* Both shapes on purpose:
   - default, so a registry install can be imported as '@/components/prism-glass'
   - named, which is what the published @morphiq/prism-glass package exposes */
export { default } from './PrismGlass.jsx'
export { default as PrismGlass } from './PrismGlass.jsx'
packages/prism-glass/src/PrismGlass.jsx
'use client'

import { useRef, useMemo, useState, useEffect, Suspense } from 'react'
import * as THREE from 'three'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import { useTexture } from '@react-three/drei'
import { easing } from 'maath'
import { vertexShader, fragmentShader } from './shader.js'
import { loadSdfTexture } from './sdf.js'

const SHAPES = { circle: 0, rect: 1, pill: 1, cursor: 2 }

// Stands in for `image` when the caller supplies a `texture` directly, so the
// loader hook stays unconditional.
const BLANK_PX =
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='

function clamp01(n) {
  return Math.max(0, Math.min(1, n))
}

// Normalise the `size` prop: number → circle radius; [w,h] → rect/pill half-extents.
function resolveGeometry({ shape, size, radius }) {
  if (shape === 'cursor') {
    const r = (typeof size === 'number' ? size : 20) / 100
    return { shape: 2, radius: r, half: [r, r], corner: r }
  }
  if (shape === 'circle') {
    const r = (typeof size === 'number' ? size : 20) / 100
    return { shape: 0, radius: r, half: [r, r], corner: r }
  }
  const [w, h] = Array.isArray(size) ? size : [size ?? 28, size ?? 34]
  const half = [w / 100, h / 100]
  const corner =
    shape === 'pill' ? Math.min(half[0], half[1]) : (radius ?? 7) / 100
  return { shape: 1, radius: Math.min(half[0], half[1]), half, corner }
}

function Surface({ image, mask, texture, optionsRef }) {
  const matRef = useRef()
  const loaded = useTexture(image)
  const tex = texture || loaded
  const { viewport, size } = useThree()


  const uniforms = useMemo(
    () => ({
      uTex: { value: null },
      uAspect: { value: 1 },
      uImgAspect: { value: 1 },
      uCenter: { value: new THREE.Vector2(0.5, 0.5) },
      uSize: { value: new THREE.Vector2(0.28, 0.34) },
      uRadius: { value: 0.2 },
      uCorner: { value: 0.07 },
      uShape: { value: 0 },
      uPx: { value: 0.001 },
      uMask: { value: null },
      uUseMask: { value: 0 },
      uMaskAspect: { value: 1 },
      uLightAngle: { value: 0 },
      uLightInt: { value: 1 },
      uRefraction: { value: 1 },
      uDepth: { value: 0.6 },
      uDispersion: { value: 0 },
      uFrost: { value: 0 },
      uSplay: { value: 0 },
      uAlpha: { value: 0 },
    }),
    []
  )

  /* The loaded field is keyed by the mask it was built from, so clearing or
     swapping the mask drops the old one during render instead of in an effect
     that would paint one frame with the stale shape first. */
  const [field, setField] = useState({ mask: null, sdf: null })
  useEffect(() => {
    if (!mask) return
    let alive = true
    loadSdfTexture(mask)
      .then((r) => { if (alive) setField({ mask, sdf: r }) })
      .catch(() => { if (alive) setField({ mask, sdf: null }) })
    return () => { alive = false }
  }, [mask])
  const sdf = mask && field.mask === mask ? field.sdf : null

  useFrame((state, delta) => {
    const u = matRef.current.uniforms
    const o = optionsRef.current
    const g = resolveGeometry(o)

    u.uTex.value = tex
    u.uAlpha.value = o.transparent ? 1 : 0
    u.uAspect.value = size.width / size.height
    u.uPx.value = 1 / Math.max(1, state.gl.domElement.height)
    u.uImgAspect.value = tex.image ? tex.image.width / tex.image.height : 1

    u.uShape.value = g.shape
    u.uUseMask.value = sdf ? 1 : 0
    u.uMask.value = sdf ? sdf.texture : null
    u.uMaskAspect.value = sdf ? sdf.aspect : 1
    u.uRadius.value = g.radius
    u.uCorner.value = g.corner
    u.uSize.value.set(g.half[0], g.half[1])

    u.uLightAngle.value = (o.lightAngle * Math.PI) / 180
    u.uLightInt.value = o.lightIntensity / 100
    u.uRefraction.value = o.refraction / 100
    u.uDepth.value = o.depth / 100
    u.uDispersion.value = o.dispersion / 100
    u.uFrost.value = o.frost / 100
    u.uSplay.value = o.splay / 100

    const target =
      o.mode === 'cursor'
        ? [state.pointer.x * 0.5 + 0.5, state.pointer.y * 0.5 + 0.5]
        : [clamp01(o.position[0]), clamp01(o.position[1])]

    if (o.follow > 0) {
      easing.damp2(u.uCenter.value, target, o.follow, delta)
    } else {
      u.uCenter.value.set(target[0], target[1])
    }
  })

  return (
    <mesh scale={[viewport.width, viewport.height, 1]}>
      <planeGeometry />
      <shaderMaterial
        ref={matRef}
        uniforms={uniforms}
        vertexShader={vertexShader}
        fragmentShader={fragmentShader}
        transparent
        depthWrite={false}
        premultipliedAlpha
      />
    </mesh>
  )
}

/**
 * PrismGlass — a programmable liquid-glass refraction surface.
 * Renders `image` full-bleed and bends it through a glass lens.
 */
export default function PrismGlass({
  image = BLANK_PX,
  mask,
  texture,
  transparent = false,
  shape = 'circle',
  mode = 'cursor',
  size = 20,
  radius = 7,
  position = [0.5, 0.5],
  follow = 0.09,
  refraction = 100,
  depth = 60,
  dispersion = 0,
  frost = 0,
  splay = 0,
  lightAngle = 45,
  lightIntensity = 100,
  dpr = [1, 2],
  className,
  style,
  ...rest
}) {
  const options = {
    shape, mode, size, radius, position, follow,
    refraction, depth, dispersion, frost, splay,
    lightAngle, lightIntensity, transparent,
  }
  // The frame loop reads the latest committed props; seeded with the first
  // render so a frame that lands before the sync effect never sees an empty object.
  const optionsRef = useRef(options)
  useEffect(() => {
    optionsRef.current = options
  })

  return (
    <div
      className={className}
      style={{
        position: 'relative',
        width: '100%',
        height: '100%',
        overflow: 'hidden',
        ...style,
      }}
      {...rest}
    >
      <Canvas
        gl={{ antialias: true, alpha: true, premultipliedAlpha: true }}
        onCreated={({ gl }) => gl.setClearAlpha(0)}
        dpr={dpr}
        style={{ display: 'block', width: '100%', height: '100%' }}
      >
        <Suspense fallback={null}>
          <Surface image={image} mask={mask} texture={texture} optionsRef={optionsRef} />
        </Suspense>
      </Canvas>
    </div>
  )
}
packages/prism-glass/src/shader.js
export const vertexShader = /* glsl */ `
  varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`

export const fragmentShader = /* glsl */ `
  precision highp float;
  uniform sampler2D uTex;
  uniform float uAspect;     // view aspect (w/h)
  uniform float uImgAspect;  // image aspect (w/h)
  uniform vec2  uCenter;
  uniform vec2  uSize;
  uniform float uRadius;
  uniform float uCorner;
  uniform float uShape;
  uniform float uPx;
  uniform sampler2D uMask;
  uniform float uUseMask;
  uniform float uMaskAspect;
  uniform float uLightAngle;
  uniform float uLightInt;
  uniform float uRefraction;
  uniform float uDepth;
  uniform float uDispersion;
  uniform float uFrost;
  uniform float uSplay;
  uniform float uAlpha;      // 1 = draw only the lens, transparent elsewhere
  varying vec2 vUv;

  // signed distance to a rounded rectangle (<0 inside)
  float sdRound(vec2 p, vec2 b, float r) {
    vec2 q = abs(p) - b + r;
    return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
  }

  // map a screen uv to a "cover" sample of the image (no stretch)
  vec2 coverUv(vec2 uv) {
    vec2 s = (uAspect > uImgAspect)
      ? vec2(1.0, uImgAspect / uAspect)
      : vec2(uAspect / uImgAspect, 1.0);
    return (uv - 0.5) * s + 0.5;
  }

  vec3 sampleBG(vec2 uv, float lod) {
    return texture2D(uTex, clamp(coverUv(uv), 0.001, 0.999), lod).rgb;
  }

  // Visible-spectrum response for t in [0,1] (red → violet) built from broad,
  // heavily OVERLAPPING Gaussian lobes. Overlap is the point: no value of t
  // ever yields a pure primary, so neighbouring wavelengths blend into a
  // continuous iridescence instead of banding into discrete colour channels.
  vec3 spectrum(float t) {
    float r = exp(-pow((t - 0.16) / 0.42, 2.0));
    float g = exp(-pow((t - 0.50) / 0.42, 2.0));
    float b = exp(-pow((t - 0.84) / 0.42, 2.0));
    return vec3(r, g, b);
  }

  /* ---- cursor arrow: exact polygon SDF from the source SVG ----
     Vertices are the SVG path, centred and normalised so the arrow's height
     is 1.0, with Y flipped (SVG is y-down). Unrolled for 4 edges so there is
     no dynamic array indexing — compiles everywhere. */
  const vec2 A0 = vec2(-0.412371,  0.500000);
  const vec2 A1 = vec2( 0.411402, -0.065866);
  const vec2 A2 = vec2(-0.085669, -0.065866);
  const vec2 A3 = vec2(-0.334205, -0.496340);

  float segD2(vec2 p, vec2 a, vec2 b) {
    vec2 e = b - a;
    vec2 w = p - a;
    vec2 q = w - e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);
    return dot(q, q);
  }

  // even-odd crossing test, division-free (handles horizontal edges safely)
  bool flipSide(vec2 p, vec2 a, vec2 b) {
    vec2 e = b - a;
    vec2 w = p - a;
    bvec3 c = bvec3(p.y >= a.y, p.y < b.y, e.x * w.y > e.y * w.x);
    return all(c) || all(not(c));
  }

  float sdArrow(vec2 p) {
    float d = min(min(segD2(p, A0, A1), segD2(p, A1, A2)),
                  min(segD2(p, A2, A3), segD2(p, A3, A0)));
    float s = 1.0;
    if (flipSide(p, A0, A1)) s = -s;
    if (flipSide(p, A1, A2)) s = -s;
    if (flipSide(p, A2, A3)) s = -s;
    if (flipSide(p, A3, A0)) s = -s;
    return s * sqrt(d);
  }

  float sdf(vec2 p) {
    // A mask overrides the built-in shapes: sample a signed-distance texture so
    // any silhouette (curves included) behaves like an analytic SDF.
    if (uUseMask > 0.5) {
      float fullH = 2.0 * uRadius;
      vec2  half_ = vec2(fullH * uMaskAspect, fullH) * 0.5;
      vec2  muv   = p / (2.0 * half_) + 0.5;
      if (muv.x < 0.0 || muv.x > 1.0 || muv.y < 0.0 || muv.y > 1.0) return fullH;
      return (texture2D(uMask, muv).r - 0.5) * fullH;
    }
    if (uShape > 1.5) {
      float k = 2.0 * uRadius;              // arrow height spans 2*uRadius
      return sdArrow(p / k) * k;
    }
    return (uShape < 0.5) ? length(p) - uRadius : sdRound(p, uSize, uCorner);
  }

  void main() {
    // aspect-corrected space so corners stay round
    vec2 q  = vUv - uCenter;
    vec2 qa = vec2(q.x * uAspect, q.y);
    // uShape: 0 = circle (uRadius), 1 = rounded rect / pill (uSize + uCorner)
    float d = sdf(qa);

    // Antialias the silhouette. d is a true distance, so one pixel of it is
    // uPx. Shade the transition band too and blend by coverage below — a hard
    // d > 0.0 cutoff is what makes the outline look pixelated.
    float aa = max(uPx * 1.5, 1e-6);
    if (d > aa) {
      // Outside the lens: either the plain backdrop, or nothing at all when
      // the surface is meant to sit over live page content.
      gl_FragColor = (uAlpha > 0.5)
        ? vec4(0.0)
        : vec4(sampleBG(vUv, 0.0), 1.0);
      return;
    }

    // DEPTH = reach: how far the refraction band extends in from the border.
    //   low depth → thin edge band; high depth → wider band but the CENTER
    //   still stays clear (matches the reference even at depth 100).
    float mn    = (uShape > 0.5 && uShape < 1.5) ? min(uSize.x, uSize.y) : uRadius;
    float bw    = mix(mn * 0.04, mn * 0.52, uDepth);
    float edge  = -d;                         // 0 at border → grows inward
    float t     = clamp(edge / bw, 0.0, 1.0); // 0 border, 1 inner (flat center)
    float slope = pow(1.0 - t, 1.8);          // strong at rim, 0 in the centre

    // outward normal = SDF gradient, WIDE stencil so it rotates smoothly around
    // the corners instead of flipping along the diagonals (kills the "X" seam).
    // Outward normal via a wide-stencil gradient. The wide stencil is what
    // makes the direction rotate smoothly around rounded-rect corners instead
    // of flipping along the diagonals (which would show as an "X" seam).
    float eps = 0.03;
    vec2 grad = vec2(
      sdf(qa + vec2(eps, 0.0)) - sdf(qa - vec2(eps, 0.0)),
      sdf(qa + vec2(0.0, eps)) - sdf(qa - vec2(0.0, eps))
    );
    grad = normalize(grad + 1e-6);

    // normal & tangent, in UV space (aspect-aware)
    vec2 nrm = normalize(vec2(grad.x / uAspect, grad.y));
    vec2 tng = vec2(-nrm.y, nrm.x);
    float qt = dot(q, tng);   // tangential position from the panel centre

    // REFRACTION is the MASTER strength — it gates the entire effect. At 0 the
    // glass is invisible no matter what Depth/Splay/Dispersion are.
    //   • base NORMAL refraction bends the content in the band (subtle).
    //   • SPLAY adds TANGENTIAL spread (stretch along the edge).
    // DEPTH scales the warp MAGNITUDE (quadratic → gentle mid-range, bold and
    // liquid-metal at 100), not just the reach.
    vec2 dispNormal     = -nrm * slope * (0.02 + uDepth * uDepth * 0.26);
    vec2 dispTangential = -tng * qt * slope * uSplay * 0.85;
    vec2 disp = (dispNormal + dispTangential) * uRefraction;

    // DISPERSION scales with the LOCAL WARP magnitude, so bold colour bands
    // spread through the whole distorted zone (not a thin line on the rim).
    // Quadratic response: ~invisible at 25, bold and saturated at 100.
    float dsp = uDispersion * uDispersion * uRefraction;
    float ds  = dsp * (length(disp) * 0.38 + slope * 0.008);
    vec2  dd  = nrm * ds;

    // frost = clean uniform mip blur; a touch of blur scaled to the warp size
    // keeps big displacements reading as smooth liquid glass (not fine ripples).
    // extra blur where dispersion is active merges the colour split into broad
    // smooth iridescent bands instead of fine multicoloured noise.
    float lod = slope * 0.6 + length(disp) * 5.0 + dsp * 1.3;

    // SPECTRAL dispersion: integrate many samples across the visible spectrum,
    // each displaced by a wavelength-dependent amount and weighted by its
    // spectral colour. This yields a continuous chromatic texture (real prism
    // behaviour) rather than three separated R/G/B ghost layers.
    vec3 col  = vec3(0.0);
    vec3 wsum = vec3(0.0);
    for (int i = 0; i < 28; i++) {
      float t = float(i) / 27.0;
      vec3  w = spectrum(t);
      vec2  off = dd * (t - 0.5) * 2.0;
      col  += sampleBG(vUv + disp + off, lod) * w;
      wsum += w;
    }
    col /= max(wsum, vec3(1e-4));

    // FROST: a uniform diffusing layer across the WHOLE glass — a constant
    // radius disc blur (golden-angle spiral for even, streak-free coverage).
    // Deliberately independent of slope/warp so the frosting is even everywhere,
    // the way a real etched/frosted layer on the lens behaves.
    if (uFrost > 0.001) {
      float r    = uFrost * 0.055;
      float flod = lod + uFrost * 4.5;   // mip blur carries most of the diffusion
                                         // so the 16 taps never read as ghosts
      vec3  acc  = vec3(0.0);
      for (int i = 0; i < 16; i++) {
        float fi  = float(i);
        float a   = fi * 2.39996323;                  // golden angle
        float rad = r * sqrt((fi + 0.5) / 16.0);      // uniform disc density
        vec2  o   = vec2(cos(a) / uAspect, sin(a)) * rad;
        acc += sampleBG(vUv + disp + o, flod);
      }
      col = mix(col, acc / 16.0, clamp(uFrost * 1.35, 0.0, 1.0));
    }

    // LIGHT: a bright rim STROKE at the very border, INDEPENDENT of refraction
    // (it shows even with refraction 0). Intensity = brightness; Angle = which
    // side of the rim catches the light, with a faint base glow all around.
    float rimW     = mn * 0.06;                          // stroke width from border
    float rim      = 1.0 - smoothstep(0.0, rimW, edge);  // 1 at border → 0 inward
    vec2  Ldir     = vec2(cos(uLightAngle), sin(uLightAngle));
    float facing   = max(dot(nrm, Ldir), 0.0);
    float topBias  = max(nrm.y, 0.0);   // the top of the rim always catches a bit more
    float rimLight = rim * uLightInt * (0.16 + 0.6 * facing + 0.28 * topBias);
    col += rimLight;

    // coverage: 1 well inside, 0 just outside, smooth across one pixel
    float cov = 1.0 - smoothstep(-aa, aa, d);

    if (uAlpha > 0.5) {
      // Premultiplied, so the lens composites cleanly over whatever is behind
      // it in the DOM instead of carrying its own copy of the backdrop.
      gl_FragColor = vec4(col * cov, cov);
      return;
    }

    col = mix(sampleBG(vUv, 0.0), col, cov);
    gl_FragColor = vec4(col, 1.0);
  }
`

packages/prism-glass/src/sdf.js
import * as THREE from 'three'

/* Turn any silhouette (SVG / PNG) into a signed-distance-field texture, so the
   shader can treat an arbitrary shape exactly like an analytic SDF.
   Encoding: r = 0.5 on the outline, <0.5 inside, >0.5 outside, in units of the
   padded image height. Bilinear filtering of an SDF keeps edges sharp. */

const SQRT2 = Math.SQRT2

// two-pass chamfer distance transform; `seed[i] === 1` means distance 0
function chamfer(seed, W, H) {
  const d = new Float32Array(W * H)
  d.fill(1e9)
  for (let i = 0; i < W * H; i++) if (seed[i]) d[i] = 0

  for (let y = 0; y < H; y++) {
    for (let x = 0; x < W; x++) {
      const i = y * W + x
      let v = d[i]
      if (x > 0) v = Math.min(v, d[i - 1] + 1)
      if (y > 0) v = Math.min(v, d[i - W] + 1)
      if (x > 0 && y > 0) v = Math.min(v, d[i - W - 1] + SQRT2)
      if (x < W - 1 && y > 0) v = Math.min(v, d[i - W + 1] + SQRT2)
      d[i] = v
    }
  }
  for (let y = H - 1; y >= 0; y--) {
    for (let x = W - 1; x >= 0; x--) {
      const i = y * W + x
      let v = d[i]
      if (x < W - 1) v = Math.min(v, d[i + 1] + 1)
      if (y < H - 1) v = Math.min(v, d[i + W] + 1)
      if (x < W - 1 && y < H - 1) v = Math.min(v, d[i + W + 1] + SQRT2)
      if (x > 0 && y < H - 1) v = Math.min(v, d[i + W - 1] + SQRT2)
      d[i] = v
    }
  }
  return d
}

function build(img, res) {
  // rasterise at `res` tall for precision, with padding so outside distances grow
  const ar = img.width / img.height
  const iw = Math.max(1, Math.round(res * ar))
  const ih = res
  const pad = Math.round(Math.max(iw, ih) * 0.35)
  const W = iw + pad * 2
  const H = ih + pad * 2

  const c = document.createElement('canvas')
  c.width = W
  c.height = H
  const ctx = c.getContext('2d', { willReadFrequently: true })
  ctx.clearRect(0, 0, W, H)
  ctx.drawImage(img, pad, pad, iw, ih)
  const px = ctx.getImageData(0, 0, W, H).data

  // opaque = inside; if the art has no alpha, fall back to "not white"
  let maxA = 0
  for (let i = 3; i < px.length; i += 4) if (px[i] > maxA) maxA = px[i]
  const useAlpha = maxA > 8

  const inside = new Uint8Array(W * H)
  const outside = new Uint8Array(W * H)
  for (let i = 0; i < W * H; i++) {
    const o = i * 4
    const on = useAlpha
      ? px[o + 3] > 127
      : px[o + 3] > 127 && (px[o] + px[o + 1] + px[o + 2]) / 3 < 240
    inside[i] = on ? 1 : 0
    outside[i] = on ? 0 : 1
  }

  const dOut = chamfer(inside, W, H)  // outside → nearest shape pixel
  const dIn = chamfer(outside, W, H)  // inside  → nearest background pixel

  // encode, flipping vertically (canvas is y-down, GL textures are y-up)
  const data = new Uint8Array(W * H)
  for (let y = 0; y < H; y++) {
    const src = (H - 1 - y) * W
    const dst = y * W
    for (let x = 0; x < W; x++) {
      const i = src + x
      const signed = inside[i] ? -dIn[i] : dOut[i]
      const enc = 0.5 + signed / H
      data[dst + x] = Math.max(0, Math.min(255, Math.round(enc * 255)))
    }
  }

  const texture = new THREE.DataTexture(data, W, H, THREE.RedFormat)
  texture.minFilter = THREE.LinearFilter
  texture.magFilter = THREE.LinearFilter
  texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping
  texture.needsUpdate = true
  return { texture, aspect: W / H }
}

export function loadSdfTexture(url, res = 256) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.crossOrigin = 'anonymous'
    img.onload = () => {
      try { resolve(build(img, res)) } catch (e) { reject(e) }
    }
    img.onerror = reject
    img.src = url
  })
}
packages/prism-glass/types/index.d.ts
import type { ComponentPropsWithoutRef } from 'react'

export interface PrismGlassProps extends ComponentPropsWithoutRef<'div'> {
  /** Image URL rendered full-bleed behind (and refracted by) the glass. */
  image?: string
  /** A three.js texture to refract instead of `image` — a render target's
   *  texture, for scene glass. Its sampler settings are left as you set them. */
  texture?: { readonly isTexture: true }
  /** Let the page show through where the lens is not, instead of drawing
   *  `image` full-bleed behind it. */
  transparent?: boolean
  /** Silhouette (SVG or PNG) used as the lens outline. Overrides `shape`.
   *  Converted to a signed-distance field on load, so curves work. */
  mask?: string
  /** Lens outline. `pill` is a rounded rect with fully-rounded ends. */
  shape?: 'circle' | 'rect' | 'pill' | 'cursor'
  /** `cursor` follows the pointer; `static` sits at `position`. */
  mode?: 'cursor' | 'static'
  /** circle/cursor: radius i.e. half-height (0-100). rect/pill: `[width, height]`. */
  size?: number | [number, number]
  /** Corner radius (0-100) for `shape="rect"`. Ignored otherwise. */
  radius?: number
  /** Position in `static` mode, normalised `[x, y]` from 0 to 1. */
  position?: [number, number]
  /** Follow easing. 0 snaps instantly; higher trails more. */
  follow?: number
  /** Master strength (0-100). At 0 the glass is invisible. */
  refraction?: number
  /** Reach of the refraction band and the warp magnitude (0-100). */
  depth?: number
  /** Spectral colour separation (0-100). */
  dispersion?: number
  /** Uniform frosting across the lens (0-100). */
  frost?: number
  /** Tangential stretch along the edge (0-100). */
  splay?: number
  /** Direction of the rim light, in degrees. */
  lightAngle?: number
  /** Rim light brightness (0-100). */
  lightIntensity?: number
  /** Device pixel ratio passed to the renderer. */
  dpr?: number | [number, number]
}

export declare function PrismGlass(props: PrismGlassProps): JSX.Element
export default PrismGlass

More from morphiq

Browse all components in this library →