mirror of
https://github.com/ClovertaTheTrilobita/SanYeCao-blog.git
synced 2026-10-25 21:43:53 +00:00
Add header animation
This commit is contained in:
parent
c4a1c21f49
commit
dd695c8237
3 changed files with 342 additions and 16 deletions
|
|
@ -8,7 +8,9 @@ const t = getTranslations(lang);
|
|||
---
|
||||
|
||||
<footer class="footer">
|
||||
<div class="footer-divider" data-footer-divider>
|
||||
<Divider />
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/ClovertaTheTrilobita/SanYeCao-blog"
|
||||
class="github"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const lang = getLangFromUrl(Astro.url);
|
|||
const t = getTranslations(lang);
|
||||
---
|
||||
|
||||
<div class="header-slot">
|
||||
<header class="site-header">
|
||||
<div class="theme-icon-wrap">
|
||||
<ThemeIcon />
|
||||
|
|
@ -30,19 +31,26 @@ const t = getTranslations(lang);
|
|||
<Divider />
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.site-header {
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.banner-link {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.banner-logo {
|
||||
display: block;
|
||||
height: 55px; /* 按你的 banner 比例改 */
|
||||
height: 55px;
|
||||
background-color: #1f2328;
|
||||
|
||||
-webkit-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) {
|
||||
|
|
@ -55,11 +63,6 @@ const t = getTranslations(lang);
|
|||
display: block;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
position: relative;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-icon-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -71,17 +74,14 @@ const t = getTranslations(lang);
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-height: 12rem;
|
||||
}
|
||||
|
||||
.header-nav .banner {
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
line-height: 1.1;
|
||||
|
||||
/* 给右上角 ThemeIcon 留空间 */
|
||||
max-width: calc(100% - 7rem);
|
||||
|
||||
/* 桌面正常,手机自动缩小 */
|
||||
font-size: clamp(1.6rem, 6vw, 2.5rem);
|
||||
}
|
||||
|
||||
|
|
@ -90,8 +90,108 @@ const t = getTranslations(lang);
|
|||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -100,4 +200,223 @@ const t = getTranslations(lang);
|
|||
font-size: clamp(1.25rem, 7vw, 2rem);
|
||||
}
|
||||
}
|
||||
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -129,10 +129,15 @@ const {
|
|||
</html>
|
||||
|
||||
<style>
|
||||
html,
|
||||
html {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue