Remove marketing-videos Remotion project

The standalone Remotion video project is no longer needed in this repo.
Also updates local Claude Code settings with additional tool permissions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-07 00:03:49 -06:00
parent 46d37875e5
commit e6ed766ccd
74 changed files with 6 additions and 24816 deletions

View File

@@ -1,185 +0,0 @@
import React from "react";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
Img,
staticFile,
} from "remotion";
import { theme } from "./theme";
type AppScreenshotProps = {
src?: string;
delay?: number;
scale?: number;
showDeviceFrame?: boolean;
children?: React.ReactNode;
};
export const AppScreenshot: React.FC<AppScreenshotProps> = ({
src,
delay = 0,
scale = 0.85,
showDeviceFrame = true,
children,
}) => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
const delayFrames = delay * fps;
const progress = spring({
frame: frame - delayFrames,
fps,
config: theme.animation.smooth,
});
const opacity = interpolate(
frame - delayFrames,
[0, fps * 0.3],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const scaleValue = interpolate(progress, [0, 1], [0.9, 1]) * scale;
if (frame < delayFrames) {
return null;
}
const phoneWidth = width * 0.75;
const phoneHeight = height * 0.8;
const cornerRadius = 60;
const bezelWidth = 12;
return (
<div
style={{
opacity,
transform: `scale(${scaleValue})`,
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "100%",
}}
>
{showDeviceFrame ? (
<div
style={{
position: "relative",
width: phoneWidth,
height: phoneHeight,
background: "#1C1C1E",
borderRadius: cornerRadius,
padding: bezelWidth,
boxShadow: `
0 50px 100px rgba(0, 0, 0, 0.5),
0 20px 40px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1)
`,
}}
>
{/* Dynamic Island */}
<div
style={{
position: "absolute",
top: bezelWidth + 15,
left: "50%",
transform: "translateX(-50%)",
width: 120,
height: 36,
background: "#000",
borderRadius: 18,
zIndex: 10,
}}
/>
{/* Screen content */}
<div
style={{
width: "100%",
height: "100%",
borderRadius: cornerRadius - bezelWidth,
overflow: "hidden",
background: theme.colors.background,
position: "relative",
}}
>
{src ? (
<Img
src={staticFile(src)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
children
)}
</div>
{/* Home indicator */}
<div
style={{
position: "absolute",
bottom: bezelWidth + 10,
left: "50%",
transform: "translateX(-50%)",
width: 140,
height: 5,
background: "rgba(255, 255, 255, 0.3)",
borderRadius: 3,
}}
/>
</div>
) : (
<div
style={{
width: phoneWidth - 40,
height: phoneHeight - 40,
borderRadius: cornerRadius - 20,
overflow: "hidden",
background: theme.colors.background,
boxShadow: `0 30px 80px rgba(0, 0, 0, 0.4)`,
}}
>
{src ? (
<Img
src={staticFile(src)}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
) : (
children
)}
</div>
)}
</div>
);
};
// Mock screen content components
type MockScreenProps = {
children: React.ReactNode;
};
export const MockScreen: React.FC<MockScreenProps> = ({ children }) => {
return (
<AbsoluteFill
style={{
background: theme.colors.background,
padding: theme.spacing.lg,
paddingTop: 80, // Account for Dynamic Island
}}
>
{children}
</AbsoluteFill>
);
};

View File

@@ -1,79 +0,0 @@
import React from "react";
import { AbsoluteFill, useCurrentFrame, interpolate } from "remotion";
import { theme } from "./theme";
type GradientBackgroundProps = {
animate?: boolean;
};
export const GradientBackground: React.FC<GradientBackgroundProps> = ({
animate = false,
}) => {
const frame = useCurrentFrame();
const gradientAngle = animate
? interpolate(frame, [0, 300], [180, 200], { extrapolateRight: "clamp" })
: 180;
return (
<AbsoluteFill
style={{
background: `linear-gradient(${gradientAngle}deg, ${theme.colors.backgroundGradientStart} 0%, ${theme.colors.backgroundGradientEnd} 100%)`,
}}
/>
);
};
// Subtle grid pattern background
export const GridBackground: React.FC = () => {
return (
<AbsoluteFill
style={{
background: theme.colors.background,
}}
>
<div
style={{
position: "absolute",
inset: 0,
backgroundImage: `
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px)
`,
backgroundSize: "40px 40px",
}}
/>
</AbsoluteFill>
);
};
// Radial glow background
type GlowBackgroundProps = {
color?: string;
intensity?: number;
};
export const GlowBackground: React.FC<GlowBackgroundProps> = ({
color = theme.colors.accent,
intensity = 0.15,
}) => {
return (
<AbsoluteFill
style={{
background: theme.colors.background,
}}
>
<div
style={{
position: "absolute",
top: "30%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "150%",
height: "80%",
background: `radial-gradient(ellipse, ${color}${Math.round(intensity * 255).toString(16).padStart(2, "0")} 0%, transparent 70%)`,
}}
/>
</AbsoluteFill>
);
};

View File

@@ -1,43 +0,0 @@
import React from "react";
import { AbsoluteFill, useCurrentFrame } from "remotion";
/**
* Subtle film grain overlay that makes videos feel organic/real.
* Uses deterministic noise per frame for reproducible renders.
*/
export const FilmGrain: React.FC<{ opacity?: number }> = ({
opacity = 0.04,
}) => {
const frame = useCurrentFrame();
// Shift the noise pattern each frame using a CSS trick
const offsetX = ((frame * 73) % 200) - 100;
const offsetY = ((frame * 47) % 200) - 100;
return (
<AbsoluteFill
style={{
pointerEvents: "none",
mixBlendMode: "overlay",
opacity,
}}
>
<svg width="100%" height="100%">
<filter id={`grain-${frame % 10}`}>
<feTurbulence
type="fractalNoise"
baseFrequency="0.65"
numOctaves="3"
seed={frame}
stitchTiles="stitch"
/>
</filter>
<rect
width="100%"
height="100%"
filter={`url(#grain-${frame % 10})`}
transform={`translate(${offsetX}, ${offsetY})`}
/>
</svg>
</AbsoluteFill>
);
};

View File

@@ -1,203 +0,0 @@
import React from "react";
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
} from "remotion";
import { theme } from "./theme";
type LogoEndcardProps = {
tagline?: string;
showAppStoreBadge?: boolean;
};
export const LogoEndcard: React.FC<LogoEndcardProps> = ({
tagline,
showAppStoreBadge = false,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Logo entrance with slight bounce
const logoScale = spring({
frame,
fps,
config: { damping: 15, stiffness: 100 },
});
const logoOpacity = interpolate(frame, [0, fps * 0.3], [0, 1], {
extrapolateRight: "clamp",
});
// Tagline entrance (delayed)
const taglineProgress = spring({
frame,
fps,
delay: fps * 0.4,
config: theme.animation.smooth,
});
const taglineOpacity = interpolate(
frame,
[fps * 0.4, fps * 0.6],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const taglineY = interpolate(taglineProgress, [0, 1], [20, 0]);
// App Store badge entrance (delayed further)
const badgeProgress = spring({
frame,
fps,
delay: fps * 0.7,
config: theme.animation.smooth,
});
const badgeOpacity = interpolate(
frame,
[fps * 0.7, fps * 0.9],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<AbsoluteFill
style={{
background: `linear-gradient(180deg, ${theme.colors.backgroundGradientStart} 0%, ${theme.colors.backgroundGradientEnd} 100%)`,
justifyContent: "center",
alignItems: "center",
}}
>
{/* App Icon */}
<div
style={{
transform: `scale(${logoScale})`,
opacity: logoOpacity,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: theme.spacing.lg,
}}
>
{/* Icon placeholder - rounded square with gradient */}
<div
style={{
width: 180,
height: 180,
borderRadius: 40,
background: `linear-gradient(135deg, ${theme.colors.accent} 0%, ${theme.colors.accentDark} 100%)`,
boxShadow: `0 20px 60px rgba(255, 107, 53, 0.4)`,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
{/* Simple stadium icon representation */}
<svg
width="100"
height="100"
viewBox="0 0 100 100"
fill="none"
>
<ellipse
cx="50"
cy="60"
rx="40"
ry="20"
stroke="white"
strokeWidth="4"
fill="none"
/>
<path
d="M10 60 L10 40 Q50 10 90 40 L90 60"
stroke="white"
strokeWidth="4"
fill="none"
/>
<line
x1="50"
y1="30"
x2="50"
y2="15"
stroke="white"
strokeWidth="3"
/>
<circle cx="50" cy="12" r="4" fill="white" />
</svg>
</div>
{/* Wordmark */}
<div
style={{
fontFamily: theme.fonts.display,
fontSize: theme.fontSizes.hero,
fontWeight: 700,
color: theme.colors.text,
letterSpacing: -2,
}}
>
SportsTime
</div>
</div>
{/* Tagline */}
{tagline && (
<div
style={{
position: "absolute",
bottom: showAppStoreBadge ? 280 : 200,
opacity: taglineOpacity,
transform: `translateY(${taglineY}px)`,
fontFamily: theme.fonts.text,
fontSize: theme.fontSizes.subtitle,
fontWeight: 500,
color: theme.colors.textSecondary,
letterSpacing: 0.5,
}}
>
{tagline}
</div>
)}
{/* App Store Badge */}
{showAppStoreBadge && (
<div
style={{
position: "absolute",
bottom: 120,
opacity: badgeOpacity,
transform: `scale(${badgeProgress})`,
}}
>
<div
style={{
padding: "16px 32px",
background: theme.colors.text,
borderRadius: 12,
display: "flex",
alignItems: "center",
gap: 12,
}}
>
<svg width="32" height="32" viewBox="0 0 32 32" fill="black">
<path d="M16 2C8.268 2 2 8.268 2 16s6.268 14 14 14 14-6.268 14-14S23.732 2 16 2zm5.788 20.683c-.314.467-.782.7-1.404.7-.311 0-.622-.078-.933-.233l-3.451-1.867-3.451 1.867c-.311.155-.622.233-.933.233-.622 0-1.09-.233-1.404-.7-.314-.467-.389-1.012-.225-1.635l.933-3.917-3.062-2.567c-.467-.389-.7-.856-.7-1.4 0-.544.233-1.012.7-1.4l3.062-2.567-.933-3.917c-.164-.623-.089-1.168.225-1.635.314-.467.782-.7 1.404-.7.311 0 .622.078.933.233L16 5.142l3.451-1.867c.311-.155.622-.233.933-.233.622 0 1.09.233 1.404.7.314.467.389 1.012.225 1.635l-.933 3.917 3.062 2.567c.467.388.7.856.7 1.4 0 .544-.233 1.011-.7 1.4l-3.062 2.567.933 3.917c.164.623.089 1.168-.225 1.635z" />
</svg>
<span
style={{
fontFamily: theme.fonts.text,
fontSize: 20,
fontWeight: 600,
color: "black",
}}
>
Download on the App Store
</span>
</div>
</div>
)}
</AbsoluteFill>
);
};

View File

@@ -1,173 +0,0 @@
import React from "react";
import {
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
} from "remotion";
import { theme } from "./theme";
type TapIndicatorProps = {
x: number;
y: number;
delay?: number;
showRipple?: boolean;
};
export const TapIndicator: React.FC<TapIndicatorProps> = ({
x,
y,
delay = 0,
showRipple = true,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const delayFrames = delay * fps;
const localFrame = frame - delayFrames;
if (localFrame < 0) {
return null;
}
// Finger appears
const fingerProgress = spring({
frame: localFrame,
fps,
config: theme.animation.smooth,
});
// Finger presses down
const pressProgress = spring({
frame: localFrame - fps * 0.2,
fps,
config: { damping: 30, stiffness: 300 },
});
const fingerScale = interpolate(fingerProgress, [0, 1], [0.5, 1]);
const fingerOpacity = interpolate(fingerProgress, [0, 1], [0, 1]);
const pressScale = interpolate(pressProgress, [0, 1], [1, 0.9]);
// Ripple effect
const rippleProgress = interpolate(
localFrame,
[fps * 0.25, fps * 0.7],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const rippleScale = interpolate(rippleProgress, [0, 1], [0.5, 2]);
const rippleOpacity = interpolate(rippleProgress, [0, 0.3, 1], [0, 0.5, 0]);
return (
<div
style={{
position: "absolute",
left: x,
top: y,
transform: "translate(-50%, -50%)",
pointerEvents: "none",
}}
>
{/* Ripple */}
{showRipple && rippleProgress > 0 && (
<div
style={{
position: "absolute",
width: 80,
height: 80,
borderRadius: "50%",
background: theme.colors.accent,
opacity: rippleOpacity,
transform: `translate(-50%, -50%) scale(${rippleScale})`,
left: "50%",
top: "50%",
}}
/>
)}
{/* Finger circle */}
<div
style={{
width: 60,
height: 60,
borderRadius: "50%",
background: `rgba(255, 255, 255, 0.9)`,
boxShadow: `0 4px 20px rgba(0, 0, 0, 0.3)`,
opacity: fingerOpacity,
transform: `scale(${fingerScale * pressScale})`,
}}
/>
</div>
);
};
// Swipe indicator for tutorial-style animations
type SwipeIndicatorProps = {
startX: number;
startY: number;
endX: number;
endY: number;
delay?: number;
duration?: number;
};
export const SwipeIndicator: React.FC<SwipeIndicatorProps> = ({
startX,
startY,
endX,
endY,
delay = 0,
duration = 0.8,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const delayFrames = delay * fps;
const durationFrames = duration * fps;
const localFrame = frame - delayFrames;
if (localFrame < 0 || localFrame > durationFrames + fps * 0.3) {
return null;
}
const progress = interpolate(
localFrame,
[0, durationFrames],
[0, 1],
{ extrapolateRight: "clamp" }
);
const x = interpolate(progress, [0, 1], [startX, endX]);
const y = interpolate(progress, [0, 1], [startY, endY]);
const opacity = interpolate(
localFrame,
[0, fps * 0.1, durationFrames, durationFrames + fps * 0.2],
[0, 1, 1, 0],
{ extrapolateRight: "clamp" }
);
return (
<div
style={{
position: "absolute",
left: x,
top: y,
transform: "translate(-50%, -50%)",
pointerEvents: "none",
opacity,
}}
>
<div
style={{
width: 50,
height: 50,
borderRadius: "50%",
background: `rgba(255, 255, 255, 0.9)`,
boxShadow: `0 4px 20px rgba(0, 0, 0, 0.3)`,
}}
/>
</div>
);
};

View File

@@ -1,188 +0,0 @@
import React from "react";
import {
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
} from "remotion";
import { theme } from "./theme";
type TextRevealProps = {
text: string;
fontSize?: number;
color?: string;
fontWeight?: number;
textAlign?: "left" | "center" | "right";
delay?: number;
style?: React.CSSProperties;
};
export const TextReveal: React.FC<TextRevealProps> = ({
text,
fontSize = theme.fontSizes.title,
color = theme.colors.text,
fontWeight = 700,
textAlign = "center",
delay = 0,
style = {},
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const delayFrames = delay * fps;
// Spring for smooth entrance
const progress = spring({
frame: frame - delayFrames,
fps,
config: theme.animation.smooth,
});
const opacity = interpolate(
frame - delayFrames,
[0, fps * 0.3],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const translateY = interpolate(progress, [0, 1], [30, 0]);
if (frame < delayFrames) {
return null;
}
return (
<div
style={{
fontFamily: theme.fonts.display,
fontSize,
fontWeight,
color,
textAlign,
opacity,
transform: `translateY(${translateY}px)`,
letterSpacing: -1,
lineHeight: 1.2,
...style,
}}
>
{text}
</div>
);
};
// Multi-line version with staggered reveal
type TextRevealMultilineProps = {
lines: string[];
fontSize?: number;
color?: string;
fontWeight?: number;
textAlign?: "left" | "center" | "right";
staggerDelay?: number;
startDelay?: number;
lineHeight?: number;
};
export const TextRevealMultiline: React.FC<TextRevealMultilineProps> = ({
lines,
fontSize = theme.fontSizes.title,
color = theme.colors.text,
fontWeight = 700,
textAlign = "center",
staggerDelay = 0.1,
startDelay = 0,
lineHeight = 1.3,
}) => {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: textAlign === "center" ? "center" : textAlign === "right" ? "flex-end" : "flex-start",
gap: 8,
}}
>
{lines.map((line, index) => (
<TextReveal
key={index}
text={line}
fontSize={fontSize}
color={color}
fontWeight={fontWeight}
textAlign={textAlign}
delay={startDelay + index * staggerDelay}
style={{ lineHeight }}
/>
))}
</div>
);
};
// Highlight style text (with background)
type HighlightTextProps = {
text: string;
fontSize?: number;
delay?: number;
};
export const HighlightText: React.FC<HighlightTextProps> = ({
text,
fontSize = theme.fontSizes.subtitle,
delay = 0,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const delayFrames = delay * fps;
const progress = spring({
frame: frame - delayFrames,
fps,
config: theme.animation.snappy,
});
const scaleX = interpolate(progress, [0, 1], [0, 1]);
const textOpacity = interpolate(
frame - delayFrames,
[fps * 0.15, fps * 0.3],
[0, 1],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
if (frame < delayFrames) {
return null;
}
return (
<div style={{ position: "relative", display: "inline-block" }}>
{/* Background highlight */}
<div
style={{
position: "absolute",
top: 0,
left: -12,
right: -12,
bottom: 0,
background: theme.colors.accent,
borderRadius: 8,
transform: `scaleX(${scaleX})`,
transformOrigin: "left",
}}
/>
{/* Text */}
<span
style={{
position: "relative",
fontFamily: theme.fonts.display,
fontSize,
fontWeight: 700,
color: theme.colors.text,
opacity: textOpacity,
padding: "4px 0",
}}
>
{text}
</span>
</div>
);
};

View File

@@ -1,306 +0,0 @@
import React from "react";
import {
useCurrentFrame,
useVideoConfig,
spring,
interpolate,
} from "remotion";
import { theme } from "./theme";
/**
* TikTok-native kinetic caption system.
*
* Unlike generic subtitle overlays, each style mimics real TikTok
* caption patterns: punch zooms, word pops, highlight boxes, etc.
*/
type CaptionEntry = {
text: string;
startSec: number;
endSec: number;
style?: "punch" | "highlight" | "stack" | "whisper" | "shake";
};
type TikTokCaptionProps = {
captions: CaptionEntry[];
/** Vertical position from bottom (px) */
bottomOffset?: number;
};
export const TikTokCaption: React.FC<TikTokCaptionProps> = ({
captions,
bottomOffset = 280,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const currentSec = frame / fps;
const active = captions.find(
(c) => currentSec >= c.startSec && currentSec < c.endSec
);
if (!active) return null;
const startFrame = active.startSec * fps;
const endFrame = active.endSec * fps;
const localFrame = frame - startFrame;
const style = active.style || "punch";
const exitOpacity = interpolate(
frame,
[endFrame - 4, endFrame],
[1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<div
style={{
position: "absolute",
bottom: bottomOffset,
left: 0,
right: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
pointerEvents: "none",
opacity: exitOpacity,
zIndex: 100,
}}
>
{style === "punch" && (
<PunchCaption text={active.text} localFrame={localFrame} fps={fps} />
)}
{style === "highlight" && (
<HighlightCaption text={active.text} localFrame={localFrame} fps={fps} />
)}
{style === "stack" && (
<StackCaption text={active.text} localFrame={localFrame} fps={fps} />
)}
{style === "whisper" && (
<WhisperCaption text={active.text} localFrame={localFrame} fps={fps} />
)}
{style === "shake" && (
<ShakeCaption text={active.text} localFrame={localFrame} fps={fps} />
)}
</div>
);
};
/** Punch zoom in - text scales from big to normal with impact */
const PunchCaption: React.FC<{
text: string;
localFrame: number;
fps: number;
}> = ({ text, localFrame, fps }) => {
const prog = spring({
frame: localFrame,
fps,
config: { damping: 10, stiffness: 280 },
});
const scale = interpolate(prog, [0, 1], [1.8, 1]);
const opacity = interpolate(prog, [0, 0.3], [0, 1], {
extrapolateRight: "clamp",
});
return (
<span
style={{
fontFamily: theme.fonts.display,
fontSize: 52,
fontWeight: 900,
color: "white",
textAlign: "center",
textTransform: "uppercase",
letterSpacing: -1,
textShadow: "0 4px 20px rgba(0,0,0,0.8), 0 2px 4px rgba(0,0,0,0.9)",
transform: `scale(${scale})`,
opacity,
maxWidth: 900,
lineHeight: 1.15,
}}
>
{text}
</span>
);
};
/** Highlight box - text with colored background box that wipes in */
const HighlightCaption: React.FC<{
text: string;
localFrame: number;
fps: number;
}> = ({ text, localFrame, fps }) => {
const boxProg = spring({
frame: localFrame,
fps,
config: { damping: 15, stiffness: 200 },
});
const textProg = spring({
frame: localFrame - 3,
fps,
config: { damping: 20, stiffness: 180 },
});
return (
<div style={{ position: "relative", display: "inline-block" }}>
<div
style={{
position: "absolute",
inset: "-8px -20px",
background: theme.colors.accent,
borderRadius: 8,
transform: `scaleX(${boxProg})`,
transformOrigin: "left",
}}
/>
<span
style={{
position: "relative",
fontFamily: theme.fonts.display,
fontSize: 46,
fontWeight: 800,
color: "white",
opacity: interpolate(textProg, [0, 1], [0, 1]),
letterSpacing: -0.5,
textShadow: "0 2px 8px rgba(0,0,0,0.3)",
}}
>
{text}
</span>
</div>
);
};
/** Stack - words stack vertically, each popping in */
const StackCaption: React.FC<{
text: string;
localFrame: number;
fps: number;
}> = ({ text, localFrame, fps }) => {
const words = text.split(" ");
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 4,
}}
>
{words.map((word, i) => {
const delay = i * 2;
const prog = spring({
frame: localFrame - delay,
fps,
config: { damping: 12, stiffness: 250 },
});
return (
<span
key={i}
style={{
fontFamily: theme.fonts.display,
fontSize: 56,
fontWeight: 900,
color: "white",
textTransform: "uppercase",
letterSpacing: 2,
transform: `scale(${interpolate(prog, [0, 1], [0.5, 1])}) translateY(${interpolate(prog, [0, 1], [20, 0])}px)`,
opacity: interpolate(prog, [0, 0.5], [0, 1], {
extrapolateRight: "clamp",
}),
textShadow:
"0 4px 20px rgba(0,0,0,0.8), 0 2px 4px rgba(0,0,0,0.9)",
lineHeight: 1.0,
}}
>
{word}
</span>
);
})}
</div>
);
};
/** Whisper - small italic text that fades in gently */
const WhisperCaption: React.FC<{
text: string;
localFrame: number;
fps: number;
}> = ({ text, localFrame, fps }) => {
const opacity = interpolate(localFrame, [0, fps * 0.3], [0, 1], {
extrapolateRight: "clamp",
});
return (
<span
style={{
fontFamily: theme.fonts.text,
fontSize: 32,
fontWeight: 400,
fontStyle: "italic",
color: "rgba(255,255,255,0.8)",
textAlign: "center",
opacity,
letterSpacing: 1,
textShadow: "0 2px 12px rgba(0,0,0,0.6)",
}}
>
{text}
</span>
);
};
/** Shake - text shakes briefly on entrance then settles */
const ShakeCaption: React.FC<{
text: string;
localFrame: number;
fps: number;
}> = ({ text, localFrame, fps }) => {
const prog = spring({
frame: localFrame,
fps,
config: { damping: 8, stiffness: 300 },
});
// Shake offsets that decay over ~5 frames
const shakeIntensity = interpolate(localFrame, [0, 5], [8, 0], {
extrapolateRight: "clamp",
});
const OFFSETS = [
{ x: -1, y: 1 },
{ x: 1, y: -1 },
{ x: -1, y: -1 },
{ x: 1, y: 1 },
{ x: 0, y: -1 },
];
const offset = OFFSETS[localFrame % OFFSETS.length];
const sx = offset.x * shakeIntensity;
const sy = offset.y * shakeIntensity;
return (
<span
style={{
fontFamily: theme.fonts.display,
fontSize: 50,
fontWeight: 900,
color: "white",
textAlign: "center",
textTransform: "uppercase",
letterSpacing: -0.5,
transform: `translate(${sx}px, ${sy}px) scale(${interpolate(prog, [0, 1], [1.3, 1])})`,
opacity: interpolate(prog, [0, 0.3], [0, 1], {
extrapolateRight: "clamp",
}),
textShadow: "0 4px 20px rgba(0,0,0,0.8), 0 2px 4px rgba(0,0,0,0.9)",
maxWidth: 900,
lineHeight: 1.15,
}}
>
{text}
</span>
);
};
export type { CaptionEntry };

View File

@@ -1,9 +0,0 @@
export { theme } from "./theme";
export { LogoEndcard } from "./LogoEndcard";
export { TextReveal, TextRevealMultiline, HighlightText } from "./TextReveal";
export { TapIndicator, SwipeIndicator } from "./TapIndicator";
export { AppScreenshot, MockScreen } from "./AppScreenshot";
export { GradientBackground, GridBackground, GlowBackground } from "./Background";
export { FilmGrain } from "./FilmGrain";
export { TikTokCaption } from "./TikTokCaption";
export type { CaptionEntry } from "./TikTokCaption";

View File

@@ -1,47 +0,0 @@
// SportsTime Marketing Video Theme
export const theme = {
colors: {
background: "#0A0A0A",
backgroundGradientStart: "#0A0A0A",
backgroundGradientEnd: "#1A1A2E",
accent: "#FF6B35",
accentDark: "#E55A25",
secondary: "#4ECDC4",
secondaryDark: "#3DBDB5",
text: "#FFFFFF",
textSecondary: "#B0B0B0",
textMuted: "#6B6B6B",
success: "#4CAF50",
gold: "#FFD700",
mapLine: "#FF6B35",
mapMarker: "#4ECDC4",
},
fonts: {
display: "SF Pro Display, -apple-system, BlinkMacSystemFont, sans-serif",
text: "SF Pro Text, -apple-system, BlinkMacSystemFont, sans-serif",
},
fontSizes: {
hero: 72,
headline: 56,
title: 48,
subtitle: 36,
body: 28,
caption: 20,
},
spacing: {
xs: 8,
sm: 16,
md: 24,
lg: 40,
xl: 64,
xxl: 96,
},
animation: {
smooth: { damping: 200 },
snappy: { damping: 20, stiffness: 200 },
bouncy: { damping: 8 },
heavy: { damping: 15, stiffness: 80, mass: 2 },
},
} as const;
export type Theme = typeof theme;