padhle - post migration

This commit is contained in:
2026-09-15 04:08:55 -04:00
parent 534b51b41b
commit b2d8109019
509 changed files with 147378 additions and 303 deletions
+7
View File
@@ -0,0 +1,7 @@
node_modules
dist
.DS_Store
.env
# Ignore the output video from Git but not videos you import into src/.
out
+5
View File
@@ -0,0 +1,5 @@
{
"useTabs": false,
"bracketSpacing": true,
"tabWidth": 2
}
+54
View File
@@ -0,0 +1,54 @@
# Remotion video
<p align="center">
<a href="https://github.com/remotion-dev/logo">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/remotion-dev/logo/raw/main/animated-logo-banner-dark.apng">
<img alt="Animated Remotion Logo" src="https://github.com/remotion-dev/logo/raw/main/animated-logo-banner-light.gif">
</picture>
</a>
</p>
Welcome to your Remotion project!
## Commands
**Install Dependencies**
```console
npm i
```
**Start Preview**
```console
npm run dev
```
**Render video**
```console
npx remotion render
```
**Upgrade Remotion**
```console
npx remotion upgrade
```
## Docs
Get started with Remotion by reading the [fundamentals page](https://www.remotion.dev/docs/the-fundamentals).
## Help
We provide help on our [Discord server](https://discord.gg/6VzzNDwUwV).
## Issues
Found an issue with Remotion? [File an issue here](https://github.com/remotion-dev/remotion/issues/new).
## License
Note that for some entities a company license is needed. [Read the terms here](https://github.com/remotion-dev/remotion/blob/main/LICENSE.md).
+3
View File
@@ -0,0 +1,3 @@
import { config } from "@remotion/eslint-config-flat";
export default config;
+5880
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
{
"name": "my-video",
"version": "1.0.0",
"description": "My Remotion video",
"repository": {},
"license": "UNLICENSED",
"private": true,
"dependencies": {
"@remotion/cli": "4.0.522",
"@remotion/google-fonts": "4.0.522",
"@remotion/media": "4.0.522",
"@remotion/tailwind-v4": "4.0.522",
"@remotion/transitions": "4.0.522",
"react": "19.2.3",
"react-dom": "19.2.3",
"remotion": "4.0.522",
"tailwindcss": "4.0.0"
},
"devDependencies": {
"@remotion/eslint-config-flat": "4.0.522",
"@types/react": "19.2.7",
"@types/web": "0.0.166",
"eslint": "9.19.0",
"prettier": "3.8.1",
"typescript": "5.9.3"
},
"scripts": {
"dev": "remotion studio",
"build": "remotion bundle",
"upgrade": "remotion upgrade",
"lint": "eslint src && tsc"
},
"sideEffects": [
"*.css"
],
"allowScripts": {
"esbuild@0.28.1": true
}
}
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
/**
* Note: When using the Node.JS APIs, the config file
* doesn't apply. Instead, pass options directly to the APIs.
*
* All configuration options: https://remotion.dev/docs/config
*/
import { Config } from "@remotion/cli/config";
import { enableTailwind } from '@remotion/tailwind-v4';
Config.setRspack(true);
Config.setVideoImageFormat("jpeg");
Config.setOverwriteOutput(true);
Config.overrideBundlerConfig(enableTailwind);
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

+2
View File
@@ -0,0 +1,2 @@
// Placeholder — all compositions are registered in Root.tsx.
export {};
+87
View File
@@ -0,0 +1,87 @@
import React from "react";
import {
AbsoluteFill,
interpolate,
staticFile,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { Audio } from "@remotion/media";
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { Intro, INTRO_FRAMES } from "./scenes/Intro";
import { Selector, SELECT_FRAMES } from "./scenes/Selector";
import { Chat, CHAT_FRAMES } from "./scenes/Chat";
import { Grounded, GROUNDED_FRAMES } from "./scenes/Grounded";
import { CTA, CTA_FRAMES } from "./scenes/CTA";
const TRANSITION = 18;
export const MAIN_FRAMES =
INTRO_FRAMES +
SELECT_FRAMES +
CHAT_FRAMES +
GROUNDED_FRAMES +
CTA_FRAMES -
4 * TRANSITION;
const Music: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames, fps } = useVideoConfig();
const fadeOut = interpolate(
frame,
[durationInFrames - 2 * fps, durationInFrames - 0.4 * fps],
[1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<Audio
src={staticFile("padhle-music.mp3")}
volume={(f) =>
interpolate(f, [0, 1 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}) * fadeOut
}
/>
);
};
export const MainVideo: React.FC = () => (
<AbsoluteFill style={{ backgroundColor: "#000" }}>
<Music />
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={INTRO_FRAMES} name="Intro">
<Intro />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: TRANSITION })}
/>
<TransitionSeries.Sequence durationInFrames={SELECT_FRAMES} name="Selector">
<Selector />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: TRANSITION })}
/>
<TransitionSeries.Sequence durationInFrames={CHAT_FRAMES} name="Chat">
<Chat />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: TRANSITION })}
/>
<TransitionSeries.Sequence durationInFrames={GROUNDED_FRAMES} name="Grounded">
<Grounded />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: TRANSITION })}
/>
<TransitionSeries.Sequence durationInFrames={CTA_FRAMES} name="CTA">
<CTA />
</TransitionSeries.Sequence>
</TransitionSeries>
</AbsoluteFill>
);
+65
View File
@@ -0,0 +1,65 @@
import React from "react";
import { Composition, Folder } from "remotion";
import "./index.css";
import { MainVideo, MAIN_FRAMES } from "./MainVideo";
import { Intro, INTRO_FRAMES } from "./scenes/Intro";
import { Selector, SELECT_FRAMES } from "./scenes/Selector";
import { Chat, CHAT_FRAMES } from "./scenes/Chat";
import { Grounded, GROUNDED_FRAMES } from "./scenes/Grounded";
import { CTA, CTA_FRAMES } from "./scenes/CTA";
import { WIDTH, HEIGHT, FPS } from "./theme";
export const RemotionRoot: React.FC = () => (
<>
<Folder name="Padhle-Demo">
<Composition
id="Intro"
component={Intro}
durationInFrames={INTRO_FRAMES}
fps={FPS}
width={WIDTH}
height={HEIGHT}
/>
<Composition
id="Selector"
component={Selector}
durationInFrames={SELECT_FRAMES}
fps={FPS}
width={WIDTH}
height={HEIGHT}
/>
<Composition
id="Chat"
component={Chat}
durationInFrames={CHAT_FRAMES}
fps={FPS}
width={WIDTH}
height={HEIGHT}
/>
<Composition
id="Grounded"
component={Grounded}
durationInFrames={GROUNDED_FRAMES}
fps={FPS}
width={WIDTH}
height={HEIGHT}
/>
<Composition
id="CTA"
component={CTA}
durationInFrames={CTA_FRAMES}
fps={FPS}
width={WIDTH}
height={HEIGHT}
/>
</Folder>
<Composition
id="Padhle"
component={MainVideo}
durationInFrames={MAIN_FRAMES}
fps={FPS}
width={WIDTH}
height={HEIGHT}
/>
</>
);
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+4
View File
@@ -0,0 +1,4 @@
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);
+308
View File
@@ -0,0 +1,308 @@
import React from "react";
import { interpolate, useCurrentFrame } from "remotion";
import { COLORS, headingFont, bodyFont, SNAP } from "./theme";
// ── Brand bolt + wordmark ──────────────────────────────────────────────
export const Bolt: React.FC<{ size?: number; color?: string }> = ({
size = 240,
color = COLORS.accent,
}) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<path d="M13.2 1.8 3.6 13.6h6.6l-1.2 8.6L18.6 10h-6.9l1.5-8.2Z" fill={color} />
</svg>
);
// Small icon chip (Material-ish glyph drawn as text) — mirrors selector icons.
export const LogoMark: React.FC<{ size?: number }> = ({ size = 64 }) => {
return (
<div
style={{
width: size,
height: size,
borderRadius: size * 0.28,
background: COLORS.accent,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div
style={{
width: size * 0.42,
height: size * 0.66,
background: COLORS.white,
clipPath:
"polygon(55% 0, 8% 55%, 45% 55%, 42% 100%, 90% 42%, 55% 42%)",
}}
/>
</div>
);
};
// ── TopNav pill (real padhle header pill) ──────────────────────────────
export const Pill: React.FC<{
label: string;
active?: boolean;
from?: number;
scale?: number;
fps?: number;
}> = ({ label, active = false, from = 0, scale = 1, fps = 30 }) => {
const frame = useCurrentFrame();
const p = interpolate(frame, [from, from + 7], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
return (
<div
style={{
fontFamily: headingFont,
fontSize: 34 * scale,
fontWeight: 700,
color: active ? COLORS.white : COLORS.accent,
whiteSpace: "nowrap",
background: active ? COLORS.black : COLORS.white,
border: `${active ? 0 : 3}px solid ${COLORS.accent}`,
borderRadius: 999,
padding: `${12 * scale}px ${26 * scale}px`,
display: "flex",
alignItems: "center",
gap: 8,
opacity: p,
translate: `0 ${(1 - p) * 30}px`,
}}
>
<span
style={{
width: 8 * scale,
height: 8 * scale,
borderRadius: "50%",
background: active ? COLORS.white : COLORS.accent,
}}
/>
{label}
</div>
);
};
// ── Selector option card (real SelectorFlow card) ─────────────────────
export const SelectCard: React.FC<{
icon?: string | null;
label: string;
selected?: boolean;
from?: number;
index?: number;
}> = ({ icon, label, selected = false, from = 0, index = 0 }) => {
const frame = useCurrentFrame();
const p = interpolate(frame, [from, from + 6], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
const scaleIn = interpolate(frame, [from, from + 5], [0.5, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
output: "perceptual-scale",
});
const lift = interpolate(frame, [from, from + 6], [index * 0.02, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 10,
padding: "26px 16px",
border: `3px solid ${selected ? COLORS.black : "#e5e5e7"}`,
borderRadius: 18,
background: selected ? COLORS.black : COLORS.white,
color: selected ? COLORS.white : COLORS.ink,
opacity: p,
scale: scaleIn,
translate: `0 ${(1 - p) * 36}px`,
boxShadow: lift * 8 >= 1 ? "0px 12px 24px rgba(17,24,39,0.18)" : "none",
}}
>
{icon && (
<span style={{ fontSize: 34, lineHeight: 1, color: selected ? COLORS.white : COLORS.accent }}>
{icon}
</span>
)}
<span
style={{
fontFamily: bodyFont,
fontSize: 22,
fontWeight: 600,
textAlign: "center",
}}
>
{label}
</span>
</div>
);
};
// ── Chat bubble (real padhle Message) ─────────────────────────────────
export const MsgBubble: React.FC<{
text: string;
user?: boolean;
from?: number;
w?: string;
}> = ({ text, user = false, from = 0, w = "86%" }) => {
const frame = useCurrentFrame();
const p = interpolate(frame, [from, from + 7], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
const userStyle = {
fontFamily: bodyFont,
fontSize: 30,
fontWeight: 600,
color: COLORS.white,
background: COLORS.accent,
borderRadius: 24,
padding: "22px 30px",
maxWidth: w,
opacity: p,
translate: `0 ${(1 - p) * 30}px`,
};
const asstStyle = {
fontFamily: bodyFont,
fontSize: 30,
fontWeight: 500,
color: COLORS.ink,
background: COLORS.white,
border: "3px solid #e5e5e7",
borderRadius: 24,
padding: "22px 30px",
maxWidth: w,
opacity: p,
translate: `0 ${(1 - p) * 30}px`,
};
return (
<div
style={{
...(user ? userStyle : asstStyle),
}}
>
{text}
</div>
);
};
// ── KaTeX-style math block (rendered as styled text) ──────────────────
export const MathBlock: React.FC<{
tex: React.ReactNode;
from?: number;
}> = ({ tex, from = 0 }) => {
const frame = useCurrentFrame();
const p = interpolate(frame, [from, from + 7], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
return (
<div
style={{
fontSize: 44,
fontStyle: "italic",
color: COLORS.ink,
textAlign: "center",
background: COLORS.surface,
border: "3px solid #e5e5e7",
borderRadius: 18,
padding: "22px 30px",
opacity: p,
}}
>
{tex}
</div>
);
};
// ── Caption bar (bottom, safe area) ───────────────────────────────────
export const CapBar: React.FC<{
caption: string;
from?: number;
k?: number;
align?: "center";
}> = ({ caption, from = 0, k = 1 }) => {
const frame = useCurrentFrame();
const p = interpolate(frame, [from, from + 14], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
return (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: 90,
display: "flex",
justifyContent: "center",
alignItems: "center",
padding: "0 60px",
opacity: p,
translate: `0 ${(1 - p) * 24}px`,
}}
>
<div
style={{
fontFamily: headingFont,
fontSize: 40 * k,
fontWeight: 700,
color: COLORS.muted,
textAlign: "center",
background: "rgba(252,248,250,0.92)",
borderRadius: 16,
padding: "14px 26px",
boxShadow: "0 6px 18px rgba(17,24,39,0.18)",
}}
>
{caption}
</div>
</div>
);
};
// ── Sidebar-ish brand rail (optional left frame) ──────────────────────
export const BrandRail: React.FC<{ w?: number }> = ({ w = 300 }) => (
<div
style={{
width: w,
height: "100%",
background: COLORS.white,
borderRight: "3px solid #e5e5e7",
position: "absolute",
left: 0,
top: 0,
}}
>
<div style={{ display: "flex", alignItems: "center", padding: "40px 40px", gap: 18 }}>
<LogoMark size={48} />
<span style={{ fontFamily: headingFont, fontSize: 40, fontWeight: 700, color: COLORS.ink }}>
padhle
</span>
</div>
<div
style={{
fontFamily: bodyFont,
fontSize: 24,
color: COLORS.muted,
padding: "0 40px",
marginTop: 40,
lineHeight: 1.6,
}}
>
{["New Chat", "Math", "Science", "History", "English"].join("\n")}
</div>
</div>
);
+108
View File
@@ -0,0 +1,108 @@
import React from "react";
import {
AbsoluteFill,
interpolate,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { COLORS, headingFont, bodyFont, FPS, SNAP } from "../theme";
import { Bolt } from "../padhle";
export const CTA_FRAMES = 5 * FPS;
export const CTA: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const boltP = interpolate(frame, [0.15 * fps, 0.5 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
const headIn = interpolate(frame, [1.2 * fps, 1.7 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const ctaIn = interpolate(frame, [2.6 * fps, 3.2 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
name="CTA"
style={{
backgroundColor: COLORS.accent,
justifyContent: "center",
alignItems: "center",
}}
>
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
gap: 60,
paddingTop: 260,
}}
>
<div style={{ scale: 1 + (1 - ctaIn) * 0.3, opacity: 1 }}>
<Bolt size={200} color={COLORS.white} />
</div>
<div
style={{
fontFamily: headingFont,
fontSize: 110,
fontWeight: 900,
color: COLORS.white,
textAlign: "center",
scale: boltP,
}}
>
Study
<br />
with confidence.
</div>
</AbsoluteFill>
<AbsoluteFill
style={{
justifyContent: "flex-end",
alignItems: "center",
flexDirection: "column",
paddingBottom: 340,
gap: 26,
}}
>
<div
style={{
fontFamily: headingFont,
fontSize: 46,
fontWeight: 700,
color: COLORS.accent,
background: COLORS.white,
borderRadius: 999,
padding: "22px 60px",
textAlign: "center",
opacity: ctaIn,
translate: `0 ${(1 - ctaIn) * 30}px`,
}}
>
Start with padhle
</div>
<div
style={{
fontFamily: bodyFont,
fontSize: 32,
fontWeight: 500,
color: "rgba(255,255,255,0.92)",
textAlign: "center",
opacity: headIn,
}}
>
Free to start. Grades 612.
</div>
</AbsoluteFill>
</AbsoluteFill>
);
};
+155
View File
@@ -0,0 +1,155 @@
import React from "react";
import {
AbsoluteFill,
interpolate,
Sequence,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { COLORS, headingFont, FPS, SNAP } from "../theme";
import { MsgBubble, MathBlock, CapBar } from "../padhle";
export const CHAT_FRAMES = 12 * FPS;
export const Chat: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// streaming typewriter for the assistant answer
const answer =
"Photosynthesis is how plants turn sunlight into food. The simplified equation:";
const typed = Math.round(
interpolate(frame, [1.6 * fps, 6.0 * fps], [0, answer.length], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
})
);
return (
<AbsoluteFill
name="Chat"
style={{
backgroundColor: COLORS.surface,
justifyContent: "center",
alignItems: "center",
}}
>
{/* chat screen header */}
<AbsoluteFill
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 220,
background: COLORS.white,
borderBottom: "3px solid #e5e5e7",
justifyContent: "center",
alignItems: "center",
display: "flex",
flexDirection: "row",
gap: 16,
padding: "0 30px",
}}
>
<span
style={{
fontFamily: headingFont,
fontSize: 40,
fontWeight: 700,
color: COLORS.ink,
}}
>
Grade 10 · Science · Photosynthesis
</span>
</AbsoluteFill>
{/* conversation */}
<AbsoluteFill
style={{
justifyContent: "flex-start",
alignItems: "center",
flexDirection: "column",
padding: "0 70px",
}}
>
{/* user question bubble */}
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginTop: 300,
}}
>
<MsgBubble
text="Explain how photosynthesis works in simple terms"
user
from={0.5 * fps}
/>
</div>
{/* assistant streaming answer + math */}
<div
style={{
display: "flex",
justifyContent: "flex-start",
marginTop: 36,
alignItems: "flex-start",
}}
>
{/* school avatar */}
<div
style={{
width: 64,
height: 64,
borderRadius: 16,
background: COLORS.accent,
display: "flex",
alignItems: "center",
justifyContent: "center",
marginRight: 18,
opacity: interpolate(frame, [1.4 * fps, 1.7 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
}),
}}
>
<div
style={{
width: 26,
height: 40,
background: COLORS.white,
clipPath:
"polygon(55% 0, 8% 55%, 45% 55%, 42% 100%, 90% 42%, 55% 42%)",
}}
/>
</div>
<div style={{ maxWidth: "72%" }}>
<MsgBubble
text={answer.slice(0, typed)}
from={1.6 * fps}
user={false}
w="100%"
/>
<div style={{ height: 14 }} />
<MathBlock
from={6.4 * fps}
tex={
<>
6CO<sub>2</sub> + 6H<sub>2</sub>O + light {"→"} C<sub>6</sub>H
<sub>12</sub>O<sub>6</sub> + 6O<sub>2</sub>
</>
}
/>
</div>
</div>
</AbsoluteFill>
<Sequence name="CaptionChat" from={0.8 * fps}>
<CapBar caption="Ask in your own words." from={0} />
</Sequence>
<CapBar caption="Watch it break down, step by step." from={7.2 * fps} />
</AbsoluteFill>
);
};
+117
View File
@@ -0,0 +1,117 @@
import React from "react";
import {
AbsoluteFill,
interpolate,
Sequence,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { COLORS, headingFont, bodyFont, FPS, SNAP } from "../theme";
import { CapBar } from "../padhle";
export const GROUNDED_FRAMES = 9 * FPS;
export const Grounded: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const bookIn = interpolate(frame, [0.2 * fps, 0.9 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
const citeIn = interpolate(frame, [1.4 * fps, 2.1 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const lineIn = interpolate(frame, [4.2 * fps, 5.0 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
name="Grounded"
style={{
backgroundColor: COLORS.surface,
justifyContent: "center",
alignItems: "center",
padding: "0 90px",
}}
>
{/* headline */}
<div
style={{
fontFamily: headingFont,
fontSize: 72,
fontWeight: 900,
color: COLORS.ink,
textAlign: "center",
opacity: bookIn,
translate: `0 ${(1 - bookIn) * 40}px`,
}}
>
Grounded in your textbook.
</div>
{/* source card — mimics a textbook reference */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 20,
border: "3px solid #e5e5e7",
borderRadius: 22,
background: COLORS.white,
padding: "30px 34px",
marginTop: 60,
opacity: citeIn,
translate: `0 ${(1 - citeIn) * 30}px`,
width: "100%",
}}
>
<div
style={{
fontFamily: bodyFont,
fontSize: 40,
color: COLORS.ink,
width: "100%",
}}
>
<div style={{ fontSize: 30, color: COLORS.muted }}>
Answered from
</div>
<div style={{ fontWeight: 900, fontSize: 42 }}>
Grade 10 · Science
</div>
<div style={{ fontWeight: 700, fontSize: 40, color: COLORS.accent }}>
Chapter 2: Photosynthesis
</div>
</div>
</div>
{/* key line */}
<div
style={{
fontFamily: bodyFont,
fontSize: 44,
color: COLORS.ink,
fontWeight: 600,
textAlign: "center",
marginTop: 70,
opacity: lineIn,
}}
>
Answers come only from the chapter you chose.
<br />
No guesswork. No made-up facts.
</div>
<Sequence name="GroundedCaps" from={0}>
<CapBar caption="Every answer cites its source." from={2.6 * fps} />
</Sequence>
<CapBar caption="Built for board exams." from={6.5 * fps} />
</AbsoluteFill>
);
};
+107
View File
@@ -0,0 +1,107 @@
import React from "react";
import {
AbsoluteFill,
interpolate,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { COLORS, headingFont, bodyFont, FPS, SNAP } from "../theme";
import { Bolt, CapBar } from "../padhle";
export const INTRO_FRAMES = 5 * FPS;
export const Intro: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const boltIn = interpolate(frame, [0.1 * fps, 0.5 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
output: "perceptual-scale",
});
const wordIn = interpolate(frame, [0.7 * fps, 1.2 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const subIn = interpolate(frame, [1.5 * fps, 2.1 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<AbsoluteFill
name="Intro"
style={{
backgroundColor: COLORS.black,
justifyContent: "center",
alignItems: "center",
}}
>
<AbsoluteFill
style={{
background:
"radial-gradient(120% 80% at 50% 42%, rgba(255,255,255,0.16) 0%, transparent 68%)",
}}
/>
<AbsoluteFill
style={{ justifyContent: "center", alignItems: "center" }}
>
<div
style={{
scale: boltIn,
opacity: interpolate(boltIn, [0, 1], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}),
}}
>
<Bolt size={360} />
</div>
</AbsoluteFill>
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
paddingTop: 520,
}}
>
<div
style={{
fontFamily: headingFont,
fontSize: 130,
fontWeight: 900,
color: COLORS.white,
opacity: wordIn,
translate: `0 ${(1 - wordIn) * 40}px`,
}}
>
padhle
</div>
</AbsoluteFill>
<AbsoluteFill
style={{
justifyContent: "flex-end",
alignItems: "center",
paddingBottom: 300,
}}
>
<div
style={{
fontFamily: bodyFont,
fontSize: 46,
fontWeight: 600,
color: "rgba(255,255,255,0.92)",
maxWidth: "80%",
textAlign: "center",
opacity: subIn,
}}
>
Your AI tutor, grounded in your actual textbook.
</div>
</AbsoluteFill>
<CapBar caption="Bring your syllabus. Ask anything." from={3.4 * fps} />
</AbsoluteFill>
);
};
+207
View File
@@ -0,0 +1,207 @@
import React from "react";
import {
AbsoluteFill,
interpolate,
Sequence,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { COLORS, headingFont, bodyFont, FPS, SNAP } from "../theme";
import { Pill, SelectCard, CapBar, LogoMark } from "../padhle";
export const SELECT_FRAMES = 12 * FPS;
const TITLE = {
grade: "Choose your standard",
subject: "Pick a subject",
chapter: "Choose your chapter",
};
const SUBT = {
grade: "Grade 6 12",
subject: "What are we studying?",
chapter: "Subject-aware chapters",
};
const GRADES: Item[] = [
{ id: "grade-6", label: "Grade 6", icon: "6" },
{ id: "grade-7", label: "Grade 7", icon: "7" },
{ id: "grade-8", label: "Grade 8", icon: "8" },
{ id: "grade-9", label: "Grade 9", icon: "9" },
{ id: "grade-10", label: "Grade 10", icon: "10" },
{ id: "grade-11", label: "Grade 11", icon: "11" },
{ id: "grade-12", label: "Grade 12", icon: "12" },
];
const SUBJECTS: Item[] = [
{ id: "math", label: "Mathematics" },
{ id: "science", label: "Science" },
{ id: "history", label: "History" },
{ id: "language", label: "Language" },
{ id: "english", label: "English" },
{ id: "geography", label: "Geography" },
];
const SCIENCE_CH = [
{ id: "ch-1", label: "Ch 1: Nutrition in Plants" },
{ id: "ch-2", label: "Ch 2: Photosynthesis" },
{ id: "ch-3", label: "Ch 3: Human Physiology" },
{ id: "ch-4", label: "Ch 4: Chemistry Basics" },
{ id: "ch-5", label: "Ch 5: Motion & Force" },
{ id: "ch-6", label: "Ch 6: Electricity" },
].map((c) => ({ ...c, icon: null }));
type Item = { id: string; label: string; icon?: string | null };
const Grid: React.FC<{ step: string; at: number }> = ({ step, at }) => {
const frame = useCurrentFrame();
const p = interpolate(frame, [at, at + 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: SNAP,
});
const items: Item[] =
step === "grade" ? GRADES : step === "subject" ? SUBJECTS : SCIENCE_CH;
return (
<div
style={{
fontFamily: headingFont,
fontWeight: 800,
color: COLORS.ink,
fontSize: 52,
textAlign: "center",
opacity: p,
translate: `0 ${(1 - p) * 40}px`,
}}
>
<div
style={{
fontFamily: headingFont,
fontSize: 52,
fontWeight: 800,
color: COLORS.ink,
textAlign: "center",
}}
>
{TITLE[step as keyof typeof TITLE]}
</div>
<div
style={{
fontFamily: bodyFont,
fontSize: 30,
color: COLORS.muted,
textAlign: "center",
marginTop: 8,
}}
>
{SUBT[step as keyof typeof SUBT]}
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: 20,
marginTop: 36,
padding: "0 20px",
}}
>
{items.map((it, i) => (
<SelectCard
key={it.id}
icon={it.icon}
label={it.label}
selected={false}
from={at + 8 + i * 1.5}
index={i}
/>
))}
</div>
</div>
);
};
export const Selector: React.FC = () => {
const { fps } = useVideoConfig();
return (
<AbsoluteFill
name="Selector"
style={{
backgroundColor: COLORS.surface,
justifyContent: "center",
alignItems: "center",
}}
>
{/* app frame: topnav pills */}
<AbsoluteFill
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 300,
background: COLORS.white,
borderBottom: "3px solid #e5e5e7",
justifyContent: "center",
alignItems: "center",
display: "flex",
flexDirection: "row",
gap: 24,
padding: "0 30px",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<LogoMark size={56} />
<span
style={{
fontFamily: headingFont,
fontSize: 44,
fontWeight: 700,
color: COLORS.ink,
}}
>
padhle
</span>
</div>
<div style={{ width: 24 }} />
<Pill label="Grade 10" from={0.9 * fps} active={false} />
<Pill
label="Science"
from={5.3 * fps}
active={false}
/>
<Pill
label="Chapter 2"
from={9.1 * fps}
active={true}
/>
</AbsoluteFill>
{/* selector grid per step — each window exclusive */}
<Sequence
from={1.1 * fps}
durationInFrames={4.5 * fps - 1.1 * fps}
layout="none"
>
<Grid step="grade" at={0.15 * fps} />
<CapBar caption="Start with your grade — 6 to 12." from={2.0 * fps} />
</Sequence>
<Sequence
from={4.7 * fps}
durationInFrames={8.5 * fps - 4.7 * fps}
layout="none"
>
<Grid step="subject" at={0} />
<CapBar caption="Then your subject…" from={0.5 * fps} />
</Sequence>
<Sequence
from={8.7 * fps}
durationInFrames={12.0 * fps - 8.7 * fps}
layout="none"
>
<Grid step="chapter" at={0} />
<CapBar caption="Chapters update for your subject." from={0.5 * fps} />
</Sequence>
</AbsoluteFill>
);
};
+31
View File
@@ -0,0 +1,31 @@
import { loadFont as loadMontserrat } from "@remotion/google-fonts/Montserrat";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
import { Easing } from "remotion";
export const { fontFamily: headingFont } = loadMontserrat("normal", {
weights: ["700", "900"],
subsets: ["latin"],
});
export const { fontFamily: bodyFont } = loadInter("normal", {
weights: ["400", "500", "600", "700"],
subsets: ["latin"],
});
// Rapid, punchy entrance — short duration + slight overshoot for staccato energy.
export const SNAP = Easing.bezier(0.05, 1.2, 0.25, 1);
export const HIT = Easing.out(Easing.back(1.7));
export const COLORS = {
accent: "#000000",
black: "#000000",
white: "#ffffff",
surface: "#fafafa",
ink: "#0a0a0a",
muted: "#6b6b6b",
line: "#e5e5e5",
};
export const FPS = 30;
export const WIDTH = 1080;
export const HEIGHT = 1920;
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "Preserve",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"lib": ["es2015"],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true
},
"exclude": ["remotion.config.ts"]
}