feat: fix travel placement bug, add theme-based alternate icons, fix animated background crash
- Fix repeat-city travel placement: use stop indices instead of global city name matching so Follow Team trips with repeat cities show travel correctly - Add TravelPlacement helper and regression tests (7 tests) - Add alternate app icons for each theme, auto-switch on theme change - Fix index-out-of-range crash in AnimatedSportsBackground (19 configs, was iterating 20) - Add marketing video configs, engine, and new video components - Add docs and data exports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
290
marketing-videos/src/videos/TheGroupChat/RouteScene.tsx
Normal file
290
marketing-videos/src/videos/TheGroupChat/RouteScene.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AbsoluteFill,
|
||||
useCurrentFrame,
|
||||
useVideoConfig,
|
||||
spring,
|
||||
interpolate,
|
||||
} from "remotion";
|
||||
import { theme } from "../../components/shared/theme";
|
||||
import { AppScreenshot, MockScreen } from "../../components/shared/AppScreenshot";
|
||||
|
||||
/**
|
||||
* Scene 4: Routes generated
|
||||
*
|
||||
* Route auto-generates inside phone frame.
|
||||
* Map with animated route line + game cards appear.
|
||||
* Overlay: "Real route. Real games."
|
||||
*/
|
||||
|
||||
type Stop = {
|
||||
city: string;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
const STOPS: Stop[] = [
|
||||
{ city: "LA", x: 180, y: 300 },
|
||||
{ city: "SF", x: 140, y: 130 },
|
||||
{ city: "SD", x: 240, y: 400 },
|
||||
{ city: "PHX", x: 480, y: 340 },
|
||||
];
|
||||
|
||||
export const RouteScene: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// Overlay text
|
||||
const overlayProgress = spring({
|
||||
frame: frame - 0.3 * fps,
|
||||
fps,
|
||||
config: theme.animation.snappy,
|
||||
});
|
||||
const overlayOpacity = interpolate(
|
||||
frame - 0.3 * fps,
|
||||
[0, 0.2 * fps],
|
||||
[0, 1],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
|
||||
return (
|
||||
<AbsoluteFill
|
||||
style={{
|
||||
background: theme.colors.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{/* Overlay: "Real route. Real games." */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
opacity: overlayOpacity,
|
||||
transform: `scale(${interpolate(overlayProgress, [0, 1], [0.85, 1])})`,
|
||||
zIndex: 20,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(0, 0, 0, 0.85)",
|
||||
padding: "14px 36px",
|
||||
borderRadius: 14,
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: theme.fonts.display,
|
||||
fontSize: 34,
|
||||
fontWeight: 800,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
Real route. Real games.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone with route */}
|
||||
<div style={{ marginTop: 40 }}>
|
||||
<AppScreenshot delay={0} scale={0.82}>
|
||||
<MockScreen>
|
||||
<RouteContent />
|
||||
</MockScreen>
|
||||
</AppScreenshot>
|
||||
</div>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
};
|
||||
|
||||
/** Route map + game list inside phone */
|
||||
const RouteContent: React.FC = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
// Route draw
|
||||
const lineProgress = interpolate(
|
||||
frame,
|
||||
[0.15 * fps, 1.5 * fps],
|
||||
[0, 1],
|
||||
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
||||
);
|
||||
|
||||
const pathSegments: string[] = [];
|
||||
for (let i = 0; i < STOPS.length - 1; i++) {
|
||||
const from = STOPS[i];
|
||||
const to = STOPS[i + 1];
|
||||
if (i === 0) pathSegments.push(`M ${from.x} ${from.y}`);
|
||||
const cx = (from.x + to.x) / 2;
|
||||
const cy = Math.min(from.y, to.y) - 30;
|
||||
pathSegments.push(`Q ${cx} ${cy} ${to.x} ${to.y}`);
|
||||
}
|
||||
const fullPath = pathSegments.join(" ");
|
||||
|
||||
// Game cards
|
||||
const games = [
|
||||
{ team: "@ Dodgers", venue: "Dodger Stadium", date: "Jun 12", color: "#005A9C" },
|
||||
{ team: "@ Giants", venue: "Oracle Park", date: "Jun 14", color: "#FD5A1E" },
|
||||
{ team: "@ Padres", venue: "Petco Park", date: "Jun 16", color: "#2F241D" },
|
||||
{ team: "@ D-backs", venue: "Chase Field", date: "Jun 18", color: "#A71930" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", width: "100%", height: "100%" }}>
|
||||
{/* Map section (top 45%) */}
|
||||
<div style={{ position: "absolute", top: 0, left: 8, right: 8, height: "45%" }}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "40px 40px",
|
||||
borderRadius: 12,
|
||||
}}
|
||||
/>
|
||||
|
||||
<svg width="100%" height="100%" viewBox="0 0 600 480" style={{ position: "absolute", inset: 0 }}>
|
||||
<path d={fullPath} fill="none" stroke="rgba(255,107,53,0.15)" strokeWidth="3.5" strokeLinecap="round" />
|
||||
<path
|
||||
d={fullPath}
|
||||
fill="none"
|
||||
stroke={theme.colors.accent}
|
||||
strokeWidth="3.5"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="1500"
|
||||
strokeDashoffset={1500 * (1 - lineProgress)}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{STOPS.map((stop, index) => {
|
||||
const markerProgress = spring({
|
||||
frame: frame - (0.2 + index * 0.35) * fps,
|
||||
fps,
|
||||
config: { damping: 12, stiffness: 180 },
|
||||
});
|
||||
const leftPct = (stop.x / 600) * 100;
|
||||
const topPct = (stop.y / 480) * 100;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={stop.city}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: `${leftPct}%`,
|
||||
top: `${topPct}%`,
|
||||
transform: `translate(-50%, -50%) scale(${interpolate(markerProgress, [0, 1], [0, 1])})`,
|
||||
opacity: interpolate(markerProgress, [0, 1], [0, 1]),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: "50%",
|
||||
background: theme.colors.secondary,
|
||||
border: "2.5px solid white",
|
||||
boxShadow: "0 2px 8px rgba(78,205,196,0.5)",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: theme.fonts.display,
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
color: "white",
|
||||
textShadow: "0 1px 6px rgba(0,0,0,0.8)",
|
||||
}}
|
||||
>
|
||||
{stop.city}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Game cards (bottom 55%) */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: "46%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: theme.fonts.display,
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
color: theme.colors.text,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Your Games
|
||||
</div>
|
||||
|
||||
{games.map((game, index) => {
|
||||
const cardDelay = 1.2 + index * 0.18;
|
||||
const cardProgress = spring({
|
||||
frame: frame - cardDelay * fps,
|
||||
fps,
|
||||
config: theme.animation.snappy,
|
||||
});
|
||||
const translateX = interpolate(cardProgress, [0, 1], [250, 0]);
|
||||
const opacity = interpolate(cardProgress, [0, 1], [0, 1]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={game.venue}
|
||||
style={{
|
||||
background: "#1C1C1E",
|
||||
borderRadius: 14,
|
||||
padding: "14px 18px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 14,
|
||||
transform: `translateX(${translateX}px)`,
|
||||
opacity,
|
||||
borderLeft: `4px solid ${game.color}`,
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 50, textAlign: "center" }}>
|
||||
<div style={{ fontFamily: theme.fonts.display, fontSize: 16, fontWeight: 700, color: theme.colors.text }}>
|
||||
{game.date.split(" ")[1]}
|
||||
</div>
|
||||
<div style={{ fontFamily: theme.fonts.text, fontSize: 11, color: theme.colors.textMuted }}>
|
||||
{game.date.split(" ")[0]}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ width: 1.5, height: 32, background: "rgba(255,255,255,0.08)" }} />
|
||||
<div>
|
||||
<div style={{ fontFamily: theme.fonts.display, fontSize: 17, fontWeight: 700, color: theme.colors.text }}>
|
||||
{game.team}
|
||||
</div>
|
||||
<div style={{ fontFamily: theme.fonts.text, fontSize: 13, color: theme.colors.textSecondary }}>
|
||||
{game.venue}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user