All demos

Changelog — Rolling Numbers

Install this video

Adds the full composition, its remocn dependencies, and the prompt to your Remotion project via the shadcn registry.

$ pnpm dlx shadcn@latest add kapishdima/remocn-demo/changelog

Render it locally

Renders the MP4 on your machine with the Remotion CLI.

$ pnpm dlx remotion render changelog out/changelog.mp4 --scale=2 --crf=15 --x264-preset=slower --jpeg-quality=95

The prompt

Reconstructed draft

A reconstruction of the prompt this video was generated from.

Make a quick changelog video for remocn v2.0.6 that shows off the rolling-number animation we added. Keep it short and focused — just enough setup to explain what changelog this is, then let the rolling-number component actually demo itself counting up so people see the motion. Use remocn's own rolling-number component for the demo, standard remocn changelog styling.

The code

The exact source the AI wrote — the same files the install command puts in your project.

import React from "react";
import { AbsoluteFill, Sequence, Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion";
import { demoAsset } from "@/lib/demo-assets";
import {
  TransitionSeries,
  linearTiming,
  type TransitionPresentation,
  type TransitionPresentationComponentProps,
} from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { loadFont } from "@remotion/google-fonts/Manrope";

import { RemocnUIProvider } from "@/lib/remocn-ui";
import { Backdrop } from "@/components/remocn/backdrop";
import { BlurReveal } from "@/components/remocn/blur-reveal";
import { Typewriter } from "@/components/remocn/typewriter";
import { RollingNumber } from "@/components/remocn/rolling-number";

// Bind Inter to the CSS variable the remocn components read for their font.
const { fontFamily } = loadFont("normal", {
  subsets: ["latin"],
  weights: ["400", "600", "700"],
});

// ---------------------------------------------------------------------------
// Scene timings (frames @ 30fps)
// ---------------------------------------------------------------------------
const INTRO = 75;
const DEMO = 105;
const OUTRO = 70;
const T_FADE = 18;
const T_SLIDE = 18;
const T_GAP = 16; // kinetic transition between consecutive metric scenes

// Shared "vanish" parameters: the intro disappears with these, and the outro
// arrives by replaying the exact same motion in reverse.
const EXIT_RISE = 80;
const EXIT_BLUR = 16;
const EXIT_SCALE_DROP = 0.06;

export const CHANGELOG_DURATION =
  INTRO + DEMO * 4 + OUTRO - T_FADE - T_SLIDE - 3 * T_GAP;

// ---------------------------------------------------------------------------
// Intro: version badge (top-right, blurReveal) + animation label
//        (bottom-right, typewriter)
// ---------------------------------------------------------------------------
const IntroScene: React.FC = () => {
  const frame = useCurrentFrame();

  // Noticeable exit during the scene's final frames: the whole intro lifts up,
  // blurs and fades away (rather than just dissolving). This plays out while
  // the cross-fade hands over to the demo, so the intro clearly "leaves".
  const exit = interpolate(frame, [INTRO - T_FADE, INTRO], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.in(Easing.cubic),
  });
  const exitStyle: React.CSSProperties = {
    opacity: 1 - exit,
    filter: exit > 0 ? `blur(${exit * EXIT_BLUR}px)` : undefined,
    transform: `translateY(${-exit * EXIT_RISE}px) scale(${
      1 - exit * EXIT_SCALE_DROP
    })`,
  };

  return (
    <AbsoluteFill style={exitStyle}>
      {/* Top-right — "Remocn v2.0.6" revealed with a blur. */}
      <div
        style={{
          position: "absolute",
          top: 56,
          left: 30,
          width: 380,
          height: 64,
        }}
      >
        {/* Window spans the whole scene so the text stays after revealing. */}
        <Sequence durationInFrames={INTRO}>
          <BlurReveal
            text="Remocn v2.0.6"
            fontSize={40}
            color="#fafafa"
            fontWeight={700}
            blur={14}
          />
        </Sequence>
      </div>

      {/* Bottom-right — "Animation: Rolling Numbers" typed into a pill badge. */}
      <div
        style={{
          position: "absolute",
          bottom: 80,
          left: 70,
          width: 430,
          height: 56,
        }}
      >
        <Sequence from={22} durationInFrames={INTRO - 22}>
          <Typewriter
            text="Animation: Rolling Numbers"
            fontSize={48}
            color="#fff"
            cursorColor="#fff"
            fontWeight={600}
            charsPerSecond={26}
          />
        </Sequence>
      </div>
    </AbsoluteFill>
  );
};

// ---------------------------------------------------------------------------
// Demo: the rolling-number component, with a fading caption.
// ---------------------------------------------------------------------------
const DemoContent: React.FC<{
  label: string;
  count: number;
  countFrames: number;
}> = ({ label, count, countFrames }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const captionOpacity = interpolate(frame, [0, 0.5 * fps], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  return (
    <AbsoluteFill>
      {/* Caption sits above the full-screen, centred RollingNumber. */}
      <div
        style={{
          position: "absolute",
          left: 0,
          right: 0,
          top: "30%",
          textAlign: "center",
          opacity: captionOpacity,
          fontFamily: `${fontFamily}, sans-serif`,
          fontSize: 18,
          fontWeight: 600,
          textTransform: "uppercase",
          color: "rgba(250,250,250,0.6)",
        }}
      >
        {label}
      </div>

      {/* Drive the count once the content begins. */}
      <Sequence durationInFrames={countFrames}>
        <RollingNumber from={0} to={count} fontSize={150} color="#fafafa" />
      </Sequence>
    </AbsoluteFill>
  );
};

const DemoScene: React.FC<{
  label: string;
  count: number;
  /** Hold the scene empty for this many frames before it starts playing. */
  startDelay?: number;
}> = ({ label, count, startDelay = 0 }) => {
  return (
    <AbsoluteFill>
      <Sequence from={startDelay} durationInFrames={DEMO - startDelay}>
        <DemoContent
          label={label}
          count={count}
          countFrames={DEMO - startDelay}
        />
      </Sequence>
    </AbsoluteFill>
  );
};

// ---------------------------------------------------------------------------
// Outro: the wordmark revealed with a blur.
// ---------------------------------------------------------------------------
const OutroScene: React.FC = () => {
  return (
    <AbsoluteFill>
      <Sequence durationInFrames={OUTRO}>
        <BlurReveal
          text="remocn"
          fontSize={132}
          color="#fafafa"
          fontWeight={700}
          blur={18}
        />
      </Sequence>
    </AbsoluteFill>
  );
};

// ---------------------------------------------------------------------------
// Outro transition: the reverse of the intro's disappearance. The leaving demo
// dissolves, and the outro arrives by replaying the intro vanish backwards —
// descending from above, un-blurring and scaling into place.
// ---------------------------------------------------------------------------
const BlurRisePresentation: React.FC<
  TransitionPresentationComponentProps<Record<string, never>>
> = ({ children, presentationProgress, presentationDirection }) => {
  const entering = presentationDirection === "entering";

  // Ease so the entrance settles softly (the inverse of the intro's
  // accelerating exit).
  const p = interpolate(presentationProgress, [0, 1], [0, 1], {
    easing: entering ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic),
  });

  const style: React.CSSProperties = entering
    ? {
        opacity: p,
        filter: p < 1 ? `blur(${(1 - p) * EXIT_BLUR}px)` : undefined,
        transform: `translateY(${-(1 - p) * EXIT_RISE}px) scale(${
          1 - (1 - p) * EXIT_SCALE_DROP
        })`,
      }
    : {
        // The outgoing demo simply dissolves so it doesn't fight the reveal.
        opacity: 1 - p,
        filter: p > 0 ? `blur(${p * EXIT_BLUR}px)` : undefined,
      };

  return <AbsoluteFill style={style}>{children}</AbsoluteFill>;
};

const blurRise = (): TransitionPresentation<Record<string, never>> => ({
  component: BlurRisePresentation,
  props: {},
});

// ---------------------------------------------------------------------------
// Kinetic slide between metric scenes: the two scenes push each other along an
// axis while a motion blur peaks mid-transition and the content dips in scale,
// giving each hand-off a fast, energetic "whoosh" instead of a hard cut.
// ---------------------------------------------------------------------------
type SlideDir = "from-bottom" | "from-top" | "from-left" | "from-right";

const KineticSlidePresentation: React.FC<
  TransitionPresentationComponentProps<{ direction: SlideDir }>
> = ({ children, presentationProgress, presentationDirection, passedProps }) => {
  const { direction } = passedProps;
  const entering = presentationDirection === "entering";

  const p = interpolate(presentationProgress, [0, 1], [0, 1], {
    easing: Easing.inOut(Easing.cubic),
  });

  const axis = direction === "from-left" || direction === "from-right" ? "x" : "y";
  // Where the entering scene starts (off-screen), as a sign of 100%.
  const enterSign = direction === "from-right" || direction === "from-bottom" ? 1 : -1;

  // Entering travels start -> 0; exiting is pushed 0 -> opposite side.
  const offsetPct = entering ? enterSign * (1 - p) * 100 : -enterSign * p * 100;
  const translate =
    axis === "x" ? `translateX(${offsetPct}%)` : `translateY(${offsetPct}%)`;

  // Speed cue: blur + scale dip peak at the mid-point of the move.
  const motion = Math.sin(p * Math.PI);
  const blur = motion * 12;
  const scale = 1 - motion * 0.08;

  const style: React.CSSProperties = {
    transform: `${translate} scale(${scale})`,
    filter: blur > 0.1 ? `blur(${blur}px)` : undefined,
  };

  return <AbsoluteFill style={style}>{children}</AbsoluteFill>;
};

const kineticSlide = (
  direction: SlideDir,
): TransitionPresentation<{ direction: SlideDir }> => ({
  component: KineticSlidePresentation,
  props: { direction },
});

// ---------------------------------------------------------------------------
// Composition root
// ---------------------------------------------------------------------------
export const ChangelogDemo: React.FC = () => {
  return (
    <RemocnUIProvider>
      <AbsoluteFill
        style={{ "--font-geist-sans": fontFamily } as React.CSSProperties}
      >
        {/* Persistent image background for the whole video. */}
        <Backdrop fill={{ type: "image", src: demoAsset("bg.png") }} />
        {/* Subtle scrim to deepen contrast for the foreground text. */}
        <AbsoluteFill
          style={{
            background:
              "radial-gradient(120% 120% at 50% 40%, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.45) 100%)",
          }}
        />

        <TransitionSeries>
          <TransitionSeries.Sequence durationInFrames={INTRO}>
            <IntroScene />
          </TransitionSeries.Sequence>

          <TransitionSeries.Transition
            timing={linearTiming({ durationInFrames: T_FADE })}
            presentation={fade()}
          />

          <TransitionSeries.Sequence durationInFrames={DEMO}>
            {/* Wait out the cross-fade so the count starts after the intro leaves. */}
            <DemoScene label="Pageview" count={51261} startDelay={T_FADE} />
          </TransitionSeries.Sequence>

          <TransitionSeries.Transition
            timing={linearTiming({ durationInFrames: T_GAP })}
            presentation={kineticSlide("from-bottom")}
          />

          <TransitionSeries.Sequence durationInFrames={DEMO}>
            <DemoScene label="Unique Visitors" count={4426}/>
          </TransitionSeries.Sequence>

          <TransitionSeries.Transition
            timing={linearTiming({ durationInFrames: T_GAP })}
            presentation={kineticSlide("from-right")}
          />

          <TransitionSeries.Sequence durationInFrames={DEMO}>
            <DemoScene label="Sessions" count={4871}/>
          </TransitionSeries.Sequence>

          <TransitionSeries.Transition
            timing={linearTiming({ durationInFrames: T_GAP })}
            presentation={kineticSlide("from-bottom")}
          />

          <TransitionSeries.Sequence durationInFrames={DEMO}>
            <DemoScene label="Bounce Rate" count={47}/>
          </TransitionSeries.Sequence>

          <TransitionSeries.Transition
            timing={linearTiming({ durationInFrames: T_SLIDE })}
            presentation={blurRise()}
          />

          <TransitionSeries.Sequence durationInFrames={OUTRO}>
            <OutroScene />
          </TransitionSeries.Sequence>
        </TransitionSeries>
      </AbsoluteFill>
    </RemocnUIProvider>
  );
};