import type { FC } from "react"; import { useCurrentFrame } from "remotion"; import { DRAW } from "../brand/tokens"; import { inOut, mix, out, progress } from "../illustrations/motion"; import { FilmStage } from "./FilmStage"; import { CAPTIONS, SHOT_FRAMES, SURFACE, T, cameraAt, glow, untilFold } from "./shot"; /** * Close up, bars drop onto a stack: a skyscraper going up. The camera pulls back: it is the pile of * invoices waiting to be typed in, and it keeps growing. Then a light runs down the pile and the * sheets leave by themselves: most into the books, the few unclear ones to a person. */ const BAR = { x: -170, width: 210, height: 16, pitch: 25, base: 290 } as const; const MAX_BARS = 24; const BOOKS = { x: 250, y: -70, size: 130 } as const; const PERSON = { x: 250, y: 170, r: 44 } as const; const OPENING_BARS = 3; const CLOSE_EVERY = 25; /** The film starts with a bar in mid-fall: the first frame is the preview image. */ const CLOSE_PHASE = 12; const WIDE_EVERY = 9; const LEAVE_EVERY = 5; const LEAVE_FRAMES = 24; /** Every sixth sheet is unclear and goes to a person. */ const isUnclear = (index: number): boolean => index % 6 === 2; const barY = (index: number): number => BAR.base - BAR.height / 2 - index * BAR.pitch; /** How many bars have arrived, as a number with a fraction: the fraction is the one still falling. */ const barsAt = (frame: number): number => { if (frame >= T.fold) return OPENING_BARS + (frame - SHOT_FRAMES + CLOSE_PHASE) / CLOSE_EVERY; const close = OPENING_BARS + (Math.min(frame, T.reveal) + CLOSE_PHASE) / CLOSE_EVERY; return Math.min(MAX_BARS, close + Math.max(0, frame - T.reveal) / WIDE_EVERY); }; const leaveStart = (index: number): number => T.resolve + (MAX_BARS - 1 - index) * LEAVE_EVERY; const LAST_LEAVE = T.resolve + MAX_BARS * LEAVE_EVERY; const UNCLEAR = Array.from({ length: MAX_BARS }, (_, index) => index).filter(isUnclear); /** Where an unclear sheet waits next to the person. */ const waitingSpot = (index: number): { x: number; y: number } => { const angle = UNCLEAR.indexOf(index) * 0.62 + 2.2; return { x: PERSON.x + Math.cos(angle) * 78, y: PERSON.y + Math.sin(angle) * 78 }; }; /** The picture alone, without stage and statements: also used frozen, as an image post. */ export const DocumentsDrawing: FC = () => { const frame = useCurrentFrame(); const camera = cameraAt(frame, 2.6, 0.95, { x: BAR.x, y: barY(OPENING_BARS) - 20 }); const { zoom, hair, others } = camera; const bars = barsAt(frame); const leaving = frame < T.fold; const scan = progress(frame, T.turn, LAST_LEAVE); const scanShown = Math.min(progress(frame, T.turn, T.turn + 10), 1 - progress(frame, LAST_LEAVE - 10, LAST_LEAVE)); const dot = 11 / Math.sqrt(zoom); return ( {Array.from({ length: MAX_BARS }, (_, index) => { if (index >= bars) return null; const arrived = Math.min(1, bars - index); const fadeIn = leaving ? 1 : progress(frame, T.fold + 8, T.fold + 30); const gone = leaving ? inOut(progress(frame, leaveStart(index), leaveStart(index) + LEAVE_FRAMES)) : 0; const unclear = isUnclear(index); const target = unclear ? waitingSpot(index) : BOOKS; const width = mix(BAR.width, dot * 2, gone); const height = mix(BAR.height, dot * 2, gone); const x = mix(BAR.x, target.x, gone); const y = mix(mix(barY(index) - 150 / zoom, barY(index), out(arrived)), target.y, gone) - Math.sin(Math.PI * gone) * 70; const opacity = arrived * fadeIn * (unclear ? 1 : 1 - progress(gone, 0.82, 1)); if (opacity === 0) return null; return ( 0 ? DRAW.mark : DRAW.fg} opacity={opacity * (gone > 0 ? 1 : 0.9)} style={unclear && gone > 0.5 ? glow(DRAW.mark, 10, zoom) : undefined} /> ); })} {!leaving && UNCLEAR.map((index) => { const spot = waitingSpot(index); return ; })} {scanShown > 0 && ( )} ); }; /** Incoming invoices: from a growing pile to documents that are read and booked by themselves. */ export const DocumentsFilm: FC = () => ( );