> ## Documentation Index
> Fetch the complete documentation index at: https://hyperframes-fix-nested-composition-media-inpoint.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Liquid Glass

> VFX composition block

export const InstallCommand = ({command, item}) => {
  const [copied, setCopied] = React.useState(false);
  const [tuned, setTuned] = React.useState("");
  React.useEffect(() => {
    if (!item) return;
    const read = () => {
      try {
        const raw = new URLSearchParams(window.location.search).get(`vars-${item}`);
        if (!raw) return setTuned("");
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return setTuned("");
        if (Object.keys(parsed).length === 0) return setTuned("");
        setTuned(` --vars '${JSON.stringify(parsed)}'`);
      } catch {
        setTuned("");
      }
    };
    read();
    window.addEventListener("hf-vars-changed", read);
    window.addEventListener("popstate", read);
    return () => {
      window.removeEventListener("hf-vars-changed", read);
      window.removeEventListener("popstate", read);
    };
  }, [item]);
  const fullCommand = `${command}${tuned}`;
  const copy = async () => {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(fullCommand);
      } else {
        const previous = document.activeElement;
        const scratch = document.createElement("textarea");
        scratch.value = fullCommand;
        scratch.setAttribute("readonly", "");
        scratch.style.position = "fixed";
        scratch.style.opacity = "0";
        document.body.appendChild(scratch);
        scratch.select();
        document.execCommand("copy");
        document.body.removeChild(scratch);
        previous?.focus?.();
      }
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch {}
  };
  return <div className="hf-install-command not-prose my-4 flex items-stretch overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
      <code className="flex-1 overflow-x-auto whitespace-nowrap border-r border-zinc-200 px-4 py-3 font-mono text-sm text-zinc-800 dark:border-zinc-800 dark:text-zinc-100">
        {fullCommand}
      </code>
      <button type="button" onClick={copy} data-copied={copied ? "true" : "false"} aria-label={`Copy ${command} to the clipboard`} className="hf-install-copy">
        <svg className="hf-install-copy-clipboard" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" />
          <path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" />
        </svg>
        <svg className="hf-install-copy-check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M2.75 9.5L6.5 13.25L15.25 4.5" />
        </svg>
      </button>
      <span className="hf-install-copy-status" role="status" aria-live="polite">
        {copied ? "Copied" : ""}
      </span>
    </div>;
};

<video className="w-full aspect-video rounded-xl object-cover bg-zinc-100 dark:bg-zinc-800" src="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-liquid-glass.mp4" poster="https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-liquid-glass.png" autoPlay muted loop playsInline />

## Install

<InstallCommand command="npx hyperframes add vfx-liquid-glass" item="vfx-liquid-glass" />

That writes one file: `compositions/vfx-liquid-glass.html`.

<Danger>
  Live preview needs the `chrome://flags/#canvas-draw-element` flag switched on.
  Without it this item's screen renders black. Rendering from the CLI switches
  it on for you. [How it works](/guides/html-in-canvas)
</Danger>

## Change the colors

Set these CSS variables on the block:

* `--bg-color` — Background. Defaults to `#030407`.
* `--accent-color` — Accent. Defaults to `#00d4ff`.

## Source

<Accordion title={`vfx-liquid-glass.html`}>
  ```html theme={null}
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=1920, height=1080" />
      <title>Liquid Glass Parallax</title>
      <link
        href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
        rel="stylesheet"
      />
      <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/shaders/CopyShader.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/EffectComposer.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/RenderPass.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/ShaderPass.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/three@0.147.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
      <style>
        *,
        *::before,
        *::after {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }
        body {
          background: #030407;
          overflow: hidden;
          font-family: "Inter", system-ui, sans-serif;
        }
        #root {
          position: relative;
          width: 1920px;
          height: 1080px;
          overflow: hidden;
          background: #030407;
        }

        .text-source {
          width: 1920px;
          height: 1080px;
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          background: linear-gradient(160deg, #030407 0%, #0a1020 40%, #070910 70%, #030407 100%);
          text-align: center;
          padding: 0 120px;
          gap: 32px;
          position: relative;
          overflow: hidden;
        }
        .text-source::before {
          content: "";
          position: absolute;
          width: 700px;
          height: 700px;
          border-radius: 50%;
          background: radial-gradient(circle, rgba(0, 212, 255, 0.06) 0%, transparent 70%);
          top: -150px;
          right: -100px;
          pointer-events: none;
        }
        .text-source::after {
          content: "";
          position: absolute;
          width: 500px;
          height: 500px;
          border-radius: 50%;
          background: radial-gradient(circle, rgba(124, 58, 237, 0.05) 0%, transparent 70%);
          bottom: -100px;
          left: -50px;
          pointer-events: none;
        }
        .badge {
          font-size: 14px;
          font-weight: 600;
          letter-spacing: 3px;
          text-transform: uppercase;
          color: rgba(0, 212, 255, 0.8);
        }
        .text-source h1 {
          font-size: 148px;
          font-weight: 900;
          line-height: 0.9;
          color: rgba(248, 249, 253, 0.96);
          text-shadow: 0 0 60px rgba(255, 255, 255, 0.15);
          letter-spacing: -5px;
          max-width: 1400px;
        }
        .text-source h1 span {
          background: linear-gradient(135deg, #00d4ff, #a855f7);
          -webkit-background-clip: text;
          -webkit-text-fill-color: transparent;
        }
        .subtitle {
          font-size: 28px;
          font-weight: 400;
          color: rgba(255, 255, 255, 0.35);
          max-width: 800px;
          line-height: 1.5;
        }
        .stats {
          display: flex;
          gap: 48px;
          margin-top: 16px;
        }
        .stat {
          text-align: center;
        }
        .stat-val {
          font-size: 42px;
          font-weight: 800;
          color: #00d4ff;
          letter-spacing: -1px;
        }
        .stat-label {
          font-size: 12px;
          font-weight: 600;
          color: rgba(255, 255, 255, 0.3);
          text-transform: uppercase;
          letter-spacing: 2px;
          margin-top: 4px;
        }
      </style>
    </head>
    <body>
      <div
        id="root"
        data-composition-id="liquid-glass"
        data-width="1920"
        data-height="1080"
        data-start="0"
        data-duration="20"
        data-root="true"
      >
        <canvas
          id="cap-text"
          layoutsubtree
          width="1920"
          height="1080"
          style="
            position: absolute;
            top: 0;
            left: 0;
            width: 1920px;
            height: 1080px;
            z-index: -1;
            pointer-events: none;
          "
        >
          <div class="text-source">
            <div class="badge">Write HTML → Render Video</div>
            <h1>Ship videos <span>10x faster</span></h1>
            <div class="subtitle">
              HTML is the source of truth for video. No timeline editors, no After Effects — just
              code.
            </div>
            <div class="stats">
              <div class="stat">
                <div class="stat-val">47x</div>
                <div class="stat-label">Faster than AE</div>
              </div>
              <div class="stat">
                <div class="stat-val">12.4K</div>
                <div class="stat-label">Creators</div>
              </div>
              <div class="stat">
                <div class="stat-val">2.4M</div>
                <div class="stat-label">Videos Rendered</div>
              </div>
            </div>
          </div>
        </canvas>

        <canvas
          id="theater"
          width="1920"
          height="1080"
          style="position: absolute; top: 0; left: 0; width: 1920px; height: 1080px"
        ></canvas>

        <div
          id="driver"
          class="clip"
          data-start="0"
          data-duration="20"
          data-track-index="0"
          style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
        ></div>
      </div>

      <script>
        var W = 1920,
          H = 1080,
          DURATION = 20,
          ASPECT = W / H;
        var TEXT_Z = -3.08;

        // ── Seeded PRNG ────────────────────────────────────────────────────
        function seededRandom(seed) {
          var v = seed >>> 0;
          return function () {
            v += 0x6d2b79f5;
            var m = v;
            m = Math.imul(m ^ (m >>> 15), m | 1);
            m ^= m + Math.imul(m ^ (m >>> 7), m | 61);
            return ((m ^ (m >>> 14)) >>> 0) / 4294967296;
          };
        }

        // ── Voronoi (self-contained, no d3) ────────────────────────────────
        function polygonArea(pts) {
          var a = 0;
          for (var i = 0; i < pts.length; i++) {
            var j = (i + 1) % pts.length;
            a += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
          }
          return a * 0.5;
        }

        function polygonCentroid(pts) {
          var sa = polygonArea(pts) || 1;
          var cx = 0,
            cy = 0;
          for (var i = 0; i < pts.length; i++) {
            var j = (i + 1) % pts.length;
            var f = pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
            cx += (pts[i][0] + pts[j][0]) * f;
            cy += (pts[i][1] + pts[j][1]) * f;
          }
          return { x: cx / (6 * sa), y: cy / (6 * sa) };
        }

        function clipPoly(poly, px, py, nx, ny) {
          var out = [];
          for (var i = 0; i < poly.length; i++) {
            var c = poly[i],
              n = poly[(i + 1) % poly.length];
            var dc = (c[0] - px) * nx + (c[1] - py) * ny;
            var dn = (n[0] - px) * nx + (n[1] - py) * ny;
            if (dc <= 0) out.push(c);
            if (dc <= 0 !== dn <= 0) {
              var t = dc / (dc - dn);
              out.push([c[0] + t * (n[0] - c[0]), c[1] + t * (n[1] - c[1])]);
            }
          }
          return out;
        }

        function computeVoronoi(points, xmin, ymin, xmax, ymax) {
          var cells = [];
          for (var i = 0; i < points.length; i++) {
            var px = points[i][0],
              py = points[i][1];
            var poly = [
              [xmin, ymin],
              [xmax, ymin],
              [xmax, ymax],
              [xmin, ymax],
            ];
            for (var j = 0; j < points.length; j++) {
              if (i === j) continue;
              var qx = points[j][0],
                qy = points[j][1];
              var mx = (px + qx) * 0.5,
                my = (py + qy) * 0.5;
              poly = clipPoly(poly, mx, my, qx - px, qy - py);
              if (poly.length < 3) break;
            }
            cells.push(poly.length >= 3 ? poly : null);
          }
          return cells;
        }

        // ── Feature Detection ──────────────────────────────────────────────
        function canCapture() {
          var tc = document.createElement("canvas");
          if (!("layoutSubtree" in tc)) return false;
          tc.setAttribute("layoutsubtree", "");
          var ctx = tc.getContext("2d");
          return ctx && typeof ctx.drawElementImage === "function";
        }
        var apiOk = canCapture();

        // ── Capture backdrop ───────────────────────────────────────────────
        var capCanvas = document.getElementById("cap-text");
        var capCtx = capCanvas.getContext("2d");
        var textEl = capCanvas.querySelector(".text-source");

        function captureBackdrop() {
          capCtx.clearRect(0, 0, W, H);
          if (apiOk) {
            capCtx.drawElementImage(textEl, 0, 0, W, H);
          } else {
            var g = capCtx.createLinearGradient(0, 0, W, H);
            g.addColorStop(0, "#030407");
            g.addColorStop(0.5, "#070910");
            g.addColorStop(1, "#030407");
            capCtx.fillStyle = g;
            capCtx.fillRect(0, 0, W, H);
            capCtx.textAlign = "center";
            capCtx.textBaseline = "middle";
            capCtx.font = "900 172px Inter, system-ui, sans-serif";
            capCtx.shadowColor = "rgba(255,255,255,0.22)";
            capCtx.shadowBlur = 20;
            capCtx.fillStyle = "rgba(248,249,253,0.94)";
            capCtx.fillText("Liquid Glass", W / 2, H / 2 - 80);
            capCtx.fillText("Design", W / 2, H / 2 + 90);
          }
        }

        // ── Three.js Renderer ──────────────────────────────────────────────
        var theaterCanvas = document.getElementById("theater");
        var renderer = new THREE.WebGLRenderer({
          canvas: theaterCanvas,
          antialias: true,
          alpha: false,
          powerPreference: "high-performance",
          preserveDrawingBuffer: true,
        });
        renderer.setSize(W, H, false);
        renderer.setPixelRatio(1);
        renderer.outputEncoding = THREE.sRGBEncoding;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 1.08;
        renderer.setClearColor(0x030407, 1);

        var scene = new THREE.Scene();
        scene.background = new THREE.Color(0x030407);

        // ── Environment Map (procedural) ───────────────────────────────────
        var envCanvas = document.createElement("canvas");
        envCanvas.width = 1024;
        envCanvas.height = 512;
        var ectx = envCanvas.getContext("2d");
        var eg = ectx.createLinearGradient(0, 0, 1024, 512);
        eg.addColorStop(0, "#020307");
        eg.addColorStop(0.18, "#ffffff");
        eg.addColorStop(0.24, "#7fdcff");
        eg.addColorStop(0.42, "#07101c");
        eg.addColorStop(0.58, "#ffffff");
        eg.addColorStop(0.64, "#ffc4a6");
        eg.addColorStop(0.82, "#0b0c12");
        eg.addColorStop(1, "#020307");
        ectx.fillStyle = eg;
        ectx.fillRect(0, 0, 1024, 512);
        ectx.globalCompositeOperation = "screen";
        var envRng = seededRandom(999);
        for (var ei = 0; ei < 9; ei++) {
          var ey = 36 + ei * 52;
          ectx.fillStyle = "rgba(255,255,255," + (ei % 3 === 0 ? 0.22 : 0.1) + ")";
          ectx.fillRect((ei % 2) * 120, ey, 1024 * 0.72, 4 + (ei % 3) * 2);
        }
        var envTex = new THREE.CanvasTexture(envCanvas);
        envTex.mapping = THREE.EquirectangularReflectionMapping;
        var pmrem = new THREE.PMREMGenerator(renderer);
        var envMap = pmrem.fromEquirectangular(envTex);
        scene.environment = envMap.texture;
        envTex.dispose();
        pmrem.dispose();

        // ── Camera ─────────────────────────────────────────────────────────
        var CAM_Z = 9.35;
        var camera = new THREE.PerspectiveCamera(34, ASPECT, 0.1, 80);
        camera.position.set(0, 0, CAM_Z);

        // ── Scene Groups ───────────────────────────────────────────────────
        var rig = new THREE.Group();
        scene.add(rig);
        var textRig = new THREE.Group();
        rig.add(textRig);

        // ── Backdrop Plane (text behind glass) ─────────────────────────────
        var backdropTex = new THREE.CanvasTexture(capCanvas);
        backdropTex.minFilter = THREE.LinearFilter;
        backdropTex.magFilter = THREE.LinearFilter;
        backdropTex.generateMipmaps = false;

        function viewportAtZ(z) {
          var d = camera.position.z - z;
          var h = 2 * Math.tan(THREE.MathUtils.degToRad(camera.fov * 0.5)) * d;
          return { w: h * camera.aspect, h: h };
        }
        var vp = viewportAtZ(TEXT_Z);
        var backdropMesh = new THREE.Mesh(
          new THREE.PlaneGeometry(vp.w * 1.18, vp.h * 1.18),
          new THREE.MeshBasicMaterial({ map: backdropTex, depthWrite: true, depthTest: true }),
        );
        backdropMesh.position.z = TEXT_Z;
        backdropMesh.renderOrder = -4;
        textRig.add(backdropMesh);

        // ── Light Bands ────────────────────────────────────────────────────
        var bandSpecs = [
          [-2.8, 1.45, -1.78, 3.2, 0.035, 0.13, 0x9edaff, 0.24],
          [2.7, -1.24, -1.76, 3.0, 0.032, -0.1, 0xffd1b5, 0.18],
          [0.0, 2.05, -1.82, 5.2, 0.026, 0.02, 0xffffff, 0.13],
          [-0.55, -1.92, -1.8, 4.2, 0.024, -0.03, 0xc7f6ff, 0.1],
        ];
        bandSpecs.forEach(function (s) {
          var m = new THREE.Mesh(
            new THREE.PlaneGeometry(s[3], s[4]),
            new THREE.MeshBasicMaterial({
              color: s[6],
              transparent: true,
              opacity: s[7],
              blending: THREE.AdditiveBlending,
              depthWrite: false,
            }),
          );
          m.position.set(s[0], s[1], s[2]);
          m.rotation.z = s[5];
          rig.add(m);
        });

        // ── Glass Shards (Voronoi fracture) ────────────────────────────────
        var LAYOUT = { width: 9.7, height: 5.78, cols: 9, rows: 6, seed: 5297 };
        var rng = seededRandom(LAYOUT.seed);
        var seedPoints = [];
        var cw = LAYOUT.width / LAYOUT.cols,
          ch = LAYOUT.height / LAYOUT.rows;

        for (var row = 0; row < LAYOUT.rows; row++) {
          for (var col = 0; col < LAYOUT.cols; col++) {
            var cp =
              1 -
              Math.min(
                0.62,
                Math.hypot((col + 0.5) / LAYOUT.cols - 0.5, (row + 0.5) / LAYOUT.rows - 0.5),
              );
            var jx = (rng() - 0.5) * cw * (0.58 + cp * 0.16);
            var jy = (rng() - 0.5) * ch * (0.58 + cp * 0.16);
            seedPoints.push([(col + 0.5) * cw + jx, (row + 0.5) * ch + jy]);
          }
        }

        var voronoiCells = computeVoronoi(seedPoints, 0, 0, LAYOUT.width, LAYOUT.height);

        var glassRig = new THREE.Group();
        glassRig.scale.setScalar(1.025);
        glassRig.position.set(0, 0, 0.08);
        rig.add(glassRig);

        var panes = [];
        var GAP_SCALE = 0.954;

        for (var pi = 0; pi < voronoiCells.length; pi++) {
          var cell = voronoiCells[pi];
          if (!cell || cell.length < 3) continue;

          var worldPts = cell.map(function (p) {
            return [p[0] - LAYOUT.width / 2, LAYOUT.height / 2 - p[1]];
          });
          var area = Math.abs(polygonArea(worldPts));
          if (area < 0.034) continue;

          var cent = polygonCentroid(worldPts);
          var rad = new THREE.Vector2(cent.x, cent.y);
          var radLen = Math.max(rad.length(), 0.001);
          rad.divideScalar(radLen);

          var localPts = worldPts.map(function (p) {
            return [(p[0] - cent.x) * GAP_SCALE, (p[1] - cent.y) * GAP_SCALE];
          });
          var outward = 0.046 + rng() * 0.105;
          var depthLift = (area / (LAYOUT.width * LAYOUT.height)) * 1.42;
          var phase = pi * 0.71 + rng() * 4;
          var depth = 0.18 + rng() * 0.1;
          var bevel = Math.min(0.108, Math.sqrt(area) * 0.095);
          var roughness = pi % 7 === 0 ? 0.08 + rng() * 0.035 : 0.012 + rng() * 0.038;
          var thickness = 1.45 + rng() * 1.25;

          // Geometry
          var shape = new THREE.Shape();
          localPts.forEach(function (p, idx) {
            idx === 0 ? shape.moveTo(p[0], p[1]) : shape.lineTo(p[0], p[1]);
          });
          shape.closePath();
          var geo = new THREE.ExtrudeGeometry(shape, {
            depth: depth,
            bevelEnabled: true,
            bevelThickness: bevel,
            bevelSize: bevel,
            bevelSegments: 9,
            curveSegments: 2,
            steps: 2,
          });
          geo.center();
          geo.computeVertexNormals();

          // Material
          var mat = new THREE.MeshPhysicalMaterial({
            color: 0xf7fbff,
            metalness: 0,
            roughness: roughness,
            transmission: 1,
            thickness: thickness,
            ior: 1.55,
            attenuationColor: new THREE.Color(0xe5f8ff),
            attenuationDistance: 12,
            clearcoat: 1,
            clearcoatRoughness: Math.min(0.12, roughness + 0.035),
            specularIntensity: 1,
            envMapIntensity: 2.1,
            transparent: true,
            opacity: 0.76,
            side: THREE.DoubleSide,
          });

          var pane = new THREE.Group();
          var mesh = new THREE.Mesh(geo, mat);
          mesh.renderOrder = 4 + pi;
          pane.add(mesh);

          // Edge highlights (white + cyan + pink for chromatic aberration look)
          var edgeGeo = new THREE.EdgesGeometry(geo, 18);
          var whiteEdge = new THREE.LineSegments(
            edgeGeo,
            new THREE.LineBasicMaterial({
              color: 0xffffff,
              transparent: true,
              opacity: roughness > 0.08 ? 0.32 : 0.44,
              blending: THREE.AdditiveBlending,
              depthWrite: false,
            }),
          );
          pane.add(whiteEdge);
          var cyanEdge = new THREE.LineSegments(
            edgeGeo.clone(),
            new THREE.LineBasicMaterial({
              color: 0x72e8ff,
              transparent: true,
              opacity: 0.18,
              blending: THREE.AdditiveBlending,
              depthWrite: false,
            }),
          );
          cyanEdge.position.set(0.008, -0.002, -0.008);
          pane.add(cyanEdge);
          var pinkEdge = new THREE.LineSegments(
            edgeGeo.clone(),
            new THREE.LineBasicMaterial({
              color: 0xff6f8d,
              transparent: true,
              opacity: 0.14,
              blending: THREE.AdditiveBlending,
              depthWrite: false,
            }),
          );
          pinkEdge.position.set(-0.009, 0.003, 0.006);
          pane.add(pinkEdge);

          var basePos = new THREE.Vector3(
            cent.x + rad.x * outward,
            cent.y + rad.y * outward,
            0.28 + depthLift + rng() * 0.18,
          );
          var baseRot = new THREE.Euler(
            (rng() - 0.5) * 0.074,
            (rng() - 0.5) * 0.086,
            (rng() - 0.5) * 0.052,
          );
          pane.position.copy(basePos);
          pane.rotation.copy(baseRot);

          pane.userData = {
            basePos: basePos.clone(),
            baseRot: { x: baseRot.x, y: baseRot.y, z: baseRot.z },
            phase: phase,
            speed: 0.28 + rng() * 0.16,
            drift: new THREE.Vector3(
              0.02 + rng() * 0.026,
              0.018 + rng() * 0.024,
              0.068 + rng() * 0.092,
            ),
            rotDrift: new THREE.Vector3(
              0.016 + rng() * 0.026,
              0.018 + rng() * 0.03,
              0.008 + rng() * 0.016,
            ),
            ptrWeight: 0.04 + rng() * 0.052,
            rad: rad.clone(),
            baseOpacity: 0.76,
            baseRoughness: roughness,
            glassMat: mat,
            edgeW: whiteEdge.material,
            edgeC: cyanEdge.material,
            edgeP: pinkEdge.material,
            edgeWBase: whiteEdge.material.opacity,
            edgeCBase: cyanEdge.material.opacity,
            edgePBase: pinkEdge.material.opacity,
          };

          panes.push(pane);
          glassRig.add(pane);
        }

        // ── Lighting ───────────────────────────────────────────────────────
        scene.add(new THREE.HemisphereLight(0xe8f0ff, 0x07040b, 1.1));
        var keyLight = new THREE.DirectionalLight(0xffffff, 3.4);
        keyLight.position.set(-3.8, 4.4, 5.6);
        scene.add(keyLight);
        var edgeLight = new THREE.PointLight(0x8bdcff, 14, 12);
        edgeLight.position.set(3.1, 1.4, 3.4);
        scene.add(edgeLight);
        var warmLight = new THREE.PointLight(0xffc8a8, 7, 12);
        warmLight.position.set(-4.4, -2.2, 3.2);
        scene.add(warmLight);

        // ── Post-Processing ────────────────────────────────────────────────
        var GrainShader = {
          uniforms: { tDiffuse: { value: null }, time: { value: 0 }, strength: { value: 0.024 } },
          vertexShader:
            "varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }",
          fragmentShader: [
            "uniform sampler2D tDiffuse; uniform float time; uniform float strength; varying vec2 vUv;",
            "float n(vec2 v){ return fract(sin(dot(v+time,vec2(12.9898,78.233)))*43758.5453); }",
            "void main(){",
            "  vec4 c=texture2D(tDiffuse,vUv);",
            "  c.rgb+=(n(vUv*vec2(1520.0,940.0))-0.5)*strength;",
            "  c.rgb=pow(max(c.rgb,vec3(0.0)),vec3(0.985));",
            "  gl_FragColor=c;",
            "}",
          ].join("\n"),
        };

        var composer = new THREE.EffectComposer(renderer);
        composer.addPass(new THREE.RenderPass(scene, camera));
        var bloomPass = new THREE.UnrealBloomPass(new THREE.Vector2(W, H), 0.32, 0.4, 0.86);
        composer.addPass(bloomPass);
        var grainPass = new THREE.ShaderPass(GrainShader);
        composer.addPass(grainPass);

        // ── Pointer State (GSAP-animated, replaces mouse) ──────────────────
        var ptr = { x: 0.16, y: 0.04 };
        var REVEAL_R = 2.18;
        var PTR_WORLD = new THREE.Vector2();

        function updatePtrWorld() {
          PTR_WORLD.set(ptr.x * 4.86, ptr.y * 2.84);
        }

        // ── Render Function (deterministic, seekable) ──────────────────────
        function renderScene(elapsed) {
          updatePtrWorld();

          camera.position.x = ptr.x * 0.38;
          camera.position.y = -ptr.y * 0.27;
          camera.position.z = CAM_Z;
          camera.lookAt(0, 0, 0);

          textRig.position.x = -ptr.x * 0.56;
          textRig.position.y = ptr.y * 0.36;
          textRig.position.z = Math.sin(elapsed * 0.28) * 0.018;
          textRig.rotation.x = ptr.y * 0.028;
          textRig.rotation.y = ptr.x * 0.042;

          glassRig.rotation.x = -ptr.y * 0.064;
          glassRig.rotation.y = ptr.x * 0.078;

          edgeLight.position.x = 3.1 + ptr.x * 0.9;
          edgeLight.position.y = 1.4 + ptr.y * 0.55;
          warmLight.position.x = -4.4 - ptr.x * 0.65;

          for (var i = 0; i < panes.length; i++) {
            var pane = panes[i];
            var d = pane.userData;
            var t = elapsed * d.speed + d.phase;
            var ptrDist = PTR_WORLD.distanceTo(new THREE.Vector2(d.basePos.x, d.basePos.y));
            var reveal = Math.pow(Math.max(0, 1 - ptrDist / REVEAL_R), 2);
            var shatter = reveal * 0.36;
            var awayX = d.basePos.x - PTR_WORLD.x;
            var awayY = d.basePos.y - PTR_WORLD.y;
            var awayLen = Math.max(Math.hypot(awayX, awayY), 0.001);
            var revealSpread = reveal * 0.24;
            var shatterSpread = shatter * 0.68;
            var pSlide = d.ptrWeight * 1.9;

            pane.position.x =
              d.basePos.x +
              Math.sin(t * 0.42) * d.drift.x +
              ptr.x * pSlide +
              (awayX / awayLen) * (revealSpread + shatterSpread);
            pane.position.y =
              d.basePos.y +
              Math.cos(t * 0.38) * d.drift.y -
              ptr.y * pSlide * 0.72 +
              (awayY / awayLen) * (revealSpread + shatterSpread * 0.84);
            pane.position.z =
              d.basePos.z + Math.sin(t * 0.52 + i) * d.drift.z + reveal * 0.32 + shatter * 0.74;

            pane.rotation.x =
              d.baseRot.x +
              Math.sin(t * 0.46) * d.rotDrift.x -
              ptr.y * d.ptrWeight * 1.15 +
              reveal * (awayY / awayLen) * 0.16 +
              shatter * (awayY / awayLen) * 0.58;
            pane.rotation.y =
              d.baseRot.y +
              Math.cos(t * 0.43) * d.rotDrift.y +
              ptr.x * d.ptrWeight * 1.38 -
              reveal * (awayX / awayLen) * 0.2 -
              shatter * (awayX / awayLen) * 0.66;
            pane.rotation.z =
              d.baseRot.z +
              Math.sin(t * 0.34) * d.rotDrift.z +
              reveal * Math.sin(t + i) * 0.028 +
              shatter * (d.rad.x * awayY - d.rad.y * awayX) * 0.18;

            d.glassMat.opacity = d.baseOpacity + shatter * 0.14;
            d.glassMat.roughness = Math.min(0.14, d.baseRoughness + shatter * 0.048);
            d.edgeW.opacity = d.edgeWBase + shatter * 0.34;
            d.edgeC.opacity = d.edgeCBase + shatter * 0.22;
            d.edgeP.opacity = d.edgePBase + shatter * 0.2;
          }

          grainPass.uniforms.time.value = elapsed;
          composer.render();
        }

        // ── GSAP Timeline ──────────────────────────────────────────────────
        window.__timelines = window.__timelines || {};
        var tl = gsap.timeline({ paused: true });

        tl.to({ _: 0 }, { _: 1, duration: DURATION, ease: "none" }, 0);

        // Pointer choreography — smooth wandering reveal
        tl.set(ptr, { x: 0.16, y: 0.04 }, 0);
        tl.to(ptr, { x: 0.45, y: -0.15, duration: 3.5, ease: "sine.inOut" }, 0.5);
        tl.to(ptr, { x: -0.35, y: 0.25, duration: 3.5, ease: "sine.inOut" }, 4);
        tl.to(ptr, { x: 0.15, y: -0.35, duration: 3, ease: "sine.inOut" }, 7.5);
        tl.to(ptr, { x: -0.45, y: 0.05, duration: 3, ease: "sine.inOut" }, 10.5);
        tl.to(ptr, { x: 0.3, y: 0.2, duration: 3, ease: "sine.inOut" }, 13.5);
        tl.to(ptr, { x: -0.1, y: -0.1, duration: 2.5, ease: "sine.inOut" }, 16.5);
        tl.to(ptr, { x: 0.16, y: 0.04, duration: 1, ease: "power2.inOut" }, 19);

        tl.eventCallback("onUpdate", function () {
          renderScene(tl.time());
        });

        window.__timelines["liquid-glass"] = tl;

        setTimeout(function () {
          captureBackdrop();
          backdropTex.needsUpdate = true;
          tl.seek(0);
        }, 0);
      </script>
    </body>
  </html>
  ```
</Accordion>

## Usage

After installing, add the block to your host composition:

```html theme={null}
<div data-composition-id="vfx-liquid-glass" data-composition-src="compositions/vfx-liquid-glass.html" data-start="0" data-duration="20" data-track-index="1" data-width="1920" data-height="1080"></div>
```

Tagged `html-in-canvas` `webgl`.

## Related topics

* [Browse the complete Catalog](/catalog)
* [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
* [Build a richer composition](/go-further)
