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 = () => (
);