Skip to content

Examples โ€‹

There are three kinds of example, each a complete file:

  • Showcase videos (this page): short finished videos, each rendered next to the file that made it. Start here to see what CutCraft can do.
  • Recipes: files for the videos people most often make (an interview cut, a product demo, a vertical clip, variants). Each passes the lint and has a test; copy one as a starting point.
  • The walkthrough: one launch video a model wrote from a brief, explained line by line.
  • Feature examples in the guide: short files that each show one feature, rendered next to their code on the page that explains it (fonts, motion blur, your own effects and transitions, something over a cut).

Where to start โ€‹

You're makingStart from
A product launch or promoProduct launch, the walkthrough, Brand spot
An interview or podcast cutTalking-head cut, Multicam highlights
A vertical shortKinetic type, Captions and key points, Vertical clip from a podcast
A product or screen demoProduct demo with zoom-ins
Charts and numbersData story, Stats with handles
An intro, outro or logo stingLogo reveal
A slideshowSlideshow with motion
One video in many versionsVariants from one file
To seeLook atExplained in
Cuts on transcript words (in="word:12")Product launch, Data story, Talking-head cutTime by meaning
Overlays on the word that names them (anchor + at)Product launch, Kinetic type, CaptionsTime by meaning
Captions from the transcriptKinetic type, Captions, Vertical clipCaptions and sound
three.js (<Three>)Product launch, Logo reveal, Brand spotFree code
Motion (<Motion>)Kinetic type, Data story, An editFree code
Any React in a <Layer>Data story (counters), Stats with handlesFree code
Transitions and keyframesAn edit, SlideshowElements
Every built-in effect and transition, one demo eachEffects and transitionsColour, Keying, blurs and pins, Time by meaning
A grade built from several effects, in orderColour gradeColour and grading
Effect order changing the pictureGrayscale then duotone, and the reverseFree code
One prop that restyles everything (themes)Brand spotThemes
Tweakable values (schema, defaults)Every showcase file, VariantsThe file
Music that ducks under speechTalking-head cut, the walkthroughCaptions and sound
Captions from an SRT, VTT or Whisper fileCaptionsGetting a transcript
Google Fonts or your own font filesFonts exampleThemes
Motion blurMotion blur exampleExporting
Your own effects and transitions in GLSLDuotone, circle reveal, colour split on a titleFree code
A flare or a sound over a cutAt a cutTime by meaning

Rendering them yourself โ€‹

Each video is one file in examples/showcase, rendered with pnpm --filter @cutcraft/example-showcase render; the guide's feature examples, in examples/showcase/guide, with pnpm --filter @cutcraft/example-showcase docs. The footage is made by ffmpeg's own sources (pnpm --filter @cutcraft/example-showcase gen), except the two coffee shots in the effect and transition demos, close and wide: Pixabay videos 209419 and 209424, under the Pixabay Content License. They are not committed; gen reads them from examples/showcase/media/stock/ and checks their hashes. Every file renders through the player in headless Chrome. The people on camera are stand-ins: their footage is a gradient, and the manifest holds the words they would say.

The logo film โ€‹

The landing page's hero, made with CutCraft: a woodblock print in the logo's colours. Waves roll in, a red sun rises through bands of mist while birds cross, a katana cuts it in two, and the print draws in to the logo's tile, where the colours turn over. The name follows, then the three ways to make a video, and a seal. It is one <Layer> of React SVG timed by useLayer().elapsed, and <Title>s in Google Fonts, and it ends on the paper it began on, so it loops.

tsx
import { Composition, googleFont, Layer, Title, Track, useLayer } from "@cutcraft/kit";

/**
 * The landing page's hero: the logo told as a woodblock print. Waves roll in, a red sun rises through bands of mist
 * while birds cross, a katana flashes, the sun falls apart along the cut, and the print draws in to the tile of the
 * logo, where the colours turn over: the red sun on paper becomes the paper sun on red. Beside it the name, then the
 * three ways to make a video, and a seal pressed beside it. It ends on the paper it began on, so it loops. Every
 * shape is a function of the layer's elapsed seconds, so the preview and the export draw the same frame.
 */
const display = googleFont("Archivo", { weights: [800] });
const mono = googleFont("Geist Mono", { weights: [500] });

const RED = "#c8402a";
const PAPER = "#f3ead6";
const INK = "#1b1712";
const W = 1280;
/** The line under the name: what it is, then the three ways to make a video with it. */
const LINES = [
  { id: "tagline", text: "VIDEO EDITOR AS CODE", start: 5.55, end: 7.3 },
  { id: "agent", text: "MADE BY YOUR AGENT", start: 7.3, end: 8.55 },
  { id: "hand", text: "EDITED BY HAND", start: 8.55, end: 9.8 },
  { id: "code", text: "OWNED AS CODE", start: 9.8, end: 11.35 },
];
const H = 720;

export default function Video() {
  return (
    <Composition fps={30} width={W} height={H} background={{ mode: "color", color: PAPER }}>
      <Track id="print" layout="free">
        <Layer id="woodblock" start={0} duration={12}>
          <Woodblock />
        </Layer>
      </Track>
      <Track id="words" layout="free">
        <Title
          id="name"
          start={5.2}
          duration={6.15}
          font={display}
          size={112}
          weight={800}
          color={INK}
          transform={{ position: { x: 250, y: -22 } }}
          enter={{ type: "slide-up", duration: 0.6, easing: "easeOutCubic" }}
          exit={{ type: "fade", duration: 0.5 }}
        >
          CutCraft
        </Title>
        {LINES.map((line) => (
          <Title
            key={line.id}
            id={line.id}
            start={line.start}
            duration={line.end - line.start}
            font={mono}
            size={26}
            weight={500}
            color={RED}
            transform={{ position: { x: 250, y: 62 } }}
            enter={{ type: "fade", duration: 0.35 }}
            exit={{ type: "fade", duration: 0.35 }}
          >
            {line.text}
          </Title>
        ))}
      </Track>
    </Composition>
  );
}

const clamp01 = (x: number) => Math.min(1, Math.max(0, x));
/** 0 before `a`, 1 after `b`, eased between. */
const span = (t: number, a: number, b: number, ease: (x: number) => number = (x) => x) =>
  ease(clamp01((t - a) / (b - a)));
const easeOutCubic = (x: number) => 1 - (1 - x) ** 3;
const easeInOutSine = (x: number) => -(Math.cos(Math.PI * x) - 1) / 2;
const easeInOutCubic = (x: number) => (x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2);
const lerp = (a: number, b: number, q: number) => a + (b - a) * q;
/** Mixes two #rrggbb colours. */
function mix(a: string, b: string, q: number): string {
  const ch = (c: string, i: number) => Number.parseInt(c.slice(1 + 2 * i, 3 + 2 * i), 16);
  const out = [0, 1, 2].map((i) =>
    Math.round(lerp(ch(a, i), ch(b, i), q))
      .toString(16)
      .padStart(2, "0"),
  );
  return `#${out.join("")}`;
}

/** The logo's sea: offset rows of seigaiha, each a paper disc with three ink rings, in the logo's 512-unit space. */
function Sea({ t }: { t: number }) {
  const rows = Array.from({ length: 9 }, (_, r) => {
    const y = 368 + r * 18;
    // Rows roll in from the right one after another, the far (top) rows last and slowest; then they keep drifting.
    const arrive = span(t, 0.05 + r * 0.06, 1.0 + r * 0.05, easeOutCubic);
    const drift = (t * (14 - r)) % 60;
    const dx = (1 - arrive) * 900 - drift;
    const offset = r % 2 === 0 ? 0 : 30;
    return (
      <g key={r} opacity={arrive}>
        {Array.from({ length: 34 }, (_, i) => -720 + offset + i * 60).map((x) => (
          <g key={x} transform={`translate(${x + dx} ${y})`}>
            <circle r={30} fill={PAPER} />
            <circle r={30} fill="none" />
            <circle r={21} fill="none" />
            <circle r={12} fill="none" />
          </g>
        ))}
      </g>
    );
  });
  return (
    <g stroke={INK} strokeWidth={3}>
      <rect x={-800} y={352} width={2200} height={400} fill={PAPER} stroke="none" />
      {rows}
    </g>
  );
}

/** Bands of mist across the sun, in front of it until the cut blows them off either side. */
function Mist({ t, sky }: { t: number; sky: string }) {
  const show = span(t, 1.3, 2.3, easeOutCubic);
  const gone = span(t, 3.9, 4.5, easeOutCubic);
  const bands = [
    { x: 300, y: 150, w: 230, dir: 1 },
    { x: -60, y: 236, w: 250, dir: -1 },
    { x: 360, y: 300, w: 180, dir: 1 },
  ];
  return (
    <g opacity={show * (1 - gone)}>
      {bands.map((b, i) => {
        const dx = b.dir * ((1 - show) * 60 + t * 6 + gone * 260);
        return (
          <g key={b.y} transform={`translate(${b.x + dx} ${b.y})`}>
            <rect width={b.w} height={26} rx={13} fill={sky} stroke={INK} strokeWidth={4} />
            <path d={`M18 13 H${b.w - 30 - i * 20}`} stroke={RED} strokeWidth={3} strokeLinecap="round" opacity={0.7} />
          </g>
        );
      })}
    </g>
  );
}

/** Three birds crossing the sky from right to left, each wing beat a function of time. */
function Birds({ t }: { t: number }) {
  return (
    <g fill="none" stroke={INK} strokeWidth={3.5} strokeLinecap="round" strokeLinejoin="round">
      {[0, 1, 2].map((i) => {
        const go = span(t, 1.5 + i * 0.15, 3.7 + i * 0.12);
        if (go <= 0 || go >= 1) return null;
        const x = lerp(620, -120, go) + i * 34;
        const y = 86 + i * 22 + Math.sin(go * 9 + i) * 6;
        const beat = Math.sin(t * 16 + i * 2) * 5;
        return (
          <path
            key={i}
            transform={`translate(${x} ${y})`}
            d={`M-12 ${-beat} Q-6 ${-6 - beat / 2} 0 0 Q6 ${-6 - beat / 2} 12 ${-beat}`}
          />
        );
      })}
    </g>
  );
}

function Woodblock() {
  // Seconds into the layer (useT() would be its 0..1 progress).
  const t = useLayer().elapsed;
  // The camera: the logo's 512-unit square, first filling the frame's height, then drawn in to a tile on the left.
  const draw = span(t, 4.6, 5.8, easeInOutCubic);
  const k = lerp(H / 512, 300 / 512, draw);
  const cx = lerp(W / 2, 430, draw);
  const cy = H / 2;
  // The tile the print becomes: from the whole frame (in logo units) to the logo's rounded square.
  const frame = { x: -(W / 2) / (H / 512) + 256, y: 0, w: W / (H / 512), h: 512 };
  const tile = {
    x: lerp(frame.x, 0, draw),
    y: lerp(frame.y, 0, draw),
    w: lerp(frame.w, 512, draw),
    h: lerp(frame.h, 512, draw),
    r: lerp(0, 112, draw),
  };
  // The colours turn over as the tile forms: red sun on paper, then paper sun on red.
  const turn = span(t, 4.8, 5.6, easeInOutSine);
  const sky = mix(PAPER, RED, turn);
  const sun = mix(RED, PAPER, turn);
  // The sun rises from behind the sea; the katana passes along the logo's cut; the halves slide apart.
  const rise = span(t, 0.8, 2.8, easeOutCubic);
  const sunY = lerp(206 + 300, 206, rise);
  const blade = span(t, 3.6, 3.9, easeInOutSine);
  const split = span(t, 3.9, 4.6, easeOutCubic) * 15;
  const flash = span(t, 3.85, 3.95) * (1 - span(t, 3.95, 4.25));
  // The cut runs from the hilt at (-60, 552) to past the tip at (600, -100); the blade travels along it.
  const ux = 660 / Math.hypot(660, 652);
  const uy = -652 / Math.hypot(660, 652);
  const along = lerp(-420, 1180, blade);
  const bx = -60 + ux * along;
  const by = 552 + uy * along;
  // The fade back to bare paper, so the last frame meets the first.
  const fade = span(t, 11.35, 11.85, easeInOutSine);
  // The seal, pressed beside the name with the last line: it lands a little large and settles.
  const press = span(t, 10.1, 10.45, easeOutCubic);

  return (
    <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} style={{ display: "block" }}>
      <title>
        A woodblock sun rising through mist over seigaiha waves, cut in two by a katana, drawing in to the CutCraft logo
      </title>
      <defs>
        <clipPath id="hero-tile">
          <rect x={tile.x} y={tile.y} width={tile.w} height={tile.h} rx={tile.r} />
        </clipPath>
        <clipPath id="hero-above">
          <path d="M-60 552 L600 -100 L-600 -100 L-600 552Z" />
        </clipPath>
        <clipPath id="hero-below">
          <path d="M-60 552 L600 -100 L1400 -100 L1400 900 L-60 900Z" />
        </clipPath>
        <pattern id="hero-grain" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(35)">
          <path d="M0 0V6" stroke={INK} strokeWidth="1.2" opacity=".22" />
        </pattern>
        <filter id="hero-paper" x="0" y="0" width="100%" height="100%">
          <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="7" />
          <feColorMatrix values="0 0 0 0 0.1  0 0 0 0 0.08  0 0 0 0 0.06  0 0 0 0.09 0" />
        </filter>
      </defs>

      <rect width={W} height={H} fill={PAPER} />
      <g transform={`translate(${cx} ${cy}) scale(${k}) translate(-256 -256)`}>
        <g clipPath="url(#hero-tile)">
          <rect x={-800} y={-200} width={2200} height={1000} fill={sky} />
          <rect x={-800} y={-200} width={2200} height={1000} fill="url(#hero-grain)" opacity={0.35 * turn} />

          {/* The sun, in two halves that slide apart along the cut. */}
          <g transform={`translate(${-split} ${-split})`} clipPath="url(#hero-above)">
            <circle cx={262} cy={sunY} r={118} fill={sun} stroke={INK} strokeWidth={7} />
            <path d={`M170 ${sunY - 64}a118 118 0 0 1 120-52`} fill="none" stroke={INK} strokeWidth={3} opacity={0.5} />
          </g>
          <g transform={`translate(${split} ${split})`} clipPath="url(#hero-below)">
            <circle cx={262} cy={sunY} r={118} fill={sun} stroke={INK} strokeWidth={7} />
            <circle cx={262} cy={sunY} r={118} fill="url(#hero-grain)" />
          </g>

          <Mist t={t} sky={sky} />
          <Birds t={t} />
          <Sea t={t} />

          {/* The cut itself, a hairline of light for a moment after the blade. */}
          <path d="M-60 552 L600 -100" stroke={PAPER} strokeWidth={5} opacity={flash} />
          {/* Ink thrown off the cut. */}
          {[0.2, 0.35, 0.5, 0.62].map((p, i) => {
            const fly = span(t, 3.9 + i * 0.03, 4.7 + i * 0.03);
            const ox = -60 + ux * (360 + p * 300);
            const oy = 552 + uy * (360 + p * 300);
            const dir = i % 2 === 0 ? 1 : -1;
            return (
              <circle
                key={p}
                cx={ox + dir * fly * (40 + i * 12)}
                cy={oy + dir * fly * (30 + i * 8) + fly * fly * 60}
                r={6 - i}
                fill={INK}
                opacity={fly > 0 && fly < 1 ? 1 - fly : 0}
              />
            );
          })}
          {/* The katana: a dark blade with a paper edge, passing along the cut. */}
          {blade > 0 && blade < 1 && (
            <g transform={`translate(${bx} ${by}) rotate(${(Math.atan2(uy, ux) * 180) / Math.PI}) scale(1.7)`}>
              {/* The light it leaves behind, then the blade, its paper edge, the guard and the hilt. */}
              <path d="M-700 -1 L-150 -3 L-150 3 L-700 1Z" fill={PAPER} opacity={0.75} />
              <path d="M-260 -6 L0 -8 Q24 -2 38 7 L-260 6Z" fill={INK} />
              <path d="M-260 5 L0 6 Q22 5 38 7" fill="none" stroke={PAPER} strokeWidth={2.5} />
              <rect x={-276} y={-16} width={12} height={32} rx={3} fill={INK} />
              <rect x={-350} y={-7} width={76} height={14} rx={4} fill={INK} />
              <path
                d="M-344 -7 L-332 7 M-330 -7 L-318 7 M-316 -7 L-304 7 M-302 -7 L-290 7"
                stroke={RED}
                strokeWidth={2}
              />
            </g>
          )}
        </g>
      </g>
      {press > 0 && (
        <g transform={`translate(${W / 2 + 250 + 292} ${H / 2 - 30}) scale(${lerp(1.8, 1.3, press)})`} opacity={press}>
          <rect x={-22} y={-22} width={44} height={44} rx={6} fill={RED} />
          <rect x={-16} y={-16} width={32} height={32} rx={3} fill="none" stroke={PAPER} strokeWidth={2} />
          <path d="M-10 10 L10 -10" stroke={PAPER} strokeWidth={3} strokeLinecap="round" />
          <circle cx={-4} cy={-4} r={4} fill={PAPER} />
        </g>
      )}
      {/* The fade to bare paper goes under the paper's texture, so the last frame is the first. */}
      <rect width={W} height={H} fill={PAPER} opacity={fade} />
      <rect width={W} height={H} filter="url(#hero-paper)" />
    </svg>
  );
}

A product launch โ€‹

The founder on camera, cut on her words, with b-roll in the middle. A <Three> layer turns a phone in on the word "Shed" and keeps it turning; its screen is the product's name drawn on a canvas. A Motion lower third names her, and each feature appears as a pill on the word that names it (anchor="talk" at="word:8"), so recutting the talk retimes every callout.

Shows: cuts on words, overlays on words, <Three>, <Motion>, a crossfade, tweakable props. Time by meaning ยท Free code ยท <Three> ยท <Motion>

tsx
import { z } from "zod";

import { Clip, Composition, Motion, Three, Track, Transition } from "@cutcraft/kit";

export const schema = z.object({ product: z.string(), founder: z.string(), accent: z.string() });
export const defaults = { product: "Shed", founder: "Maya Chen", accent: "#e8a33d" };

const pill = {
  position: "absolute",
  left: 56,
  padding: "14px 22px",
  borderRadius: 999,
  background: "rgba(16, 20, 33, 0.82)",
  color: "#f4efe6",
  font: "600 30px 'Space Grotesk', sans-serif",
  letterSpacing: "-0.01em",
} as const;

/**
 * A product launch: the founder on camera, a three.js device turning beside them, and each feature called out on the
 * word that names it. Trim the founder's clip and every callout moves with its word.
 */
export default function Video({ product, founder, accent }: z.infer<typeof schema>) {
  return (
    <Composition fps={30} width={960} height={540} theme={{ extends: "midnight", palette: { primary: accent } }}>
      <Track id="main" name="Founder">
        <Clip id="talk" src="asset:founder" in="word:0" out="word:11.end" fit="cover" />
        <Transition id="x1" type="crossfade" duration={0.4} />
        <Clip id="broll" src="asset:product" in={0} out={1.6} fit="cover" />
        <Transition id="x2" type="crossfade" duration={0.4} />
        <Clip id="close" src="asset:founder" in="word:12" out="word:25.end" fit="cover" />
      </Track>

      <Track id="device" name="Device" layout="free">
        {/* The device turns in as the founder says the product's name, and keeps turning to the end. */}
        <Three
          id="phone"
          anchor="talk"
          at="word:1"
          duration={12}
          camera={{ position: [0, 0, 6], fov: 35 }}
          setup={({ scene, THREE }) => {
            const body = new THREE.Mesh(
              new THREE.BoxGeometry(1.5, 3, 0.16),
              new THREE.MeshStandardMaterial({ color: "#1b2a4a", metalness: 0.5, roughness: 0.3 }),
            );
            // The screen: the product's name drawn on a canvas, shown on the front face.
            const art = document.createElement("canvas");
            art.width = 300;
            art.height = 600;
            const g = art.getContext("2d") as CanvasRenderingContext2D;
            const grad = g.createLinearGradient(0, 0, 0, 600);
            grad.addColorStop(0, accent);
            grad.addColorStop(1, "#c8402a");
            g.fillStyle = grad;
            g.fillRect(0, 0, 300, 600);
            g.fillStyle = "#101421";
            g.font = "700 64px 'Space Grotesk', sans-serif";
            g.textAlign = "center";
            g.fillText(product, 150, 320);
            const texture = new THREE.CanvasTexture(art);
            texture.colorSpace = THREE.SRGBColorSpace;
            const screen = new THREE.Mesh(
              new THREE.PlaneGeometry(1.36, 2.8),
              new THREE.MeshBasicMaterial({ map: texture }),
            );
            screen.position.z = 0.082;
            const phone = new THREE.Group();
            phone.add(body, screen);
            phone.position.x = 1.7;
            scene.add(phone);
            scene.add(new THREE.AmbientLight("#ffffff", 0.6));
            const key = new THREE.DirectionalLight("#ffffff", 2.2);
            key.position.set(-3, 4, 5);
            scene.add(key);
            return ({ time }) => {
              const enter = Math.min(1, time / 1.2);
              const ease = 1 - (1 - enter) ** 3;
              phone.scale.setScalar(0.6 + 0.4 * ease);
              phone.rotation.y = (1 - ease) * -Math.PI + Math.sin(time * 0.7) * 0.45;
              phone.rotation.x = Math.sin(time * 0.5) * 0.08;
            };
          }}
        />
      </Track>

      <Track id="text" name="Text" layout="free">
        <Motion
          id="who"
          anchor="talk"
          offset={0.3}
          duration={3}
          sequence={[
            [".bar", { scaleX: [0, 1] }, { duration: 0.45, ease: "easeOut" }],
            [".name", { opacity: [0, 1], x: [-24, 0] }, { at: 0.15, duration: 0.4 }],
            [".role", { opacity: [0, 1], x: [-24, 0] }, { at: 0.3, duration: 0.4 }],
          ]}
        >
          <div
            className="bar"
            style={{
              position: "absolute",
              left: 56,
              top: 400,
              width: 8,
              height: 78,
              background: accent,
              transformOrigin: "left",
            }}
          />
          <div
            className="name"
            style={{
              position: "absolute",
              left: 80,
              top: 398,
              color: "#f4efe6",
              font: "700 38px 'Space Grotesk', sans-serif",
            }}
          >
            {founder}
          </div>
          <div
            className="role"
            style={{ position: "absolute", left: 80, top: 446, color: "#c9c2b8", font: "500 22px Inter, sans-serif" }}
          >
            Founder, {product}
          </div>
        </Motion>
        <Motion
          id="builds"
          anchor="talk"
          at="word:8"
          duration={2.6}
          sequence={[[".pill", { opacity: [0, 1], x: [-40, 0] }, { duration: 0.4, ease: "easeOut" }]]}
        >
          <div className="pill" style={{ ...pill, top: 200 }}>
            Builds from your repo
          </div>
        </Motion>
        <Motion
          id="sandbox"
          anchor="close"
          at="word:17"
          duration={2.4}
          sequence={[[".pill", { opacity: [0, 1], x: [-40, 0] }, { duration: 0.4, ease: "easeOut" }]]}
        >
          <div className="pill" style={{ ...pill, top: 200 }}>
            Every app in a sandbox
          </div>
        </Motion>
        <Motion
          id="zero"
          anchor="close"
          at="word:21"
          duration={2}
          sequence={[
            [".pill", { opacity: [0, 1], scale: [0.8, 1] }, { duration: 0.35, ease: "easeOut" }],
            [".pill", { backgroundColor: ["rgba(16, 20, 33, 0.82)", accent] }, { at: 0.35, duration: 0.3 }],
          ]}
        >
          <div className="pill" style={{ ...pill, top: 200 }}>
            Scales to zero
          </div>
        </Motion>
      </Track>
    </Composition>
  );
}

Kinetic type โ€‹

A vertical short. Each phrase is a <Motion> sequence anchored to the word that starts it: one slams in, one stacks word by word, one slides across. Karaoke captions run underneath.

Shows: <Motion> sequences anchored to words, karaoke captions, a vertical composition. Time by meaning ยท Captions and sound ยท <Motion>

tsx
import { z } from "zod";

import { Captions, Clip, Composition, Layer, Motion, Track } from "@cutcraft/kit";

export const schema = z.object({ accent: z.string() });
export const defaults = { accent: "#e8a33d" };

const big = {
  position: "absolute",
  left: 0,
  right: 0,
  textAlign: "center",
  color: "#f4efe6",
  font: "700 92px 'Space Grotesk', sans-serif",
  letterSpacing: "-0.03em",
  lineHeight: 1,
} as const;
const word = { display: "inline-block", marginRight: "0.22em" } as const;
/** One sequence step: a selector, its keyframes, its options. */
const slam: [string, Record<string, unknown>, Record<string, unknown>] = [
  ".k",
  { scale: [2.4, 1], opacity: [0, 1] },
  { duration: 0.35, ease: [0.2, 0.9, 0.2, 1] },
];

/**
 * Kinetic type for a vertical short: each phrase slams or stacks in on the word that starts it, over the speaker and
 * karaoke captions. Every layer is anchored to a word, so recutting the talk retimes the type.
 */
export default function Video({ accent }: z.infer<typeof schema>) {
  return (
    <Composition fps={30} width={540} height={960} theme={{ extends: "midnight", palette: { primary: accent } }}>
      <Track id="main" name="Speaker">
        <Clip id="talk" src="asset:speaker" in="word:0" out="word:24.end" fit="cover" />
      </Track>
      <Track id="shade" name="Shade" layout="free">
        <Layer id="dim" start={0} duration={11.1}>
          <div style={{ position: "absolute", inset: 0, background: "rgba(10, 12, 20, 0.45)" }} />
        </Layer>
      </Track>
      <Track id="type" name="Type" layout="free">
        <Motion id="every-cut" anchor="talk" at="word:0" duration={1.5} sequence={[slam]}>
          <div className="k" style={{ ...big, top: 300 }}>
            Every
            <br />
            <span style={{ color: accent }}>cut</span>
          </div>
        </Motion>
        <Motion
          id="line-of-code"
          anchor="talk"
          at="word:4"
          duration={1.5}
          sequence={[
            [".a", { y: [80, 0], opacity: [0, 1] }, { duration: 0.3, ease: "easeOut" }],
            [".b", { y: [80, 0], opacity: [0, 1] }, { at: 0.12, duration: 0.3, ease: "easeOut" }],
            [
              ".c",
              { y: [80, 0], opacity: [0, 1], color: ["#f4efe6", accent] },
              { at: 0.24, duration: 0.3, ease: "easeOut" },
            ],
          ]}
        >
          <div style={{ ...big, top: 320, fontSize: 80 }}>
            <span className="a" style={word}>
              a line
            </span>
            <span className="b" style={word}>
              of
            </span>
            <br />
            <span className="c" style={word}>
              code.
            </span>
          </div>
        </Motion>
        <Motion
          id="move-a-clip"
          anchor="talk"
          at="word:7"
          duration={1.6}
          sequence={[
            [".k", { x: [-540, 0] }, { duration: 0.4, ease: [0.2, 0.9, 0.2, 1] }],
            [".k", { x: [0, 30] }, { at: 0.5, duration: 1, ease: "linear" }],
          ]}
        >
          <div className="k" style={{ ...big, top: 340, fontSize: 84 }}>
            Move a clip
          </div>
        </Motion>
        <Motion
          id="words-move"
          anchor="talk"
          at="word:11"
          duration={1.9}
          sequence={[
            [".a", { opacity: [0, 1], rotate: [-8, 0] }, { duration: 0.3 }],
            [".b", { opacity: [0, 1], rotate: [8, 0] }, { at: 0.15, duration: 0.3 }],
            [".c", { opacity: [0, 1], scale: [0.6, 1] }, { at: 0.3, duration: 0.3 }],
          ]}
        >
          <div style={{ ...big, top: 300, fontSize: 80 }}>
            <span className="a" style={word}>
              the words
            </span>
            <br />
            <span className="b" style={{ ...word, color: accent }}>
              move
            </span>
            <br />
            <span className="c" style={word}>
              with it.
            </span>
          </div>
        </Motion>
        <Motion id="point-lands" anchor="talk" at="word:16" duration={2.1} sequence={[slam]}>
          <div className="k" style={{ ...big, top: 320, fontSize: 84 }}>
            The point
            <br />
            lands
          </div>
        </Motion>
        <Motion
          id="names-it"
          anchor="talk"
          at="word:21"
          duration={1.6}
          sequence={[
            [".k", { opacity: [0, 1], y: [40, 0] }, { duration: 0.3 }],
            [".u", { scaleX: [0, 1] }, { at: 0.25, duration: 0.35, ease: "easeOut" }],
          ]}
        >
          <div className="k" style={{ ...big, top: 330, fontSize: 84 }}>
            on the word.
          </div>
          <div
            className="u"
            style={{
              position: "absolute",
              left: 90,
              right: 90,
              top: 430,
              height: 10,
              background: accent,
              transformOrigin: "left",
            }}
          />
        </Motion>
      </Track>
      <Captions id="captions" src="asset:speaker" style={{ size: 30 }} animation="karaoke" />
    </Composition>
  );
}

A data story โ€‹

A chart written as plain SVG and animated by Motion: each bar grows on the word that says its number. The counters are a few lines of React in a <Layer>, counting up on the layer's clock.

Shows: SVG animated by <Motion>, React counters in a <Layer> reading useLayer(), numbers on the words that say them. Free code ยท <Layer> ยท <Motion>

tsx
import { z } from "zod";

import { Clip, Composition, Layer, Motion, Track, useLayer } from "@cutcraft/kit";

export const schema = z.object({ accent: z.string() });
export const defaults = { accent: "#e8a33d" };

const label = { fill: "#c9c2b8", font: "500 18px Inter, sans-serif" } as const;
const value = { fill: "#f4efe6", font: "700 26px 'Space Grotesk', sans-serif" } as const;
const card = { position: "absolute", left: 520, width: 380, height: 260, top: 60 } as const;

/** A number that counts up over its layer's first second: any React, on the video's clock. */
function Count({
  to,
  prefix = "",
  suffix = "",
  label,
  color,
}: {
  to: number;
  prefix?: string;
  suffix?: string;
  label: string;
  color: string;
}) {
  const { elapsed } = useLayer();
  const t = Math.min(1, elapsed / 0.9);
  const shown = Math.round(to * (1 - (1 - t) ** 3));
  return (
    <div style={{ position: "absolute", left: 72, top: 150, opacity: Math.min(1, elapsed / 0.2) }}>
      <div style={{ color, font: "700 150px 'Space Grotesk', sans-serif", letterSpacing: "-0.05em", lineHeight: 1 }}>
        {prefix}
        {shown}
        {suffix}
      </div>
      <div style={{ color: "#f4efe6", font: "500 30px Inter, sans-serif", marginTop: 12 }}>{label}</div>
    </div>
  );
}

/**
 * A data story: each bar grows, and each number lands, on the word that says it. The chart is plain SVG animated by
 * Motion; the counters are a few lines of React reading the layer's clock.
 */
export default function Video({ accent }: z.infer<typeof schema>) {
  return (
    <Composition fps={30} width={960} height={540} theme={{ extends: "midnight", palette: { primary: accent } }}>
      <Track id="main" name="Analyst">
        <Clip id="talk" src="asset:analyst" in="word:0" out="word:23.end" fit="cover" />
      </Track>
      <Track id="chart" name="Chart" layout="free">
        {/* The frame draws in as the analyst starts. */}
        <Motion
          id="frame"
          anchor="talk"
          at="word:0"
          duration={7.4}
          sequence={[
            [".panel", { opacity: [0, 1] }, { duration: 0.4 }],
            [".axis", { pathLength: [0, 1] }, { at: 0.2, duration: 0.6, ease: "easeOut" }],
            [".title", { opacity: [0, 1], y: [10, 0] }, { at: 0.4, duration: 0.4 }],
          ]}
        >
          <div className="panel" style={{ ...card, borderRadius: 18, background: "rgba(16, 20, 33, 0.78)" }} />
          <svg width="960" height="540" style={{ position: "absolute", inset: 0 }}>
            <title>Release time</title>
            <text className="title" x="548" y="100" style={{ ...label, font: "600 20px Inter, sans-serif" }}>
              Hours per release
            </text>
            <path className="axis" d="M 560 290 L 860 290" stroke="#5a6478" strokeWidth="2" fill="none" />
          </svg>
        </Motion>
        {/* Six hours: the tall bar, on "six". */}
        <Motion
          id="before"
          anchor="talk"
          at="word:5"
          duration={5.1}
          sequence={[
            [".bar", { scaleY: [0, 1] }, { duration: 0.6, ease: [0.2, 0.9, 0.2, 1] }],
            [".num", { opacity: [0, 1] }, { at: 0.4, duration: 0.3 }],
          ]}
        >
          <svg width="960" height="540" style={{ position: "absolute", inset: 0 }}>
            <title>Last year: six hours</title>
            <rect
              className="bar"
              x="600"
              y="160"
              width="90"
              height="130"
              fill="#5a6478"
              style={{ transformOrigin: "645px 290px" }}
            />
            <text className="num" x="645" y="150" textAnchor="middle" style={value}>
              6 h
            </text>
            <text x="645" y="312" textAnchor="middle" style={label}>
              last year
            </text>
          </svg>
        </Motion>
        {/* Ninety minutes: the short bar, on "ninety". */}
        <Motion
          id="after"
          anchor="talk"
          at="word:10"
          duration={2.6}
          sequence={[
            [".bar", { scaleY: [0, 1] }, { duration: 0.6, ease: [0.2, 0.9, 0.2, 1] }],
            [".num", { opacity: [0, 1] }, { at: 0.4, duration: 0.3 }],
          ]}
        >
          <svg width="960" height="540" style={{ position: "absolute", inset: 0 }}>
            <title>Today: ninety minutes</title>
            <rect
              className="bar"
              x="730"
              y="258"
              width="90"
              height="32"
              fill={accent}
              style={{ transformOrigin: "775px 290px" }}
            />
            <text className="num" x="775" y="248" textAnchor="middle" style={value}>
              1.5 h
            </text>
            <text x="775" y="312" textAnchor="middle" style={label}>
              today
            </text>
          </svg>
        </Motion>
      </Track>
      <Track id="numbers" name="Numbers" layout="free">
        <Layer id="four" anchor="talk" at="word:14" duration={2.4}>
          <Count to={4} suffix="ร—" label="as many releases" color={accent} />
        </Layer>
        <Layer id="forty" anchor="talk" at="word:22" duration={0.9}>
          <Count to={40} prefix="โˆ’" suffix="%" label="fewer incidents" color="#7fd1b9" />
        </Layer>
      </Track>
    </Composition>
  );
}

A logo reveal โ€‹

Three thousand three.js particles, scattered by a seeded random, settle into the name sampled from a canvas, then a Motion tagline slides in under it. Every particle's place is a function of time, so a frame looks the same however it is reached.

Shows: a three.js particle system written as a function of time, a seeded random, a <Motion> hand-off. Free code ยท <Three>

tsx
import { z } from "zod";

import { Composition, Motion, Three, Track } from "@cutcraft/kit";

export const schema = z.object({ name: z.string(), tagline: z.string(), accent: z.string() });
export const defaults = { name: "Shed", tagline: "A cloud for small software", accent: "#e8a33d" };

/**
 * A logo reveal for an intro or outro: three.js particles drift in and settle into the name, which then hands off
 * to a lower third. Every particle's place is a function of time, so the export draws exactly what the preview shows.
 */
export default function Video({ name, tagline, accent }: z.infer<typeof schema>) {
  return (
    <Composition fps={30} width={960} height={540} theme={{ extends: "midnight", palette: { primary: accent } }}>
      <Track id="scene" name="Scene" layout="free">
        <Three
          id="particles"
          start={0}
          duration={6}
          background="#101421"
          camera={{ position: [0, 0, 10], fov: 40 }}
          setup={({ scene, THREE }) => {
            // Where the particles end: points sampled from the name drawn on a canvas.
            const W = 480;
            const H = 160;
            const art = document.createElement("canvas");
            art.width = W;
            art.height = H;
            const g = art.getContext("2d") as CanvasRenderingContext2D;
            g.fillStyle = "#fff";
            g.font = "700 132px 'Space Grotesk', sans-serif";
            g.textAlign = "center";
            g.textBaseline = "middle";
            g.fillText(name, W / 2, H / 2);
            const pixels = g.getImageData(0, 0, W, H).data;
            const targets: number[] = [];
            for (let y = 0; y < H; y += 3)
              for (let x = 0; x < W; x += 3)
                if ((pixels[(y * W + x) * 4 + 3] as number) > 128) targets.push((x - W / 2) / 48, (H / 2 - y) / 48, 0);

            // Seeded, so every run (and every export) scatters the same way.
            let seed = 7;
            const random = () => {
              seed = (seed * 16807) % 2147483647;
              return seed / 2147483647;
            };
            const count = 3000;
            const from = new Float32Array(count * 3);
            const to = new Float32Array(count * 3);
            const delay = new Float32Array(count);
            for (let i = 0; i < count; i++) {
              const k = (i % (targets.length / 3)) * 3;
              to.set([targets[k] as number, targets[k + 1] as number, targets[k + 2] as number], i * 3);
              const r = 6 + random() * 6;
              const a = random() * Math.PI * 2;
              from.set([Math.cos(a) * r, Math.sin(a) * r * 0.6, (random() - 0.5) * 8], i * 3);
              delay[i] = random() * 0.8;
            }
            const positions = new Float32Array(count * 3);
            const geometry = new THREE.BufferGeometry();
            geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
            const points = new THREE.Points(geometry, new THREE.PointsMaterial({ color: accent, size: 0.07 }));
            scene.add(points);
            return ({ time }) => {
              for (let i = 0; i < count; i++) {
                const p = Math.min(1, Math.max(0, (time - (delay[i] as number)) / 1.8));
                const e = 1 - (1 - p) ** 4;
                for (let c = 0; c < 3; c++) {
                  const j = i * 3 + c;
                  positions[j] = (from[j] as number) + ((to[j] as number) - (from[j] as number)) * e;
                }
              }
              geometry.attributes.position.needsUpdate = true;
              points.rotation.y = Math.max(0, 2.6 - time) * 0.12;
              // Once settled, the name lifts to make room for the lower third.
              points.position.y = Math.min(1, Math.max(0, (time - 3) / 0.6)) * 0.8;
            };
          }}
        />
      </Track>
      <Track id="text" name="Text" layout="free">
        <Motion
          id="tagline"
          start={3.2}
          duration={2.8}
          sequence={[
            [".rule", { scaleX: [0, 1] }, { duration: 0.5, ease: "easeOut" }],
            [".line", { opacity: [0, 1], y: [16, 0] }, { at: 0.2, duration: 0.45, ease: "easeOut" }],
          ]}
        >
          <div
            className="rule"
            style={{ position: "absolute", left: 330, width: 300, top: 352, height: 4, background: accent }}
          />
          <div
            className="line"
            style={{
              position: "absolute",
              left: 0,
              right: 0,
              top: 372,
              textAlign: "center",
              color: "#f4efe6",
              font: "500 28px Inter, sans-serif",
            }}
          >
            {tagline}
          </div>
        </Motion>
      </Track>
    </Composition>
  );
}

An edit โ€‹

Three shots cut back to back on a sequence track: a wipe and a slide sit between them as siblings, the middle shot pushes in with four keyframes, a title builds word by word, and a Motion lower third comes in over the second shot. The overlays anchor to the shots, so they move when a cut moves.

Shows: a sequence track, transitions as siblings, keyframes, titles anchored to shots, a <Motion> lower third. Time by meaning ยท <Transition> ยท <Keyframe>

tsx
import { Clip, Composition, Keyframe, Motion, Title, Track, Transition } from "@cutcraft/kit";

/** An edit: three shots cut back to back, a wipe and a slide between them, a slow push-in, a title and a Motion lower third. */
export default function Video() {
  return (
    <Composition fps={30} width={960} height={540} theme={{ extends: "midnight", palette: { primary: "#c8402a" } }}>
      <Track id="main" name="Story">
        <Clip id="open" src="asset:aurora" in={0} out={3} fit="cover" />
        <Transition id="x1" type="wipe" duration={0.6} params={{ direction: "left" }} />
        <Clip id="zoom" src="asset:spiral" in={2} out={5.5} fit="cover">
          {/* A slow push-in: two keys on each scale axis. */}
          <Keyframe id="zoom-sx-0" property="transform.scale.x" at={0} value={1} />
          <Keyframe id="zoom-sx-1" property="transform.scale.x" at={3.5} value={1.2} easing="easeInOutSine" />
          <Keyframe id="zoom-sy-0" property="transform.scale.y" at={0} value={1} />
          <Keyframe id="zoom-sy-1" property="transform.scale.y" at={3.5} value={1.2} easing="easeInOutSine" />
        </Clip>
        <Transition id="x2" type="slide" duration={0.6} params={{ direction: "up" }} />
        <Clip id="close" src="asset:cells" in={0} out={2.6} fit="cover" />
      </Track>
      <Track id="text" name="Text">
        <Title
          id="title"
          anchor="open"
          offset={0.3}
          duration={2.2}
          style="h1"
          size={64}
          animation={{ preset: "word-by-word" }}
        >
          Cut by words
        </Title>
        <Motion
          id="who"
          anchor="zoom"
          offset={0.4}
          duration={2.6}
          sequence={[
            [".bar", { scaleX: [0, 1] }, { duration: 0.4, ease: "easeOut" }],
            [".name", { opacity: [0, 1], x: [-20, 0] }, { at: 0.15, duration: 0.35 }],
            [".role", { opacity: [0, 1], x: [-20, 0] }, { at: 0.3, duration: 0.35 }],
          ]}
        >
          <div
            className="bar"
            style={{
              position: "absolute",
              left: 48,
              top: 404,
              width: 6,
              height: 70,
              background: "#c8402a",
              transformOrigin: "left",
            }}
          />
          <div
            className="name"
            style={{
              position: "absolute",
              left: 68,
              top: 402,
              color: "#f4efe6",
              font: "700 32px 'Space Grotesk', sans-serif",
            }}
          >
            Every cut is a line
          </div>
          <div
            className="role"
            style={{ position: "absolute", left: 68, top: 444, color: "#c9c2b8", font: "500 20px Inter, sans-serif" }}
          >
            and every line is editable
          </div>
        </Motion>
        <Title id="end" anchor="close" offset={0.2} duration={2.2} style="h2" animation={{ preset: "slide-up" }}>
          The file is the video.
        </Title>
      </Track>
    </Composition>
  );
}

Captions and key points โ€‹

A vertical short. The captions are generated from the words the clip plays and highlight word by word; the key point is anchored to the clip and timed to the word that names it (at="word:16"), so trimming the clip keeps it on that word.

Shows: cuts on words, generated karaoke captions, a title on the word that names it. Captions and sound ยท Time by meaning

tsx
import { Captions, Clip, Composition, Title, Track } from "@cutcraft/kit";

/** A vertical short: captions come from the words the clip plays, and the key point starts on the word that names it. */
export default function Video() {
  return (
    <Composition fps={30} width={540} height={960} theme={{ extends: "midnight", palette: { primary: "#c8402a" } }}>
      <Track id="main" name="Speaker">
        <Clip id="talk" src="asset:speaker" in="word:0" out="word:24.end" fit="cover" />
      </Track>
      <Track id="points" name="Key points">
        <Title
          id="point"
          anchor="talk"
          at="word:16"
          duration={3.4}
          style="note"
          width={420}
          height={84}
          size={34}
          transform={{ position: { x: 0, y: 115 }, borderRadius: 18 }}
        >
          The point lands here
        </Title>
      </Track>
      <Captions id="captions" src="asset:speaker" style={{ size: 34 }} animation="karaoke" />
    </Composition>
  );
}

MIT licensed.