This commit is contained in:
NotSoToothless
2026-06-04 13:55:14 -07:00
committed by GitHub
parent 62d25125cc
commit 732730829d
+146 -36
View File
@@ -417,6 +417,8 @@
}; };
</script> </script>
<script src="/js/main.js?v=3"></script> <script src="/js/main.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
<!-- Serpentine timeline renderer --> <!-- Serpentine timeline renderer -->
<script> <script>
@@ -430,9 +432,16 @@
var nodes = Array.prototype.slice.call(timeline.querySelectorAll('.timeline-node')); var nodes = Array.prototype.slice.call(timeline.querySelectorAll('.timeline-node'));
var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var useGsap = !!(window.gsap && window.ScrollTrigger);
var drawProgress = 0; // 0 to 1, how much of the line is drawn var drawProgress = 0; // 0 to 1, how much of the line is drawn
var maxDrawProgress = 0; var targetDrawProgress = 0;
var ticking = false; var pathLength = 0;
var nodeProgresses = [];
var nodeRevealed = [];
var state = { progress: 0 };
var lineTrigger = null;
var nodeTriggers = [];
var scrollNormalized = false;
// Build a rounded-corner path that threads through a list of points. // Build a rounded-corner path that threads through a list of points.
function roundedPath(pts, radius) { function roundedPath(pts, radius) {
@@ -473,11 +482,14 @@
track.setAttribute('d', d); track.setAttribute('d', d);
progress.setAttribute('d', d); progress.setAttribute('d', d);
var len = progress.getTotalLength(); pathLength = progress.getTotalLength();
progress.style.strokeDasharray = len; progress.style.strokeDasharray = pathLength;
progress.style.strokeDashoffset = len * (1 - drawProgress); progress.style.strokeDashoffset = pathLength * (1 - drawProgress);
updateComet(len); nodeProgresses = nodes.map(function (node, index) {
return { pts: pts, len: len }; return nodePathProgress(node, index, pathLength);
});
updateComet(pathLength);
return { pts: pts, len: pathLength };
} }
function updateComet(len) { function updateComet(len) {
@@ -511,19 +523,8 @@
return best; return best;
} }
function updateFromScroll() { function nodePathProgress(node, index, len) {
ticking = false; if (index === nodes.length - 1) return 1;
var len = progress.getTotalLength();
var viewport = window.innerHeight || document.documentElement.clientHeight;
var revealLine = viewport * 0.9;
var targetProgress = maxDrawProgress;
nodes.forEach(function (node, index) {
var cardRect = node.getBoundingClientRect();
if (cardRect.top > revealLine) return;
node.classList.add('is-visible');
var marker = node.querySelector('.timeline-marker'); var marker = node.querySelector('.timeline-marker');
var markerRect = (marker || node).getBoundingClientRect(); var markerRect = (marker || node).getBoundingClientRect();
var box = timeline.getBoundingClientRect(); var box = timeline.getBoundingClientRect();
@@ -531,36 +532,139 @@
x: markerRect.left - box.left + markerRect.width / 2, x: markerRect.left - box.left + markerRect.width / 2,
y: markerRect.top - box.top + markerRect.height / 2 y: markerRect.top - box.top + markerRect.height / 2
}; };
var nodeProgress = index === nodes.length - 1 ? 1 : lengthAtPoint(pt, len) / len; return lengthAtPoint(pt, len) / len;
targetProgress = Math.max(targetProgress, nodeProgress); }
});
maxDrawProgress = Math.max(maxDrawProgress, targetProgress); function revealReachedNodes(len) {
drawProgress = maxDrawProgress; nodes.forEach(function (node, index) {
progress.style.strokeDashoffset = len * (1 - drawProgress); if (nodeRevealed[index]) return;
updateComet(len); if (drawProgress >= (nodeProgresses[index] || 0) - 0.012) {
nodeRevealed[index] = true;
node.classList.add('is-visible');
if (useGsap) {
gsap.to(node, {
autoAlpha: 1,
y: 0,
scale: 1,
duration: 0.7,
ease: 'power3.out',
overwrite: true
});
}
}
});
}
function renderProgress() {
drawProgress = state.progress;
progress.style.strokeDashoffset = pathLength * (1 - drawProgress);
updateComet(pathLength);
revealReachedNodes(pathLength);
if (drawProgress >= 1) { if (drawProgress >= 1) {
comet.style.opacity = 0; comet.style.opacity = 0;
nodes.forEach(function (n) { n.classList.add('is-visible'); }); nodes.forEach(function (n, index) {
nodeRevealed[index] = true;
n.classList.add('is-visible');
if (useGsap) gsap.set(n, { autoAlpha: 1, y: 0, scale: 1 });
});
} }
} }
function requestScrollUpdate() { function advanceTo(value, duration) {
if (ticking) return; var next = Math.max(targetDrawProgress, value);
ticking = true; targetDrawProgress = Math.min(1, Math.max(0, next));
requestAnimationFrame(updateFromScroll); if (targetDrawProgress <= state.progress + 0.001) return;
if (!useGsap) {
state.progress = targetDrawProgress;
renderProgress();
return;
}
gsap.to(state, {
progress: targetDrawProgress,
duration: duration || 0.85,
ease: 'power3.out',
overwrite: true,
onUpdate: renderProgress
});
}
function destroyScrollTriggers() {
if (lineTrigger) {
lineTrigger.kill();
lineTrigger = null;
}
nodeTriggers.forEach(function (trigger) { trigger.kill(); });
nodeTriggers = [];
}
function createScrollTriggers() {
if (!useGsap) return;
gsap.registerPlugin(ScrollTrigger);
if (!scrollNormalized) {
try {
ScrollTrigger.normalizeScroll({
allowNestedScroll: true,
lockAxis: false,
momentum: function (self) {
return Math.min(2.4, Math.abs(self.velocityY) / 1100);
}
});
} catch (e) {
ScrollTrigger.normalizeScroll(true);
}
scrollNormalized = true;
}
destroyScrollTriggers();
nodes.forEach(function (node, index) {
gsap.set(node, nodeRevealed[index]
? { autoAlpha: 1, y: 0, scale: 1, transformOrigin: '50% 50%' }
: { autoAlpha: 0, y: 34, scale: 0.985, transformOrigin: '50% 50%' });
});
lineTrigger = ScrollTrigger.create({
trigger: timeline,
start: 'top 82%',
endTrigger: nodes[nodes.length - 1] || timeline,
end: 'top 82%',
scrub: 0.75,
invalidateOnRefresh: true,
onUpdate: function (self) {
advanceTo(self.progress, 0.45);
}
});
nodes.forEach(function (node, index) {
nodeTriggers.push(ScrollTrigger.create({
trigger: node,
start: 'top 82%',
once: true,
onEnter: function () {
advanceTo(nodeProgresses[index] || 0, 0.85);
}
}));
});
} }
function init() { function init() {
build(); build();
if (reduceMotion) { if (reduceMotion) {
drawProgress = 1; drawProgress = 1;
state.progress = 1;
progress.style.strokeDashoffset = 0; progress.style.strokeDashoffset = 0;
nodes.forEach(function (n) { n.classList.add('is-visible'); }); nodes.forEach(function (n) { n.classList.add('is-visible'); });
return; return;
} }
updateFromScroll(); if (useGsap) {
window.addEventListener('scroll', requestScrollUpdate, { passive: true }); createScrollTriggers();
renderProgress();
ScrollTrigger.refresh();
} else {
advanceTo(1, 0);
}
} }
// Recompute geometry on resize (layout flips between 1 and 3 columns). // Recompute geometry on resize (layout flips between 1 and 3 columns).
@@ -570,7 +674,10 @@
resizeTimer = setTimeout(function () { resizeTimer = setTimeout(function () {
_samples = null; _samples = null;
build(); build();
if (!reduceMotion) updateFromScroll(); if (!reduceMotion && useGsap) {
createScrollTriggers();
ScrollTrigger.refresh();
}
}, 120); }, 120);
}); });
@@ -583,7 +690,10 @@
window.addEventListener('load', function () { window.addEventListener('load', function () {
_samples = null; _samples = null;
build(); build();
if (!reduceMotion) updateFromScroll(); if (!reduceMotion && useGsap) {
createScrollTriggers();
ScrollTrigger.refresh();
}
}); });
})(); })();
</script> </script>