[object Object]

← back to Prestige Car Wash Video

Remotion showcase: capture pipeline + composition scaffold

091f48725931bd317be166c8685cdb0b0a154ad0 · 2026-07-12 08:18:54 -0700 · Steve Abrams

Files touched

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 →