- Remove Scripts/ directory (scraper no longer needed) - Add themed background documentation to CLAUDE.md - Add .gitignore for marketing-videos to prevent node_modules tracking Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
220 lines
6.0 KiB
TypeScript
220 lines
6.0 KiB
TypeScript
import React from "react";
|
|
import {
|
|
AbsoluteFill,
|
|
useCurrentFrame,
|
|
useVideoConfig,
|
|
spring,
|
|
interpolate,
|
|
} from "remotion";
|
|
import { theme } from "../../components/shared/theme";
|
|
|
|
type Message = {
|
|
text: string;
|
|
sender: string;
|
|
color: string;
|
|
side: "left" | "right";
|
|
};
|
|
|
|
const messages: Message[] = [
|
|
{ text: "Lakers game?", sender: "Mike", color: "#552583", side: "left" },
|
|
{ text: "Dodgers?", sender: "Sarah", color: "#005A9C", side: "right" },
|
|
{ text: "Both??", sender: "Jake", color: "#007AFF", side: "left" },
|
|
{ text: "When though", sender: "Sarah", color: "#005A9C", side: "right" },
|
|
{ text: "idk June maybe", sender: "Mike", color: "#552583", side: "left" },
|
|
{ text: "too many options 😩", sender: "Jake", color: "#007AFF", side: "right" },
|
|
];
|
|
|
|
export const ChatBubbles: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps, width } = useVideoConfig();
|
|
|
|
return (
|
|
<AbsoluteFill
|
|
style={{
|
|
background: theme.colors.background,
|
|
padding: 40,
|
|
paddingTop: 100,
|
|
}}
|
|
>
|
|
{/* Header - looks like a group chat */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 16,
|
|
marginBottom: 40,
|
|
paddingBottom: 20,
|
|
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
|
}}
|
|
>
|
|
{/* Avatar stack */}
|
|
<div style={{ display: "flex", marginLeft: 20 }}>
|
|
{["#FF6B6B", "#4ECDC4", "#45B7D1"].map((color, i) => (
|
|
<div
|
|
key={i}
|
|
style={{
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: "50%",
|
|
background: color,
|
|
border: `3px solid ${theme.colors.background}`,
|
|
marginLeft: i > 0 ? -15 : 0,
|
|
zIndex: 3 - i,
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div>
|
|
<div
|
|
style={{
|
|
fontFamily: theme.fonts.display,
|
|
fontSize: 22,
|
|
fontWeight: 600,
|
|
color: theme.colors.text,
|
|
}}
|
|
>
|
|
LA Trip Planning
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontFamily: theme.fonts.text,
|
|
fontSize: 16,
|
|
color: theme.colors.textMuted,
|
|
}}
|
|
>
|
|
3 people
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Messages */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 12,
|
|
}}
|
|
>
|
|
{messages.map((message, index) => {
|
|
// Stagger each message
|
|
const messageDelay = index * 6;
|
|
const localFrame = frame - messageDelay;
|
|
|
|
const entranceProgress = spring({
|
|
frame: localFrame,
|
|
fps,
|
|
config: theme.animation.snappy,
|
|
});
|
|
|
|
const opacity = interpolate(
|
|
localFrame,
|
|
[0, 5],
|
|
[0, 1],
|
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
);
|
|
|
|
const translateX = message.side === "left"
|
|
? interpolate(entranceProgress, [0, 1], [-50, 0])
|
|
: interpolate(entranceProgress, [0, 1], [50, 0]);
|
|
|
|
const scale = interpolate(entranceProgress, [0, 1], [0.8, 1]);
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: message.side === "left" ? "flex-start" : "flex-end",
|
|
opacity,
|
|
transform: `translateX(${translateX}px) scale(${scale})`,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
maxWidth: "75%",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: message.side === "left" ? "flex-start" : "flex-end",
|
|
}}
|
|
>
|
|
{/* Sender name */}
|
|
<span
|
|
style={{
|
|
fontFamily: theme.fonts.text,
|
|
fontSize: 14,
|
|
color: theme.colors.textMuted,
|
|
marginBottom: 4,
|
|
marginLeft: message.side === "left" ? 12 : 0,
|
|
marginRight: message.side === "right" ? 12 : 0,
|
|
}}
|
|
>
|
|
{message.sender}
|
|
</span>
|
|
|
|
{/* Bubble */}
|
|
<div
|
|
style={{
|
|
background: message.side === "left" ? "#2C2C2E" : message.color,
|
|
padding: "14px 20px",
|
|
borderRadius: 22,
|
|
borderBottomLeftRadius: message.side === "left" ? 6 : 22,
|
|
borderBottomRightRadius: message.side === "right" ? 6 : 22,
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontFamily: theme.fonts.text,
|
|
fontSize: 20,
|
|
color: theme.colors.text,
|
|
}}
|
|
>
|
|
{message.text}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
// "Stop the chaos" text overlay
|
|
export const StopTheChaos: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
const progress = spring({
|
|
frame,
|
|
fps,
|
|
config: theme.animation.smooth,
|
|
});
|
|
|
|
const opacity = interpolate(progress, [0, 1], [0, 1]);
|
|
const translateY = interpolate(progress, [0, 1], [30, 0]);
|
|
|
|
return (
|
|
<AbsoluteFill
|
|
style={{
|
|
background: theme.colors.background,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
opacity,
|
|
transform: `translateY(${translateY}px)`,
|
|
fontFamily: theme.fonts.display,
|
|
fontSize: 56,
|
|
fontWeight: 700,
|
|
color: theme.colors.text,
|
|
}}
|
|
>
|
|
Stop the chaos.
|
|
</div>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|