← GPT-AI-VIDEO
WEBSITE · 12.0 KB

vendor/OpenMontage/.agents/skills/hyperframes-animation/examples/brand-reveal-assemble-zoom.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 3 — Assembly Focus Reveal (hyperframes)</title>

    <!--
      HyperFrames composition.tsx.

      Choreography (5 phases, 5 seconds total):
        0.00 – 0.67s   "Just use" assembles in a discrete sequence (with hold)
        0.73 – 1.23s   Pink logo pops in with back.out elastic
        1.50 – 2.20s   "Just use" slides left + fades; container recenters around brand
        2.67 – 3.57s   Camera zooms 5.5× into the logo (scale + counter-translate)
        3.67 – 5.00s   Logo breathes (sine onUpdate, multiplicative on pop scale)

      Key differences :
        - Single paused GSAP timeline registered to window.__timelines["main"]
        - brandTextWidth measured via getBoundingClientRect after document.fonts.ready
        - HERO_FINAL_OFFSET_X derived from real measurement, not estimate
        - Breathing uses the onUpdate (multiplicative) form so it multiplies
          onto the pop scale, not a fromTo + yoyo which would overwrite the pop value
        - Three nested transform layers: zoom-scale → zoom-translate → recenter-shift
        - Logo is loaded from a static PNG asset
    -->

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

    <style>
      :root {
        --bg: #000000;
        --text-white: #ffffff;
        --pink: #e91e63;
      }
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        margin: 0;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        background: var(--bg);
        font-family: "Google Sans", "Roboto", Inter, system-ui, sans-serif;
        color: var(--text-white);
      }

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

      /* Three nested transform layers (outer → inner) */
      .zoom-scale {
        transform-origin: center center;
        will-change: transform;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .zoom-translate {
        will-change: transform;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .recenter-shift {
        display: flex;
        align-items: center;
      }

      /* Companion text — fixed width to prevent assembly jitter */
      .companion {
        width: 600px;
        display: flex;
        justify-content: flex-end;
        margin-right: 30px;
        white-space: nowrap;
        color: var(--text-white);
        font-size: 140px;
        font-weight: 400;
        line-height: 1;
      }

      /* Brand group: text + hero icon */
      .brand-group {
        display: flex;
        align-items: center;
        gap: 20px;
      }
      .brand-text {
        color: var(--text-white);
        font-size: 140px;
        font-weight: 700;
        white-space: nowrap;
        line-height: 1;
      }
      .hero {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 140px;
        height: 140px;
        /* initial scale(0) set by GSAP fromTo */
      }
      .hero .logo-mark {
        width: 100%;
        height: 100%;
        display: block;
      }

      /* Hidden probe used for text measurement */
      .measure-probe {
        position: absolute;
        left: -99999px;
        top: -99999px;
        visibility: hidden;
        white-space: pre;
      }
    </style>
  </head>
  <body>
    <div
      id="root"
      data-composition-id="main"
      data-start="0"
      data-duration="5"
      data-width="1920"
      data-height="1080"
    >
      <div
        class="stage clip"
        data-start="0"
        data-duration="5"
        data-track-index="1"
        id="brand-stage"
      >
        <div class="zoom-scale" data-layout-allow-overflow>
          <div class="zoom-translate">
            <div class="recenter-shift">
              <div class="companion">
                <span class="companion-text">J</span>
              </div>

              <div class="brand-group" data-layout-allow-overflow>
                <span class="brand-text">Hyperframes</span>
                <div class="hero">
                  <!-- Inline-SVG placeholder mark — swap for your logo image -->
                  <svg
                    class="logo-mark"
                    viewBox="0 0 100 100"
                    role="img"
                    aria-label="hyperframes logo"
                  >
                    <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>
          </div>
        </div>
      </div>
    </div>

    <script>
      /* ================================================================
         CONSTANTS — composition dimensions baked in.
         ================================================================ */
      const W = 1920,
        H = 1080;
      const TOTAL_DURATION = 5.0;

      const COMPANION_WIDTH = 600;
      const COMPANION_GAP = 30; // margin-right on .companion
      const BRAND_FONT_SIZE = 140;
      const HERO_GAP = 20;
      const HERO_SIZE = 140;

      const TIMING = {
        // Phase 1: companion assembly
        textStart: 0.0,
        textEnd: 0.67,

        // Phase 2: hero pop
        popStart: 0.73,
        popDur: 0.5,

        // Phase 3: slide-out + recenter
        slideStart: 1.5,
        slideDur: 0.7,

        // Phase 4: zoom
        zoomStart: 2.67,
        zoomDur: 0.9,

        // Phase 5: breathing
        breathStart: 3.67,
      };

      const FINAL_RECENTER_OFFSET = -180; // pre-calculated, tuned for visual feel

      /* ================================================================
         DISCRETE TEXT SEQUENCE for the companion assembly.
         Converted from the source frame-based sequence to seconds at 30fps.
         ================================================================ */
      const SEQUENCE = [
        { t: 0.0, text: "J" },
        { t: 0.07, text: "Jus" },
        { t: 0.13, text: "Just" },
        { t: 0.27, text: "Just" }, // hold for pacing
        { t: 0.4, text: "Just u" },
        { t: 0.53, text: "Just us" },
        { t: 0.67, text: "Just use" },
      ];

      /* ================================================================
         TIMELINE — built synchronously so HyperFrames can seek it.
         ================================================================ */
      window.__timelines = window.__timelines || {};
      const tl = gsap.timeline({ paused: true });
      window.__timelines["main"] = tl;

      {
        /* Measure the brand text width with a hidden DOM probe. */
        const probe = document.createElement("span");
        probe.className = "measure-probe";
        probe.style.font = `700 ${BRAND_FONT_SIZE}px "Google Sans", "Roboto", Inter, system-ui, sans-serif`;
        probe.style.whiteSpace = "pre";
        probe.style.lineHeight = "1";
        probe.textContent = "Hyperframes"; // MUST match the rendered .brand-text casing
        document.body.appendChild(probe);
        const brandTextWidth = probe.getBoundingClientRect().width;
        probe.remove();

        /* Derive the hero's post-Phase-3 offset from viewport center.
           baseHeroOffset = (C + G + B + L) / 2     <- heroSize cancels out
           HERO_FINAL_OFFSET_X = baseHeroOffset + FINAL_RECENTER_OFFSET */
        const baseHeroOffset = (COMPANION_WIDTH + COMPANION_GAP + brandTextWidth + HERO_GAP) / 2;
        const HERO_FINAL_OFFSET_X = baseHeroOffset + FINAL_RECENTER_OFFSET;

        /* ============================================================
           PHASE 1: Discrete companion assembly
           ============================================================ */
        const textEl = document.querySelector(".companion-text");

        for (const entry of SEQUENCE) {
          tl.set(textEl, { textContent: entry.text }, entry.t);
        }

        /* ============================================================
           PHASE 2: Hero pops in (elastic)
           ============================================================ */
        tl.fromTo(
          ".hero",
          { scale: 0 },
          {
            scale: 1,
            duration: TIMING.popDur,
            ease: "back.out(2)", // spring(stiffness:200, damping:12)
          },
          TIMING.popStart,
        );

        /* ============================================================
           PHASE 3: Companion exit + container recenter
           Two concurrent tweens at the same timeline position.
           ============================================================ */
        tl.to(
          ".companion",
          {
            opacity: 0,
            x: -80,
            duration: TIMING.slideDur,
            ease: "power3.out", // spring(stiffness:100, damping:20)
          },
          TIMING.slideStart,
        );

        tl.to(
          ".recenter-shift",
          {
            x: FINAL_RECENTER_OFFSET,
            duration: TIMING.slideDur,
            ease: "power3.out",
          },
          TIMING.slideStart,
        );

        /* ============================================================
           PHASE 4: Zoom — scale (outer) + counter-translate (middle) + brand text exits.
           ============================================================ */
        tl.to(
          ".zoom-scale",
          {
            scale: 5.5,
            duration: TIMING.zoomDur,
            ease: "power2.out", // spring(stiffness:80, damping:20, mass:1.5)
          },
          TIMING.zoomStart,
        );

        tl.to(
          ".zoom-translate",
          {
            x: -HERO_FINAL_OFFSET_X,
            y: 0,
            duration: TIMING.zoomDur,
            ease: "power2.out",
          },
          TIMING.zoomStart,
        );

        // Brand text fades out + slides left so the logo gets all the zoom space.
        tl.to(
          ".brand-text",
          {
            opacity: 0,
            x: -600,
            duration: TIMING.zoomDur * 0.4,
            ease: "power2.out",
          },
          TIMING.zoomStart,
        );

        /* ============================================================
           PHASE 5: Breathing — onUpdate so it MULTIPLIES on the hero's
           final pop scale, doesn't overwrite it.
           ============================================================ */
        const heroEl = document.querySelector(".hero");
        const HERO_FINAL_SCALE = 1.0;
        const SCALE_PERIOD = 1.5; // seconds per cycle
        const SCALE_AMP = 0.04;
        const ROTATE_AMP = 2;

        const breathDur = TOTAL_DURATION - TIMING.breathStart;

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