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:
1 parent
6c612911b3
commit
c0ad7331d4
19 files changed
+272
-128
No files matched your search
@@ -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
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -3,7 +3,7 @@ import Layout from "../layouts/Layout.astro";
|
||||
---
|
||||
|
||||
<!--
|
||||
页面布局契约(Halo 2.26+)
|
||||
页面布局契约(Halo 2.27+)
|
||||
供插件前台页面通过 ~{layout :: html(head = ..., content = ...)} 复用主题外观,
|
||||
避免这些页面回退到 Halo 内置的 fallback 布局导致风格不一致。
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user