diff --git a/web/views/timeline.ejs b/web/views/timeline.ejs index 900928b..c397b52 100644 --- a/web/views/timeline.ejs +++ b/web/views/timeline.ejs @@ -423,7 +423,6 @@ var lineTrigger = null; var nodeTriggers = []; var nextRevealIndex = 0; - var preRevealProgress = 0; // Build a rounded-corner path that threads through a list of points. function roundedPath(pts, radius) { @@ -547,21 +546,24 @@ return lengthAtPoint(getMarkerCenter(node), len) / len; } - function initPreReveal() { - var vh = window.innerHeight; - var sp = 0; - nodes.forEach(function (node, idx) { - if (node.getBoundingClientRect().top < vh * 0.95) { - sp = Math.max(sp, nodeProgresses[idx] || 0); - } - }); - if (sp > state.progress) { - preRevealProgress = sp; - drawProgress = sp; - state.progress = sp; - targetDrawProgress = sp; - renderProgress(); + // Map the on-screen "reveal line" (~72% down the viewport) to a point on + // the path. The serpentine only ever descends, so sample Y increases + // monotonically with path length — the farthest sample at/above the + // reveal line is the frontier. This keeps the comet just below the last + // in-view card on load, then leading the draw as you scroll, instead of + // jumping ahead off-screen. + var REVEAL_FRACTION = 0.72; + function frontierProgress() { + if (!pathLength || !_samples) return state.progress; + var revealY = window.innerHeight * REVEAL_FRACTION - timeline.getBoundingClientRect().top; + var frontierL = 0; + for (var i = 0; i < _samples.length; i++) { + if (_samples[i].y <= revealY) frontierL = _samples[i].l; } + return Math.min(1, Math.max(0, frontierL / pathLength)); + } + function updateFromScroll() { + advanceTo(frontierProgress(), 0.4); } function revealReachedNodes() { @@ -653,24 +655,16 @@ } }); - // Single trigger drives the whole line — no per-node triggers needed. - // start: timeline top at 70% of viewport (just entering view) - // end: last node top at 40% of viewport (card well into view before fully revealed) + // One trigger fires onUpdate across the whole scroll range; the draw + // amount comes from frontierProgress() (viewport-relative), not the + // trigger's own 0..1, so the comet tracks the on-screen reveal line. lineTrigger = ScrollTrigger.create({ trigger: timeline, - start: 'top 70%', + start: 'top bottom', endTrigger: nodes[nodes.length - 1] || timeline, - end: 'top 60%', - scrub: true, + end: 'bottom top', invalidateOnRefresh: true, - onUpdate: function (self) { - var p = Math.min(1, preRevealProgress + self.progress * (1 - preRevealProgress)); - if (p <= state.progress) return; - state.progress = p; - targetDrawProgress = p; - drawProgress = p; - renderProgress(); - } + onUpdate: updateFromScroll }); } @@ -686,7 +680,7 @@ if (useGsap) { createScrollTriggers(); ScrollTrigger.refresh(); - initPreReveal(); + updateFromScroll(); var hint = document.getElementById('scroll-hint'); function hideScrollHint() { @@ -726,7 +720,7 @@ if (!reduceMotion && useGsap) { createScrollTriggers(); ScrollTrigger.refresh(); - initPreReveal(); + updateFromScroll(); } }); })();