Files
padhle/remotion-testing/src/scenes/AiReveal.tsx
T
2026-09-15 04:08:55 -04:00

226 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import {
AbsoluteFill,
Interactive,
interpolate,
useCurrentFrame,
} from "remotion";
import { COLORS, monoFont, SNAP } from "../theme";
/**
* Scene 4 — The AI Reveal (8.211.0s)
* ------------------------------------------------------------
* The narrative turn: S1S3 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 }) => (
<div
style={{
position: "absolute",
left: 540 - w / 2,
top: 920 - h / 2,
width: w,
height: h,
borderRadius: "50%",
backgroundColor: COLORS.white,
filter: `blur(${blur}px)`,
opacity,
scale: String((1 + 0.012 * breathe).toFixed(4)),
}}
/>
);
// ── 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 (
<div
style={{
position: "absolute",
left: x,
top: y,
width: Math.max(10, size * 0.2),
height: size * 0.86,
backgroundColor: COLORS.white,
opacity: on ? 1 : 0.06,
}}
/>
);
};
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 (
<AbsoluteFill name="Scene" style={{ backgroundColor: COLORS.black }}>
<Interactive.Div
name="Drift"
style={{
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
translate: `${driftX.toFixed(2)}px ${driftY.toFixed(2)}px`,
scale: String(push),
}}
>
{/* Spotlight pool */}
<Interactive.Div name="Spotlight">
<SpotlightLayer w={1650} h={1050} blur={120} opacity={0.05} breathe={breathe} />
<SpotlightLayer w={1180} h={700} blur={72} opacity={0.06} breathe={breathe} />
<SpotlightLayer w={800} h={440} blur={34} opacity={0.09} breathe={breathe} />
</Interactive.Div>
{/* Dust motes in the light */}
<Interactive.Div name="Dust">
{[
{ 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 (
<div
key={i}
style={{
position: "absolute",
left: m.x + Math.sin((frame + i * 40) / 30) * 30,
top: m.y - rise * 420,
width: m.r * 2,
height: m.r * 2,
borderRadius: "50%",
backgroundColor: COLORS.white,
filter: "blur(3px)",
opacity: 0.16,
}}
/>
);
})}
</Interactive.Div>
{/* Typewriter lines */}
<Interactive.Div name="Typewriter">
{LINES.map((line, li) => {
const visible = Math.min(
line.text.length,
Math.max(0, Math.floor((frame - line.start) / line.step) + 1),
);
return (
<Interactive.Div
key={li}
name={`Line-${li}`}
style={{
position: "absolute",
left: X0 + 18,
top: line.y,
whiteSpace: "nowrap",
fontFamily: monoFont,
fontSize: line.size,
fontWeight: 500,
color: COLORS.white,
letterSpacing: "0.02em",
}}
>
{line.text.split("").map((ch, i) => (
<span
key={i}
style={{
opacity: i < visible ? 1 : 0,
}}
>
{ch === " " ? "\u00A0" : ch}
</span>
))}
</Interactive.Div>
);
})}
<Caret
x={caretX}
y={caretY}
size={active?.size ?? 96}
blink={activeDone}
/>
</Interactive.Div>
</Interactive.Div>
{/* White-out exit */}
<Interactive.Div
name="RevealOut"
style={{
position: "absolute",
inset: 0,
backgroundColor: COLORS.white,
opacity: out,
}}
/>
</AbsoluteFill>
);
};