← back to Prestige Car Wash Video
Remotion showcase: capture pipeline + composition scaffold
091f48725931bd317be166c8685cdb0b0a154ad0 · 2026-07-12 08:18:54 -0700 · Steve Abrams
Files touched
A src/BrowserShot.tsxA src/Caption.tsxA src/Card.tsxA src/Root.tsxA src/Video.tsxA src/index.tsA tsconfig.json
Diff
commit 091f48725931bd317be166c8685cdb0b0a154ad0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 12 08:18:54 2026 -0700
Remotion showcase: capture pipeline + composition scaffold
---
src/BrowserShot.tsx | 100 ++++++++++++++++++++++++++++++++++++++++++++
src/Caption.tsx | 69 +++++++++++++++++++++++++++++++
src/Card.tsx | 71 +++++++++++++++++++++++++++++++
src/Root.tsx | 16 +++++++
src/Video.tsx | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++
src/index.ts | 4 ++
tsconfig.json | 15 +++++++
7 files changed, 392 insertions(+)
diff --git a/src/BrowserShot.tsx b/src/BrowserShot.tsx
new file mode 100644
index 0000000..ed7a0c3
--- /dev/null
+++ b/src/BrowserShot.tsx
@@ -0,0 +1,100 @@
+import { AbsoluteFill, Img, staticFile, useCurrentFrame, interpolate, Easing } from 'remotion';
+import type { Section } from './sections';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+
+// A macOS-style browser window that "Ken Burns" pans/zooms the captured page.
+export const BrowserShot: React.FC<{ s: Section }> = ({ s }) => {
+ const frame = useCurrentFrame();
+ const local = frame; // inside a <Sequence>, useCurrentFrame() is section-local
+
+ // window geometry on the 1920x1080 stage
+ const FRAME_W = 1560;
+ const CHROME_H = 46;
+ const VIEW_H = 748;
+ const nativeW = s.w || 1440;
+ const nativeH = s.h || 900;
+ const scale = FRAME_W / nativeW;
+ const scaledH = nativeH * scale;
+
+ const dur = s.frames;
+ // vertical pan for tall pages; gentle zoom for short ones
+ const overflow = Math.max(0, scaledH - VIEW_H);
+ const eased = interpolate(local, [0, dur], [0, 1], {
+ easing: Easing.inOut(Easing.ease),
+ extrapolateRight: 'clamp',
+ });
+ const translateY = overflow > 8 ? -overflow * eased : 0;
+ const zoom = overflow > 8 ? 1 : interpolate(eased, [0, 1], [1.0, 1.05]);
+
+ // whole-window intro: rise + settle
+ const rise = interpolate(local, [0, 18], [40, 0], {
+ easing: Easing.out(Easing.cubic),
+ extrapolateRight: 'clamp',
+ });
+
+ return (
+ <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'flex-start' }}>
+ <div
+ style={{
+ marginTop: 74 + rise,
+ width: FRAME_W,
+ borderRadius: 16,
+ overflow: 'hidden',
+ boxShadow: '0 40px 120px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06)',
+ background: '#0f1a2b',
+ }}
+ >
+ {/* browser chrome */}
+ <div
+ style={{
+ height: CHROME_H,
+ display: 'flex',
+ alignItems: 'center',
+ gap: 10,
+ padding: '0 16px',
+ background: 'linear-gradient(#182740,#111d31)',
+ borderBottom: '1px solid rgba(255,255,255,0.06)',
+ }}
+ >
+ <Dot c="#ff5f57" /><Dot c="#febc2e" /><Dot c="#28c840" />
+ <div
+ style={{
+ marginLeft: 14,
+ flex: 1,
+ height: 26,
+ borderRadius: 8,
+ background: 'rgba(255,255,255,0.06)',
+ display: 'flex',
+ alignItems: 'center',
+ padding: '0 12px',
+ color: '#9fb3cd',
+ fontFamily: FONT,
+ fontSize: 14,
+ letterSpacing: 0.2,
+ }}
+ >
+ <span style={{ opacity: 0.6, marginRight: 8 }}>🔒</span>
+ {s.url}
+ </div>
+ </div>
+ {/* viewport */}
+ <div style={{ height: VIEW_H, overflow: 'hidden', background: '#0b1220' }}>
+ <Img
+ src={staticFile(s.file!)}
+ style={{
+ width: FRAME_W,
+ transform: `translateY(${translateY}px) scale(${zoom})`,
+ transformOrigin: 'top center',
+ display: 'block',
+ }}
+ />
+ </div>
+ </div>
+ </AbsoluteFill>
+ );
+};
+
+const Dot: React.FC<{ c: string }> = ({ c }) => (
+ <div style={{ width: 12, height: 12, borderRadius: 12, background: c }} />
+);
diff --git a/src/Caption.tsx b/src/Caption.tsx
new file mode 100644
index 0000000..ee24050
--- /dev/null
+++ b/src/Caption.tsx
@@ -0,0 +1,69 @@
+import { useCurrentFrame, interpolate, Easing } from 'remotion';
+import type { Section } from './sections';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+
+export const Caption: React.FC<{ s: Section; index: number; total: number }> = ({ s, index, total }) => {
+ const f = useCurrentFrame();
+ const inX = interpolate(f, [6, 24], [-60, 0], { easing: Easing.out(Easing.cubic), extrapolateRight: 'clamp', extrapolateLeft: 'clamp' });
+ const inO = interpolate(f, [6, 24], [0, 1], { extrapolateRight: 'clamp', extrapolateLeft: 'clamp' });
+
+ return (
+ <div
+ style={{
+ position: 'absolute',
+ left: 60,
+ bottom: 70,
+ transform: `translateX(${inX}px)`,
+ opacity: inO,
+ maxWidth: 1200,
+ }}
+ >
+ <div
+ style={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: 12,
+ padding: '8px 16px',
+ borderRadius: 999,
+ background: `${s.accent}22`,
+ border: `1px solid ${s.accent}55`,
+ color: s.accent,
+ fontFamily: FONT,
+ fontWeight: 700,
+ fontSize: 20,
+ letterSpacing: 2,
+ textTransform: 'uppercase',
+ }}
+ >
+ <span style={{ width: 9, height: 9, borderRadius: 9, background: s.accent }} />
+ {s.act}
+ </div>
+ <div
+ style={{
+ marginTop: 16,
+ fontFamily: FONT,
+ fontWeight: 800,
+ fontSize: 68,
+ lineHeight: 1.02,
+ color: '#f2f7ff',
+ textShadow: '0 6px 30px rgba(0,0,0,0.55)',
+ }}
+ >
+ {s.title}
+ </div>
+ <div
+ style={{
+ marginTop: 12,
+ fontFamily: FONT,
+ fontWeight: 500,
+ fontSize: 30,
+ color: '#b8c9e0',
+ textShadow: '0 4px 20px rgba(0,0,0,0.6)',
+ }}
+ >
+ {s.sub}
+ </div>
+ </div>
+ );
+};
diff --git a/src/Card.tsx b/src/Card.tsx
new file mode 100644
index 0000000..79e6c37
--- /dev/null
+++ b/src/Card.tsx
@@ -0,0 +1,71 @@
+import { AbsoluteFill, useCurrentFrame, interpolate, Easing, spring, useVideoConfig } from 'remotion';
+import type { Section } from './sections';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+
+// Full-screen title card used for intro / act dividers / outro.
+export const Card: React.FC<{ s: Section }> = ({ s }) => {
+ const f = useCurrentFrame();
+ const { fps } = useVideoConfig();
+
+ const pop = spring({ frame: f, fps, config: { damping: 200, mass: 0.7 } });
+ const y = interpolate(pop, [0, 1], [30, 0]);
+ const eyebrowO = interpolate(f, [2, 16], [0, 1], { extrapolateRight: 'clamp' });
+ const subO = interpolate(f, [14, 30], [0, 1], { extrapolateRight: 'clamp' });
+ const lineW = interpolate(f, [10, 34], [0, 320], { easing: Easing.out(Easing.cubic), extrapolateRight: 'clamp' });
+
+ return (
+ <AbsoluteFill style={{ alignItems: 'center', justifyContent: 'center' }}>
+ <div style={{ textAlign: 'center', transform: `translateY(${y}px)` }}>
+ <div
+ style={{
+ opacity: eyebrowO,
+ fontFamily: FONT,
+ fontWeight: 700,
+ fontSize: 26,
+ letterSpacing: 8,
+ textTransform: 'uppercase',
+ color: s.accent,
+ }}
+ >
+ {s.act}
+ </div>
+ <div
+ style={{
+ opacity: pop,
+ marginTop: 22,
+ fontFamily: FONT,
+ fontWeight: 900,
+ fontSize: 108,
+ lineHeight: 1.0,
+ color: '#f4f9ff',
+ textShadow: '0 10px 50px rgba(0,0,0,0.5)',
+ }}
+ >
+ {s.title}
+ </div>
+ <div
+ style={{
+ width: lineW,
+ height: 4,
+ borderRadius: 4,
+ background: s.accent,
+ margin: '30px auto 0',
+ }}
+ />
+ <div
+ style={{
+ opacity: subO,
+ marginTop: 26,
+ fontFamily: FONT,
+ fontWeight: 500,
+ fontSize: 34,
+ color: '#c2d2e8',
+ }}
+ >
+ {s.sub}
+ </div>
+ </div>
+ </AbsoluteFill>
+ );
+};
diff --git a/src/Root.tsx b/src/Root.tsx
new file mode 100644
index 0000000..f7f1281
--- /dev/null
+++ b/src/Root.tsx
@@ -0,0 +1,16 @@
+import { Composition } from 'remotion';
+import { PrestigeShowcase } from './Video';
+import { FPS, WIDTH, HEIGHT, TOTAL_FRAMES } from './sections';
+
+export const RemotionRoot: React.FC = () => {
+ return (
+ <Composition
+ id="PrestigeShowcase"
+ component={PrestigeShowcase}
+ durationInFrames={TOTAL_FRAMES}
+ fps={FPS}
+ width={WIDTH}
+ height={HEIGHT}
+ />
+ );
+};
diff --git a/src/Video.tsx b/src/Video.tsx
new file mode 100644
index 0000000..a57f15f
--- /dev/null
+++ b/src/Video.tsx
@@ -0,0 +1,117 @@
+import { AbsoluteFill, Sequence, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
+import { SECTIONS } from './sections';
+import { Background } from './Background';
+import { BrowserShot } from './BrowserShot';
+import { Caption } from './Caption';
+import { Card } from './Card';
+
+const FONT = 'Inter, "SF Pro Display", system-ui, -apple-system, sans-serif';
+
+// fade each section in/out over the persistent background
+const FadeWrap: React.FC<{ dur: number; children: React.ReactNode }> = ({ dur, children }) => {
+ const f = useCurrentFrame();
+ const o = interpolate(f, [0, 12, dur - 10, dur], [0, 1, 1, 0], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ return <AbsoluteFill style={{ opacity: o }}>{children}</AbsoluteFill>;
+};
+
+export const PrestigeShowcase: React.FC = () => {
+ const frame = useCurrentFrame();
+ const { durationInFrames } = useVideoConfig();
+
+ const offsets: number[] = [];
+ let acc = 0;
+ for (const s of SECTIONS) { offsets.push(acc); acc += s.frames; }
+ let active = 0;
+ for (let i = 0; i < SECTIONS.length; i++) if (frame >= offsets[i]) active = i;
+ const accent = SECTIONS[active].accent;
+ const shotIndex = SECTIONS.slice(0, active + 1).filter((s) => s.kind === 'shot').length;
+ const shotTotal = SECTIONS.filter((s) => s.kind === 'shot').length;
+
+ return (
+ <AbsoluteFill style={{ backgroundColor: '#060c16' }}>
+ <Background accent={accent} />
+
+ {SECTIONS.map((s, i) => (
+ <Sequence key={s.id} from={offsets[i]} durationInFrames={s.frames} name={s.title}>
+ <FadeWrap dur={s.frames}>
+ {s.kind === 'card' ? (
+ <Card s={s} />
+ ) : (
+ <>
+ <BrowserShot s={s} />
+ <Caption s={s} index={i} total={SECTIONS.length} />
+ </>
+ )}
+ </FadeWrap>
+ </Sequence>
+ ))}
+
+ {/* top progress bar */}
+ <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 6, background: 'rgba(255,255,255,0.08)' }}>
+ <div
+ style={{
+ height: 6,
+ width: `${(frame / durationInFrames) * 100}%`,
+ background: accent,
+ boxShadow: `0 0 16px ${accent}`,
+ }}
+ />
+ </div>
+
+ {/* section counter (only during shot sections) */}
+ {SECTIONS[active].kind === 'shot' && (
+ <div
+ style={{
+ position: 'absolute',
+ top: 34,
+ right: 54,
+ fontFamily: FONT,
+ fontWeight: 700,
+ fontSize: 24,
+ color: '#8ea6c4',
+ letterSpacing: 2,
+ }}
+ >
+ {String(shotIndex).padStart(2, '0')}
+ <span style={{ opacity: 0.4 }}> / {String(shotTotal).padStart(2, '0')}</span>
+ </div>
+ )}
+
+ {/* persistent wordmark */}
+ <div
+ style={{
+ position: 'absolute',
+ top: 30,
+ left: 54,
+ display: 'flex',
+ alignItems: 'center',
+ gap: 12,
+ fontFamily: FONT,
+ fontWeight: 800,
+ fontSize: 24,
+ color: '#e8f1ff',
+ letterSpacing: 0.5,
+ }}
+ >
+ <span
+ style={{
+ display: 'inline-flex',
+ width: 34,
+ height: 34,
+ borderRadius: 9,
+ background: 'linear-gradient(135deg,#38bdf8,#2563eb)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ fontSize: 18,
+ }}
+ >
+ 🚗
+ </span>
+ Prestige<span style={{ opacity: 0.5, fontWeight: 600 }}>Car Wash</span>
+ </div>
+ </AbsoluteFill>
+ );
+};
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..91fa0f3
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,4 @@
+import { registerRoot } from 'remotion';
+import { RemotionRoot } from './Root';
+
+registerRoot(RemotionRoot);
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..6ad72ba
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": false,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "lib": ["ES2021", "DOM"],
+ "types": ["react"]
+ },
+ "include": ["src"]
+}
← cb8087d auto-save: 2026-07-12T08:17:05 (5 files) — .gitignore captur
·
back to Prestige Car Wash Video
·
auto-save: 2026-07-12T08:47:12 (1 files) — public/ 7985b9d →