← GPT-AI-VIDEO
WEBSITE · 10.7 KB

vendor/OpenMontage/.agents/skills/hyperframes-animation/examples/takeover-ticker-displace.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 5 — HyperFrames Displace Reveal</title>

    <!--
      HyperFrames composition.tsx.

      Choreography (4 phases, 7.5 seconds total):
        0.00 – 0.67s   Typewriter reveals "HyperFrames turns" (3 → 17 chars)
        1.67 – 2.22s   Ticker scrolls: HTML → motion
        3.33 – 3.88s   Ticker scrolls: motion → video
        4.60 – 5.45s   Logo enters from offscreen-right with rotation + scale impact
                       Text group pushed left + fades (40-50% of hero duration)
        6.60 – 7.50s   Logo breathes (dual-frequency sine onUpdate, multiplicative on 1.3 scale)
          tuned to 40-50% of hero duration so the impact reads as causal
        - Breathing uses Form 2 (onUpdate) so it multiplies onto the 1.3 final scale,
          not overwrites it (Form 1 yoyo would undo the impact)
        - Two breathing periods (1.0s scale, 1.33s rotation) for organic feel
        - Logo uses a local static image asset referenced with a plain URL
    -->

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

    <style>
      :root {
        --bg: linear-gradient(
          135deg,
          #3a3a3a 0%,
          #17211f 30%,
          #0b2328 48%,
          #1f3518 70%,
          #343434 100%
        );
        --text: #f8fafc;
        --accent: #18d9e8;
        --accent-2: #7bea5a;
      }
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        margin: 0;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        background: var(--bg);
        font-family:
          "Inter",
          system-ui,
          -apple-system,
          sans-serif;
        color: var(--text);
      }

      .stage {
        position: absolute;
        inset: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
      }

      /* Text group — typewriter + ticker side by side, displaced as a unit */
      .text-group {
        position: absolute;
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: 20px;
      }

      .typewriter {
        height: 168px; /* = ITEM_HEIGHT (FONT_SIZE × 1.2) */
        display: flex;
        align-items: center;
        color: var(--text);
        font-weight: 400;
        font-size: 140px;
        line-height: 1;
        white-space: pre;
        text-shadow: 0 0 36px rgba(24, 217, 232, 0.18);
      }

      /* Vertical ticker */
      .ticker-window {
        height: 168px;
        overflow: hidden;
        display: inline-flex;
        flex-direction: column;
        align-items: flex-start;
      }
      .ticker-stack {
        display: flex;
        flex-direction: column;
        will-change: transform;
      }
      .ticker-item {
        height: 168px;
        display: flex;
        align-items: center;
        color: var(--accent);
        background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
        -webkit-background-clip: text;
        background-clip: text;
        -webkit-text-fill-color: transparent;
        font-weight: 700;
        font-size: 140px;
        padding-left: 20px;
        line-height: 1;
        white-space: pre;
      }

      /* Hero logo — enters from offscreen right */
      .hero {
        position: absolute;
        width: 440px;
        height: 440px;
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 20; /* above text during overlap */
        opacity: 0; /* fromTo will animate */
      }
      .hero .logo-mark {
        width: 100%;
        height: 100%;
        display: block;
        border-radius: 52px;
        box-shadow:
          0 0 70px rgba(24, 217, 232, 0.28),
          0 0 120px rgba(123, 234, 90, 0.18);
      }
    </style>
  </head>
  <body>
    <div
      id="root"
      data-composition-id="main"
      data-start="0"
      data-duration="7.5"
      data-width="1920"
      data-height="1080"
    >
      <div
        class="stage clip"
        data-start="0"
        data-duration="7.5"
        data-track-index="1"
        id="displace-stage"
      >
        <!-- Text group: typewriter + ticker -->
        <div class="text-group" id="text-group">
          <div class="typewriter">
            <span class="typewriter-text">Hyp</span>
          </div>
          <div class="ticker-window">
            <div class="ticker-stack">
              <div class="ticker-item">HTML</div>
              <div class="ticker-item">motion</div>
              <div class="ticker-item">video</div>
            </div>
          </div>
        </div>

        <!-- Hero logo asset -->
        <div class="hero" id="hero-logo">
          <!-- Inline-SVG placeholder mark — swap for your logo image -->
          <svg class="logo-mark" viewBox="0 0 100 100" role="img" aria-label="HyperFrames">
            <defs>
              <linearGradient id="hfMark" x1="0" y1="0" x2="1" y2="1">
                <stop offset="0" stop-color="#8b7bff" />
                <stop offset="1" stop-color="#3ddc97" />
              </linearGradient>
            </defs>
            <rect x="4" y="4" width="92" height="92" rx="22" fill="url(#hfMark)" />
            <text
              x="50"
              y="63"
              text-anchor="middle"
              font-family="Inter, system-ui, sans-serif"
              font-size="40"
              font-weight="800"
              fill="#fff"
            >
              HF
            </text>
          </svg>
        </div>
      </div>
    </div>

    <script>
      /* ================================================================
         CONSTANTS
         ================================================================ */
      const TOTAL_DURATION = 7.5;
      const FONT_SIZE = 140;
      const ITEM_HEIGHT = FONT_SIZE * 1.2; // = 168 px

      const TIMING = {
        // Phase 1: typewriter
        typeStart: 0.0,
        typeDur: 0.67,
        typeStartLen: 3, // pre-render the first 3 chars

        // Phase 2: ticker steps
        ticker1At: 1.67, // HTML → motion
        ticker2At: 3.33, // motion → video
        stepDur: 0.55,

        // Phase 3: reactive displacement
        displaceAt: 4.6,
        heroDur: 0.85, // matches spring(mass:1.5) settle
        offscreenX: 800, // hero starts here
        pushDist: -150, // text pushed THIS direction

        // Phase 4: breathing
        idleStart: 6.6, // displaceAt + heroDur + ~1.15s buffer
      };

      const FULL_TEXT = "HyperFrames turns";

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

      /* ----------------------------------------------------------------
         PHASE 1: Typewriter (smooth slice)
         ---------------------------------------------------------------- */
      const typewriterEl = document.querySelector(".typewriter-text");
      const typeProxy = { progress: TIMING.typeStartLen };

      tl.to(
        typeProxy,
        {
          progress: FULL_TEXT.length,
          duration: TIMING.typeDur,
          ease: "none",
          onUpdate: () => {
            const len = Math.floor(typeProxy.progress);
            const next = FULL_TEXT.slice(0, len);
            if (typewriterEl.textContent !== next) typewriterEl.textContent = next;
          },
        },
        TIMING.typeStart,
      );

      /* ----------------------------------------------------------------
         PHASE 2: Vertical ticker (2 steps: HTML → motion → video)
         ---------------------------------------------------------------- */
      tl.to(
        ".ticker-stack",
        {
          y: `-=${ITEM_HEIGHT}`,
          duration: TIMING.stepDur,
          ease: "back.out(1.4)", // spring(stiffness:120, damping:14)
        },
        TIMING.ticker1At,
      );

      tl.to(
        ".ticker-stack",
        {
          y: `-=${ITEM_HEIGHT}`,
          duration: TIMING.stepDur,
          ease: "back.out(1.4)",
        },
        TIMING.ticker2At,
      );

      /* ----------------------------------------------------------------
         PHASE 3: Reactive displacement — three concurrent tweens.
         ---------------------------------------------------------------- */
      // (1) Hero enters with rotation + scale impact, lands at scale 1.3.
      tl.fromTo(
        ".hero",
        { x: TIMING.offscreenX, scale: 0.5, rotation: -45, opacity: 0 },
        {
          x: 0,
          scale: 1.3,
          rotation: 0,
          opacity: 1,
          duration: TIMING.heroDur,
          ease: "power2.out", // spring(stiffness:100, damping:20, mass:1.5)
        },
        TIMING.displaceAt,
      );

      // (2) Text pushed left. Completes at 50% of hero duration → immediate impact.
      tl.to(
        ".text-group",
        { x: TIMING.pushDist, duration: TIMING.heroDur * 0.5, ease: "power2.out" },
        TIMING.displaceAt,
      );

      // (3) Text fades. Completes at 40% → fades slightly before push lands.
      tl.to(
        ".text-group",
        { opacity: 0, duration: TIMING.heroDur * 0.4, ease: "power2.out" },
        TIMING.displaceAt,
      );

      /* ----------------------------------------------------------------
         PHASE 4: Breathing — onUpdate so it MULTIPLIES onto the 1.3 scale.
         Dual frequencies (1.0s scale, 1.33s rotation) for organic motion.
         ---------------------------------------------------------------- */
      const heroEl = document.querySelector(".hero");
      const HERO_FINAL_SCALE = 1.3;
      const HERO_FINAL_ROTATION = 0;
      const SCALE_PERIOD = 1.0; // seconds per scale cycle
      const ROTATE_PERIOD = 1.333; // seconds per rotation cycle — not a simple ratio of SCALE_PERIOD
      const SCALE_AMP = 0.05;
      const ROTATE_AMP = 3;

      const breathDur = TOTAL_DURATION - TIMING.idleStart;

      tl.to(
        { tick: 0 },
        {
          tick: 1,
          duration: breathDur,
          ease: "none",
          onUpdate: function () {
            const idleTime = Math.max(0, tl.time() - TIMING.idleStart);
            const omegaS = (idleTime / SCALE_PERIOD) * Math.PI * 2;
            const omegaR = (idleTime / ROTATE_PERIOD) * Math.PI * 2;
            gsap.set(heroEl, {
              scale: HERO_FINAL_SCALE * (1 + Math.sin(omegaS) * SCALE_AMP),
              rotation: HERO_FINAL_ROTATION + Math.sin(omegaR) * ROTATE_AMP,
            });
          },
        },
        TIMING.idleStart,
      );
    </script>
  </body>
</html>