← GPT-AI-VIDEO
WEBSITE · 24.4 KB

vendor/OpenMontage/.agents/skills/hyperframes-animation/examples/demo-page-scroll-spotlight.html

Workspace snapshot · 09/04 14:52

Preview

Source
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=1920, height=1080" />
    <title>Scene 04 — Contextual Product Showcase</title>

    <!--
      HyperFrames composition.tsx.

      Choreography (4 phases, 9 seconds total):
        0.00 – 0.80s   3D-tilted page card scales 0.95 → 1.0
                       Navbar / title / CTA fade in (staggered)
        0.12 – 2.80s   Title keywords glow synced to ASR words:
                         "1" "long" "video" "10" "viral" "clips"
        3.08 – 4.08s   Page content scrolls up 280 px (programmatic scroll feel)
                       Carousel section fades in + scales from 0.9
        3.58 – 8.84s   Main video pops forward in 3D (translateZ 80 px) + scales up
                       Radial spotlight dims the surroundings; decays at end
          (attack 0→1 then decay 1→REST_LEVEL). All visual effects (color,
          text-shadow, scale) derive from --glow via CSS calc()
        - Pop-target uses the same --glow pattern; translateZ + scale derived
        - Spotlight is a separate overlay with simple opacity tween
        - Video placeholders are colored divs (no asset files needed)
        - Tilt is static CSS, never tweened
    -->

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>

    <style>
      :root {
        --page-bg: #0a0a0f;
        --text-primary: #ffffff;
        --text-secondary: #a1a1aa;
        --accent: #edcb50;
        --input-bg: #1a1a1f;
        --green: #22c55e;
      }
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        margin: 0;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        background: #000;
        font-family:
          "Inter",
          system-ui,
          -apple-system,
          sans-serif;
        color: var(--text-primary);
      }

      /* ============================================================
         BACKGROUND
         ============================================================ */
      .bg {
        position: absolute;
        inset: 0;
        background:
          radial-gradient(
            ellipse 100% 60% at 50% -10%,
            rgba(100, 100, 150, 0.08) 0%,
            transparent 60%
          ),
          var(--page-bg);
      }

      /* ============================================================
         3D PERSPECTIVE + TILTED CARD
         ============================================================ */
      .perspective-wrap {
        position: absolute;
        inset: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        perspective: 1200px;
      }
      .page-card {
        width: 92%;
        height: 88%;
        background-color: var(--page-bg);
        border-radius: 20px;
        overflow: hidden;
        transform-style: preserve-3d;
        /* tilt + initial scale set by gsap.set() below so subsequent tweens preserve them */
        box-shadow:
          -30px 30px 60px rgba(0, 0, 0, 0.4),
          -15px 15px 30px rgba(0, 0, 0, 0.3),
          0 0 80px rgba(0, 0, 0, 0.2);
        border: 1px solid rgba(255, 255, 255, 0.1);
        position: relative;
      }

      /* ============================================================
         NAVBAR (sticks to top of card; NOT inside scroll content)
         ============================================================ */
      .page-navbar {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 72px;
        padding: 0 48px;
        display: flex;
        align-items: center;
        justify-content: space-between;
        z-index: 100;
        opacity: 0; /* GSAP fades in */
        background: linear-gradient(to bottom, rgba(10, 10, 15, 0.95), rgba(10, 10, 15, 0.6));
      }
      .nav-left {
        display: flex;
        align-items: center;
        gap: 10px;
      }
      .nav-logo {
        width: 32px;
        height: 32px;
        color: var(--text-primary);
      }
      .nav-brand {
        font-size: 20px;
        font-weight: 600;
        letter-spacing: -0.5px;
      }
      .nav-center {
        display: flex;
        align-items: center;
        gap: 28px;
      }
      .nav-item {
        font-size: 14px;
        font-weight: 500;
        color: var(--text-secondary);
        display: flex;
        align-items: center;
      }
      .nav-item .chev {
        margin-left: 4px;
        font-size: 10px;
      }
      .nav-tag {
        font-size: 10px;
        background: var(--green);
        color: #fff;
        padding: 2px 6px;
        border-radius: 4px;
        font-weight: 600;
        margin-right: 6px;
      }
      .nav-right {
        display: flex;
        align-items: center;
        gap: 12px;
      }
      .nav-signin {
        padding: 8px 16px;
        border: 1px solid rgba(255, 255, 255, 0.2);
        border-radius: 8px;
        color: var(--text-primary);
        font-size: 14px;
        font-weight: 500;
      }
      .nav-signup {
        padding: 8px 16px;
        background: var(--accent);
        border-radius: 8px;
        color: var(--page-bg);
        font-size: 14px;
        font-weight: 600;
      }

      /* ============================================================
         SCROLL CONTENT — GSAP tweens its y
         ============================================================ */
      .scroll-content {
        padding-top: 140px;
        padding-bottom: 60px;
        display: flex;
        flex-direction: column;
        align-items: center;
        will-change: transform;
      }

      /* ============================================================
         HERO SECTION
         ============================================================ */
      .hero {
        text-align: center;
        max-width: 1200px;
        width: 90%;
        opacity: 0; /* GSAP fades in */
      }
      .hero-eyebrow {
        font-size: 14px;
        color: var(--accent);
        text-transform: uppercase;
        letter-spacing: 2px;
        margin-bottom: 24px;
        font-weight: 600;
      }
      .hero-title {
        font-size: 64px;
        font-weight: 700;
        line-height: 1.1;
        letter-spacing: -1px;
      }
      .hero-sub {
        font-size: 20px;
        color: rgba(255, 255, 255, 0.6);
        margin-top: 24px;
        font-weight: 400;
        line-height: 1.5;
        max-width: 800px;
        margin-left: auto;
        margin-right: auto;
      }

      /* ============================================================
         KEYWORD GLOW — all derived from --glow CSS custom property
         ============================================================ */
      .kw {
        --glow: 0;
        display: inline-block;
        position: relative;
        z-index: 0;
        padding: 0 0.015em;
        color: rgb(
          calc(255 - var(--glow) * 18) calc(255 - var(--glow) * 30) calc(255 - var(--glow) * 150)
        );
        text-shadow:
          0 0 calc(var(--glow) * 10px) rgba(255, 235, 120, calc(var(--glow) * 0.95)),
          0 0 calc(var(--glow) * 24px) rgba(237, 203, 80, calc(var(--glow) * 0.78)),
          0 0 calc(var(--glow) * 44px) rgba(237, 203, 80, calc(var(--glow) * 0.38));
        transform: scale(calc(1 + var(--glow) * 0.035));
      }
      .kw::before {
        content: "";
        position: absolute;
        left: -0.025em;
        right: -0.025em;
        top: 0.08em;
        bottom: 0.05em;
        z-index: -1;
        border-radius: 0.16em;
        background: linear-gradient(
          90deg,
          rgba(237, 203, 80, 0.08),
          rgba(255, 222, 80, 0.72),
          rgba(237, 203, 80, 0.12)
        );
        opacity: calc(var(--glow) * 0.78);
        transform: scaleX(calc(0.72 + var(--glow) * 0.24)) skewX(-5deg);
        transform-origin: left center;
        box-shadow:
          0 0 calc(var(--glow) * 14px) rgba(237, 203, 80, calc(var(--glow) * 0.7)),
          0 0 calc(var(--glow) * 28px) rgba(237, 203, 80, calc(var(--glow) * 0.32));
      }

      /* ============================================================
         CTA INPUT ROW
         ============================================================ */
      .cta-row {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 16px;
        margin-top: 36px;
        opacity: 0; /* GSAP fades in */
      }
      .cta-input {
        display: flex;
        align-items: center;
        background: var(--input-bg);
        border-radius: 28px;
        padding: 6px 6px 6px 18px;
        gap: 12px;
      }
      .cta-link-icon {
        width: 16px;
        height: 16px;
        color: rgba(255, 255, 255, 0.4);
      }
      .cta-placeholder {
        font-size: 14px;
        color: rgba(255, 255, 255, 0.4);
        min-width: 130px;
      }
      .cta-button {
        padding: 12px 24px;
        background: #fff;
        border-radius: 22px;
        color: var(--page-bg);
        font-size: 14px;
        font-weight: 600;
      }
      .cta-or {
        color: rgba(255, 255, 255, 0.4);
        font-size: 14px;
      }
      .cta-upload {
        padding: 12px 24px;
        border: 1px solid rgba(255, 255, 255, 0.2);
        border-radius: 22px;
        color: var(--text-primary);
        font-size: 14px;
        font-weight: 500;
      }

      /* ============================================================
         VIDEO CAROUSEL (Phase 2 entry, Phase 4 pop-out)
         ============================================================ */
      .carousel-wrap {
        opacity: 0; /* GSAP fades in */
        transform: scale(0.9); /* GSAP scales to 1 */
        transform-origin: center top;
        margin-top: 48px;
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 20px;
        width: 100%;
        max-width: 1400px;
        padding: 0 20px;
        perspective: 800px;
        transform-style: preserve-3d;
      }
      .carousel-arrow {
        width: 44px;
        height: 44px;
        border-radius: 50%;
        background: rgba(60, 60, 65, 0.8);
        display: flex;
        align-items: center;
        justify-content: center;
        color: #fff;
        flex-shrink: 0;
        font-size: 24px;
        font-weight: 300;
      }

      /* Main video — Phase 4 pop-out target */
      .carousel-main {
        --glow: 0;
        width: 720px;
        height: 420px;
        background: var(--input-bg);
        border-radius: 16px;
        position: relative;
        overflow: hidden;
        flex-shrink: 0;
        transform-style: preserve-3d;
        transform: translateZ(calc(var(--glow) * 80px)) scale(calc(1 + var(--glow) * 0.15));
        box-shadow:
          0 0 calc(var(--glow) * 25px) rgba(237, 203, 80, calc(var(--glow) * 0.7)),
          0 0 calc(var(--glow) * 50px) rgba(237, 203, 80, calc(var(--glow) * 0.4)),
          0 calc(20px + var(--glow) * 40px) calc(60px + var(--glow) * 40px)
            rgba(0, 0, 0, calc(0.6 + var(--glow) * 0.2));
        border: 3px solid rgba(237, 203, 80, calc(var(--glow) * 0.8));
        z-index: 200;
      }
      .carousel-main-placeholder {
        position: absolute;
        inset: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 28px;
        font-weight: 600;
        color: rgba(255, 255, 255, 0.5);
        background: linear-gradient(135deg, #1a2540 0%, #3d2a4f 50%, #4f1d2e 100%);
      }
      .carousel-main-placeholder span {
        background: rgba(0, 0, 0, 0.4);
        padding: 8px 20px;
        border-radius: 8px;
      }

      /* Side panel */
      .carousel-side {
        width: 420px;
        height: 280px;
        background: var(--input-bg);
        border-radius: 16px;
        position: relative;
        overflow: hidden;
        flex-shrink: 0;
      }
      .carousel-side-placeholder {
        position: absolute;
        inset: 0;
        background: linear-gradient(135deg, #2a3550 0%, #4a3a5a 100%);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 18px;
        font-weight: 600;
        color: rgba(255, 255, 255, 0.5);
      }
      .presets-stack {
        position: absolute;
        top: 16px;
        right: 16px;
        display: flex;
        flex-direction: column;
        gap: 8px;
        z-index: 5;
      }
      .preset-label {
        padding: 8px 14px;
        background: rgba(255, 255, 255, 0.12);
        border-radius: 8px;
        color: rgba(255, 255, 255, 0.8);
        font-size: 12px;
        font-weight: 500;
      }
      .preset-tile {
        width: 44px;
        height: 44px;
        background: rgba(255, 255, 255, 0.08);
        border-radius: 8px;
        align-self: flex-end;
      }

      /* ============================================================
         SPOTLIGHT OVERLAY (above page card, dims surroundings)
         ============================================================ */
      .spotlight {
        position: absolute;
        inset: 0;
        background: radial-gradient(
          ellipse 850px 550px at 40% 60%,
          transparent 0%,
          transparent 50%,
          rgba(0, 0, 0, 0.65) 100%
        );
        pointer-events: none;
        opacity: 0; /* GSAP fades in during Phase 4 */
        z-index: 150;
      }

      /* ============================================================
         VIGNETTE
         ============================================================ */
      .vignette {
        position: absolute;
        inset: 0;
        background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.3) 100%);
        pointer-events: none;
        z-index: 400;
      }
    </style>
  </head>
  <body>
    <div
      id="root"
      data-composition-id="main"
      data-start="0"
      data-duration="9"
      data-width="1920"
      data-height="1080"
    >
      <div class="bg"></div>

      <div class="perspective-wrap">
        <div
          class="page-card clip"
          data-start="0"
          data-duration="9"
          data-track-index="1"
          id="page-card"
        >
          <!-- Navbar (anchored to card top) -->
          <nav class="page-navbar" id="navbar">
            <div class="nav-left">
              <svg
                class="nav-logo"
                viewBox="0 0 24 24"
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
                aria-hidden="true"
              >
                <path
                  fill="currentColor"
                  d="M11.954 3.817c-4.49 0-8.132 3.644-8.152 8.146v12H0V12C0 5.373 5.352 0 11.954 0s11.953 5.373 11.953 12-5.351 12-11.953 12a12 12 0 0 1-1.718-.123v-3.876q.831.181 1.718.182c4.502 0 8.152-3.663 8.152-8.183s-3.65-8.183-8.152-8.183"
                />
                <path
                  fill="currentColor"
                  d="M5.118 24V11.995c0-3.79 3.062-6.857 6.836-6.857 3.773 0 6.836 3.068 6.836 6.857s-3.063 6.857-6.836 6.857c-.594 0-1.17-.076-1.718-.218V14.5c.488.337 1.08.534 1.718.534a3.037 3.037 0 0 0 3.034-3.04 3.037 3.037 0 0 0-3.034-3.04 3.037 3.037 0 0 0-3.034 3.008V24z"
                />
              </svg>
              <span class="nav-brand">OpusClip</span>
            </div>
            <div class="nav-center">
              <span class="nav-item">Features<span class="chev">▾</span></span>
              <span class="nav-item">Solutions<span class="chev">▾</span></span>
              <span class="nav-item">Resources<span class="chev">▾</span></span>
              <span class="nav-item">Pricing</span>
              <span class="nav-item">For business<span class="chev">▾</span></span>
              <span class="nav-item"><span class="nav-tag">New</span>Agent Opus</span>
            </div>
            <div class="nav-right">
              <span class="nav-signin">Sign in</span>
              <span class="nav-signup">Sign up - It's FREE</span>
            </div>
          </nav>

          <!-- Scrollable content -->
          <div class="scroll-content" id="scroll-content">
            <div class="hero" id="hero">
              <div class="hero-eyebrow">#1 AI VIDEO CLIPPING TOOL</div>
              <h1 class="hero-title">
                <span class="kw" data-glow-start="0.12" data-glow-end="0.28">1</span>
                <span class="kw" data-glow-start="0.52" data-glow-end="0.72">long</span>
                <span class="kw" data-glow-start="0.78" data-glow-end="1.44">video,</span>
                <span class="kw" data-glow-start="1.48" data-glow-end="1.80">10</span>
                <span class="kw" data-glow-start="1.94" data-glow-end="2.22">viral</span>
                <span class="kw" data-glow-start="2.28" data-glow-end="2.80">clips.</span>
                <br />Create 10x faster.
              </h1>
              <p class="hero-sub">
                OpusClip turns long videos into shorts, and publishes them to all social platforms
                in one click.
              </p>
            </div>

            <div class="cta-row" id="cta-row">
              <div class="cta-input">
                <svg
                  class="cta-link-icon"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  stroke-width="2"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  aria-hidden="true"
                >
                  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
                  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
                </svg>
                <span class="cta-placeholder">Drop a video link</span>
                <div class="cta-button">Get free clips</div>
              </div>
              <span class="cta-or">or</span>
              <div class="cta-upload">Upload files</div>
            </div>

            <!-- Video carousel section -->
            <div class="carousel-wrap" id="carousel">
              <div class="carousel-arrow">‹</div>

              <div class="carousel-main pop-target" id="pop-target">
                <div class="carousel-main-placeholder">
                  <span>Hero animation</span>
                </div>
              </div>

              <div class="carousel-arrow">›</div>

              <div class="carousel-side">
                <div class="carousel-side-placeholder">Demo · reframe</div>
                <div class="presets-stack">
                  <div class="preset-label">Presets</div>
                  <div class="preset-tile"></div>
                  <div class="preset-tile"></div>
                  <div class="preset-tile"></div>
                  <div class="preset-tile"></div>
                </div>
              </div>
            </div>
          </div>
          <!-- /.scroll-content -->
        </div>
        <!-- /.page-card -->
      </div>
      <!-- /.perspective-wrap -->

      <div class="spotlight" id="spotlight"></div>
      <div class="vignette"></div>
    </div>

    <script>
      /* ================================================================
         TIMING — all in local seconds (scene starts at 0).
         ASR timestamps are direct from the source, with SCENE_START = 16
         subtracted to get local time.
         ================================================================ */
      const ASR = {
        // Phase 2 title keywords
        one: { start: 0.119, end: 0.279 },
        long: { start: 0.52, end: 0.719 },
        video: { start: 0.779, end: 1.44 },
        ten: { start: 1.479, end: 1.799 },
        viral: { start: 1.94, end: 2.219 },
        clips: { start: 2.279, end: 2.8 },
      };

      const TIMING = {
        // Phase 1: entry
        cardEntryStart: 0.0,
        cardEntryDur: 0.8,
        navbarFadeAt: 0.0,
        navbarFadeDur: 0.6,
        titleFadeAt: 0.17,
        titleFadeDur: 0.7,
        ctaFadeAt: 0.5,
        ctaFadeDur: 0.7,

        // Phase 2: keyword glows handled per-element via data attrs

        // Phase 3: scroll + carousel
        phase2StartAt: 3.08,
        scrollDur: 1.0,
        scrollDistance: 280,
        carouselFadeDur: 0.5,

        // Phase 4: pop-out + spotlight
        popStart: 3.58,
        popAttackDur: 0.67, // ramp 0 → 1
        popDecayDur: 0.33, // ramp 1 → 0.5 near end
        popEnd: 8.84,
        popRestLevel: 0.5,
      };

      const KEYWORD_REST_LEVEL = 0.14;
      const KEYWORD_SUSTAIN = 0.5; // seconds after ASR end before decay completes

      /* ================================================================
         TIMELINE
         ================================================================ */
      window.__timelines = window.__timelines || {};
      const tl = gsap.timeline({ paused: true });
      window.__timelines["main"] = tl;

      // Set the static 3D tilt + initial scale via GSAP so subsequent
      // scale tweens preserve the rotation aliases. (If we left
      // `transform: rotateY(...) rotateX(...) scale(0.95)` in CSS, a GSAP
      // tween touching only `scale` could overwrite the full matrix and
      // lose the rotation. Owning the transform state in GSAP avoids the
      // problem entirely.)
      gsap.set(".page-card", {
        rotationY: -8,
        rotationX: 3,
        scale: 0.95,
      });

      /* ----------------------------------------------------------------
         PHASE 1: Page card entry + navbar/title/CTA fade-in
         ---------------------------------------------------------------- */
      tl.to(
        ".page-card",
        { scale: 1.0, duration: TIMING.cardEntryDur, ease: "power2.out" },
        TIMING.cardEntryStart,
      );

      tl.fromTo(
        "#navbar",
        { opacity: 0 },
        { opacity: 1, duration: TIMING.navbarFadeDur, ease: "power2.out" },
        TIMING.navbarFadeAt,
      );

      tl.fromTo(
        "#hero",
        { opacity: 0 },
        { opacity: 1, duration: TIMING.titleFadeDur, ease: "power2.out" },
        TIMING.titleFadeAt,
      );

      tl.fromTo(
        "#cta-row",
        { opacity: 0 },
        { opacity: 1, duration: TIMING.ctaFadeDur, ease: "power2.out" },
        TIMING.ctaFadeAt,
      );

      /* ----------------------------------------------------------------
         PHASE 2: Keyword glows — two tweens per word.
         Attack (0 → 1) + decay (1 → REST_LEVEL). GSAP holds REST forever.
         ---------------------------------------------------------------- */
      document.querySelectorAll(".kw").forEach((kw) => {
        const start = Number(kw.dataset.glowStart);
        const end = Number(kw.dataset.glowEnd);
        const peak = start + (end - start) / 2;
        const restAt = end + KEYWORD_SUSTAIN;

        tl.fromTo(
          kw,
          { "--glow": 0 },
          { "--glow": 1.18, duration: peak - start, ease: "power2.out" },
          start,
        );

        tl.to(
          kw,
          { "--glow": KEYWORD_REST_LEVEL, duration: restAt - peak, ease: "power2.out" },
          peak,
        );
      });

      /* ----------------------------------------------------------------
         PHASE 3: Scroll + carousel entry
         ---------------------------------------------------------------- */
      tl.fromTo(
        "#scroll-content",
        { y: 0 },
        { y: -TIMING.scrollDistance, duration: TIMING.scrollDur, ease: "power2.inOut" },
        TIMING.phase2StartAt,
      );

      tl.fromTo(
        "#carousel",
        { opacity: 0, scale: 0.9 },
        { opacity: 1, scale: 1.0, duration: TIMING.carouselFadeDur, ease: "power2.out" },
        TIMING.phase2StartAt,
      );

      /* ----------------------------------------------------------------
         PHASE 4: Pop-out (driven by --glow on .carousel-main) + spotlight
         ---------------------------------------------------------------- */
      // Attack the pop-out glow.
      tl.fromTo(
        "#pop-target",
        { "--glow": 0 },
        { "--glow": 1, duration: TIMING.popAttackDur, ease: "power2.out" },
        TIMING.popStart,
      );

      // Decay at the end of the scene (mirrors original phase2End-10 / phase2End ramp).
      tl.to(
        "#pop-target",
        { "--glow": TIMING.popRestLevel, duration: TIMING.popDecayDur, ease: "power2.out" },
        TIMING.popEnd - TIMING.popDecayDur,
      );

      // Spotlight fades in alongside the pop attack, decays with the pop.
      tl.fromTo(
        "#spotlight",
        { opacity: 0 },
        { opacity: 1, duration: 0.5, ease: "power2.out" },
        TIMING.popStart,
      );

      tl.to(
        "#spotlight",
        { opacity: TIMING.popRestLevel, duration: TIMING.popDecayDur, ease: "power2.out" },
        TIMING.popEnd - TIMING.popDecayDur,
      );
    </script>
  </body>
</html>