← back to Crcp Pitch Video

src/Walkthrough.tsx

76 lines

import React from 'react';
import { AbsoluteFill, Img, staticFile, Sequence, useCurrentFrame, useVideoConfig, interpolate, spring, Easing } from 'remotion';
import data from './walkthrough-data.json';

export const WALK_FRAMES = data.total;
export const WALK_FPS = data.fps;

const GOLD = '#C8A24B';
const SANS = '-apple-system, "Helvetica Neue", Arial, sans-serif';

type Box = { x: number; y: number; width: number; height: number } | null;

const Step: React.FC<{ shot: string; box: Box; label: string; cap: string; index: number; total: number; dur: number }> = ({ shot, box, label, cap, index, total, dur }) => {
  const f = useCurrentFrame();
  const { fps } = useVideoConfig();
  // subtle ken-burns; image + highlight share the transform so the box stays on the element
  const z = interpolate(f, [0, dur], [1.0, 1.035], { extrapolateRight: 'clamp' });
  const appear = interpolate(f, [0, 10], [0, 1], { extrapolateRight: 'clamp' });
  // highlight pop + pulse
  const pop = spring({ frame: f - 6, fps, config: { damping: 12, mass: 0.6 } });
  const pulse = 0.55 + 0.45 * Math.abs(Math.sin(f / 11));
  const pad = 10;

  return (
    <AbsoluteFill style={{ background: '#05070b' }}>
      <AbsoluteFill style={{ transform: `scale(${z})`, transformOrigin: '50% 45%' }}>
        <Img src={staticFile(shot)} style={{ width: 1920, height: 1080 }} />
        <AbsoluteFill style={{ background: 'rgba(3,5,10,0.22)', opacity: appear }} />
        {box && (
          <div style={{
            position: 'absolute', left: box.x - pad, top: box.y - pad, width: box.width + pad * 2, height: box.height + pad * 2,
            border: `4px solid ${GOLD}`, borderRadius: 10,
            boxShadow: `0 0 ${18 + pulse * 22}px ${GOLD}, inset 0 0 12px rgba(200,162,75,0.25)`,
            opacity: appear * (0.85 + pulse * 0.15),
            transform: `scale(${interpolate(pop, [0, 1], [1.12, 1])})`, transformOrigin: 'center',
          }} />
        )}
      </AbsoluteFill>
      {/* caption bar */}
      <div style={{
        position: 'absolute', left: 0, right: 0, bottom: 0, height: 132,
        background: 'linear-gradient(180deg, rgba(5,7,11,0) 0%, rgba(5,7,11,0.92) 42%)',
        display: 'flex', alignItems: 'flex-end', padding: '0 56px 26px',
        transform: `translateY(${interpolate(appear, [0, 1], [40, 0])}px)`, opacity: appear,
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 22 }}>
          <div style={{ fontFamily: SANS, fontSize: 22, fontWeight: 700, color: '#05070b', background: GOLD, borderRadius: 20, padding: '5px 14px' }}>
            {index + 1} / {total}
          </div>
          <div>
            <div style={{ fontFamily: SANS, fontSize: 38, fontWeight: 800, color: GOLD, letterSpacing: 0.3 }}>{label}</div>
            <div style={{ fontFamily: SANS, fontSize: 26, color: '#EAECEF', marginTop: 2 }}>{cap}</div>
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

export const CRCPWalkthrough: React.FC = () => {
  let from = 0;
  return (
    <AbsoluteFill style={{ background: '#05070b' }}>
      {data.steps.map((s: any, i: number) => {
        const el = (
          <Sequence key={i} from={from} durationInFrames={s.durFrames}>
            <Step shot={s.shot} box={s.box} label={s.label} cap={s.cap} index={i} total={data.steps.length} dur={s.durFrames} />
          </Sequence>
        );
        from += s.durFrames;
        return el;
      })}
    </AbsoluteFill>
  );
};