> ## 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.

# Cinematic Zoom

> Shader transition with dramatic zoom blur

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>;
};

<iframe className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800" title="cinematic-zoom preview" loading="lazy" srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/blocks/cinematic-zoom.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/cinematic-zoom.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add cinematic-zoom" item="cinematic-zoom" />

That writes one file: `compositions/cinematic-zoom.html`.

## Source

<Accordion title={`cinematic-zoom.html`}>
  ```html theme={null}
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
      <style>
        @import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Bebas+Neue&family=JetBrains+Mono:wght@400;700&family=Lato:wght@400;700&display=block");
        * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }
        html,
        body {
          width: 1920px;
          height: 1080px;
          overflow: hidden;
        }
      </style>
    </head>
    <body>
      <div
        id="root"
        data-composition-id="main"
        data-start="0"
        data-duration="4"
        data-width="1920"
        data-height="1080"
      >
        <style>
          [data-composition-id="main"] {
            width: 1920px;
            height: 1080px;
            overflow: hidden;
            position: relative;
            background: #3d348b;
          }

          /* Blueprint grid */
          .bp-grid {
            position: absolute;
            inset: 0;
            background-image:
              linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
              linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px);
            background-size: 40px 40px;
            pointer-events: none;
          }

          /* Layout */
          .bp-left {
            position: absolute;
            left: 0;
            top: 0;
            bottom: 0;
            width: 1200px;
            display: flex;
            align-items: center;
            justify-content: center;
          }
          .bp-right {
            position: absolute;
            right: 0;
            top: 0;
            bottom: 0;
            width: 720px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            padding: 60px 60px 60px 40px;
          }
          .bp-divider {
            position: absolute;
            left: 1200px;
            top: 60px;
            bottom: 60px;
            width: 1px;
            background: #f7b801;
            opacity: 0.15;
          }
          .bp-number {
            font-family: "Space Mono", monospace;
            font-size: 14px;
            font-weight: 700;
            letter-spacing: 0.4em;
            text-transform: uppercase;
            position: absolute;
            top: 40px;
            left: 60px;
            color: #f7b801;
          }

          /* Right panel typography */
          .bp-name {
            font-family: "Bebas Neue", sans-serif;
            font-size: 38px;
            letter-spacing: 0.08em;
            margin-bottom: 20px;
            color: #f7b801;
          }
          .bp-plabel {
            font-family: "Space Mono", monospace;
            font-size: 11px;
            font-weight: 700;
            letter-spacing: 0.3em;
            text-transform: uppercase;
            margin-bottom: 10px;
            color: #f7b801;
            opacity: 0.5;
          }
          .bp-prompt {
            font-family: "JetBrains Mono", monospace;
            font-size: 20px;
            line-height: 1.6;
            padding: 16px 20px;
            border-radius: 6px;
            margin-bottom: 20px;
            color: #f7b801;
            background: rgba(247, 184, 1, 0.06);
            border: 1px solid rgba(247, 184, 1, 0.12);
          }
          .bp-desc {
            font-family: "Lato", sans-serif;
            font-size: 17px;
            line-height: 1.6;
            color: #a0a0c0;
            opacity: 0.55;
          }

          /* GL scenes (hidden, just for texture capture) */
          .scene {
            position: absolute;
            top: 0;
            left: 0;
            width: 1200px;
            height: 1080px;
            overflow: hidden;
            display: flex;
            align-items: center;
            justify-content: center;
          }
          .scene-label {
            font-family: "Bebas Neue", sans-serif;
            font-size: 140px;
            letter-spacing: 0.06em;
            opacity: 0.12;
          }
        </style>

        <div class="bp-grid"></div>
        <div class="bp-divider"></div>
        <div class="bp-number">07 / 14</div>

        <!-- GL demo scenes (captured as textures, then hidden) -->
        <div id="s1" class="scene" style="background: #3d348b">
          <div class="scene-label" style="color: #f7b801">SCENE A</div>
        </div>
        <div id="s2" class="scene" style="background: #f7b801; opacity: 0">
          <div class="scene-label" style="color: #3d348b">SCENE B</div>
        </div>

        <!-- GL canvas (left panel only) -->
        <canvas
          id="gl-canvas"
          width="1200"
          height="1080"
          style="
            position: absolute;
            top: 0;
            left: 0;
            width: 1200px;
            height: 1080px;
            z-index: 10;
            pointer-events: none;
            display: none;
          "
        >
        </canvas>

        <!-- Right panel metadata -->
        <div class="bp-right">
          <div class="bp-name">Cinematic Zoom</div>
          <div class="bp-plabel">Prompt</div>
          <div class="bp-prompt">"use cinematic zoom shader transition"</div>
          <div class="bp-desc">
            Both scenes zoom-blur with per-channel radial offset. From zooms outward, to zooms inward
            from tight.
          </div>
        </div>

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

      <script>
        window.__timelines = window.__timelines || {};
        var glCanvas = document.getElementById("gl-canvas");
        var gl = glCanvas.getContext("webgl", { preserveDrawingBuffer: true });
        if (!gl) {
          console.warn("WebGL not available");
          window.__timelines["main"] = gsap.timeline({ paused: true });
        } else {
          gl.viewport(0, 0, 1200, 1080);
          gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
          var sceneTextures = {};

          function captureScene(sceneId) {
            var scene = document.getElementById(sceneId);
            var origOpacity = scene.style.opacity;
            var origZ = scene.style.zIndex;
            scene.style.opacity = "1";
            scene.style.zIndex = "999";
            scene.offsetHeight;
            var c = document.createElement("canvas");
            c.width = 1200;
            c.height = 1080;
            var ctx = c.getContext("2d");
            ctx.fillStyle = window.getComputedStyle(scene).backgroundColor;
            ctx.fillRect(0, 0, 1200, 1080);
            var sr = scene.getBoundingClientRect();
            var els = scene.querySelectorAll("*");
            for (var i = 0; i < els.length; i++) {
              var el = els[i],
                cs = window.getComputedStyle(el);
              if (cs.display === "none" || cs.visibility === "hidden") continue;
              var r = el.getBoundingClientRect();
              if (r.width < 1 || r.height < 1) continue;
              var x = r.left - sr.left,
                y = r.top - sr.top,
                w = r.width,
                h = r.height;
              ctx.save();
              ctx.globalAlpha = parseFloat(cs.opacity) || 1;
              var bg = cs.backgroundColor;
              if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
                ctx.fillStyle = bg;
                ctx.fillRect(x, y, w, h);
              }
              var hasChildEls = el.querySelector("div,span,img,video");
              var text = "";
              for (var j = 0; j < el.childNodes.length; j++)
                if (el.childNodes[j].nodeType === 3) text += el.childNodes[j].textContent;
              text = text.trim();
              if (text && !hasChildEls) {
                ctx.font = cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
                ctx.fillStyle = cs.color;
                if (cs.letterSpacing && cs.letterSpacing !== "normal")
                  ctx.letterSpacing = cs.letterSpacing;
                ctx.textAlign = "center";
                ctx.textBaseline = "middle";
                ctx.fillText(text, x + w / 2, y + h / 2);
              }
              ctx.restore();
            }
            scene.style.opacity = origOpacity;
            scene.style.zIndex = origZ;
            var tex = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, tex);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, c);
            sceneTextures[sceneId] = tex;
          }

          var vertSrc =
            "attribute vec2 a_pos; varying vec2 v_uv; void main(){" +
            "v_uv=a_pos*0.5+0.5; v_uv.y=1.0-v_uv.y; gl_Position=vec4(a_pos,0,1);}";
          var quadBuf = gl.createBuffer();
          gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
          gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
            gl.STATIC_DRAW,
          );
          function compileShader(src, type) {
            var s = gl.createShader(type);
            gl.shaderSource(s, src);
            gl.compileShader(s);
            if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
              console.error("Shader:", gl.getShaderInfoLog(s));
            return s;
          }
          function mkProg(fragSrc) {
            var p = gl.createProgram();
            gl.attachShader(p, compileShader(vertSrc, gl.VERTEX_SHADER));
            gl.attachShader(p, compileShader(fragSrc, gl.FRAGMENT_SHADER));
            gl.linkProgram(p);
            if (!gl.getProgramParameter(p, gl.LINK_STATUS))
              console.error("Link:", gl.getProgramInfoLog(p));
            return p;
          }

          var H =
            "precision mediump float;" +
            "varying vec2 v_uv;" +
            "uniform sampler2D u_from, u_to;" +
            "uniform float u_progress;" +
            "uniform vec2 u_resolution;\n";

          var progPass = mkProg(H + "void main(){gl_FragColor=texture2D(u_from,v_uv);}");
          var progTrans = mkProg(
            H +
              "void main(){vec2 d=v_uv-vec2(.5);float fromS=u_progress*.08;float toS=(1.-u_progress)*.06;float fr=0.,fg=0.,fb=0.;for(int i=0;i<12;i++){float f=float(i)/12.;fr+=texture2D(u_from,v_uv-d*(fromS*1.06)*f).r;fg+=texture2D(u_from,v_uv-d*fromS*f).g;fb+=texture2D(u_from,v_uv-d*(fromS*.94)*f).b;}vec3 fromBl=vec3(fr,fg,fb)/12.;float tr=0.,tg=0.,tb=0.;for(int i=0;i<12;i++){float f=float(i)/12.;tr+=texture2D(u_to,v_uv+d*(toS*1.06)*f).r;tg+=texture2D(u_to,v_uv+d*toS*f).g;tb+=texture2D(u_to,v_uv+d*(toS*.94)*f).b;}vec3 toBl=vec3(tr,tg,tb)/12.;gl_FragColor=vec4(mix(fromBl,toBl,u_progress),1.);}",
          );

          function renderShader(prog, texFrom, texTo, progress) {
            gl.useProgram(prog);
            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, texFrom);
            gl.uniform1i(gl.getUniformLocation(prog, "u_from"), 0);
            gl.activeTexture(gl.TEXTURE1);
            gl.bindTexture(gl.TEXTURE_2D, texTo);
            gl.uniform1i(gl.getUniformLocation(prog, "u_to"), 1);
            gl.uniform1f(gl.getUniformLocation(prog, "u_progress"), progress);
            gl.uniform2f(gl.getUniformLocation(prog, "u_resolution"), 1200, 1080);
            var pos = gl.getAttribLocation(prog, "a_pos");
            gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
            gl.enableVertexAttribArray(pos);
            gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
          }

          function easeInOut(p) {
            return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
          }

          captureScene("s1");
          captureScene("s2");
          glCanvas.style.display = "block";
          renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
          document.querySelectorAll(".scene").forEach(function (s) {
            s.style.opacity = "0";
          });

          var tl = gsap.timeline({
            paused: true,
            onUpdate: function () {
              var t = tl.time();
              if (t >= 1.0 && t < 3.0) {
                renderShader(
                  progTrans,
                  sceneTextures["s1"],
                  sceneTextures["s2"],
                  easeInOut((t - 1.0) / 2.0),
                );
              } else if (t >= 3.0) {
                renderShader(progPass, sceneTextures["s2"], sceneTextures["s2"], 0);
              } else {
                renderShader(progPass, sceneTextures["s1"], sceneTextures["s1"], 0);
              }
            },
          });
          tl.to({ v: 0 }, { v: 1, duration: 4, ease: "none" }, 0);
          window.__timelines["main"] = tl;
        } // end if (gl)
      </script>
    </body>
  </html>
  ```
</Accordion>

## Usage

After installing, add the block to your host composition:

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

Tagged `transition` `shader`.

## Related topics

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