← GPT-AI-VIDEO
WEBSITE · 11.9 KB

vendor/OpenMontage/.agents/skills/hyperframes-animation/examples/messaging-multi-phrase.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 — HyperFrames Messaging Multi-Phrase</title>

    <!--
      Blueprint: messaging-multi-phrase (HyperFrames preview)
      HyperFrames-native version of the scene-9-multi-phrase concept.

      Choreography (3 phrases, ~7.5 seconds total, content-driven duration):

        Phrase 1 [0.00 – 3.86 s]
          "With an AI assistant " (21 chars × 0.083 s = 1.75 s)
        + "you can trust"          (13 chars × 0.083 s = 1.08 s, accent pink)
        + hold 1.00 s
        → total 1.75 + 1.08 + 1.00 ≈ 3.83 s (computed by JS)

        Phrase 2 [3.86 – 5.27 s]
          "Instant " (8 chars × 0.083 s = 0.67 s)
        + "insights" (8 chars × 0.083 s = 0.67 s, accent pink)
        + hold 1.00 s
        → total 0.67 + 0.67 + 1.00 ≈ 2.33 s, but starts at 3.86 → endTime ≈ 6.18 s

        Phrase 3 [6.18 – 9.45 s]
          "Endless "      (8 chars × 0.083 = 0.67 s)
        + "possibilities" (13 chars × 0.083 = 1.08 s, accent pink)
        + hold 2.00 s     (longer — closing beat)
        → total 0.67 + 1.08 + 2.00 ≈ 3.75 s, but starts at 6.18 → endTime ≈ 9.93 s

        Continuous: cursor blink (square wave via `tl.time() % 1.0`)
          color, cursor blink. No per-phrase tweens.
        - No conditional DOM — phrase container exists from t=0 with empty text;
          onUpdate overwrites textContent in-place at each frame.
        - TIMELINE is a plain const computed once at script load (no useMemo).
        - Frames become seconds: charSpeed 2.5 frames → 0.083 s; hold 30 frames → 1.0 s.
        - Cursor blink: `(t % BLINK_CYCLE) < BLINK_CYCLE / 2 ? 1 : 0` inside the same
          onUpdate, not a separate component with `useCurrentFrame`.
        - textContent !== visible guard skips redundant DOM writes during hold windows.
    -->

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

    <style>
      :root {
        --text-primary: #ffffff;
        --text-accent: #32fff6;
        --text-accent-2: #a3ff7a;
        --bg: linear-gradient(
          135deg,
          #3a3a3a 0%,
          #17211f 30%,
          #0b2328 48%,
          #1f3518 70%,
          #343434 100%
        );
      }
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        margin: 0;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        background: var(--bg);
        font-family:
          "Inter",
          -apple-system,
          BlinkMacSystemFont,
          "Segoe UI",
          Roboto,
          sans-serif;
        color: var(--text-primary);
        -webkit-font-smoothing: antialiased;
        text-rendering: geometricPrecision;
      }

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

      /* ============================================================
         PHRASE STAGE — one shared line with three inline children:
           .phrase-main   primary-color portion
           .phrase-accent accent-color portion
           .phrase-cursor block cursor (color overridden per-frame)
         `white-space: pre` keeps the trailing space in textMain so the
         accent doesn't merge with the lead-in.
         ============================================================ */
      .phrase-stage {
        position: absolute;
        inset: 0;
        width: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 150px;
        font-weight: 800;
        line-height: 1;
        letter-spacing: 0;
        white-space: pre;
        text-align: center;
        text-shadow: 0 0 10px rgba(255, 255, 255, 0.16);
      }
      .phrase-main {
        color: var(--text-primary);
      }
      .phrase-accent {
        color: var(--text-accent);
        text-shadow: 0 0 12px rgba(50, 255, 246, 0.28);
      }
      .phrase-cursor {
        display: inline-block;
        flex: 0 0 auto;
        width: 8px;
        height: 162px; /* ≈ 1.08 × fontSize */
        background: var(--text-primary);
        margin-left: 12px;
        vertical-align: middle;
        transform: translateY(10px); /* baseline alignment */
        will-change: opacity, background-color;
        box-shadow: 0 0 12px rgba(50, 255, 246, 0.36);
      }

      /* ============================================================
         BACKGROUND TINT — subtle radial glow so pure black doesn't
         feel sterile. Drops to ~rgb(8,8,12) at corners.
         ============================================================ */
      .bg-tint {
        position: absolute;
        inset: 0;
        background:
          radial-gradient(ellipse at 42% 48%, rgba(24, 217, 232, 0.32), transparent 42%),
          radial-gradient(ellipse at 58% 52%, rgba(123, 234, 90, 0.26), transparent 44%),
          radial-gradient(ellipse at 50% 50%, rgba(8, 16, 18, 0.42), transparent 70%);
        pointer-events: none;
      }
    </style>
  </head>
  <body>
    <div
      id="root"
      data-composition-id="main"
      data-start="0"
      data-duration="7.5"
      data-width="1920"
      data-height="1080"
    >
      <div
        id="multi-phrase-scene"
        class="stage clip"
        data-start="0"
        data-duration="7.5"
        data-track-index="1"
      >
        <div class="bg-tint"></div>
        <div class="phrase-stage">
          <span class="phrase-main"></span><span class="phrase-accent"></span
          ><span class="phrase-cursor"></span>
        </div>
      </div>
    </div>

    <script>
      window.__timelines = window.__timelines || {};
      const tl = gsap.timeline({ paused: true });

      /* ============================================================
         SCRIPT — content + per-phrase timing config.
         Change SCRIPT and the timeline reshapes itself.
         ============================================================ */
      const SCRIPT = [
        {
          textMain: "Build video with ",
          textAccent: "HTML",
          charSpeed: 0.083, // seconds per character (= 2.5 frames @ 30 fps)
          hold: 1.0,
        },
        {
          textMain: "Seek ",
          textAccent: "any frame",
          charSpeed: 0.083,
          hold: 1.0,
        },
        {
          textMain: "Render to ",
          textAccent: "MP4",
          charSpeed: 0.083,
          hold: 2.0, // longer closing beat
        },
      ];

      /* ============================================================
         TIMELINE — flat reduce, computed once.
         Each entry gains absolute startTime + endTime in seconds.
         ============================================================ */
      let acc = 0;
      const TIMELINE = SCRIPT.map((item) => {
        const totalChars = item.textMain.length + item.textAccent.length;
        const typingDuration = totalChars * item.charSpeed;
        const totalDuration = typingDuration + item.hold;
        const start = acc;
        const end = start + totalDuration;
        acc = end;
        return Object.assign({}, item, {
          startTime: start,
          endTime: end,
          typingDuration,
        });
      });
      const TOTAL = TIMELINE[TIMELINE.length - 1].endTime;
      // TOTAL ≈ 9.93s with the SCRIPT above. The composition root caps at 7.5s
      // (data-duration), which is the actual render window — phrases 1+2 fit
      // comfortably; phrase 3 holds long enough that any time after ~6.18s is
      // already on the closing line. Adjust SCRIPT durations to fit your budget.

      /* ============================================================
         COLORS & BLINK
         ============================================================ */
      const MAIN_COLOR = "#FFFFFF";
      const ACCENT_COLOR = "#32FFF6";
      const BLINK_CYCLE = 1.0; // seconds — 0.5s on, 0.5s off (matches 30-frame square wave)

      const mainEl = document.querySelector(".phrase-main");
      const accentEl = document.querySelector(".phrase-accent");
      const cursorEl = document.querySelector(".phrase-cursor");

      /* ============================================================
         MASTER ENGINE — single onUpdate writes text + cursor every frame.
         ============================================================ */
      // Cache the most-recently-active phrase index for seek-safe scans.
      let lastIdx = 0;

      function findPhrase(t) {
        // Hot path: same phrase still active
        if (t >= TIMELINE[lastIdx].startTime && t < TIMELINE[lastIdx].endTime) {
          return TIMELINE[lastIdx];
        }
        // Cold path: linear scan (handles forward + backward seeks)
        for (let i = 0; i < TIMELINE.length; i++) {
          if (t >= TIMELINE[i].startTime && t < TIMELINE[i].endTime) {
            lastIdx = i;
            return TIMELINE[i];
          }
        }
        return null;
      }

      function renderAt(t) {
        // ----- Cursor blink (square wave, runs always) -----
        cursorEl.style.opacity = t % BLINK_CYCLE < BLINK_CYCLE / 2 ? "1" : "0";

        // ----- Find current phrase -----
        const phrase = findPhrase(t);
        if (!phrase) {
          if (mainEl.textContent !== "") mainEl.textContent = "";
          if (accentEl.textContent !== "") accentEl.textContent = "";
          cursorEl.style.background = MAIN_COLOR;
          return;
        }

        // ----- Compute visible characters -----
        const activeT = t - phrase.startTime;
        const charIdx = Math.floor(activeT / phrase.charSpeed);
        const mainLen = phrase.textMain.length;

        const visMain = phrase.textMain.slice(0, Math.min(charIdx, mainLen));
        const accentLen = Math.max(0, charIdx - mainLen);
        const visAccent = phrase.textAccent.slice(0, accentLen);

        // ----- DOM writes (guarded) -----
        if (mainEl.textContent !== visMain) mainEl.textContent = visMain;
        if (accentEl.textContent !== visAccent) accentEl.textContent = visAccent;

        // ----- Cursor color follows active segment -----
        const inAccent = visMain.length === mainLen && visAccent.length > 0;
        cursorEl.style.background = inAccent ? ACCENT_COLOR : MAIN_COLOR;
      }

      tl.to(
        { tick: 0 },
        {
          tick: 1,
          duration: 7.5, // matches data-duration; clips the timeline
          ease: "none",
          onUpdate: () => renderAt(tl.time()),
        },
        0,
      );

      const pauseTimeline = tl.pause.bind(tl);
      tl.pause = (time, suppressEvents) => {
        const result = pauseTimeline(time, suppressEvents);
        if (typeof time === "number") renderAt(time);
        return result;
      };

      function wrapSeekOwner(owner) {
        if (!owner || owner.__multiPhraseSeekWrapped) return;
        let currentSeek = owner.seek;
        Object.defineProperty(owner, "seek", {
          configurable: true,
          get: () => currentSeek,
          set: (value) => {
            if (typeof value !== "function") {
              currentSeek = value;
              return;
            }
            currentSeek = function (time) {
              const result = value.apply(this, arguments);
              if (typeof time === "number") renderAt(time);
              return result;
            };
          },
        });
        owner.seek = currentSeek;
        owner.__multiPhraseSeekWrapped = true;
      }

      function installWindowSeekHook(key) {
        if (window[key]) {
          wrapSeekOwner(window[key]);
          return;
        }
        let pendingOwner;
        Object.defineProperty(window, key, {
          configurable: true,
          get: () => pendingOwner,
          set: (value) => {
            pendingOwner = value;
            wrapSeekOwner(value);
          },
        });
      }
      installWindowSeekHook("__player");
      installWindowSeekHook("__hf");

      renderAt(0);

      window.__timelines["main"] = tl;
    </script>
  </body>
</html>