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

250 lines
8.7 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 {
AppCursor,
Ripple,
TopBar,
} from "../components/ui";
import { COLORS, bodyFont, headingFont, HIT, SNAP } from "../theme";
/**
* Scene 5 — Choosing Context (10.614.6s)
* ------------------------------------------------------------
* First UI moment. The app, in close-up: top bar drops in, the
* context selector card unfolds, and the oversized cursor walks
* three clicks — Grade 10 → Science → Chapter 4 · Photosynthesis.
* Each click: ripple ring + pill flips black + micro-bounce +
* subtle camera push. Then a confirmation row locks the context.
*/
export const CHOOSING_FRAMES = 240; // 4.0s @ 60fps
type StepType = "grade" | "subject" | "chapter";
const GRADE_OPTIONS = ["Grade 6", "Grade 7", "Grade 8", "Grade 9", "Grade 10", "Grade 11", "Grade 12"];
const SUBJECT_OPTIONS = ["Mathematics", "Science", "History", "Language Arts", "English", "Geography"];
const CHAPTER_OPTIONS = ["Ch 1 · Nutrition in Plants", "Ch 2 · Photosynthesis", "Ch 3 · Respiration", "Ch 4 · Human Body", "Ch 5 · Food Chains", "Ch 6 · Adaptations"];
// beat frames
const CARD_IN = 10;
const CLICK1 = 78;
const CLICK2 = 126;
const CLICK3 = 174;
const CONFIRM = 214;
export const ChoosingContext: React.FC = () => {
const frame = useCurrentFrame();
const step: StepType =
frame < CLICK1 ? "grade" : frame < CLICK2 ? "subject" : frame < CLICK3 ? "chapter" : "chapter";
const options =
step === "grade" ? GRADE_OPTIONS : step === "subject" ? SUBJECT_OPTIONS : CHAPTER_OPTIONS;
const pickIdx = step === "grade" ? 4 : step === "subject" ? 1 : 1; // Grade 10 / Science / Ch2
// camera push-in per click (pulses)
const push =
1 +
interpolate(frame, [CLICK1, CLICK1 + 10], [0, 0.016], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: SNAP }) +
interpolate(frame, [CLICK2, CLICK2 + 10], [0, 0.02], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: SNAP }) +
interpolate(frame, [CLICK3, CLICK3 + 10], [0, 0.024], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: SNAP });
// card entrance
const cardIn = interpolate(frame, [CARD_IN, CARD_IN + 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
const cardY = 560 + (1 - cardIn) * 46;
// Simple cursor path: move to target, click, move to next target
const curClick = (from: number, to: number, at: number) => {
const t = interpolate(frame, [at, at + 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: HIT,
});
return from + (to - from) * t;
};
const press = (at: number) =>
interpolate(frame, [at, at + 6], [1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
// cursor position timeline
let cx = 640,
cy = 760,
cp = 0;
if (frame < 56) {
cx = 1180 - (1180 - 476) * interpolate(frame, [24, 56], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: HIT });
cy = 900;
} else if (frame < CLICK1) {
cx = curClick(476, 476, 56);
cy = 732;
} else if (frame < CLICK1 + 8) {
cx = 470;
cy = 728;
cp = press(CLICK1);
} else if (frame < CLICK2 - 12) {
cx = curClick(470, 460, CLICK1 + 8);
cy = 728 + (880 - 728) * interpolate(frame, [CLICK1 + 8, CLICK2 - 12], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: HIT });
} else if (frame < CLICK2 + 8) {
cx = 460;
cy = 876;
cp = press(CLICK2);
} else if (frame < CLICK3 - 14) {
cx = curClick(460, 480, CLICK2 + 8);
cy = 876 + (1030 - 876) * interpolate(frame, [CLICK2 + 8, CLICK3 - 14], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: HIT });
} else if (frame < CLICK3 + 10) {
cx = 480;
cy = 1026;
cp = press(CLICK3);
} else {
cx = curClick(480, 300, CLICK3 + 10);
cy = 1026 + (620 - 1026) * interpolate(frame, [CLICK3 + 10, CLICK3 + 26], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: HIT });
}
const cursorOpacity = interpolate(frame, [24, 40], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
// ripple activations
const ripples = [
<Ripple at={CLICK1} x={470} y={745} />,
<Ripple at={CLICK2} x={460} y={895} />,
<Ripple at={CLICK3} x={480} y={1045} />,
];
// confirm row
const confirmIn = interpolate(frame, [CONFIRM, CONFIRM + 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
return (
<AbsoluteFill name="Scene" style={{ backgroundColor: COLORS.background }}>
<Interactive.Div name="CameraPush" style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%", scale: String(push.toFixed(4)) }}>
<TopBar
from={6}
pills={[
{ label: "Grade 10", active: frame >= CLICK1 },
{ label: "Science", active: frame >= CLICK2 },
{ label: "Ch 2", active: frame >= CLICK3 },
]}
/>
{/* selector card */}
<div
style={{
position: "absolute",
left: 540 - 410,
top: cardY,
width: 820,
height: 880,
backgroundColor: COLORS.surface,
border: `2.5px solid ${COLORS.outlineVariant}`,
borderRadius: 24,
boxShadow: "0 4px 18px rgba(17,24,39,0.10)",
opacity: cardIn,
padding: "40px 48px",
}}
>
<div
style={{
fontFamily: headingFont,
fontSize: 44,
fontWeight: 800,
color: COLORS.onSurface,
}}
>
{step === "grade" ? "Choose your standard" : step === "subject" ? "Choose your subject" : "Choose your chapter"}
</div>
<div
style={{
fontFamily: bodyFont,
fontSize: 26,
color: COLORS.onSurfaceVariant,
marginTop: 10,
}}
>
{step === "grade"
? "For CBSE · Grades 612"
: step === "subject"
? "For Grade 10"
: "Grounded in your textbook"}
</div>
{/* options list (3-col grid for grades, list for subject/chapter) */}
<div style={{ marginTop: 40, display: "flex", flexDirection: "column", gap: 18 }}>
{options.map((opt, i) => {
const isPick = i === pickIdx;
const lit = isPick && frame >= (step === "grade" ? CLICK1 : step === "subject" ? CLICK2 : CLICK3);
return (
<div
key={`${step}-${opt}`}
style={{
display: "flex",
alignItems: "center",
height: 84,
fontFamily: bodyFont,
fontSize: 30,
fontWeight: 600,
color: lit ? COLORS.onPrimary : COLORS.onSurface,
backgroundColor: lit ? COLORS.primary : COLORS.surface,
border: lit ? "none" : `2.5px solid ${COLORS.outlineVariant}`,
borderRadius: 16,
padding: "0 26px",
scale: String((isPick ? 1 + 0.02 : 1).toFixed(4)),
}}
>
<span style={{ width: 16, height: 16, borderRadius: "50%", border: `2.5px solid ${lit ? COLORS.onPrimary : COLORS.outline}`, marginRight: 18 }} />
{opt}
</div>
);
})}
</div>
</div>
{/* cursor + ripples */}
<AppCursor x={cx} y={cy} opacity={cursorOpacity} press={cp} />
{ripples}
{/* confirm bar */}
{frame >= CONFIRM && (
<div
style={{
position: "absolute",
left: 540,
top: 1620,
translate: "-50% 0",
opacity: confirmIn,
scale: String((1.06 - 0.06 * confirmIn).toFixed(4)),
fontFamily: bodyFont,
fontSize: 30,
fontWeight: 600,
color: COLORS.onSurface,
backgroundColor: COLORS.surface,
border: `2.5px solid ${COLORS.primary}`,
borderRadius: 18,
padding: "18px 34px",
display: "flex",
alignItems: "center",
gap: 16,
}}
>
<span style={{ fontSize: 30, color: COLORS.primary }}></span>
Grade 10 · Science · Chapter 2 · Photosynthesis
</div>
)}
</Interactive.Div>
</AbsoluteFill>
);
};
// (Ripple is imported from ../components/ui)