โœฆ StealThis .dev

Scrolling Ticker Bar (Remotion)

A cinematic broadcast-quality news ticker built with Remotion at 1280x720. Features a continuously scrolling bottom news crawl, a secondary market data bar, a live lower-third chyron, and a full broadcast overlay with the NNX network logo, blinking live badge, and clock. Four distinct animated scenes move from intro through spotlight and outro in 15 seconds.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

import {
  AbsoluteFill,
  Easing,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from "remotion";

// โ”€โ”€โ”€ Customizable Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const NETWORK_NAME = "NNX";
const SHOW_NAME = "NNX NIGHTLY NEWS";
const CURRENT_TIME = "14:32 EST";
const TICKER_SPEED = 2.8; // pixels per frame โ€” increase to scroll faster
const MARKET_SPEED = 1.4; // pixels per frame for secondary market bar

const TICKER_ITEMS: string[] = [
  "BREAKING: Senate passes landmark infrastructure bill with bipartisan support",
  "โ—† MARKETS: Federal Reserve signals interest rate decision next Tuesday",
  "โ—† WEATHER ALERT: Category 3 hurricane approaching Gulf Coast โ€” evacuation orders issued",
  "โ—† SPORTS: Eagles defeat Cowboys 28โ€“17 in Monday Night Football showdown",
  "โ—† TECH: Major cybersecurity breach exposes 40 million user records at FinCorp",
  "โ—† WORLD: G7 leaders convene emergency summit over escalating energy crisis",
  "โ—† HEALTH: CDC issues advisory on rising flu cases across 12 states",
  "โ—† ECONOMY: US unemployment rate falls to 3.7% โ€” lowest in two decades",
  "โ—† SPORTS: World Series Game 5 tonight โ€” Yankees vs. Dodgers โ€” first pitch at 8 PM",
  "โ—† POLITICS: Governor announces emergency budget cuts amid fiscal shortfall",
  "โ—† SCIENCE: NASA confirms successful Mars mission communication window",
  "โ—† LOCAL: City council votes to expand public transit infrastructure by 2026",
];

const MARKET_ITEMS: string[] = [
  "S&P 500 โ–ฒ 1.24%",
  "ยท",
  "DOW โ–ฒ 0.87%",
  "ยท",
  "NASDAQ โ–ฒ 2.11%",
  "ยท",
  "CRUDE OIL $78.42 โ–ผ 0.32%",
  "ยท",
  "GOLD $1,923.40 โ–ฒ 0.14%",
  "ยท",
  "BTC $43,820 โ–ฒ 3.87%",
  "ยท",
  "EUR/USD 1.0842 โ–ผ 0.06%",
  "ยท",
  "10Y TREASURY 4.38% โ–ฒ 0.05",
  "ยท",
  "AAPL $189.32 โ–ฒ 1.44%",
  "ยท",
  "TSLA $248.71 โ–ผ 2.10%",
  "ยท",
  "NVDA $492.18 โ–ฒ 4.31%",
  "ยท",
  "AMZN $178.25 โ–ฒ 0.92%",
  "ยท",
];

// โ”€โ”€โ”€ Color Palette โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const BG = "#0a0e1a";
const TICKER_BG = "#0f0f0f";
const TICKER_LABEL_BG = "#cc0000";
const TICKER_LABEL_RED_DEEP = "#8b0000";
const MARKET_BAR_BG = "#1a1a2e";
const ACCENT_RED = "#e8001e";
const MARKET_CYAN = "#00d4ff";
const MARKET_GREEN = "#00c853";
const MARKET_RED = "#ff3b30";
const WHITE = "#ffffff";
const OFF_WHITE = "rgba(255,255,255,0.88)";
const SUBTLE = "rgba(255,255,255,0.4)";
const GRID_LINE = "rgba(255,255,255,0.035)";
const SEPARATOR = "rgba(255,255,255,0.12)";

// โ”€โ”€โ”€ Dimensions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const TICKER_HEIGHT = 58;
const MARKET_BAR_HEIGHT = 28;
const TICKER_LABEL_WIDTH = 188;
const MARKET_LABEL_WIDTH = 130;
const TOTAL_BOTTOM = TICKER_HEIGHT + MARKET_BAR_HEIGHT;

// โ”€โ”€โ”€ Helper: colored market token โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

function tokenColor(item: string): string {
  if (item === "ยท") return "rgba(255,255,255,0.25)";
  if (item.includes("โ–ฒ")) return MARKET_GREEN;
  if (item.includes("โ–ผ")) return MARKET_RED;
  return "rgba(255,255,255,0.65)";
}

// โ”€โ”€โ”€ Sub-component: Background โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const Background: React.FC<{ frame: number }> = ({ frame }) => {
  const bgOpacity = interpolate(frame, [0, 20], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <>
      {/* Base dark navy */}
      <div style={{ position: "absolute", inset: 0, backgroundColor: BG }} />

      {/* Radial ambient glows */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          opacity: bgOpacity,
          background: [
            "radial-gradient(ellipse at 15% 20%, rgba(232,0,30,0.06) 0%, transparent 40%)",
            "radial-gradient(ellipse at 85% 25%, rgba(0,212,255,0.05) 0%, transparent 38%)",
            "radial-gradient(ellipse at 50% 80%, rgba(0,212,255,0.03) 0%, transparent 30%)",
          ].join(", "),
          pointerEvents: "none",
        }}
      />

      {/* Grid overlay */}
      {Array.from({ length: 14 }).map((_, i) => (
        <div
          key={`col-${i}`}
          style={{
            position: "absolute",
            top: 0,
            bottom: TOTAL_BOTTOM,
            left: `${(i / 14) * 100}%`,
            width: 1,
            backgroundColor: GRID_LINE,
          }}
        />
      ))}
      {Array.from({ length: 8 }).map((_, i) => (
        <div
          key={`row-${i}`}
          style={{
            position: "absolute",
            left: 0,
            right: 0,
            top: `${(i / 8) * 100}%`,
            height: 1,
            backgroundColor: GRID_LINE,
          }}
        />
      ))}
    </>
  );
};

// โ”€โ”€โ”€ Sub-component: BroadcastOverlay โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const BroadcastOverlay: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const logoOpacity = interpolate(frame, [8, 30], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const logoY = spring({
    frame,
    fps,
    from: -14,
    to: 0,
    config: { damping: 20, stiffness: 130 },
  });

  const infoOpacity = interpolate(frame, [18, 42], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const infoY = spring({
    frame: Math.max(0, frame - 12),
    fps,
    from: -10,
    to: 0,
    config: { damping: 22, stiffness: 140 },
  });

  // Blinking live dot
  const liveBlink = Math.floor(frame / 18) % 2 === 0;

  return (
    <div
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        right: 0,
        bottom: TOTAL_BOTTOM,
        display: "flex",
        flexDirection: "column",
        justifyContent: "space-between",
        padding: "32px 52px 28px",
        pointerEvents: "none",
      }}
    >
      {/* Top row: NNX logo left, time right */}
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
        {/* Network logo block */}
        <div
          style={{
            opacity: logoOpacity,
            transform: `translateY(${logoY}px)`,
            display: "flex",
            flexDirection: "column",
            gap: 4,
          }}
        >
          {/* Logo bar */}
          <div style={{ display: "flex", alignItems: "center", gap: 0 }}>
            <div
              style={{
                width: 6,
                height: 42,
                backgroundColor: ACCENT_RED,
                marginRight: 10,
                borderRadius: 1,
              }}
            />
            <div>
              <div
                style={{
                  fontFamily: "Inter, system-ui, sans-serif",
                  fontWeight: 900,
                  fontSize: 34,
                  color: WHITE,
                  letterSpacing: "-1px",
                  lineHeight: 1,
                }}
              >
                {NETWORK_NAME}
              </div>
              <div
                style={{
                  fontFamily: "Inter, system-ui, sans-serif",
                  fontWeight: 400,
                  fontSize: 9,
                  color: ACCENT_RED,
                  letterSpacing: "3.5px",
                  textTransform: "uppercase",
                  marginTop: 2,
                }}
              >
                NEWS NETWORK
              </div>
            </div>
          </div>
        </div>

        {/* Top-right: time + live badge */}
        <div
          style={{
            opacity: infoOpacity,
            transform: `translateY(${infoY}px)`,
            display: "flex",
            flexDirection: "column",
            alignItems: "flex-end",
            gap: 8,
          }}
        >
          {/* Clock */}
          <div
            style={{
              fontFamily: "Inter, system-ui, sans-serif",
              fontWeight: 700,
              fontSize: 26,
              color: WHITE,
              letterSpacing: "2px",
              lineHeight: 1,
            }}
          >
            {CURRENT_TIME}
          </div>
          {/* Live badge */}
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 6,
              background: "rgba(232,0,30,0.15)",
              border: `1px solid rgba(232,0,30,0.4)`,
              borderRadius: 4,
              padding: "4px 12px",
            }}
          >
            <div
              style={{
                width: 7,
                height: 7,
                borderRadius: "50%",
                backgroundColor: ACCENT_RED,
                boxShadow: `0 0 8px ${ACCENT_RED}`,
                opacity: liveBlink ? 1 : 0.25,
              }}
            />
            <span
              style={{
                fontFamily: "Inter, system-ui, sans-serif",
                fontWeight: 800,
                fontSize: 11,
                color: ACCENT_RED,
                letterSpacing: "2.5px",
              }}
            >
              LIVE
            </span>
          </div>
        </div>
      </div>

      {/* Center: Show name */}
      <div
        style={{
          opacity: infoOpacity,
          transform: `translateY(${infoY}px)`,
          textAlign: "center",
          paddingBottom: 16,
        }}
      >
        <div
          style={{
            fontFamily: "Inter, system-ui, sans-serif",
            fontWeight: 300,
            fontSize: 13,
            color: SUBTLE,
            letterSpacing: "5px",
            textTransform: "uppercase",
          }}
        >
          {SHOW_NAME}
        </div>
      </div>
    </div>
  );
};

// โ”€โ”€โ”€ Sub-component: MarketBar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Thin secondary bar sitting above the main ticker. Scrolls market data.

const MarketBar: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const ITEM_GAP = 22; // px between tokens

  // Measure approximate widths per token (monospace estimation)
  const AVG_CHAR_WIDTH = 8.5;
  const FONT_SIZE = 12;
  const itemWidths = MARKET_ITEMS.map((t) =>
    t === "ยท" ? 14 : t.length * AVG_CHAR_WIDTH * (FONT_SIZE / 12) + ITEM_GAP
  );
  const totalWidth = itemWidths.reduce((a, b) => a + b, 0);
  // Duplicate for seamless loop
  const offset = (frame * MARKET_SPEED) % totalWidth;

  const barOpacity = interpolate(frame, [20, 50], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const barSlide = spring({
    frame: Math.max(0, frame - 18),
    fps,
    from: MARKET_BAR_HEIGHT,
    to: 0,
    config: { damping: 22, stiffness: 160 },
  });

  const allItems = [...MARKET_ITEMS, ...MARKET_ITEMS, ...MARKET_ITEMS];

  return (
    <div
      style={{
        position: "absolute",
        bottom: TICKER_HEIGHT,
        left: 0,
        right: 0,
        height: MARKET_BAR_HEIGHT,
        backgroundColor: MARKET_BAR_BG,
        borderTop: `1px solid rgba(0,212,255,0.18)`,
        overflow: "hidden",
        opacity: barOpacity,
        transform: `translateY(${barSlide}px)`,
        display: "flex",
        alignItems: "center",
      }}
    >
      {/* MARKETS label */}
      <div
        style={{
          position: "absolute",
          left: 0,
          top: 0,
          bottom: 0,
          width: MARKET_LABEL_WIDTH,
          background: `linear-gradient(90deg, #0a1628 0%, #0d1e3a 100%)`,
          borderRight: `1px solid rgba(0,212,255,0.25)`,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          zIndex: 3,
          flexShrink: 0,
        }}
      >
        <span
          style={{
            fontFamily: "Inter, system-ui, sans-serif",
            fontWeight: 700,
            fontSize: 10,
            color: MARKET_CYAN,
            letterSpacing: "2.5px",
            textTransform: "uppercase",
          }}
        >
          MARKETS
        </span>
      </div>

      {/* Fade left */}
      <div
        style={{
          position: "absolute",
          left: MARKET_LABEL_WIDTH,
          top: 0,
          bottom: 0,
          width: 32,
          background: `linear-gradient(90deg, ${MARKET_BAR_BG} 0%, transparent 100%)`,
          zIndex: 2,
          pointerEvents: "none",
        }}
      />

      {/* Scrolling market tokens */}
      <div
        style={{
          position: "absolute",
          left: MARKET_LABEL_WIDTH,
          right: 0,
          top: 0,
          bottom: 0,
          overflow: "hidden",
        }}
      >
        <div
          style={{
            display: "flex",
            alignItems: "center",
            height: "100%",
            transform: `translateX(-${offset}px)`,
            willChange: "transform",
            gap: 0,
          }}
        >
          {allItems.map((item, i) => (
            <span
              key={i}
              style={{
                fontFamily: "Inter, system-ui, sans-serif",
                fontWeight: item === "ยท" ? 400 : 600,
                fontSize: 11,
                color: tokenColor(item),
                whiteSpace: "nowrap",
                paddingRight: item === "ยท" ? 14 : ITEM_GAP,
                paddingLeft: item === "ยท" ? 0 : 0,
                flexShrink: 0,
              }}
            >
              {item}
            </span>
          ))}
        </div>
      </div>

      {/* Fade right */}
      <div
        style={{
          position: "absolute",
          right: 0,
          top: 0,
          bottom: 0,
          width: 60,
          background: `linear-gradient(270deg, ${MARKET_BAR_BG} 0%, transparent 100%)`,
          zIndex: 2,
          pointerEvents: "none",
        }}
      />
    </div>
  );
};

// โ”€โ”€โ”€ Sub-component: TickerBar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// The main bottom news crawl. Scrolls TICKER_ITEMS continuously.

const TickerBar: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  // Approximate character widths at fontSize=15 Inter
  const CHAR_WIDTH = 8.6;
  const ITEM_FONT_SIZE = 15;
  const SEPARATOR_WIDTH = 32; // px for "โ—†" separator
  const ITEM_PADDING = 28; // right padding per item

  // Compute widths per item
  const itemWidths = TICKER_ITEMS.map((t) => {
    const text = t.startsWith("โ—† ") ? t.slice(2) : t;
    const hasSep = t.startsWith("โ—† ");
    return text.length * CHAR_WIDTH * (ITEM_FONT_SIZE / 15) + ITEM_PADDING + (hasSep ? SEPARATOR_WIDTH : 0);
  });
  const totalWidth = itemWidths.reduce((a, b) => a + b, 0);
  // Loop by duplicating twice
  const offset = (frame * TICKER_SPEED) % totalWidth;

  const barOpacity = interpolate(frame, [30, 60], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const barSlide = spring({
    frame: Math.max(0, frame - 28),
    fps,
    from: TICKER_HEIGHT + 10,
    to: 0,
    config: { damping: 20, stiffness: 150 },
  });

  const allItems = [...TICKER_ITEMS, ...TICKER_ITEMS, ...TICKER_ITEMS];

  return (
    <div
      style={{
        position: "absolute",
        bottom: 0,
        left: 0,
        right: 0,
        height: TICKER_HEIGHT,
        backgroundColor: TICKER_BG,
        borderTop: `2px solid ${ACCENT_RED}`,
        overflow: "hidden",
        opacity: barOpacity,
        transform: `translateY(${barSlide}px)`,
        display: "flex",
        alignItems: "center",
      }}
    >
      {/* BREAKING label โ€” left anchor */}
      <div
        style={{
          position: "absolute",
          left: 0,
          top: 0,
          bottom: 0,
          width: TICKER_LABEL_WIDTH,
          background: `linear-gradient(90deg, ${TICKER_LABEL_RED_DEEP} 0%, ${TICKER_LABEL_BG} 100%)`,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          zIndex: 4,
          flexShrink: 0,
        }}
      >
        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2 }}>
          <span
            style={{
              fontFamily: "Inter, system-ui, sans-serif",
              fontWeight: 900,
              fontSize: 15,
              color: WHITE,
              letterSpacing: "3px",
              textTransform: "uppercase",
              lineHeight: 1,
            }}
          >
            BREAKING
          </span>
          <div
            style={{
              width: 36,
              height: 1,
              backgroundColor: "rgba(255,255,255,0.35)",
              borderRadius: 1,
            }}
          />
          <span
            style={{
              fontFamily: "Inter, system-ui, sans-serif",
              fontWeight: 500,
              fontSize: 9,
              color: "rgba(255,255,255,0.7)",
              letterSpacing: "2.5px",
              textTransform: "uppercase",
            }}
          >
            NEWS
          </span>
        </div>
        {/* Right edge divider */}
        <div
          style={{
            position: "absolute",
            right: 0,
            top: 0,
            bottom: 0,
            width: 2,
            background: "rgba(255,255,255,0.2)",
          }}
        />
      </div>

      {/* Left fade gradient after label */}
      <div
        style={{
          position: "absolute",
          left: TICKER_LABEL_WIDTH,
          top: 0,
          bottom: 0,
          width: 40,
          background: `linear-gradient(90deg, ${TICKER_BG} 0%, transparent 100%)`,
          zIndex: 3,
          pointerEvents: "none",
        }}
      />

      {/* Scrolling news items */}
      <div
        style={{
          position: "absolute",
          left: TICKER_LABEL_WIDTH,
          right: 0,
          top: 0,
          bottom: 0,
          overflow: "hidden",
        }}
      >
        <div
          style={{
            display: "flex",
            alignItems: "center",
            height: "100%",
            transform: `translateX(-${offset}px)`,
            willChange: "transform",
          }}
        >
          {allItems.map((item, i) => {
            const hasSep = item.startsWith("โ—† ");
            const text = hasSep ? item.slice(2) : item;
            return (
              <div
                key={i}
                style={{
                  display: "flex",
                  alignItems: "center",
                  flexShrink: 0,
                  height: "100%",
                  paddingRight: ITEM_PADDING,
                }}
              >
                {hasSep && (
                  <span
                    style={{
                      fontFamily: "Inter, system-ui, sans-serif",
                      fontWeight: 900,
                      fontSize: 13,
                      color: ACCENT_RED,
                      marginRight: 18,
                      flexShrink: 0,
                    }}
                  >
                    โ—†
                  </span>
                )}
                <span
                  style={{
                    fontFamily: "Inter, system-ui, sans-serif",
                    fontWeight: 600,
                    fontSize: ITEM_FONT_SIZE,
                    color: OFF_WHITE,
                    whiteSpace: "nowrap",
                    letterSpacing: "0.2px",
                  }}
                >
                  {text}
                </span>
              </div>
            );
          })}
        </div>
      </div>

      {/* Right fade gradient */}
      <div
        style={{
          position: "absolute",
          right: 0,
          top: 0,
          bottom: 0,
          width: 80,
          background: `linear-gradient(270deg, ${TICKER_BG} 0%, transparent 100%)`,
          zIndex: 3,
          pointerEvents: "none",
        }}
      />
    </div>
  );
};

// โ”€โ”€โ”€ Sub-component: MainContentArea โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// The main broadcast visual above the tickers: a simulated live news desk frame.

const MainContentArea: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const contentOpacity = interpolate(frame, [12, 45], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.quad),
  });

  // โ”€โ”€ Scene 1 (0โ€“120): Main broadcast area wipes in
  // Lower-third chyron entrance
  const chyronDelay = 55;
  const chyronOpacity = interpolate(frame, [chyronDelay, chyronDelay + 25], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const chyronX = spring({
    frame: Math.max(0, frame - chyronDelay),
    fps,
    from: -60,
    to: 0,
    config: { damping: 18, stiffness: 110 },
  });

  // โ”€โ”€ Scene 2 (120โ€“270): Secondary info cards appear
  const scene2Start = 120;
  const infoCardOpacity = interpolate(frame, [scene2Start, scene2Start + 30], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const infoCardY = spring({
    frame: Math.max(0, frame - scene2Start),
    fps,
    from: 20,
    to: 0,
    config: { damping: 22, stiffness: 130 },
  });

  // โ”€โ”€ Scene 3 (270โ€“390): Spotlight on ticker scrolling โ€” dim content overlay
  const scene3Start = 270;
  const spotlightDimOpacity = interpolate(frame, [scene3Start, scene3Start + 40], [0, 0.45], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const spotlightLabelOpacity = interpolate(frame, [scene3Start + 20, scene3Start + 50], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const spotlightLabelY = spring({
    frame: Math.max(0, frame - (scene3Start + 20)),
    fps,
    from: 12,
    to: 0,
    config: { damping: 20, stiffness: 140 },
  });

  // โ”€โ”€ Scene 4 (390โ€“450): Outro โ€” network stamp
  const scene4Start = 390;
  const outroOpacity = interpolate(frame, [scene4Start, scene4Start + 30], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const outroScale = spring({
    frame: Math.max(0, frame - scene4Start),
    fps,
    from: 0.85,
    to: 1,
    config: { damping: 16, stiffness: 120 },
  });

  // Blinking LIVE dot for chyron
  const livePulse = Math.floor(frame / 16) % 2 === 0;

  return (
    <div
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        right: 0,
        bottom: TOTAL_BOTTOM,
        opacity: contentOpacity,
        overflow: "hidden",
      }}
    >
      {/* โ”€โ”€ Simulated broadcast frame area โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */}

      {/* Central visual: widescreen "live feed" placeholder frame */}
      <div
        style={{
          position: "absolute",
          top: 100,
          left: 52,
          right: 52,
          bottom: 80,
          border: `1px solid rgba(255,255,255,0.06)`,
          borderRadius: 4,
          overflow: "hidden",
          background: "linear-gradient(160deg, #0f1520 0%, #080b13 60%, #0a0d17 100%)",
        }}
      >
        {/* Horizontal scan-line texture */}
        {Array.from({ length: 28 }).map((_, i) => (
          <div
            key={`scan-${i}`}
            style={{
              position: "absolute",
              left: 0,
              right: 0,
              top: `${(i / 28) * 100}%`,
              height: 1,
              backgroundColor: "rgba(255,255,255,0.012)",
            }}
          />
        ))}

        {/* Subtle vignette */}
        <div
          style={{
            position: "absolute",
            inset: 0,
            background:
              "radial-gradient(ellipse at 50% 50%, transparent 35%, rgba(0,0,0,0.6) 100%)",
            pointerEvents: "none",
          }}
        />

        {/* Corner brackets โ€” broadcast frame markers */}
        {[
          { top: 10, left: 10 },
          { top: 10, right: 10 },
          { bottom: 10, left: 10 },
          { bottom: 10, right: 10 },
        ].map((pos, i) => (
          <div
            key={`bracket-${i}`}
            style={{
              position: "absolute",
              ...pos,
              width: 20,
              height: 20,
              borderTop: i < 2 ? `2px solid rgba(232,0,30,0.5)` : "none",
              borderBottom: i >= 2 ? `2px solid rgba(232,0,30,0.5)` : "none",
              borderLeft: i % 2 === 0 ? `2px solid rgba(232,0,30,0.5)` : "none",
              borderRight: i % 2 === 1 ? `2px solid rgba(232,0,30,0.5)` : "none",
            }}
          />
        ))}

        {/* Center NNX watermark on live feed */}
        <div
          style={{
            position: "absolute",
            top: "50%",
            left: "50%",
            transform: "translate(-50%, -50%)",
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            gap: 10,
            opacity: 0.08,
          }}
        >
          <div
            style={{
              fontFamily: "Inter, system-ui, sans-serif",
              fontWeight: 900,
              fontSize: 90,
              color: WHITE,
              letterSpacing: "-4px",
              lineHeight: 1,
            }}
          >
            {NETWORK_NAME}
          </div>
          <div
            style={{
              fontFamily: "Inter, system-ui, sans-serif",
              fontWeight: 300,
              fontSize: 14,
              color: WHITE,
              letterSpacing: "8px",
              textTransform: "uppercase",
            }}
          >
            LIVE BROADCAST
          </div>
        </div>

        {/* Scene 2: Info cards (frames 120+) */}
        <div
          style={{
            position: "absolute",
            top: 32,
            right: 32,
            display: "flex",
            flexDirection: "column",
            gap: 12,
            opacity: infoCardOpacity,
            transform: `translateY(${infoCardY}px)`,
          }}
        >
          {[
            { label: "VIEWERS", value: "4.2M", color: MARKET_CYAN },
            { label: "ON-AIR", value: "3:42:17", color: MARKET_GREEN },
            { label: "FEEDS", value: "12 ACTIVE", color: "rgba(255,255,255,0.6)" },
          ].map((stat) => (
            <div
              key={stat.label}
              style={{
                background: "rgba(0,0,0,0.55)",
                border: `1px solid rgba(255,255,255,0.08)`,
                borderRadius: 4,
                padding: "8px 14px",
                textAlign: "right",
              }}
            >
              <div
                style={{
                  fontFamily: "Inter, system-ui, sans-serif",
                  fontWeight: 400,
                  fontSize: 9,
                  color: SUBTLE,
                  letterSpacing: "2px",
                  textTransform: "uppercase",
                  marginBottom: 2,
                }}
              >
                {stat.label}
              </div>
              <div
                style={{
                  fontFamily: "Inter, system-ui, sans-serif",
                  fontWeight: 700,
                  fontSize: 16,
                  color: stat.color,
                  letterSpacing: "0.5px",
                }}
              >
                {stat.value}
              </div>
            </div>
          ))}
        </div>

        {/* Scene 3: spotlight dim */}
        <div
          style={{
            position: "absolute",
            inset: 0,
            backgroundColor: `rgba(0,0,0,${spotlightDimOpacity})`,
            pointerEvents: "none",
          }}
        />

        {/* Scene 3: TICKER SPOTLIGHT label */}
        <div
          style={{
            position: "absolute",
            top: "50%",
            left: "50%",
            transform: `translate(-50%, calc(-50% + ${spotlightLabelY}px))`,
            opacity: spotlightLabelOpacity,
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            gap: 8,
          }}
        >
          <div
            style={{
              fontFamily: "Inter, system-ui, sans-serif",
              fontWeight: 300,
              fontSize: 12,
              color: MARKET_CYAN,
              letterSpacing: "5px",
              textTransform: "uppercase",
            }}
          >
            LIVE TICKER
          </div>
          <div
            style={{
              fontFamily: "Inter, system-ui, sans-serif",
              fontWeight: 800,
              fontSize: 32,
              color: WHITE,
              letterSpacing: "0px",
              textAlign: "center",
              lineHeight: 1.1,
            }}
          >
            CONTINUOUS
            <br />
            NEWS CRAWL
          </div>
          <div
            style={{
              width: 60,
              height: 2,
              backgroundColor: ACCENT_RED,
              borderRadius: 1,
            }}
          />
        </div>

        {/* Scene 4: Outro NNX stamp */}
        <div
          style={{
            position: "absolute",
            inset: 0,
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            justifyContent: "center",
            opacity: outroOpacity,
            transform: `scale(${outroScale})`,
            background: "rgba(0,0,0,0.75)",
          }}
        >
          <div
            style={{
              fontFamily: "Inter, system-ui, sans-serif",
              fontWeight: 900,
              fontSize: 72,
              color: WHITE,
              letterSpacing: "-3px",
              lineHeight: 1,
            }}
          >
            {NETWORK_NAME}
          </div>
          <div
            style={{
              fontFamily: "Inter, system-ui, sans-serif",
              fontWeight: 300,
              fontSize: 13,
              color: ACCENT_RED,
              letterSpacing: "7px",
              textTransform: "uppercase",
              marginTop: 8,
            }}
          >
            NIGHTLY NEWS
          </div>
          <div
            style={{
              width: 80,
              height: 2,
              background: `linear-gradient(90deg, transparent, ${ACCENT_RED}, transparent)`,
              marginTop: 16,
            }}
          />
        </div>
      </div>

      {/* โ”€โ”€ Lower-third chyron (frames 55+) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */}
      <div
        style={{
          position: "absolute",
          bottom: 92,
          left: 52,
          right: 52,
          opacity: chyronOpacity,
          transform: `translateX(${chyronX}px)`,
        }}
      >
        <div
          style={{
            display: "flex",
            alignItems: "stretch",
            height: 54,
            overflow: "hidden",
          }}
        >
          {/* Red accent block */}
          <div
            style={{
              width: 8,
              backgroundColor: ACCENT_RED,
              flexShrink: 0,
            }}
          />
          {/* BREAKING label */}
          <div
            style={{
              backgroundColor: ACCENT_RED,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              padding: "0 18px",
              flexShrink: 0,
            }}
          >
            <span
              style={{
                fontFamily: "Inter, system-ui, sans-serif",
                fontWeight: 900,
                fontSize: 11,
                color: WHITE,
                letterSpacing: "3px",
                textTransform: "uppercase",
              }}
            >
              BREAKING NEWS
            </span>
          </div>
          {/* Headline text */}
          <div
            style={{
              flex: 1,
              background: "rgba(0,0,0,0.88)",
              display: "flex",
              alignItems: "center",
              padding: "0 20px",
              borderRight: `1px solid ${SEPARATOR}`,
            }}
          >
            <span
              style={{
                fontFamily: "Inter, system-ui, sans-serif",
                fontWeight: 700,
                fontSize: 17,
                color: WHITE,
                letterSpacing: "0.1px",
                lineHeight: 1.2,
              }}
            >
              Senate passes landmark infrastructure bill with bipartisan support
            </span>
          </div>
          {/* Live indicator block */}
          <div
            style={{
              backgroundColor: "rgba(0,0,0,0.88)",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              padding: "0 16px",
              gap: 6,
              flexShrink: 0,
              borderLeft: `1px solid ${SEPARATOR}`,
            }}
          >
            <div
              style={{
                width: 6,
                height: 6,
                borderRadius: "50%",
                backgroundColor: ACCENT_RED,
                boxShadow: `0 0 6px ${ACCENT_RED}`,
                opacity: livePulse ? 1 : 0.2,
              }}
            />
            <span
              style={{
                fontFamily: "Inter, system-ui, sans-serif",
                fontWeight: 800,
                fontSize: 10,
                color: ACCENT_RED,
                letterSpacing: "2px",
              }}
            >
              LIVE
            </span>
          </div>
        </div>

        {/* Thin accent line below chyron */}
        <div
          style={{
            height: 2,
            background: `linear-gradient(90deg, ${ACCENT_RED} 0%, rgba(232,0,30,0.3) 60%, transparent 100%)`,
          }}
        />
      </div>
    </div>
  );
};

// โ”€โ”€โ”€ Main Component โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// โ”€โ”€ Scene breakdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Scene 0 โ€“  0โ€“30:   Background + overlay fade in. Ticker starts scrolling.
// Scene 1 โ€“ 30โ€“120:  Broadcast frame + chyron entrance (frame 55). Market bar slides up.
// Scene 2 โ€“ 120โ€“270: Info cards appear top-right. Main crawl at full speed.
// Scene 3 โ€“ 270โ€“390: Spotlight dim on broadcast area. "CONTINUOUS NEWS CRAWL" label.
// Scene 4 โ€“ 390โ€“450: NNX outro stamp fades in. Ticker keeps rolling.

export default function TickerBar() {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill style={{ backgroundColor: BG, overflow: "hidden" }}>
      {/* Background */}
      <Background frame={frame} />

      {/* Main broadcast content area */}
      <MainContentArea frame={frame} fps={fps} />

      {/* Broadcast overlay (logo, time, live badge) */}
      <BroadcastOverlay frame={frame} fps={fps} />

      {/* Secondary market bar */}
      <MarketBar frame={frame} fps={fps} />

      {/* Primary news ticker */}
      <TickerBar frame={frame} fps={fps} />

      {/* Bottom-right watermark */}
      <div
        style={{
          position: "absolute",
          bottom: TOTAL_BOTTOM + 10,
          right: 52,
          fontFamily: "Inter, system-ui, sans-serif",
          fontWeight: 400,
          fontSize: 9,
          color: "rgba(255,255,255,0.1)",
          letterSpacing: "2px",
          textTransform: "uppercase",
          opacity: interpolate(frame, [60, 90], [0, 1], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
          }),
        }}
      >
        FICTIONAL DATA ยท STEALTHIS
      </div>
    </AbsoluteFill>
  );
}

Scrolling Ticker Bar

This composition recreates the iconic 24-hour news channel ticker with broadcast-quality visual fidelity. The layout occupies the full 1280x720 canvas: a dark navy #0a0e1a background overlaid with a subtle grid and radial glows provides depth, while a simulated widescreen live-feed frame fills the center content area. The NNX logo with a red accent bar appears top-left, a blinking LIVE badge and 14:32 EST clock sit top-right, and the show name anchors the center bottom of the overlay.

The bottom of the frame is divided into two scrolling bars. The primary Ticker Bar (58px tall) carries 12 breaking-news headlines separated by red โ—† diamond separators, scrolling left at TICKER_SPEED pixels per frame with a dark-red gradient BREAKING label on the left. Directly above it, a thinner Market Bar (28px) scrolls cyan-styled market tokens โ€” S&P, DOW, NASDAQ, crude, gold, BTC, individual equities โ€” at half the speed, prefixed by a MARKETS label. A lower-third chyron slides in at frame 55 with a spring-driven x-entrance (damping: 18, stiffness: 110), displaying the lead headline against a full-bleed red label block.

Four scenes structure the 450-frame runtime. The broadcast frame and bars fade in during the opening 30 frames using interpolate over [0, 20]. Info cards with viewer counts and feed stats spring up from translateY(20px) at frame 120. From frame 270 a spotlight dim (rgba(0,0,0,0.45)) descends over the live feed area, revealing a centered CONTINUOUS NEWS CRAWL label. The final scene (frame 390) springs in an NNX network stamp at scale(0.85 โ†’ 1) while the ticker continues uninterrupted beneath.

Composition specs

PropertyValue
Resolution1280 ร— 720
FPS30
Duration15.0 s (450 frames)

Timeline

TimeFramesAction
0:00 โ€“ 1:00 s0 โ€“ 30Background fades in, grid and radial glows appear, ticker starts scrolling
0:18 โ€“ 1:00 s8 โ€“ 30NNX logo slides down from translateY(-14px) via spring
0:60 โ€“ 1:40 s18 โ€“ 50Market bar slides up from bottom, time and live badge fade in
1:83 โ€“ 2:67 s55 โ€“ 80Lower-third chyron slides in from translateX(-60px)
4:00 โ€“ 5:00 s120 โ€“ 150Info stat cards spring into top-right corner
9:00 โ€“ 10:33 s270 โ€“ 310Spotlight dim layer descends; CONTINUOUS NEWS CRAWL label springs up
13:00 โ€“ 14:00 s390 โ€“ 420NNX outro stamp fades and scales in over live feed area
0 โ€“ 15:00 s0 โ€“ 450Primary ticker and market bar scroll continuously throughout

Customization

  • NETWORK_NAME โ€” channel identifier displayed in the logo block (default: "NNX")
  • SHOW_NAME โ€” centered show title in the broadcast overlay (default: "NNX NIGHTLY NEWS")
  • CURRENT_TIME โ€” time string shown top-right (default: "14:32 EST")
  • TICKER_SPEED โ€” pixels per frame for the primary news crawl (default: 2.8; increase to scroll faster)
  • MARKET_SPEED โ€” pixels per frame for the market bar (default: 1.4)
  • TICKER_ITEMS โ€” array of news headline strings; items starting with โ—† render a red diamond separator
  • MARKET_ITEMS โ€” array of market tokens and "ยท" dot separators with auto-colored deltas
  • TICKER_HEIGHT / MARKET_BAR_HEIGHT โ€” pixel heights of the two bottom bars
  • TICKER_LABEL_WIDTH โ€” width of the red BREAKING label block (default: 188px)
  • ACCENT_RED โ€” primary brand red used on BREAKING label, chyron, separators (default: #e8001e)
  • MARKET_CYAN โ€” accent color for the MARKETS label and upward market moves (default: #00d4ff)