fix: replace scroll-then-snap with direction-based one-screen-at-a-time navigation
This commit is contained in:
parent
7a5430d40a
commit
c705bc96e9
181
index.html
181
index.html
|
|
@ -974,106 +974,115 @@
|
||||||
|
|
||||||
// 移动端:入场动画 + 视差滚动
|
// 移动端:入场动画 + 视差滚动
|
||||||
if (window.innerWidth <= 900) {
|
if (window.innerWidth <= 900) {
|
||||||
// 移除 CSS scroll-snap(不可靠),用 JS 实现
|
// 3. 逐屏滚动 - 方向感知,禁止自由滚动
|
||||||
document.body.style.scrollSnapType = 'none';
|
|
||||||
document.querySelectorAll('[style*="scroll-snap"]').forEach(el => {
|
|
||||||
el.style.scrollSnapType = 'none';
|
|
||||||
el.style.scrollSnapAlign = 'none';
|
|
||||||
el.style.scrollSnapStop = 'normal';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1. 入场动画(一次性)
|
|
||||||
const mobileItems = document.querySelectorAll('#pain-points .pain-item, #approach .approach-item');
|
|
||||||
const mobileObserver = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) entry.target.classList.add('in-view');
|
|
||||||
});
|
|
||||||
}, { threshold: 0.05, rootMargin: "0px 0px -5% 0px" });
|
|
||||||
mobileItems.forEach(item => mobileObserver.observe(item));
|
|
||||||
|
|
||||||
// 2. 数字编号滚动视差(基于卡片相对视口位置)
|
|
||||||
let ticking = false;
|
|
||||||
const painNums = document.querySelectorAll('#pain-points .pain-num');
|
|
||||||
|
|
||||||
function updateParallax() {
|
|
||||||
painNums.forEach(num => {
|
|
||||||
const card = num.parentElement;
|
|
||||||
if (!card) return;
|
|
||||||
const rect = card.getBoundingClientRect();
|
|
||||||
const speed = 0.15;
|
|
||||||
const yOffset = -(rect.top * speed);
|
|
||||||
num.style.transform = `translateY(${yOffset}px)`;
|
|
||||||
});
|
|
||||||
ticking = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
if (!ticking) {
|
|
||||||
requestAnimationFrame(updateParallax);
|
|
||||||
ticking = true;
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
updateParallax();
|
|
||||||
|
|
||||||
// 3. JS 逐屏吸附
|
|
||||||
// 收集所有吸附点
|
|
||||||
const snapTargets = [];
|
const snapTargets = [];
|
||||||
// Hero
|
|
||||||
const hero = document.querySelector('.hero');
|
const hero = document.querySelector('.hero');
|
||||||
if (hero) snapTargets.push(hero);
|
if (hero) snapTargets.push(hero);
|
||||||
// 痛点每个卡片
|
|
||||||
document.querySelectorAll('#pain-points .pain-item').forEach(item => snapTargets.push(item));
|
document.querySelectorAll('#pain-points .pain-item').forEach(item => snapTargets.push(item));
|
||||||
// 其他 section
|
|
||||||
['solutions', 'approach', 'geo', 'contact'].forEach(id => {
|
['solutions', 'approach', 'geo', 'contact'].forEach(id => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) snapTargets.push(el);
|
if (el) snapTargets.push(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
let scrollEndTimer = null;
|
let currentIndex = 0;
|
||||||
let isSnapping = false;
|
let isAnimating = false;
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
let lastScrollTop = 0;
|
let touchStartX = 0;
|
||||||
|
let animFrame = null;
|
||||||
|
|
||||||
document.addEventListener('touchstart', (e) => {
|
// 找到离视口顶部最近的 section 索引
|
||||||
touchStartY = e.touches[0].clientY;
|
function findCurrentIndex() {
|
||||||
}, { passive: true });
|
let best = 0;
|
||||||
|
let bestDist = Infinity;
|
||||||
window.addEventListener('scroll', () => {
|
snapTargets.forEach((target, i) => {
|
||||||
// 清除之前的定时器
|
const top = target.getBoundingClientRect().top + window.scrollY;
|
||||||
clearTimeout(scrollEndTimer);
|
const dist = Math.abs(top - window.scrollY);
|
||||||
// 300ms 无滚动视为停止
|
if (dist < bestDist) {
|
||||||
scrollEndTimer = setTimeout(() => {
|
bestDist = dist;
|
||||||
if (isSnapping) return; // 正在吸附中不触发
|
best = i;
|
||||||
snapToNearest();
|
|
||||||
}, 200);
|
|
||||||
lastScrollTop = window.scrollY;
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
function snapToNearest() {
|
|
||||||
const vh = window.innerHeight;
|
|
||||||
const center = window.scrollY + vh * 0.4; // 吸附点偏上 40% 位置
|
|
||||||
|
|
||||||
let nearest = null;
|
|
||||||
let minDist = Infinity;
|
|
||||||
|
|
||||||
snapTargets.forEach(target => {
|
|
||||||
const rect = target.getBoundingClientRect();
|
|
||||||
const top = rect.top + window.scrollY;
|
|
||||||
const dist = Math.abs(top - center);
|
|
||||||
if (dist < minDist) {
|
|
||||||
minDist = dist;
|
|
||||||
nearest = target;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return best;
|
||||||
if (nearest) {
|
|
||||||
isSnapping = true;
|
|
||||||
const targetY = nearest.getBoundingClientRect().top + window.scrollY;
|
|
||||||
window.scrollTo({ top: targetY, behavior: 'smooth' });
|
|
||||||
// 吸附完成后解锁
|
|
||||||
setTimeout(() => { isSnapping = false; }, 800);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换到指定 section
|
||||||
|
function goTo(index) {
|
||||||
|
if (index < 0 || index >= snapTargets.length || isAnimating) return;
|
||||||
|
isAnimating = true;
|
||||||
|
const targetY = snapTargets[index].getBoundingClientRect().top + window.scrollY;
|
||||||
|
const startY = window.scrollY;
|
||||||
|
const diff = targetY - startY;
|
||||||
|
const duration = 600;
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
// 自定义缓动:先快后慢,有惯性感觉
|
||||||
|
function easeOutCubic(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function step(now) {
|
||||||
|
const elapsed = now - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
const eased = easeOutCubic(progress);
|
||||||
|
window.scrollTo(0, startY + diff * eased);
|
||||||
|
if (progress < 1) {
|
||||||
|
animFrame = requestAnimationFrame(step);
|
||||||
|
} else {
|
||||||
|
isAnimating = false;
|
||||||
|
currentIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animFrame = requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
currentIndex = findCurrentIndex();
|
||||||
|
|
||||||
|
// 完全接管触摸滚动 - 禁止原生滚动
|
||||||
|
let canScroll = true;
|
||||||
|
document.addEventListener('touchmove', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
document.addEventListener('touchstart', (e) => {
|
||||||
|
if (!canScroll || isAnimating) return;
|
||||||
|
touchStartY = e.touches[0].clientY;
|
||||||
|
touchStartX = e.touches[0].clientX;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener('touchend', (e) => {
|
||||||
|
if (!canScroll || isAnimating) return;
|
||||||
|
const dy = touchStartY - e.changedTouches[0].clientY;
|
||||||
|
const dx = touchStartX - e.changedTouches[0].clientX;
|
||||||
|
|
||||||
|
// 水平滑动不处理(可能是浏览器前进后退手势)
|
||||||
|
if (Math.abs(dx) > Math.abs(dy)) return;
|
||||||
|
// 最小滑动阈值,防止误触
|
||||||
|
if (Math.abs(dy) < 40) return;
|
||||||
|
|
||||||
|
if (dy > 0) {
|
||||||
|
// 上滑 -> 下一屏
|
||||||
|
goTo(currentIndex + 1);
|
||||||
|
} else {
|
||||||
|
// 下滑 -> 上一屏
|
||||||
|
goTo(currentIndex - 1);
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
// 导航链接点击:定位到对应 section 并更新索引
|
||||||
|
document.querySelectorAll('.nav-links a, .btn').forEach(link => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
const href = link.getAttribute('href');
|
||||||
|
if (!href || !href.startsWith('#')) return;
|
||||||
|
const targetId = href.substring(1);
|
||||||
|
const idx = snapTargets.findIndex(t => t.id === targetId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
currentIndex = idx;
|
||||||
|
goTo(idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue