Catalog

Edit Props (live)

Source

387 lines
import React from "react";
import {
  AbsoluteFill,
  Sequence,
  Img,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  Easing,
  staticFile,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
import { ShaderTransition } from "./ShaderTransition";
import { PageBurn } from "./PageBurn";
import { FrostedGlassCard } from "./FrostedGlassCard";
import { AnimatedChart } from "./AnimatedChart";

loadInter("normal", { weights: ["400", "700", "900"] });
loadInter("italic", { weights: ["400", "900"] });

const FONT = "'Inter', sans-serif";

// ---- Demo timeline (30 fps) ----
// 0.0 - 3.0s  (0-90)    : Intro + GLITCH shader
// 3.0 - 5.5s  (90-165)  : DOMAIN-WARP shader
// 5.5 - 7.5s  (165-225) : WHIP-PAN shader
// 7.5 - 11.0s (225-330) : PAGE BURN transition
// 11.0 - 16.0s (330-480): Frosted glass card
// 16.0 - 21.0s (480-630): Animated bar chart
// 21.0 - 26.0s (630-780): Animated line chart
export const TIER3_DEMO_DURATION_FRAMES = 780;

// Big section label that slides up then out
const SectionLabel: React.FC<{ text: string; sub?: string }> = ({ text, sub }) => {
  const frame = useCurrentFrame();
  const { durationInFrames } = useVideoConfig();
  const inOp = interpolate(frame, [0, 8], [0, 1], { extrapolateRight: "clamp" });
  const outOp = interpolate(
    frame,
    [durationInFrames - 12, durationInFrames - 2],
    [1, 0],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
  );
  const op = Math.min(inOp, outOp);
  const yIn = interpolate(frame, [0, 14], [24, 0], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 130,
        left: 0,
        right: 0,
        textAlign: "center",
        opacity: op,
        transform: `translateY(${yIn}px)`,
        zIndex: 100,
        pointerEvents: "none",
      }}
    >
      <div
        style={{
          fontFamily: FONT,
          fontSize: 28,
          fontWeight: 700,
          letterSpacing: 10,
          color: "rgba(255,255,255,0.72)",
          marginBottom: 8,
          textShadow: "0 2px 16px rgba(0,0,0,0.85)",
        }}
      >
        TIER 3
      </div>
      <div
        style={{
          fontFamily: FONT,
          fontSize: 86,
          fontWeight: 900,
          letterSpacing: -3,
          color: "#fff",
          textShadow: "0 4px 30px rgba(0,0,0,0.9)",
          lineHeight: 1,
        }}
      >
        {text}
      </div>
      {sub && (
        <div
          style={{
            fontFamily: FONT,
            fontSize: 26,
            fontWeight: 400,
            fontStyle: "italic",
            color: "rgba(255,255,255,0.7)",
            marginTop: 10,
            textShadow: "0 2px 12px rgba(0,0,0,0.85)",
          }}
        >
          {sub}
        </div>
      )}
    </div>
  );
};

// Static image with subtle zoom — used between transitions
const StillImage: React.FC<{ src: string; scaleFrom?: number; scaleTo?: number }> = ({
  src,
  scaleFrom = 1.02,
  scaleTo = 1.08,
}) => {
  const frame = useCurrentFrame();
  const { durationInFrames } = useVideoConfig();
  const t = frame / Math.max(1, durationInFrames - 1);
  const scale = interpolate(t, [0, 1], [scaleFrom, scaleTo]);
  return (
    <AbsoluteFill style={{ backgroundColor: "#000", overflow: "hidden" }}>
      <Img
        src={staticFile(src)}
        style={{
          width: "100%",
          height: "100%",
          objectFit: "cover",
          transform: `scale(${scale})`,
        }}
      />
    </AbsoluteFill>
  );
};

// Opening title card
const OpeningTitle: React.FC = () => {
  const frame = useCurrentFrame();
  const op = interpolate(frame, [0, 10, 50, 60], [0, 1, 1, 0], {
    extrapolateRight: "clamp",
  });
  const y = interpolate(frame, [0, 14], [30, 0], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });
  // Bar draws in under title
  const barW = interpolate(frame, [8, 28], [0, 360], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  return (
    <AbsoluteFill
      style={{
        backgroundColor: "#0a0612",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        flexDirection: "column",
      }}
    >
      {/* Soft radial glow */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          background:
            "radial-gradient(circle at 50% 55%, rgba(120,60,180,0.35), transparent 60%)",
        }}
      />
      <div
        style={{
          fontFamily: FONT,
          fontSize: 30,
          fontWeight: 700,
          letterSpacing: 12,
          color: "rgba(255,255,255,0.75)",
          marginBottom: 20,
          opacity: op,
          transform: `translateY(${y}px)`,
        }}
      >
        HYPERFRAMES × REMOTION
      </div>
      <div
        style={{
          fontFamily: FONT,
          fontSize: 120,
          fontWeight: 900,
          letterSpacing: -5,
          color: "#fff",
          opacity: op,
          transform: `translateY(${y}px)`,
          lineHeight: 1,
        }}
      >
        TIER 3
      </div>
      <div
        style={{
          width: barW,
          height: 5,
          background: "#c94f3d",
          marginTop: 28,
          opacity: op,
        }}
      />
      <div
        style={{
          fontFamily: FONT,
          fontSize: 34,
          fontWeight: 400,
          fontStyle: "italic",
          color: "rgba(255,255,255,0.82)",
          marginTop: 32,
          opacity: op,
          transform: `translateY(${y}px)`,
        }}
      >
        WebGL · Burn · Glass · Charts
      </div>
    </AbsoluteFill>
  );
};

export const Tier3Demo: React.FC = () => {
  // Timeline (all in frames @ 30fps)
  const INTRO = 60;
  const SHADER_DUR = 50; // Each shader ~1.67s
  const PAGEBURN_DUR = 75; // 2.5s
  const SCENE_DUR = 150; // 5s for scenes

  let t = 0;
  const introStart = t; t += INTRO;
  const glitchStart = t; t += SHADER_DUR;
  const domainStart = t; t += SHADER_DUR;
  const whipStart = t; t += SHADER_DUR;
  const burnStart = t; t += PAGEBURN_DUR;
  const glassStart = t; t += SCENE_DUR;
  const barStart = t; t += SCENE_DUR;
  const lineStart = t; t += SCENE_DUR;

  return (
    <AbsoluteFill style={{ backgroundColor: "#000" }}>
      {/* 0. Opening title */}
      <Sequence from={introStart} durationInFrames={INTRO}>
        <OpeningTitle />
      </Sequence>

      {/* 1. GLITCH shader transition */}
      <Sequence from={glitchStart} durationInFrames={SHADER_DUR}>
        <ShaderTransition
          fromSrc="visuals/asset_001.jpg"
          toSrc="visuals/asset_002.jpg"
          shader="glitch"
          accent={[0.8, 0.2, 0.9]}
        />
        <Sequence durationInFrames={SHADER_DUR}>
          <SectionLabel text="GLITCH" sub="shader · VHS corruption" />
        </Sequence>
      </Sequence>

      {/* 2. DOMAIN-WARP shader */}
      <Sequence from={domainStart} durationInFrames={SHADER_DUR}>
        <ShaderTransition
          fromSrc="visuals/asset_002.jpg"
          toSrc="visuals/asset_003.jpg"
          shader="domain-warp"
          accent={[0.31, 0.6, 0.86]}
        />
        <Sequence durationInFrames={SHADER_DUR}>
          <SectionLabel text="DOMAIN WARP" sub="shader · ink dissolve" />
        </Sequence>
      </Sequence>

      {/* 3. WHIP-PAN shader */}
      <Sequence from={whipStart} durationInFrames={SHADER_DUR}>
        <ShaderTransition
          fromSrc="visuals/asset_003.jpg"
          toSrc="visuals/asset_004.jpg"
          shader="whip-pan"
          accent={[1.0, 0.7, 0.2]}
        />
        <Sequence durationInFrames={SHADER_DUR}>
          <SectionLabel text="WHIP PAN" sub="shader · motion blur" />
        </Sequence>
      </Sequence>

      {/* 4. PAGE BURN transition */}
      <Sequence from={burnStart} durationInFrames={PAGEBURN_DUR}>
        <PageBurn
          fromSrc="visuals/asset_004.jpg"
          toSrc="visuals/asset_005.jpg"
          accent={[255, 120, 30]}
        />
        <Sequence durationInFrames={PAGEBURN_DUR}>
          <SectionLabel text="PAGE BURN" sub="canvas · fire + embers" />
        </Sequence>
      </Sequence>

      {/* 5. FROSTED GLASS card */}
      <Sequence from={glassStart} durationInFrames={SCENE_DUR}>
        <FrostedGlassCard
          backgroundSrc="visuals/asset_005.jpg"
          artworkSrc="visuals/asset_006.jpg"
          title="Late Night Drive"
          subtitle="Midnight Tapes · Neon Nights"
          trackLabel="NOW PLAYING"
          tintColor="rgba(20, 10, 40, 0.4)"
        />
        <Sequence durationInFrames={SCENE_DUR}>
          <SectionLabel text="FROSTED GLASS" sub="scene · backdrop-filter" />
        </Sequence>
      </Sequence>

      {/* 6. ANIMATED BAR CHART */}
      <Sequence from={barStart} durationInFrames={SCENE_DUR}>
        <AnimatedChart
          mode="bar"
          title={"Engagement\nby category"}
          subtitle="Share of total views, Q1 2026"
          bars={[
            { label: "Hooks", value: 42 },
            { label: "Demos", value: 58 },
            { label: "Stories", value: 71 },
            { label: "CTAs", value: 88 },
          ]}
          counterValue={88}
          counterSuffix="%"
          accentColor="#c94f3d"
        />
      </Sequence>

      {/* 7. ANIMATED LINE CHART */}
      <Sequence from={lineStart} durationInFrames={SCENE_DUR}>
        <AnimatedChart
          mode="line"
          title={"Reel growth\nin ninety days"}
          subtitle="Weekly unique views (thousands)"
          linePoints={[12, 18, 15, 22, 28, 35, 31, 42, 58, 71, 85, 112]}
          lineLabels={["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8", "W9", "W10", "W11", "W12"]}
          counterValue={112000}
          counterSuffix=""
          accentColor="#1e6091"
          backgroundColor="#f6f4ed"
        />
      </Sequence>

      {/* Persistent grain overlay across everything */}
      <GrainOverlay />
    </AbsoluteFill>
  );
};

// Subtle film grain overlay - ported from HyperFrames grain-overlay component
const GrainOverlay: React.FC = () => {
  const frame = useCurrentFrame();
  // 10 keyframe positions, jumping every 0.5s (15 frames)
  const pos = frame % 150;
  const step = Math.floor(pos / 15);
  const offsets = [
    [0, 0], [-4, -2], [3, 1], [-2, 4], [1, -3],
    [-3, 2], [4, 0], [0, -4], [-1, 3], [2, 1],
  ];
  const [ox, oy] = offsets[step % offsets.length];
  return (
    <AbsoluteFill
      style={{
        opacity: 0.08,
        pointerEvents: "none",
        mixBlendMode: "overlay",
        zIndex: 1000,
      }}
    >
      <svg
        width="200%"
        height="200%"
        style={{ position: "absolute", transform: `translate(${ox}%, ${oy}%)` }}
      >
        <filter id="grain-noise">
          <feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="3" stitchTiles="stitch" />
          <feColorMatrix values="0 0 0 0 0.5   0 0 0 0 0.5   0 0 0 0 0.5   0 0 0 1 0" />
        </filter>
        <rect width="100%" height="100%" filter="url(#grain-noise)" />
      </svg>
    </AbsoluteFill>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: Tier3Demo
// 0 props detected · 0 port(s)

Tier3Demo

tier3demo

Tier3Demo — Andy's hand-crafted Remotion scene. 0 props. no ports detected — source-only preview.

Workflow Inputs (0)

This block has no workflow-connected inputs.

Meta

Updated
4/21/2026