padhle - post migration
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
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 }) => (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user