feat(v1.2.5): 优化首页滚动顺滑性、恢复整屏吸附并提升 Halo 要求至 2.27

- 首页固定模糊半径只改 opacity;Hero 离屏停绘;Navbar 滚动状态去重
- 恢复首屏整屏吸附:仅接管 0 ↔ 内容区顶部边界,scrollend + 600ms 兜底解锁
- CSS scroll-snap proximity 实际不吸附(缺页面顶部吸附点),已移除避免与 scrollTo 冲突
- 版本 1.2.5,requires >=2.27.0;源码仓库与问题反馈改为 git.nxxy335.top
- 更新 README / FAQ / 开发指南 / 更新日志,补充性能与发版说明
This commit is contained in:
nxxy335 committed 2026-11-10 21:22:37 +08:00
1 parent 6c612911b3
commit c0ad7331d4
19 files changed
+272 -128

No files matched your search

+30 -3
View File
@@ -229,12 +229,28 @@
position: absolute;
inset: 0;
z-index: 1;
backdrop-filter: blur(0px);
-webkit-backdrop-filter: blur(0px);
will-change: backdrop-filter;
/* 模糊半径固定,滚动时只改这层的 opacity(合成层操作)。
逐帧改写 backdrop-filter 的 px 值会让浏览器每帧重做全屏背景采样,
是首页滚动掉帧的主因;固定值 + opacity 可复用已合成的结果。 */
background: color-mix(in srgb, var(--bg) 26%, transparent);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
opacity: 0;
will-change: opacity;
pointer-events: none;
}
/* 滚过首屏后 Hero 已被不透明内容区完全盖住,隐藏它可停掉
光球动画与模糊层的持续绘制(由首页滚动脚本切换 wi-hero--offscreen) */
.hero.wi-hero--offscreen {
visibility: hidden;
}
.hero.wi-hero--offscreen .hero__orb,
.hero.wi-hero--offscreen .hero__scroll-indicator {
animation-play-state: paused;
}
.hero__orb {
position: absolute;
border-radius: 50%;
@@ -454,6 +470,17 @@
animation: none;
}
@media (prefers-reduced-motion: reduce) {
.hero__orb,
.hero__scroll-indicator {
animation: none;
}
.hero__content {
transition: none;
}
}
html.dark .hero {
background: linear-gradient(
160deg,
+25 -20
View File
@@ -97,29 +97,34 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
const navbar = document.getElementById("wi-navbar");
let lastScrollY = window.scrollY;
let ticking = false;
/* 缓存状态:只在状态真正变化时才写 class,避免每个滚动帧都触发样式重算 */
let scrolled = null;
let hidden = null;
function onScroll() {
if (!ticking) {
requestAnimationFrame(() => {
const currentScrollY = window.scrollY;
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
ticking = false;
const currentScrollY = window.scrollY;
const nextScrolled = currentScrollY > 20;
const nextHidden =
currentScrollY > lastScrollY && currentScrollY > 80
? true
: currentScrollY < lastScrollY
? false
: hidden;
if (currentScrollY > 20) {
navbar?.classList.add("wi-navbar--scrolled");
} else {
navbar?.classList.remove("wi-navbar--scrolled");
}
if (currentScrollY > lastScrollY && currentScrollY > 80) {
navbar?.classList.add("wi-navbar--hidden");
} else if (currentScrollY < lastScrollY) {
navbar?.classList.remove("wi-navbar--hidden");
}
lastScrollY = currentScrollY;
ticking = false;
});
ticking = true;
}
if (nextScrolled !== scrolled) {
scrolled = nextScrolled;
navbar?.classList.toggle("wi-navbar--scrolled", scrolled);
}
if (nextHidden !== hidden) {
hidden = nextHidden;
navbar?.classList.toggle("wi-navbar--hidden", !!hidden);
}
lastScrollY = currentScrollY;
});
}
window.addEventListener("scroll", onScroll, { passive: true });
-1
View File
@@ -21,7 +21,6 @@ const fragmentAttr = layoutFragment ? "html (head, content)" : undefined;
<!doctype html>
<html
lang="zh-CN"
class={home ? 'wi-home-snap' : ''}
style="overflow-x:hidden"
th:classappend="${theme.config?.animation?.animation_enabled == false} ? 'wi-no-animations'"
th:attr="data-palette=${theme.config?.style?.color_palette ?: 'warm-island'}"
+122 -76
View File
@@ -277,106 +277,152 @@ import WelcomePopup from "../components/WelcomePopup.astro";
if (!hero || !blurOverlay) return;
var maxBlur = 20;
var triggerDistance = 300;
/* 首屏滚动渐隐只写 opacity / transform 这类合成属性。
旧实现每帧改写全屏 backdrop-filter 的模糊半径,Chromium 每帧都要
重做一次全屏背景采样 + 高斯模糊,是首页滚动掉帧的主因。 */
var fadeStart = 200;
var fadeEnd = 500;
var ticking = false;
var lastProgress = -1;
var lastHidden = null;
var content = document.querySelector(".wi-home-content--with-hero");
var contentTop = window.innerHeight;
var reduceMotion =
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* 内容区顶部位置只在初始化 / resize 时测量,避免滚动帧里读布局 */
function measure() {
if (content) {
contentTop = content.getBoundingClientRect().top + (window.scrollY || window.pageYOffset);
}
}
function update() {
ticking = false;
var scrollY = window.scrollY || window.pageYOffset;
/* 滚过首屏后 Hero 已被不透明内容区完全盖住,直接隐藏:
光球动画、模糊层都不再绘制,长页面后半段零额外开销。 */
var hidden = scrollY >= contentTop - 1;
if (hidden !== lastHidden) {
lastHidden = hidden;
hero.classList.toggle("wi-hero--offscreen", hidden);
}
if (hidden) return;
var progress = Math.max(0, Math.min(1, (scrollY - fadeStart) / (fadeEnd - fadeStart)));
if (progress === lastProgress) return;
lastProgress = progress;
/* 模糊半径固定,只调整整层不透明度:GPU 合成,无需重新采样背景 */
blurOverlay.style.opacity = String(Math.min(1, scrollY / 300));
if (heroContent) {
heroContent.style.opacity = String(1 - progress);
if (!reduceMotion) {
heroContent.style.transform = "translateY(" + (-20 * progress) + "px)";
}
}
if (scrollIndicator) {
scrollIndicator.style.opacity = String(1 - progress);
}
}
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(function () {
ticking = false;
var scrollY = window.scrollY || window.pageYOffset;
requestAnimationFrame(update);
}
var currentBlur = Math.min(scrollY / triggerDistance * maxBlur, maxBlur);
blurOverlay.style.backdropFilter = "blur(" + currentBlur + "px)";
blurOverlay.style.webkitBackdropFilter = "blur(" + currentBlur + "px)";
if (heroContent) {
var progress = Math.max(0, Math.min(1, (scrollY - fadeStart) / (fadeEnd - fadeStart)));
heroContent.style.opacity = String(1 - progress);
heroContent.style.transform = "translateY(" + (-20 * progress) + "px)";
}
if (scrollIndicator) {
var indicatorProgress = Math.max(0, Math.min(1, (scrollY - fadeStart) / (fadeEnd - fadeStart)));
scrollIndicator.style.opacity = String(1 - indicatorProgress);
}
});
function onResize() {
measure();
onScroll();
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
})();
</script>
window.addEventListener("resize", onResize, { passive: true });
measure();
update();
<script is:inline th:inline="none">
(function () {
var hero = document.getElementById("wi-hero");
var content = document.querySelector(".wi-home-content--with-hero");
if (!hero || !content) return;
/* ---- 首屏整屏吸附:只接管 0 ↔ 内容区顶部这一个边界 ----
纯 CSS scroll-snap 表达不了这个行为:内容区顶部是唯一吸附点,页面
顶部 0 没有吸附点,proximity 几乎不会触发;改成 mandatory 又会在
长内容的每一次滚动后把页面拉回内容区顶部,导致正文无法阅读。
因此只在首屏边界接管一次滚轮 / 触摸,进入内容区后完全交给原生滚动。
掉帧主因(逐帧改写 backdrop-filter)已在上面的 update() 中修复,
这里只做一次 scrollTo,不参与滚动帧绘制。 */
var snapping = false;
var snapTimer = null;
var SNAP_EDGE = 8;
var isSnapping = false;
var heroHeight = window.innerHeight;
window.addEventListener("resize", function () {
heroHeight = window.innerHeight;
}, { passive: true });
function snapTo(targetY) {
if (isSnapping) return;
isSnapping = true;
window.scrollTo({ top: targetY, behavior: "smooth" });
var timer = setTimeout(function () {
isSnapping = false;
}, 800);
if ("onscrollend" in window) {
window.addEventListener(
"scrollend",
function () {
clearTimeout(timer);
isSnapping = false;
},
{ once: true }
);
function endSnap() {
snapping = false;
if (snapTimer) {
clearTimeout(snapTimer);
snapTimer = null;
}
window.removeEventListener("scrollend", endSnap);
}
window.addEventListener("wheel", function (e) {
if (isSnapping) {
e.preventDefault();
return;
function snapTo(top) {
if (snapping) return;
snapping = true;
window.scrollTo({ top: top, behavior: reduceMotion ? "auto" : "smooth" });
/* scrollend 精确解锁;短定时器兜底,避免不支持 scrollend 时锁死输入 */
if ("onscrollend" in window) {
window.addEventListener("scrollend", endSnap, { once: true });
}
var y = window.scrollY;
if (y < heroHeight && e.deltaY > 0) {
snapTimer = setTimeout(endSnap, reduceMotion ? 100 : 600);
}
window.addEventListener(
"wheel",
function (e) {
if (!content || e.ctrlKey || e.defaultPrevented) return;
var y = window.scrollY || window.pageYOffset;
/* 内容区内部不再干预,保留原生惯性与长文阅读体验 */
if (y > contentTop + SNAP_EDGE) return;
if (snapping) {
e.preventDefault();
return;
}
if (Math.abs(e.deltaY) < 2) return;
var target = e.deltaY > 0 ? contentTop : 0;
if (Math.abs(target - y) < 2) return;
e.preventDefault();
snapTo(heroHeight);
} else if (y > 0 && y <= heroHeight && e.deltaY < 0) {
e.preventDefault();
snapTo(0);
}
}, { passive: false });
snapTo(target);
},
{ passive: false }
);
var touchStartY = 0;
window.addEventListener("touchstart", function (e) {
touchStartY = e.touches[0].clientY;
}, { passive: true });
window.addEventListener(
"touchstart",
function (e) {
touchStartY = e.touches[0].clientY;
},
{ passive: true }
);
window.addEventListener("touchend", function (e) {
if (isSnapping) return;
var diff = touchStartY - e.changedTouches[0].clientY;
var y = window.scrollY;
if (y < heroHeight && diff > 30) {
snapTo(heroHeight);
} else if (y > 0 && y <= heroHeight && diff < -30) {
snapTo(0);
}
}, { passive: true });
window.addEventListener(
"touchend",
function (e) {
if (!content || snapping) return;
var y = window.scrollY || window.pageYOffset;
if (y > contentTop + SNAP_EDGE) return;
var diff = touchStartY - e.changedTouches[0].clientY;
if (Math.abs(diff) < 30) return;
var target = diff > 0 ? contentTop : 0;
if (Math.abs(target - y) < 2) return;
snapTo(target);
},
{ passive: true }
);
})();
</script>
<!-- 首屏整屏吸附由上面的脚本在 0 ↔ 内容区顶部边界接管;
CSS 侧不再设置 scroll-snap,避免与 scrollTo 平滑动画互相抢夺滚动位置。 -->
<style>
.wi-home-content {
position: relative;
+1 -1
View File
@@ -3,7 +3,7 @@ import Layout from "../layouts/Layout.astro";
---
<!--
页面布局契约(Halo 2.26+)
页面布局契约(Halo 2.27+)
供插件前台页面通过 ~{layout :: html(head = ..., content = ...)} 复用主题外观,
避免这些页面回退到 Halo 内置的 fallback 布局导致风格不一致。
+9
View File
@@ -27,6 +27,15 @@ html {
hanging-punctuation: first last;
}
/* 首页整屏吸附由 src/pages/index.astro 的边界吸附脚本处理:CSS scroll-snap
无法只吸附 0 ↔ 内容区顶部这一个边界(见脚本注释)。这里只保留全局平滑滚动。 */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
body {
margin: 0;
padding: 0;