Add header animation

This commit is contained in:
ClovertaTheTrilobita 2026-09-17 12:05:55 +08:00
parent c4a1c21f49
commit dd695c8237
3 changed files with 342 additions and 16 deletions

View file

@ -8,7 +8,9 @@ const t = getTranslations(lang);
--- ---
<footer class="footer"> <footer class="footer">
<Divider /> <div class="footer-divider" data-footer-divider>
<Divider />
</div>
<a <a
href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog" href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog"
class="github" class="github"

View file

@ -8,6 +8,7 @@ const lang = getLangFromUrl(Astro.url);
const t = getTranslations(lang); const t = getTranslations(lang);
--- ---
<div class="header-slot">
<header class="site-header"> <header class="site-header">
<div class="theme-icon-wrap"> <div class="theme-icon-wrap">
<ThemeIcon /> <ThemeIcon />
@ -30,19 +31,26 @@ const t = getTranslations(lang);
<Divider /> <Divider />
</div> </div>
</header> </header>
</div>
<style> <style>
.site-header {
position: relative;
z-index: 30;
padding-top: 0.5rem;
}
.banner-link { .banner-link {
display: inline-block; display: block;
} }
.banner-logo { .banner-logo {
display: block; display: block;
height: 55px; /* 按你的 banner 比例改 */ height: 55px;
background-color: #1f2328; background-color: #1f2328;
-webkit-mask: url("/images/banner-black.svg") no-repeat left / contain; -webkit-mask: url("/images/banner-black.svg") no-repeat left / contain;
mask: url("/images/banner-black.svg") no-repeat left / contain; mask: url("/images/banner-black.svg") no-repeat left / contain;
transform-origin: left center;
} }
:global(html.dark .banner-logo) { :global(html.dark .banner-logo) {
@ -55,11 +63,6 @@ const t = getTranslations(lang);
display: block; display: block;
} }
.site-header {
position: relative;
padding-top: 0.5rem;
}
.theme-icon-wrap { .theme-icon-wrap {
position: absolute; position: absolute;
top: 0; top: 0;
@ -71,17 +74,14 @@ const t = getTranslations(lang);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
max-height: 12rem;
} }
.header-nav .banner { .header-nav .banner {
margin: 0; margin: 0;
font-style: italic; font-style: italic;
line-height: 1.1; line-height: 1.1;
/* 给右上角 ThemeIcon 留空间 */
max-width: calc(100% - 7rem); max-width: calc(100% - 7rem);
/* 桌面正常,手机自动缩小 */
font-size: clamp(1.6rem, 6vw, 2.5rem); font-size: clamp(1.6rem, 6vw, 2.5rem);
} }
@ -90,8 +90,108 @@ const t = getTranslations(lang);
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@media (max-width: 768px) { @media (min-width: 769px) {
.header-slot {
/* Keep document height independent of scroll-driven header height. */
display: flow-root;
position: sticky;
top: 0;
z-index: 30;
height: var(--header-slot-height, auto);
pointer-events: none;
}
.site-header { .site-header {
pointer-events: auto;
width: var(--header-width, 100%);
max-width: none;
margin-left: var(--header-offset, 0);
margin-right: var(--header-offset, 0);
padding: 0.5rem var(--header-inline-padding, 0) 0;
background-color: color-mix(
in srgb,
var(--background-color) var(--header-surface-weight, 0%),
transparent
);
backdrop-filter: blur(var(--header-blur, 0));
transform: translateY(0);
transition: transform 0.3s ease;
}
.header-slot.is-exiting .site-header {
transform: translateY(calc(-100% - 0.5rem));
}
:global(html.dark) .site-header {
background-color: color-mix(
in srgb,
#1e1e1e var(--header-surface-weight, 0%),
transparent
);
}
.theme-icon-wrap {
top: var(--theme-top, 0);
right: var(--theme-right, 0);
}
.header-nav {
gap: var(--header-gap, 1rem);
height: var(--header-nav-height, auto);
max-height: var(--header-nav-height, 12rem);
transform: translateY(var(--header-nav-y, 0));
}
.header-nav .banner {
flex-shrink: 0;
width: var(--banner-width, auto);
max-width: var(--banner-width, calc(100% - 7rem));
}
.banner-logo {
height: var(--logo-height, 55px);
}
.site-header :global(.site-nav) {
width: var(--site-nav-width, auto);
margin: var(--site-nav-margin, 1rem) 0;
padding-bottom: var(--site-nav-padding, 1rem);
transform: translate(
var(--site-nav-x, 0),
var(--site-nav-y, 0)
);
}
.site-header :global(.site-nav-desktop) {
gap: var(--site-nav-gap, 1.5rem);
}
.site-header :global(.site-nav a) {
font-size: var(--site-nav-font-size, 1.2rem);
}
.site-header > .divider {
width: var(--divider-width, 100%);
margin-left: var(--divider-offset, 0);
margin-right: var(--divider-offset, 0);
margin-top: var(--divider-margin-top, 0.2rem);
margin-bottom: var(--divider-margin-bottom, 2rem);
transform: translateY(var(--divider-y, 0));
}
.site-header > .divider :global(.divider) {
width: 100%;
margin-bottom: var(--divider-inner-margin-bottom, 1rem);
}
}
@media (max-width: 768px) {
.header-slot {
display: contents;
}
.site-header {
position: relative;
padding-top: 0.25rem; padding-top: 0.25rem;
} }
@ -100,4 +200,223 @@ const t = getTranslations(lang);
font-size: clamp(1.25rem, 7vw, 2rem); font-size: clamp(1.25rem, 7vw, 2rem);
} }
} }
</style> </style>
<script>
const desktopHeaderQuery = window.matchMedia("(min-width: 769px)");
const HEADER_SCROLL_DISTANCE = 120;
const headerStyleProperties = [
"--header-width",
"--header-offset",
"--header-inline-padding",
"--header-surface-weight",
"--header-blur",
"--theme-top",
"--theme-right",
"--header-gap",
"--header-nav-height",
"--header-nav-y",
"--banner-width",
"--logo-height",
"--site-nav-width",
"--site-nav-margin",
"--site-nav-padding",
"--site-nav-x",
"--site-nav-y",
"--site-nav-gap",
"--site-nav-font-size",
"--divider-margin-top",
"--divider-margin-bottom",
"--divider-inner-margin-bottom",
"--divider-width",
"--divider-offset",
"--divider-y",
];
type HeaderMetrics = {
header: HTMLElement;
expandedWidth: number;
expandedNavHeight: number;
expandedBannerWidth: number;
expandedSiteNavWidth: number;
rootFontSize: number;
};
let headerFrame = 0;
let headerMetrics: HeaderMetrics | null = null;
let footerDividerObserver: IntersectionObserver | null = null;
const lerp = (start: number, end: number, progress: number) =>
start + (end - start) * progress;
function clearHeaderStyles(header: HTMLElement) {
headerStyleProperties.forEach((property) => {
header.style.removeProperty(property);
});
}
function measureHeader(header: HTMLElement): HeaderMetrics | null {
clearHeaderStyles(header);
const slot = header.parentElement;
if (!slot) return null;
slot.style.removeProperty("--header-slot-height");
const headerNav = header.querySelector<HTMLElement>(".header-nav");
const banner = header.querySelector<HTMLElement>(".banner");
const siteNav = header.querySelector<HTMLElement>(".site-nav");
if (!headerNav || !banner || !siteNav) return null;
// Measure the expanded footprint, including its bottom spacing, once.
// Only the contents animate; the following page content never reflows.
slot.style.setProperty(
"--header-slot-height",
`${slot.getBoundingClientRect().height}px`,
);
return {
header,
expandedWidth: header.getBoundingClientRect().width,
expandedNavHeight: headerNav.getBoundingClientRect().height,
expandedBannerWidth: banner.getBoundingClientRect().width,
expandedSiteNavWidth: siteNav.getBoundingClientRect().width,
rootFontSize: Number.parseFloat(
getComputedStyle(document.documentElement).fontSize,
),
};
}
function updateHeaderState() {
const header = document.querySelector<HTMLElement>(".site-header");
if (!header) return;
if (!desktopHeaderQuery.matches) {
clearHeaderStyles(header);
header.parentElement?.style.removeProperty("--header-slot-height");
headerMetrics = null;
return;
}
if (!headerMetrics || headerMetrics.header !== header) {
headerMetrics = measureHeader(header);
}
if (!headerMetrics) return;
const {
expandedWidth,
expandedNavHeight,
expandedBannerWidth,
expandedSiteNavWidth,
rootFontSize,
} = headerMetrics;
const rawProgress = Math.min(
Math.max(window.scrollY / HEADER_SCROLL_DISTANCE, 0),
1,
);
const progress = rawProgress;
const viewportWidth = document.documentElement.clientWidth;
const compactLogoWidth = Math.min(
Math.max(9 * rootFontSize, viewportWidth * 0.22),
12 * rootFontSize,
);
const headerWidth = lerp(expandedWidth, viewportWidth, progress);
const headerOffset = -(headerWidth - expandedWidth) / 2;
const inlinePadding = lerp(0, rootFontSize, progress);
const compactSiteNavWidth = Math.max(
viewportWidth -
2 * rootFontSize -
compactLogoWidth -
7.5 * rootFontSize,
0,
);
const compactNavGap = Math.min(
Math.max(0.75 * rootFontSize, viewportWidth * 0.02),
1.5 * rootFontSize,
);
const values: Record<string, string> = {
"--header-width": `${headerWidth}px`,
"--header-offset": `${headerOffset}px`,
"--header-inline-padding": `${inlinePadding}px`,
"--header-surface-weight": `${lerp(0, 94, progress)}%`,
"--header-blur": `${lerp(0, 10, progress)}px`,
"--theme-top": `${lerp(0, 0.85 * rootFontSize, progress)}px`,
"--theme-right": `${inlinePadding}px`,
"--header-gap": `${lerp(rootFontSize, 0, progress)}px`,
"--header-nav-height": `${lerp(expandedNavHeight, 3 * rootFontSize, progress)}px`,
"--header-nav-y": `${lerp(0, 0.5 * rootFontSize, progress)}px`,
"--banner-width": `${lerp(expandedBannerWidth, compactLogoWidth, progress)}px`,
"--logo-height": `${lerp(55, 36, progress)}px`,
"--site-nav-width": `${lerp(expandedSiteNavWidth, compactSiteNavWidth, progress)}px`,
"--site-nav-margin": `${lerp(rootFontSize, 0, progress)}px`,
"--site-nav-padding": `${lerp(rootFontSize, 0, progress)}px`,
"--site-nav-x": `${lerp(0, compactLogoWidth + 1.5 * rootFontSize, progress)}px`,
"--site-nav-y": `${lerp(0, -36, progress)}px`,
"--site-nav-gap": `${lerp(1.5 * rootFontSize, compactNavGap, progress)}px`,
"--site-nav-font-size": `${lerp(1.2 * rootFontSize, 1.05 * rootFontSize, progress)}px`,
"--divider-margin-top": `${lerp(0.2 * rootFontSize, 0.45 * rootFontSize, progress)}px`,
"--divider-margin-bottom": `${lerp(2 * rootFontSize, 0, progress)}px`,
"--divider-inner-margin-bottom": `${lerp(rootFontSize, 0, progress)}px`,
"--divider-width": `${headerWidth}px`,
"--divider-offset": `${-inlinePadding}px`,
"--divider-y": `${lerp(0, -0.1 * rootFontSize, progress)}px`,
};
Object.entries(values).forEach(([property, value]) => {
header.style.setProperty(property, value);
});
}
function requestHeaderUpdate() {
if (headerFrame) return;
headerFrame = window.requestAnimationFrame(() => {
headerFrame = 0;
updateHeaderState();
});
}
function bindFooterDividerObserver() {
footerDividerObserver?.disconnect();
footerDividerObserver = null;
const slot = document.querySelector<HTMLElement>(".header-slot");
const footerDivider = document.querySelector<HTMLElement>(
"[data-footer-divider]",
);
if (!slot) return;
slot.classList.remove("is-exiting");
if (!desktopHeaderQuery.matches || !footerDivider) return;
footerDividerObserver = new IntersectionObserver(([entry]) => {
slot.classList.toggle("is-exiting", entry.isIntersecting);
});
footerDividerObserver.observe(footerDivider);
}
window.addEventListener("scroll", requestHeaderUpdate, { passive: true });
window.addEventListener("resize", () => {
headerMetrics = null;
requestHeaderUpdate();
});
desktopHeaderQuery.addEventListener("change", () => {
requestHeaderUpdate();
bindFooterDividerObserver();
});
document.addEventListener("astro:page-load", () => {
headerMetrics = null;
updateHeaderState();
bindFooterDividerObserver();
});
document.fonts.ready.then(() => {
headerMetrics = null;
requestHeaderUpdate();
});
updateHeaderState();
bindFooterDividerObserver();
</script>

View file

@ -129,10 +129,15 @@ const {
</html> </html>
<style> <style>
html, html {
margin: 0;
width: 100%;
min-height: 100%;
}
body { body {
margin: 0; margin: 0;
width: 100%; width: 100%;
height: 100%; min-height: 100%;
} }
</style> </style>