|
|
@ -21,6 +21,10 @@ function getHeaderOffset() { |
|
|
return h + 24; |
|
|
return h + 24; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function clamp(n, min, max) { |
|
|
|
|
|
return Math.min(max, Math.max(min, n)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function initHistorySpy() { |
|
|
function initHistorySpy() { |
|
|
const root = document.querySelector("[data-ah-spy]"); |
|
|
const root = document.querySelector("[data-ah-spy]"); |
|
|
if (!root) return; |
|
|
if (!root) return; |
|
|
@ -33,43 +37,62 @@ function initHistorySpy() { |
|
|
|
|
|
|
|
|
let activeYear = navItems[0].dataset.year; |
|
|
let activeYear = navItems[0].dataset.year; |
|
|
let clicking = false; |
|
|
let clicking = false; |
|
|
|
|
|
let rafId = 0; |
|
|
|
|
|
|
|
|
|
|
|
const isMobile = () => window.matchMedia("(max-width: 992px)").matches; |
|
|
|
|
|
|
|
|
const syncRailHeight = () => { |
|
|
const syncRailHeight = () => { |
|
|
if (!yearsWrap || !rail) return; |
|
|
|
|
|
|
|
|
if (!yearsWrap || !rail || isMobile()) return; |
|
|
rail.style.height = `${yearsWrap.offsetHeight}px`; |
|
|
rail.style.height = `${yearsWrap.offsetHeight}px`; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const updateLine = (item) => { |
|
|
|
|
|
if (!indicator || !item || !yearsWrap) return; |
|
|
|
|
|
syncRailHeight(); |
|
|
|
|
|
const itemRect = item.getBoundingClientRect(); |
|
|
|
|
|
const yearsRect = yearsWrap.getBoundingClientRect(); |
|
|
|
|
|
// 原点固定在顶部,线条延伸到当前高亮年份底部
|
|
|
|
|
|
const height = Math.max(itemRect.bottom - yearsRect.top, 8); |
|
|
|
|
|
indicator.style.height = `${height}px`; |
|
|
|
|
|
indicator.style.transform = ""; |
|
|
|
|
|
|
|
|
const setProgress = (progress) => { |
|
|
|
|
|
if (!indicator) return; |
|
|
|
|
|
indicator.style.transform = `scaleY(${clamp(progress, 0.04, 1)})`; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const updateLineSoon = (item) => { |
|
|
|
|
|
updateLine(item); |
|
|
|
|
|
requestAnimationFrame(() => updateLine(item)); |
|
|
|
|
|
window.setTimeout(() => updateLine(item), 320); |
|
|
|
|
|
|
|
|
const getActiveIndex = () => { |
|
|
|
|
|
const offset = getHeaderOffset() + 8; |
|
|
|
|
|
let idx = 0; |
|
|
|
|
|
for (let i = 0; i < blocks.length; i++) { |
|
|
|
|
|
if (blocks[i].getBoundingClientRect().top <= offset) idx = i; |
|
|
|
|
|
} |
|
|
|
|
|
return idx; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const setActive = (year) => { |
|
|
|
|
|
if (!year || year === activeYear) { |
|
|
|
|
|
const current = navItems.find((el) => el.dataset.year === activeYear); |
|
|
|
|
|
updateLineSoon(current); |
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 进度只跟内容滚动的年份序号插值,不依赖左侧字号布局。 |
|
|
|
|
|
* 跨年时 (idx+local)/(n-1) 单调连续,避免「先变短再变长」。 |
|
|
|
|
|
*/ |
|
|
|
|
|
const updateProgressFromScroll = () => { |
|
|
|
|
|
if (!indicator || !rail || isMobile()) return; |
|
|
|
|
|
syncRailHeight(); |
|
|
|
|
|
const n = navItems.length; |
|
|
|
|
|
if (n <= 1) { |
|
|
|
|
|
setProgress(1); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
const offset = getHeaderOffset() + 8; |
|
|
|
|
|
const idx = getActiveIndex(); |
|
|
|
|
|
let local = 1; |
|
|
|
|
|
if (blocks[idx + 1]) { |
|
|
|
|
|
const curTop = blocks[idx].getBoundingClientRect().top; |
|
|
|
|
|
const nextTop = blocks[idx + 1].getBoundingClientRect().top; |
|
|
|
|
|
const span = nextTop - curTop; |
|
|
|
|
|
local = span > 0 ? clamp((offset - curTop) / span, 0, 1) : 1; |
|
|
|
|
|
} |
|
|
|
|
|
setProgress((idx + local) / (n - 1)); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const setActive = (year, { scrollNav = true } = {}) => { |
|
|
|
|
|
if (!year || year === activeYear) return; |
|
|
activeYear = year; |
|
|
activeYear = year; |
|
|
navItems.forEach((el) => { |
|
|
navItems.forEach((el) => { |
|
|
el.classList.toggle("is-active", el.dataset.year === year); |
|
|
el.classList.toggle("is-active", el.dataset.year === year); |
|
|
}); |
|
|
}); |
|
|
|
|
|
if (scrollNav && isMobile()) { |
|
|
const current = navItems.find((el) => el.dataset.year === year); |
|
|
const current = navItems.find((el) => el.dataset.year === year); |
|
|
updateLineSoon(current); |
|
|
|
|
|
if (current && current.closest(".ah-nav__years") && window.matchMedia("(max-width: 992px)").matches) { |
|
|
|
|
|
current.scrollIntoView({ |
|
|
|
|
|
|
|
|
current?.scrollIntoView({ |
|
|
inline: "center", |
|
|
inline: "center", |
|
|
block: "nearest", |
|
|
block: "nearest", |
|
|
behavior: "smooth" |
|
|
behavior: "smooth" |
|
|
@ -77,6 +100,20 @@ function initHistorySpy() { |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const onScroll = () => { |
|
|
|
|
|
if (rafId) return; |
|
|
|
|
|
rafId = requestAnimationFrame(() => { |
|
|
|
|
|
rafId = 0; |
|
|
|
|
|
// 点击滚动过程中仍跟手更新进度;仅锁定高亮年份,避免中途抢焦点
|
|
|
|
|
|
if (!clicking) { |
|
|
|
|
|
const idx = getActiveIndex(); |
|
|
|
|
|
const year = navItems[idx]?.dataset.year; |
|
|
|
|
|
if (year) setActive(year); |
|
|
|
|
|
} |
|
|
|
|
|
updateProgressFromScroll(); |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const scrollToYear = (year) => { |
|
|
const scrollToYear = (year) => { |
|
|
const target = blocks.find((el) => el.dataset.year === year); |
|
|
const target = blocks.find((el) => el.dataset.year === year); |
|
|
if (!target) return; |
|
|
if (!target) return; |
|
|
@ -85,6 +122,7 @@ function initHistorySpy() { |
|
|
const top = window.scrollY + target.getBoundingClientRect().top - getHeaderOffset(); |
|
|
const top = window.scrollY + target.getBoundingClientRect().top - getHeaderOffset(); |
|
|
const done = () => { |
|
|
const done = () => { |
|
|
clicking = false; |
|
|
clicking = false; |
|
|
|
|
|
updateProgressFromScroll(); |
|
|
}; |
|
|
}; |
|
|
if (window.lenis) { |
|
|
if (window.lenis) { |
|
|
window.lenis.scrollTo(top, { |
|
|
window.lenis.scrollTo(top, { |
|
|
@ -107,39 +145,25 @@ function initHistorySpy() { |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const io = new IntersectionObserver((entries) => { |
|
|
|
|
|
if (clicking) return; |
|
|
|
|
|
const visible = entries.filter((e) => e.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio); |
|
|
|
|
|
if (!visible.length) return; |
|
|
|
|
|
const year = visible[0].target.getAttribute("data-year"); |
|
|
|
|
|
if (year) setActive(year); |
|
|
|
|
|
}, { |
|
|
|
|
|
root: null, |
|
|
|
|
|
rootMargin: `-${getHeaderOffset()}px 0px -45% 0px`, |
|
|
|
|
|
threshold: [0.05, 0.2, 0.4, 0.6] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
blocks.forEach((block) => io.observe(block)); |
|
|
|
|
|
|
|
|
if (window.lenis) window.lenis.on("scroll", onScroll); |
|
|
|
|
|
else window.addEventListener("scroll", onScroll, { passive: true }); |
|
|
|
|
|
|
|
|
const onScrollFallback = () => { |
|
|
|
|
|
if (clicking) return; |
|
|
|
|
|
const offset = getHeaderOffset() + 8; |
|
|
|
|
|
let current = blocks[0]?.dataset.year; |
|
|
|
|
|
for (const block of blocks) { |
|
|
|
|
|
if (block.getBoundingClientRect().top <= offset) current = block.dataset.year; |
|
|
|
|
|
|
|
|
if (typeof ResizeObserver !== "undefined" && yearsWrap) { |
|
|
|
|
|
new ResizeObserver(() => { |
|
|
|
|
|
syncRailHeight(); |
|
|
|
|
|
updateProgressFromScroll(); |
|
|
|
|
|
}).observe(yearsWrap); |
|
|
} |
|
|
} |
|
|
if (current) setActive(current); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
if (window.lenis) window.lenis.on("scroll", onScrollFallback); |
|
|
|
|
|
else window.addEventListener("scroll", onScrollFallback, { passive: true }); |
|
|
|
|
|
|
|
|
|
|
|
setActive(activeYear); |
|
|
|
|
|
window.addEventListener("resize", () => { |
|
|
window.addEventListener("resize", () => { |
|
|
const current = navItems.find((el) => el.dataset.year === activeYear); |
|
|
|
|
|
updateLineSoon(current); |
|
|
|
|
|
|
|
|
syncRailHeight(); |
|
|
|
|
|
updateProgressFromScroll(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
navItems[0]?.classList.add("is-active"); |
|
|
|
|
|
syncRailHeight(); |
|
|
|
|
|
updateProgressFromScroll(); |
|
|
|
|
|
|
|
|
const hashYear = (location.hash || "").replace(/^#year-/, ""); |
|
|
const hashYear = (location.hash || "").replace(/^#year-/, ""); |
|
|
if (hashYear && blocks.some((el) => el.dataset.year === hashYear)) { |
|
|
if (hashYear && blocks.some((el) => el.dataset.year === hashYear)) { |
|
|
requestAnimationFrame(() => scrollToYear(hashYear)); |
|
|
requestAnimationFrame(() => scrollToYear(hashYear)); |
|
|
|