import React from "react"; import { AbsoluteFill, Interactive, interpolate, useCurrentFrame, } from "remotion"; import { COLORS, monoFont, SNAP } from "../theme"; /** * Scene 4 — The AI Reveal (8.2–11.0s) * ------------------------------------------------------------ * The narrative turn: S1–S3 were the problem and the promise. * Now the screen goes dark and the experience is revealed — * softly, in a pool of light, line by line, like a terminal * coming alive. This is the first "AI" mention (~9s, per the * late-reveal direction). * * Your AI tutor. * Built for CBSE. * Grounded in your textbook. * * Strictly monochrome. The soft light is made of blurred white * ellipses (no gradients / no glassmorphism). */ export const THE_AI_REVEAL_FRAMES = 168; // 2.8s @ 60fps type Line = { text: string; start: number; step: number; size: number; y: number }; const LINES: Line[] = [ { text: "Your AI tutor.", start: 24, step: 2.1, size: 96, y: 700 }, { text: "Built for CBSE.", start: 78, step: 2.0, size: 72, y: 952 }, { text: "Grounded in your textbook.", start: 116, step: 1.5, size: 64, y: 1180 }, ]; const X0 = 150; // left margin (gap between caret-block and glyphs) const monoAdvance = 0.6; // JetBrains Mono advance ratio (~0.6em) // ── Soft spotlight pool (blurred white ellipses, no gradients) ── const SpotlightLayer: React.FC<{ w: number; h: number; blur: number; opacity: number; breathe: number; }> = ({ w, h, blur, opacity, breathe }) => (
); // ── Terminal caret (blinks after the last typed line) ───────── const Caret: React.FC<{ x: number; y: number; size: number; blink: boolean }> = ({ x, y, size, blink, }) => { const frame = useCurrentFrame(); const on = !blink || (frame % 40) < 24; return (
); }; export const AiReveal: React.FC = () => { const frame = useCurrentFrame(); // Spotlight breathing + ambient drift const breathe = Math.sin((frame / 60) * (2 * Math.PI) / 9); const driftX = Math.sin((frame / 60) * (2 * Math.PI) / 13) * 7; const driftY = Math.cos((frame / 60) * (2 * Math.PI) / 17) * 5; const push = interpolate(frame, [0, 120], [1, 1.03], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: SNAP, }); // White out at the end → clean seam into Scene 5 (white UI) const out = interpolate(frame, [158, 166], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: SNAP, }); // Active line (the one most recently typed) let active: Line | null = null; for (const l of LINES) { if (frame >= l.start) active = l; } const activeCount = active ? Math.min(active.text.length, Math.max(0, Math.floor((frame - active.start) / active.step) + 1)) : 0; const activeDone = active ? activeCount >= active.text.length : false; const caretX = active ? X0 + activeCount * active.size * monoAdvance : X0; const caretY = active ? active.y : 700; return ( {/* Spotlight pool */} {/* Dust motes in the light */} {[ { x: 300, y: 1180, r: 7, d: 90 }, { x: 760, y: 940, r: 5, d: 120 }, { x: 420, y: 1330, r: 4, d: 150 }, ].map((m, i) => { const rise = ((frame + m.d) % 220) / 220; return (
); })} {/* Typewriter lines */} {LINES.map((line, li) => { const visible = Math.min( line.text.length, Math.max(0, Math.floor((frame - line.start) / line.step) + 1), ); return ( {line.text.split("").map((ch, i) => ( {ch === " " ? "\u00A0" : ch} ))} ); })} {/* White-out exit */} ); };