Skip to content

Product demo with zoom-ins ​

A screen recording with a crop that zooms into the first click and back, a highlight ring on every click, a narrated voice-over with generated captions.

What the manifest gives it ​

The screen asset has no transcript, so it is trimmed by seconds. The voice asset has one, so its captions are generated. The clicks are markers at 3, 9 and 15 s.

What the file shows ​

  • transform.crop.* keyframes zoom to the click and back; the times are element-relative seconds.
  • The rings anchor to the recording with an offset, so a retrim moves them with it.
  • Markers name the clicks for the next edit; a marker takes seconds.
  • The bed ducks under the voice track.

The file ​

tsx
import { z } from "zod";

import { Audio, Captions, Clip, Composition, Keyframe, Marker, Shape, Title, Track } from "@cutcraft/kit";

export const schema = z.object({
  accent: z.string().describe("Highlight ring colour"),
});

export const defaults = { accent: "#ffd400" };

/**
 * A product demo over a screen recording. The crop keyframes zoom into the
 * first click and back out; the markers name every click so the next edit
 * can find them. The voice-over has a transcript, so its captions are
 * generated.
 */
export default function Video({ accent }: z.infer<typeof schema>) {
  return (
    <Composition fps={30} width={1920} height={1080} background={{ mode: "color", color: "#101418" }}>
      <Marker id="click-new" at={3} label="new project" />
      <Marker id="click-template" at={9} label="template" />
      <Marker id="click-deploy" at={15} label="deploy" />

      <Track id="screen" name="Screen">
        {/* The recording has no transcript, so a numeric trim. The crop keyframes are element-relative seconds. */}
        <Clip id="rec" src="asset:screen" in={0} duration={20} fit="cover">
          <Keyframe id="z1-x0" property="transform.crop.x" at={2.6} value={0} easing="easeInOutCubic" />
          <Keyframe id="z1-x1" property="transform.crop.x" at={3} value={0.55} />
          <Keyframe id="z1-x2" property="transform.crop.x" at={5.5} value={0.55} easing="easeInOutCubic" />
          <Keyframe id="z1-x3" property="transform.crop.x" at={6} value={0} />
          <Keyframe id="z1-y0" property="transform.crop.y" at={2.6} value={0} easing="easeInOutCubic" />
          <Keyframe id="z1-y1" property="transform.crop.y" at={3} value={0.1} />
          <Keyframe id="z1-y2" property="transform.crop.y" at={5.5} value={0.1} easing="easeInOutCubic" />
          <Keyframe id="z1-y3" property="transform.crop.y" at={6} value={0} />
          <Keyframe id="z1-w0" property="transform.crop.width" at={2.6} value={1} easing="easeInOutCubic" />
          <Keyframe id="z1-w1" property="transform.crop.width" at={3} value={0.4} />
          <Keyframe id="z1-w2" property="transform.crop.width" at={5.5} value={0.4} easing="easeInOutCubic" />
          <Keyframe id="z1-w3" property="transform.crop.width" at={6} value={1} />
          <Keyframe id="z1-h0" property="transform.crop.height" at={2.6} value={1} easing="easeInOutCubic" />
          <Keyframe id="z1-h1" property="transform.crop.height" at={3} value={0.4} />
          <Keyframe id="z1-h2" property="transform.crop.height" at={5.5} value={0.4} easing="easeInOutCubic" />
          <Keyframe id="z1-h3" property="transform.crop.height" at={6} value={1} />
        </Clip>
      </Track>

      <Track id="callouts" name="Callouts">
        {/* A ring on the button for each click; anchored to the recording so a retrim moves them with it. */}
        <Shape
          id="ring-new"
          anchor="rec"
          offset={3}
          duration={1.5}
          shape="circle"
          width={140}
          height={140}
          fill={{ type: "none" }}
          stroke={{ color: accent, width: 8 }}
          transform={{ position: { x: 520, y: -280 } }}
        />
        <Shape
          id="ring-template"
          anchor="rec"
          offset={9}
          duration={1.5}
          shape="circle"
          width={140}
          height={140}
          fill={{ type: "none" }}
          stroke={{ color: accent, width: 8 }}
          transform={{ position: { x: -300, y: 40 } }}
        />
        <Shape
          id="ring-deploy"
          anchor="rec"
          offset={15}
          duration={1.5}
          shape="circle"
          width={140}
          height={140}
          fill={{ type: "none" }}
          stroke={{ color: accent, width: 8 }}
          transform={{ position: { x: 640, y: 380 } }}
        />
        <Title
          id="step-1"
          anchor="rec"
          offset={0.4}
          duration={2.4}
          size={56}
          weight={700}
          valign="top"
          transform={{ position: { x: 0, y: 80 } }}
          animation={{ preset: "fade", in: 0.3, out: 0.3 }}
        >
          1. Open the dashboard
        </Title>
      </Track>

      <Track id="voice" layout="free" role="dialogue">
        <Audio id="vo" src="asset:voice" start={0.5} in="word:0" out="word:13.end" />
      </Track>

      <Track id="music" layout="free" role="music">
        <Audio
          id="bed"
          src="asset:music"
          start={0}
          duration="fill"
          volume={0.25}
          fade={{ in: 0.5, out: 1 }}
          duck={{ under: "voice", to: 0.1 }}
        />
      </Track>

      <Captions id="captions" src="asset:voice" style={{ position: "bottom", size: 40 }} />
    </Composition>
  );
}

What the editor can do ​

  • Move a ring: its offset changes, nothing else.
  • Drag a keyframe in the timeline: at changes; the crop path is otherwise locked to the file.
  • The ring colour is bound to accent.

What to check ​

  • No lint warnings expected.
  • captureSheet([2.5, 3.5, 5, 9.5]): full frame, the zoomed frame, the hold, the second ring.

MIT licensed.