chore: remove scraper, add docs, add marketing-videos gitignore
- 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>
This commit is contained in:
185
marketing-videos/src/components/shared/AppScreenshot.tsx
Normal file
185
marketing-videos/src/components/shared/AppScreenshot.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
79
marketing-videos/src/components/shared/Background.tsx
Normal file
79
marketing-videos/src/components/shared/Background.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
203
marketing-videos/src/components/shared/LogoEndcard.tsx
Normal file
203
marketing-videos/src/components/shared/LogoEndcard.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
173
marketing-videos/src/components/shared/TapIndicator.tsx
Normal file
173
marketing-videos/src/components/shared/TapIndicator.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
188
marketing-videos/src/components/shared/TextReveal.tsx
Normal file
188
marketing-videos/src/components/shared/TextReveal.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
6
marketing-videos/src/components/shared/index.ts
Normal file
6
marketing-videos/src/components/shared/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
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";
|
||||
47
marketing-videos/src/components/shared/theme.ts
Normal file
47
marketing-videos/src/components/shared/theme.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user