Skip to content

Slideshow with motion ​

Four photos held four seconds each with a slow push, crossfades between them, a bed that fades out with the last one.

What the manifest gives it ​

Image assets report a duration of 0 and are held for duration. The push is two scale keyframes per photo.

What the file shows ​

  • Photo is a helper used four times, so its clips and keyframes are generated; the first edit to one bakes that call into literals.
  • Transitions sit between siblings on the sequence track and overlap the neighbours by 0.6 s.
  • The bed is duration="fill" with a 2 s fade out.

The file ​

tsx
import { z } from "zod";

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

export const schema = z.object({
  title: z.string().describe("Opening title"),
});

export const defaults = { title: "Summer, mostly" };

/** Four photos held four seconds each, a slow push on every one, crossfades between, a bed that fades out at the end. */
export default function Video({ title }: z.infer<typeof schema>) {
  return (
    <Composition fps={30} width={1920} height={1080} background={{ mode: "color", color: "#000000" }}>
      <Track id="photos" name="Photos">
        <Photo id="p1" src="asset:photo-1" from={1} to={1.12} />
        <Transition id="x1" type="crossfade" duration={0.6} />
        <Photo id="p2" src="asset:photo-2" from={1.12} to={1} />
        <Transition id="x2" type="crossfade" duration={0.6} />
        <Photo id="p3" src="asset:photo-3" from={1} to={1.1} />
        <Transition id="x3" type="crossfade" duration={0.6} />
        <Photo id="p4" src="asset:photo-4" from={1.1} to={1} />
      </Track>

      <Track id="overlays" name="Overlays">
        <Title
          id="title"
          anchor="start"
          offset={0.5}
          duration={3}
          size={110}
          weight={300}
          animation={{ preset: "fade", in: 0.8, out: 0.6 }}
        >
          {title}
        </Title>
      </Track>

      <Track id="music" layout="free" role="music">
        <Audio id="bed" src="asset:music" start={0} duration="fill" volume={0.6} fade={{ in: 1, out: 2 }} />
      </Track>
    </Composition>
  );
}

/** A held photo with a push from `from` to `to`; used four times, so the editor sees generated elements until it bakes one. */
function Photo({ id, src, from, to }: { id: string; src: string; from: number; to: number }) {
  return (
    <Clip id={id} src={src} duration={4} fit="cover">
      <Keyframe id={`${id}-sx0`} property="transform.scale.x" at={0} value={from} easing="easeInOutSine" />
      <Keyframe id={`${id}-sx1`} property="transform.scale.x" at={4} value={to} />
      <Keyframe id={`${id}-sy0`} property="transform.scale.y" at={0} value={from} easing="easeInOutSine" />
      <Keyframe id={`${id}-sy1`} property="transform.scale.y" at={4} value={to} />
    </Clip>
  );
}

What the editor can do ​

  • Trim a photo: the keyframes keep their at, the push ends early.
  • Change from or to on a Photo call: edit the call site, the file re-evaluates.

What to check ​

  • No lint warnings expected.
  • captureSheet([0.5, 3.7, 7.5, 13.5]): the title, a crossfade mid-way, a pushed-in photo, the last photo.

MIT licensed.