789 lines
19 KiB
TypeScript
789 lines
19 KiB
TypeScript
import {
|
|
AbsoluteFill,
|
|
Img,
|
|
staticFile,
|
|
useCurrentFrame,
|
|
useVideoConfig,
|
|
interpolate,
|
|
spring,
|
|
Sequence,
|
|
Easing,
|
|
} from "remotion";
|
|
|
|
// Cinematic letterbox bars
|
|
const LetterBox: React.FC = () => (
|
|
<>
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 120,
|
|
background: "black",
|
|
zIndex: 100,
|
|
}}
|
|
/>
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 120,
|
|
background: "black",
|
|
zIndex: 100,
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
|
|
// Scan lines for that heist movie feel
|
|
const ScanLines: React.FC<{ opacity?: number }> = ({ opacity = 0.1 }) => {
|
|
const frame = useCurrentFrame();
|
|
const offset = (frame * 2) % 4;
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
background: `repeating-linear-gradient(
|
|
0deg,
|
|
transparent,
|
|
transparent 2px,
|
|
rgba(0,0,0,${opacity}) 2px,
|
|
rgba(0,0,0,${opacity}) 4px
|
|
)`,
|
|
transform: `translateY(${offset}px)`,
|
|
pointerEvents: "none",
|
|
zIndex: 50,
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
// Glitch effect text
|
|
const GlitchText: React.FC<{
|
|
children: string;
|
|
style?: React.CSSProperties;
|
|
}> = ({ children, style }) => {
|
|
const frame = useCurrentFrame();
|
|
const glitchOffset = Math.sin(frame * 0.5) * 2;
|
|
const shouldGlitch = frame % 30 < 3;
|
|
|
|
return (
|
|
<div style={{ position: "relative", ...style }}>
|
|
{shouldGlitch && (
|
|
<>
|
|
<span
|
|
style={{
|
|
position: "absolute",
|
|
left: glitchOffset,
|
|
color: "#ff0000",
|
|
opacity: 0.8,
|
|
clipPath: "inset(10% 0 60% 0)",
|
|
}}
|
|
>
|
|
{children}
|
|
</span>
|
|
<span
|
|
style={{
|
|
position: "absolute",
|
|
left: -glitchOffset,
|
|
color: "#00ffff",
|
|
opacity: 0.8,
|
|
clipPath: "inset(60% 0 10% 0)",
|
|
}}
|
|
>
|
|
{children}
|
|
</span>
|
|
</>
|
|
)}
|
|
<span style={{ position: "relative" }}>{children}</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Scene 1: The Setup - "They took something from you"
|
|
const SetupScene: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
const fadeIn = interpolate(frame, [0, fps * 0.5], [0, 1], {
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
const textReveal = interpolate(frame, [fps * 0.5, fps * 2], [0, 1], {
|
|
extrapolateLeft: "clamp",
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
const typewriterLength = Math.floor(textReveal * 28);
|
|
const fullText = "They took something from you";
|
|
const displayText = fullText.slice(0, typewriterLength);
|
|
|
|
// Flicker effect
|
|
const flicker = frame % 60 < 2 ? 0.3 : 1;
|
|
|
|
return (
|
|
<AbsoluteFill
|
|
style={{
|
|
backgroundColor: "#0a0a0a",
|
|
opacity: fadeIn * flicker,
|
|
}}
|
|
>
|
|
<ScanLines opacity={0.15} />
|
|
<LetterBox />
|
|
|
|
{/* Dramatic spotlight */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
width: 800,
|
|
height: 800,
|
|
transform: "translate(-50%, -50%)",
|
|
background:
|
|
"radial-gradient(circle, rgba(255,255,255,0.05) 0%, transparent 70%)",
|
|
}}
|
|
/>
|
|
|
|
{/* Main text */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
textAlign: "center",
|
|
}}
|
|
>
|
|
<GlitchText
|
|
style={{
|
|
fontSize: 72,
|
|
fontWeight: 300,
|
|
color: "white",
|
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
letterSpacing: 8,
|
|
textTransform: "uppercase",
|
|
}}
|
|
>
|
|
{displayText}
|
|
</GlitchText>
|
|
|
|
{/* Blinking cursor */}
|
|
{typewriterLength < fullText.length && (
|
|
<span
|
|
style={{
|
|
opacity: frame % 30 < 15 ? 1 : 0,
|
|
color: "#ef4444",
|
|
fontSize: 72,
|
|
fontWeight: 300,
|
|
}}
|
|
>
|
|
_
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Bottom text */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
bottom: 180,
|
|
left: 0,
|
|
right: 0,
|
|
textAlign: "center",
|
|
opacity: interpolate(frame, [fps * 3, fps * 4], [0, 1], {
|
|
extrapolateLeft: "clamp",
|
|
extrapolateRight: "clamp",
|
|
}),
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: 24,
|
|
color: "#ef4444",
|
|
fontFamily: "monospace",
|
|
letterSpacing: 4,
|
|
}}
|
|
>
|
|
YOUR EMOTIONS
|
|
</div>
|
|
</div>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
// Scene 2: The Crew - Mood emojis as heist team
|
|
const CrewScene: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
const crew = [
|
|
{ emoji: "😊", name: "THE OPTIMIST", color: "#10b981", role: "INFILTRATION" },
|
|
{ emoji: "😤", name: "THE MUSCLE", color: "#ef4444", role: "FIREPOWER" },
|
|
{ emoji: "🤔", name: "THE BRAINS", color: "#3b82f6", role: "STRATEGY" },
|
|
{ emoji: "😌", name: "THE COOL", color: "#8b5cf6", role: "EXTRACTION" },
|
|
];
|
|
|
|
return (
|
|
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
|
<ScanLines opacity={0.1} />
|
|
<LetterBox />
|
|
|
|
{/* Title */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 180,
|
|
left: 0,
|
|
right: 0,
|
|
textAlign: "center",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: 32,
|
|
color: "#fbbf24",
|
|
fontFamily: "monospace",
|
|
letterSpacing: 8,
|
|
opacity: interpolate(frame, [0, fps * 0.5], [0, 1]),
|
|
}}
|
|
>
|
|
ASSEMBLING THE CREW
|
|
</div>
|
|
</div>
|
|
|
|
{/* Crew grid */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
display: "grid",
|
|
gridTemplateColumns: "repeat(2, 1fr)",
|
|
gap: 60,
|
|
}}
|
|
>
|
|
{crew.map((member, i) => {
|
|
const delay = i * 8;
|
|
const memberProgress = spring({
|
|
frame: frame - delay,
|
|
fps,
|
|
config: { damping: 12, stiffness: 100 },
|
|
});
|
|
|
|
return (
|
|
<div
|
|
key={member.name}
|
|
style={{
|
|
textAlign: "center",
|
|
opacity: interpolate(memberProgress, [0, 1], [0, 1]),
|
|
transform: `translateY(${interpolate(memberProgress, [0, 1], [50, 0])}px)`,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: 100,
|
|
marginBottom: 15,
|
|
filter: `drop-shadow(0 0 20px ${member.color})`,
|
|
}}
|
|
>
|
|
{member.emoji}
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: 20,
|
|
color: member.color,
|
|
fontFamily: "monospace",
|
|
fontWeight: 700,
|
|
letterSpacing: 2,
|
|
}}
|
|
>
|
|
{member.name}
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: 14,
|
|
color: "#666",
|
|
fontFamily: "monospace",
|
|
marginTop: 5,
|
|
}}
|
|
>
|
|
{member.role}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
// Scene 3: The Plan - Blueprint style
|
|
const PlanScene: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps, width, height } = useVideoConfig();
|
|
|
|
// Grid lines drawing animation
|
|
const gridProgress = interpolate(frame, [0, fps * 2], [0, 1], {
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
return (
|
|
<AbsoluteFill style={{ backgroundColor: "#0a1628" }}>
|
|
<ScanLines opacity={0.05} />
|
|
<LetterBox />
|
|
|
|
{/* Blueprint grid */}
|
|
<svg
|
|
style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%" }}
|
|
>
|
|
{/* Vertical lines */}
|
|
{[...Array(20)].map((_, i) => (
|
|
<line
|
|
key={`v-${i}`}
|
|
x1={(i * width) / 20}
|
|
y1={0}
|
|
x2={(i * width) / 20}
|
|
y2={height * gridProgress}
|
|
stroke="#1e40af"
|
|
strokeWidth={0.5}
|
|
opacity={0.3}
|
|
/>
|
|
))}
|
|
{/* Horizontal lines */}
|
|
{[...Array(30)].map((_, i) => (
|
|
<line
|
|
key={`h-${i}`}
|
|
x1={0}
|
|
y1={(i * height) / 30}
|
|
x2={width * gridProgress}
|
|
y2={(i * height) / 30}
|
|
stroke="#1e40af"
|
|
strokeWidth={0.5}
|
|
opacity={0.3}
|
|
/>
|
|
))}
|
|
</svg>
|
|
|
|
{/* Phone blueprint */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
opacity: interpolate(frame, [fps, fps * 2], [0, 1], {
|
|
extrapolateLeft: "clamp",
|
|
extrapolateRight: "clamp",
|
|
}),
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 300,
|
|
height: 600,
|
|
border: "2px solid #3b82f6",
|
|
borderRadius: 40,
|
|
position: "relative",
|
|
}}
|
|
>
|
|
{/* Target markers */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "30%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
width: 100,
|
|
height: 100,
|
|
border: "2px dashed #ef4444",
|
|
borderRadius: "50%",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
fontSize: 14,
|
|
color: "#ef4444",
|
|
fontFamily: "monospace",
|
|
}}
|
|
>
|
|
TARGET
|
|
</div>
|
|
</div>
|
|
|
|
{/* Entry point */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
bottom: 80,
|
|
left: "50%",
|
|
transform: "translateX(-50%)",
|
|
fontSize: 12,
|
|
color: "#10b981",
|
|
fontFamily: "monospace",
|
|
}}
|
|
>
|
|
ENTRY POINT
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Title */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 180,
|
|
left: 0,
|
|
right: 0,
|
|
textAlign: "center",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: 32,
|
|
color: "#3b82f6",
|
|
fontFamily: "monospace",
|
|
letterSpacing: 8,
|
|
}}
|
|
>
|
|
THE PLAN
|
|
</div>
|
|
</div>
|
|
|
|
{/* Steps */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
bottom: 200,
|
|
left: 60,
|
|
right: 60,
|
|
display: "flex",
|
|
justifyContent: "space-around",
|
|
}}
|
|
>
|
|
{["DOWNLOAD", "TAP MOOD", "REPEAT"].map((step, i) => (
|
|
<div
|
|
key={step}
|
|
style={{
|
|
opacity: interpolate(
|
|
frame,
|
|
[fps * 2 + i * 10, fps * 2.5 + i * 10],
|
|
[0, 1],
|
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
),
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
fontSize: 48,
|
|
color: "#fbbf24",
|
|
fontFamily: "monospace",
|
|
fontWeight: 700,
|
|
}}
|
|
>
|
|
{i + 1}
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: 16,
|
|
color: "#94a3b8",
|
|
fontFamily: "monospace",
|
|
}}
|
|
>
|
|
{step}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
// Scene 4: The Execution - Dramatic mood selection
|
|
const ExecutionScene: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
const moods = ["😢", "😕", "😐", "🙂", "😊"];
|
|
const selectedIndex = 4; // Great mood
|
|
|
|
// Dramatic slow reveal
|
|
const revealProgress = interpolate(frame, [0, fps * 2], [0, 1], {
|
|
extrapolateRight: "clamp",
|
|
easing: Easing.out(Easing.cubic),
|
|
});
|
|
|
|
// Finger approaching
|
|
const fingerProgress = interpolate(frame, [fps * 2, fps * 3], [0, 1], {
|
|
extrapolateLeft: "clamp",
|
|
extrapolateRight: "clamp",
|
|
});
|
|
|
|
// Selection flash
|
|
const selectFlash = frame > fps * 3 && frame < fps * 3.5;
|
|
|
|
return (
|
|
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
|
<ScanLines opacity={0.1} />
|
|
<LetterBox />
|
|
|
|
{/* Dramatic lighting */}
|
|
{selectFlash && (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
backgroundColor: "rgba(16, 185, 129, 0.3)",
|
|
zIndex: 10,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Mood buttons */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
display: "flex",
|
|
gap: 30,
|
|
}}
|
|
>
|
|
{moods.map((mood, i) => {
|
|
const isSelected = i === selectedIndex && frame > fps * 3;
|
|
const buttonProgress = interpolate(
|
|
revealProgress,
|
|
[i * 0.15, i * 0.15 + 0.3],
|
|
[0, 1],
|
|
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
);
|
|
|
|
return (
|
|
<div
|
|
key={i}
|
|
style={{
|
|
width: 120,
|
|
height: 120,
|
|
borderRadius: 30,
|
|
backgroundColor: isSelected ? "#10b981" : "rgba(255,255,255,0.1)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: 60,
|
|
opacity: buttonProgress,
|
|
transform: `scale(${isSelected ? 1.2 : 1}) translateY(${interpolate(buttonProgress, [0, 1], [50, 0])}px)`,
|
|
boxShadow: isSelected ? "0 0 60px #10b981" : "none",
|
|
transition: "all 0.3s",
|
|
}}
|
|
>
|
|
{mood}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Approaching finger/cursor */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "60%",
|
|
left: `${50 + 20 - fingerProgress * 8}%`,
|
|
fontSize: 80,
|
|
transform: `translateX(-50%) rotate(-30deg)`,
|
|
opacity: fingerProgress < 1 ? fingerProgress : 0,
|
|
}}
|
|
>
|
|
👆
|
|
</div>
|
|
|
|
{/* "ACQUIRED" text */}
|
|
{frame > fps * 3.2 && (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
bottom: 250,
|
|
left: 0,
|
|
right: 0,
|
|
textAlign: "center",
|
|
}}
|
|
>
|
|
<GlitchText
|
|
style={{
|
|
fontSize: 48,
|
|
color: "#10b981",
|
|
fontFamily: "monospace",
|
|
letterSpacing: 16,
|
|
fontWeight: 700,
|
|
}}
|
|
>
|
|
MOOD ACQUIRED
|
|
</GlitchText>
|
|
</div>
|
|
)}
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
// Scene 5: The Getaway - Success celebration
|
|
const GetawayScene: React.FC = () => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig();
|
|
|
|
const logoProgress = spring({
|
|
frame,
|
|
fps,
|
|
config: { damping: 10, stiffness: 80 },
|
|
});
|
|
|
|
// Vault door opening effect
|
|
const vaultOpen = interpolate(frame, [0, fps], [0, 1], {
|
|
extrapolateRight: "clamp",
|
|
easing: Easing.out(Easing.cubic),
|
|
});
|
|
|
|
return (
|
|
<AbsoluteFill style={{ backgroundColor: "#0a0a0a" }}>
|
|
<ScanLines opacity={0.05} />
|
|
<LetterBox />
|
|
|
|
{/* Vault doors */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
width: "50%",
|
|
height: "100%",
|
|
backgroundColor: "#1a1a1a",
|
|
transform: `translateX(${-vaultOpen * 100}%)`,
|
|
borderRight: "4px solid #333",
|
|
zIndex: 20,
|
|
}}
|
|
/>
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
right: 0,
|
|
width: "50%",
|
|
height: "100%",
|
|
backgroundColor: "#1a1a1a",
|
|
transform: `translateX(${vaultOpen * 100}%)`,
|
|
borderLeft: "4px solid #333",
|
|
zIndex: 20,
|
|
}}
|
|
/>
|
|
|
|
{/* Bright light behind vault */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: "translate(-50%, -50%)",
|
|
width: 600,
|
|
height: 600,
|
|
background: `radial-gradient(circle, rgba(251,191,36,${vaultOpen * 0.5}) 0%, transparent 70%)`,
|
|
}}
|
|
/>
|
|
|
|
{/* App icon revealed */}
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: "50%",
|
|
transform: `translate(-50%, -50%) scale(${interpolate(logoProgress, [0, 1], [0.5, 1])})`,
|
|
textAlign: "center",
|
|
opacity: vaultOpen,
|
|
}}
|
|
>
|
|
<Img
|
|
src={staticFile("app-icon.png")}
|
|
style={{
|
|
width: 200,
|
|
height: 200,
|
|
borderRadius: 200 * 0.22,
|
|
marginBottom: 40,
|
|
filter: `drop-shadow(0 0 60px rgba(251,191,36,0.8))`,
|
|
}}
|
|
/>
|
|
|
|
<div
|
|
style={{
|
|
fontSize: 80,
|
|
fontWeight: 800,
|
|
color: "white",
|
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
textShadow: "0 0 40px rgba(255,255,255,0.5)",
|
|
}}
|
|
>
|
|
Feels
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
fontSize: 28,
|
|
color: "#fbbf24",
|
|
fontFamily: "monospace",
|
|
marginTop: 20,
|
|
letterSpacing: 4,
|
|
opacity: interpolate(frame, [fps * 2, fps * 2.5], [0, 1], {
|
|
extrapolateLeft: "clamp",
|
|
extrapolateRight: "clamp",
|
|
}),
|
|
}}
|
|
>
|
|
TAKE BACK YOUR EMOTIONS
|
|
</div>
|
|
</div>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|
|
|
|
// Main composition - 25 seconds total
|
|
export const ConceptHMoodHeist: React.FC = () => {
|
|
const { fps } = useVideoConfig();
|
|
|
|
return (
|
|
<AbsoluteFill>
|
|
<Sequence from={0} durationInFrames={Math.round(5 * fps)}>
|
|
<SetupScene />
|
|
</Sequence>
|
|
|
|
<Sequence from={Math.round(5 * fps)} durationInFrames={Math.round(5 * fps)}>
|
|
<CrewScene />
|
|
</Sequence>
|
|
|
|
<Sequence from={Math.round(10 * fps)} durationInFrames={Math.round(5 * fps)}>
|
|
<PlanScene />
|
|
</Sequence>
|
|
|
|
<Sequence from={Math.round(15 * fps)} durationInFrames={Math.round(5 * fps)}>
|
|
<ExecutionScene />
|
|
</Sequence>
|
|
|
|
<Sequence from={Math.round(20 * fps)} durationInFrames={Math.round(5 * fps)}>
|
|
<GetawayScene />
|
|
</Sequence>
|
|
</AbsoluteFill>
|
|
);
|
|
};
|