import React from 'react';
import {
AbsoluteFill,
Img,
interpolate,
Sequence,
spring,
staticFile,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { colors, fonts } from '../lib/theme';
import { IPhone17ProFrame, SCREEN_W, SCREEN_H } from '../components/IPhone17ProFrame';
import { IOSKeyboard } from '../components/IOSKeyboard';
// ─── Scene timing (frames @ 30fps) ───
const TITLE_DUR = 60;
const PHONES_DUR = 30;
const TYPING_DUR = 420;
const HOLD_DUR = 60;
const OUTRO_DUR = 45;
const TITLE_START = 0;
const PHONES_START = TITLE_DUR; // 60
const TYPING_START = PHONES_START + PHONES_DUR; // 90
const HOLD_START = TYPING_START + TYPING_DUR; // 510
const OUTRO_START = HOLD_START + HOLD_DUR; // 570
export const ZEROLAG_TOTAL_FRAMES = OUTRO_START + OUTRO_DUR; // 615
// ─── iPhone 17 Pro safe area ───
const SAFE_AREA_TOP = 59; // Below Dynamic Island
const PHONE_SCALE = 1.12; // Scale up to fill more of the frame
// The Codeman screenshot starts content at y=0 (the session tab).
// On a real device it would sit below the safe area, so we offset it.
const SCREENSHOT_Y_OFFSET = SAFE_AREA_TOP;
// Terminal typing overlay position (relative to screenshot top)
// Session tab is ~44px, then terminal starts. Adding safe area offset:
const TERMINAL_TOP = SAFE_AREA_TOP + 52;
const TERMINAL_LEFT = 14;
const TERMINAL_FONT = 22; // Large for video readability
// ─── Typing schedule ───
const TYPED_TEXT = 'fix the auth bug in the login flow';
const FRAME_GAP = 12; // ~400ms between keystrokes
// Remote connection lag: 600ms-1.2s+ per char (18-36+ frames)
const LAGGY_DELAYS = [
24, 30, 36, 32, 26, 22, 34, 28, 38, 20, 30, 24, 32, 26, 36, 22,
30, 24, 32, 28, 34, 26, 30, 22, 28, 36, 24, 30, 32, 26, 34, 28, 24, 30,
];
type KeyEvent = { frame: number; char: string; lagDelay: number };
const TYPING_SCHEDULE: KeyEvent[] = TYPED_TEXT.split('').map((char, i) => ({
frame: i * FRAME_GAP,
char,
lagDelay: LAGGY_DELAYS[i % LAGGY_DELAYS.length],
}));
const CONFIRM_DELAY = 14;
// ─── iOS Status Bar (sits in the safe area, flanking Dynamic Island) ───
const IOSStatusBar: React.FC = () => (
9:41
{/* Signal */}
{/* WiFi */}
{/* Battery */}
);
// ─── Typing overlay ───
const TypingOverlay: React.FC<{
typed: string;
overlayChars?: { char: string; confirmed: boolean }[];
cursorVisible: boolean;
}> = ({ typed, overlayChars, cursorVisible }) => {
const frame = useCurrentFrame();
const cursorOpacity = cursorVisible ? (Math.floor(frame / 18) % 2 === 0 ? 0.85 : 0.5) : 0;
const lineH = Math.round(TERMINAL_FONT * 1.4);
const charW = TERMINAL_FONT * 0.62;
return (
{'❯ '}
{overlayChars
? overlayChars.map((oc, i) => (
{oc.char}
))
: {typed} }
);
};
// ─── Single phone: iPhone 17 Pro + real screenshot + typing + keyboard ───
const MobileCodeman: React.FC<{
typed: string;
overlayChars?: { char: string; confirmed: boolean }[];
cursorVisible: boolean;
activeKey?: string;
pressAge?: number;
showKeyboard?: boolean;
noAnimation?: boolean;
}> = ({ typed, overlayChars, cursorVisible, activeKey, pressAge, showKeyboard = true, noAnimation }) => (
{/* Real Codeman mobile screenshot, pushed down by safe area */}
{/* iOS status bar in the safe area */}
{/* Typing animation */}
{/* iOS keyboard at bottom */}
{showKeyboard && (
)}
);
// ─── Label beneath phone ───
const PhoneLabel: React.FC<{
title: string;
detail: string;
dotColor: string;
detailColor: string;
}> = ({ title, detail, dotColor, detailColor }) => (
);
// ─── Title scene ───
const TitleScene: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const titleScale = spring({ frame, fps, config: { damping: 15, stiffness: 80 } });
const titleOpacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
const subtitleOpacity = interpolate(frame, [15, 35], [0, 1], { extrapolateRight: 'clamp' });
return (
Zerolag Input
Local echo for remote terminal sessions
);
};
// ─── Outro ───
const OutroScene: React.FC = () => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
return (
Codeman
Zero-latency mobile input
npm i xterm-zerolag-input
);
};
// ─── Typing demo scene ───
const TypingDemo: React.FC = () => {
const frame = useCurrentFrame();
const laggyChars = TYPING_SCHEDULE.filter((ev) => frame >= ev.frame + ev.lagDelay).map((ev) => ev.char);
const laggyTyped = laggyChars.join('');
const zerolagOverlay = TYPING_SCHEDULE.filter((ev) => frame >= ev.frame).map((ev) => ({
char: ev.char,
confirmed: frame >= ev.frame + CONFIRM_DELAY,
}));
let activeKey: string | undefined;
let pressAge = 99;
for (let i = TYPING_SCHEDULE.length - 1; i >= 0; i--) {
const ev = TYPING_SCHEDULE[i];
if (frame >= ev.frame && frame < ev.frame + 5) {
activeKey = ev.char;
pressAge = frame - ev.frame;
break;
}
}
return (
);
};
// ─── Phones entrance ───
const PanelsEntrance: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({ frame, fps, config: { damping: 15, stiffness: 80 } });
const opacity = interpolate(frame, [0, 10], [0, 1], { extrapolateRight: 'clamp' });
return (
);
};
// ─── Main composition ───
export const ZerolagDemo: React.FC = () => {
return (
);
};