Keyloom

Image Scene

A full-bleed image that settles with a slow cinematic zoom, with an optional caption overlaid at the bottom.

Preview

A full-bleed image that settles with a slow cinematic zoom, with an optional caption overlaid at the bottom.

Open editor

Props

NameTypeDefault
srcstring (url)"backgrounds/beach.jpg"
captionstring""

Composition

ID
ImageScene
Resolution
1920×1080
FPS
60
Duration
3.0s

Source

Copy or download the React source — drop it into your own Remotion project. The only runtime dependency is remotion.

"use client";
import { AbsoluteFill, Easing, Img, interpolate, staticFile } from "remotion";
import { type ClipStyle, resolveClipStyle } from "../../clip-style";
import { proxyExternalImg } from "../../proxy-image";
import { snap } from "../../snap";
import { useCanvasLayout } from "../../use-canvas-layout";
import { useDesignFrame } from "../../use-design-frame";

export type ImageSceneProps = {
  src: string;
  caption: string;
  clipStyle?: ClipStyle;
};

const APPLE_EASE = Easing.bezier(0.16, 1, 0.3, 1);

function resolveAsset(src: string | undefined): string | undefined {
  if (!src) return undefined;
  if (/^(data:|blob:)/i.test(src)) return src;
  if (/^https?:/i.test(src)) return proxyExternalImg(src);
  return staticFile(src.replace(/^\//, ""));
}

export const ImageScene: React.FC<ImageSceneProps> = ({
  src,
  caption,
  clipStyle,
}) => {
  const frame = useDesignFrame();
  const { vmin } = useCanvasLayout();
  const s = resolveClipStyle(clipStyle, {
    background: "#0f1014",
    color: "#ffffff",
    fontFamily:
      "-apple-system, BlinkMacSystemFont, 'SF Pro Display', Inter, sans-serif",
    accent: "#0a84ff",
  });

  const zoom = interpolate(frame, [0, 170], [1.04, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: APPLE_EASE,
  });
  const captionReveal = interpolate(frame, [8, 30], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: APPLE_EASE,
  });

  const resolved = resolveAsset(src);
  const trimmedCaption = caption.trim();

  return (
    <AbsoluteFill
      style={{
        background: s.background,
        color: s.color,
        fontFamily: s.fontFamily,
        overflow: "hidden",
      }}
    >
      {resolved ? (
        <Img
          src={resolved}
          crossOrigin="anonymous"
          style={{
            position: "absolute",
            inset: 0,
            width: "100%",
            height: "100%",
            objectFit: "cover",
            transform: `scale(${zoom})`,
            transformOrigin: "50% 50%",
          }}
        />
      ) : null}
      {trimmedCaption ? (
        <>
          <div
            style={{
              position: "absolute",
              left: 0,
              right: 0,
              bottom: 0,
              height: "30%",
              background:
                "linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.45) 100%)",
            }}
          />
          <div
            style={{
              position: "absolute",
              left: vmin(7),
              right: vmin(7),
              bottom: vmin(6),
              textAlign: "center",
              fontSize: vmin(3.2),
              fontWeight: 600,
              letterSpacing: "-0.01em",
              color: s.color,
              textShadow: "0 1px 14px rgba(0,0,0,0.35)",
              opacity: captionReveal,
              transform: `translate3d(0, ${snap((1 - captionReveal) * vmin(1.2))}px, 0)`,
            }}
          >
            {trimmedCaption}
          </div>
        </>
      ) : null}
    </AbsoluteFill>
  );
};
Save as ImageScene/ImageScene.tsx