← 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>
);
};