Menu
Public repository / master
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();
}
}