feakyyy (#1300)
This commit is contained in:
+146
-36
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user