AkurAI Build
Menu

Public repository / 7c98a6fe64be6cf4971a37c541e219b5dfcd684b

AkurAI-Platform / frontend/story.js

Latest change 7c98a6fe64be6cf4971a37c541e219b5dfcd684b - OLI-105: baseline snapshot of live platform.olibuijr.com frontend (source was missing from repo) by DevLead

// story.js — scroll-driven request-path story. Native ESM, zero deps.
//
// The spine (SVG line) draws itself as the visitor scrolls through the
// chapters, and a packet dot rides the drawn edge. Chapters light up as
// the packet passes them. Pure rAF + scroll math keeps this path independent
// from the optional Three.js hero.
// ponytail: linear single-column spine only; swap for a curved multi-path
// schematic if the story ever branches.

const track = document.querySelector("[data-story]");
const drawLine = document.querySelector("[data-spine-draw]");
const packet = document.querySelector("[data-spine-packet]");
const chapters = [...document.querySelectorAll("[data-chapter]")];

const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;

if (track && drawLine && packet && chapters.length) {
  const SPINE_LEN = 1200; // viewBox length of the spine line
  drawLine.style.strokeDasharray = String(SPINE_LEN);

  if (reduceMotion) {
    // Static: fully drawn spine, packet parked at the end, all chapters lit.
    drawLine.style.strokeDashoffset = "0";
    packet.setAttribute("cy", String(SPINE_LEN));
    chapters.forEach((c) => c.classList.add("is-active", "is-passed"));
  } else {
    let ticking = false;

    const update = () => {
      ticking = false;
      const rect = track.getBoundingClientRect();
      const vh = innerHeight;
      // Progress: 0 when the track top reaches 70% of viewport,
      // 1 when the track bottom reaches 45% of viewport.
      const start = vh * 0.7;
      const end = vh * 0.45;
      const total = rect.height - (start - end);
      const progress = Math.min(1, Math.max(0, (start - rect.top) / total));

      drawLine.style.strokeDashoffset = String(SPINE_LEN * (1 - progress));
      packet.setAttribute("cy", String(SPINE_LEN * progress));

      const packetY = rect.top + rect.height * progress;
      for (const chapter of chapters) {
        const cRect = chapter.getBoundingClientRect();
        const lit = packetY >= cRect.top;
        chapter.classList.toggle("is-passed", lit);
        chapter.classList.toggle(
          "is-active",
          lit && packetY <= cRect.bottom + 40
        );
      }
    };

    const onScroll = () => {
      if (!ticking) {
        ticking = true;
        requestAnimationFrame(update);
      }
    };

    addEventListener("scroll", onScroll, { passive: true });
    addEventListener("resize", onScroll, { passive: true });
    update();
  }
}