mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Animation Updates
This commit is contained in:
@@ -127,17 +127,19 @@ export default function AnimatedBackground({
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
// Dense field tuned for ~40% constellation coverage on any slice. Stars
|
||||
// live in a sky disk larger than the viewport so rotation never reveals
|
||||
// an empty edge. Radius = distance to the far viewport corner.
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const maxR = Math.sqrt(cx * cx + cy * cy) + 40;
|
||||
// live in a sky disk large enough that rotation never reveals an empty
|
||||
// edge. The disk is centered on the PIVOT (the top-right corner) so the
|
||||
// whole field rotates around that corner like a star chart turning about
|
||||
// its corner mount; maxR reaches the far (bottom-left) corner.
|
||||
const cx = width;
|
||||
const cy = 0;
|
||||
const maxR = Math.sqrt(cx * cx + height * height) + 40;
|
||||
|
||||
// One star per ~4000 px^2 of the disk, capped for perf (~12% anchors).
|
||||
// One star per ~3200 px^2 of the disk, capped for perf (~14% anchors).
|
||||
const diskArea = Math.PI * maxR * maxR;
|
||||
const count = Math.min(420, Math.floor(diskArea / 4000));
|
||||
const count = Math.min(520, Math.floor(diskArea / 3200));
|
||||
stars = Array.from({ length: count }, (_, i) => {
|
||||
const anchor = i % 8 === 0; // ~12% anchors -> denser constellation backbone
|
||||
const anchor = i % 7 === 0; // ~14% anchors -> dense constellation backbone
|
||||
// Uniform-ish distribution across the disk via sqrt on radius.
|
||||
const rr = maxR * Math.sqrt(Math.random());
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
@@ -178,8 +180,9 @@ export default function AnimatedBackground({
|
||||
angle += ROT_SPEED * dt; // whole sky rotates together
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
// Pivot = top-right corner (matches the disk center used in resize()).
|
||||
const cx = width;
|
||||
const cy = 0;
|
||||
const cosA = Math.cos(angle);
|
||||
const sinA = Math.sin(angle);
|
||||
|
||||
|
||||
@@ -94,14 +94,16 @@ export default function ConstellationField({
|
||||
canvas.height = Math.floor(height * dpr);
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
const maxR = Math.sqrt(cx * cx + cy * cy) + 40;
|
||||
// Tuned for ~40% constellation coverage. One star per ~6500 px^2, scaled
|
||||
// by density; ~14% anchors for a rich constellation backbone.
|
||||
// Pivot = top-right corner. The disk is centered there so the field
|
||||
// rotates around that corner; maxR reaches the far (bottom-left) corner.
|
||||
const cx = width;
|
||||
const cy = 0;
|
||||
const maxR = Math.sqrt(cx * cx + height * height) + 40;
|
||||
// Denser field for richer constellations. One star per ~4500 px^2, scaled
|
||||
// by density; ~15% anchors for a dense constellation backbone.
|
||||
const count = Math.min(
|
||||
260,
|
||||
Math.floor(((Math.PI * maxR * maxR) / 6500) * density),
|
||||
360,
|
||||
Math.floor(((Math.PI * maxR * maxR) / 4500) * density),
|
||||
);
|
||||
stars = Array.from({ length: count }, (_, i) => {
|
||||
const anchor = i % 7 === 0;
|
||||
@@ -143,8 +145,9 @@ export default function ConstellationField({
|
||||
angle += ROT_SPEED * dt;
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
// Pivot = top-right corner (matches the disk center used in resize()).
|
||||
const cx = width;
|
||||
const cy = 0;
|
||||
const cosA = Math.cos(angle);
|
||||
const sinA = Math.sin(angle);
|
||||
const project = (rx: number, ry: number): [number, number] => [
|
||||
|
||||
@@ -18,12 +18,21 @@ import { navGroups, siteIdentity } from '@lib/site';
|
||||
const path = Astro.url.pathname;
|
||||
const isActive = (href: string) =>
|
||||
href === '/' ? path === '/' : path === href || path.startsWith(href + '/');
|
||||
|
||||
interface Props {
|
||||
/** True on the Home page. When false, the nav carries the hero-sky transition
|
||||
* name so the Home hero can morph into / out of it (expand/collapse). */
|
||||
isHome?: boolean;
|
||||
}
|
||||
const { isHome = false } = Astro.props;
|
||||
// On non-Home pages the NavBar is the morph target for the Home hero.
|
||||
const heroSkyClass = isHome ? '' : 'vt-hero-sky';
|
||||
---
|
||||
|
||||
<header
|
||||
id="site-nav"
|
||||
data-nav
|
||||
class="fixed inset-x-0 top-0 z-50 transition-[background,box-shadow,border-color] duration-300"
|
||||
class:list={['fixed inset-x-0 top-0 z-50 transition-[background,box-shadow,border-color] duration-300', heroSkyClass]}
|
||||
>
|
||||
<div class="border-b border-transparent transition-colors duration-300" data-nav-inner>
|
||||
<nav
|
||||
|
||||
@@ -43,6 +43,10 @@ const {
|
||||
ogImage,
|
||||
} = Astro.props;
|
||||
|
||||
// True on the Home page. Used to switch the NavBar's view-transition-name so
|
||||
// the Home hero can morph into / out of the nav bar (expand/collapse effect).
|
||||
const isHome = Astro.url.pathname === '/';
|
||||
|
||||
const fullTitle = title
|
||||
? `${title} | ${siteIdentity.name}`
|
||||
: `${siteIdentity.name} — Developer. Maker. Storyteller.`;
|
||||
@@ -95,14 +99,28 @@ const ogImageUrl = ogImage
|
||||
import '@/scripts/reveal-init';
|
||||
</script>
|
||||
|
||||
<!-- View Transitions flash-killer. The incoming page is snapshotted with its
|
||||
`.reveal` elements still at opacity:0 (the observer hasn't fired yet),
|
||||
which makes content vanish during the morph. This toggles a class on
|
||||
<html> around the swap so global.css forces reveals visible mid-flight,
|
||||
then lets the observer take over for scroll reveals on the new page. -->
|
||||
<script is:inline>
|
||||
document.addEventListener('astro:before-swap', () => {
|
||||
document.documentElement.classList.add('vt-transitioning');
|
||||
});
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
document.documentElement.classList.remove('vt-transitioning');
|
||||
});
|
||||
</script>
|
||||
|
||||
<ClientRouter />
|
||||
</head>
|
||||
<body data-surface={surface}>
|
||||
<body data-surface={surface} data-page={isHome ? 'home' : 'other'}>
|
||||
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:rounded-lg focus:bg-(--color-amber) focus:px-4 focus:py-2 focus:text-(--color-ink)">
|
||||
Skip to content
|
||||
</a>
|
||||
|
||||
<NavBar />
|
||||
<NavBar isHome={isHome} />
|
||||
|
||||
<main id="main">
|
||||
<slot />
|
||||
|
||||
@@ -51,7 +51,7 @@ alt: 'Resin crafting in progress',
|
||||
|
||||
<BaseLayout>
|
||||
<!-- ===================== HERO ===================== -->
|
||||
<section class="relative flex min-h-[88vh] items-center overflow-hidden pt-16">
|
||||
<section class="vt-hero-sky relative flex min-h-[88vh] items-center overflow-hidden pt-16">
|
||||
<AnimatedBackground client:idle transitionName="sky" />
|
||||
|
||||
<div class="container-prose relative z-10 flex flex-col items-start gap-8 py-20">
|
||||
|
||||
@@ -308,3 +308,71 @@
|
||||
.vt-sky {
|
||||
view-transition-name: sky;
|
||||
}
|
||||
|
||||
/* Hero expand/collapse morph. The Home hero section carries this name on Home;
|
||||
* the NavBar carries it on every OTHER page. So leaving Home morphs the tall
|
||||
* hero down into the nav bar (collapse), and arriving Home expands it back up
|
||||
* from the nav bar. The custom animation below makes the morph a smooth
|
||||
* vertical scale + position interpolation rather than the default cross-fade,
|
||||
* so the starry field reads as growing/shrinking rather than fading. */
|
||||
.vt-hero-sky {
|
||||
view-transition-name: hero-sky;
|
||||
}
|
||||
|
||||
::view-transition-group(hero-sky) {
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
::view-transition-old(hero-sky),
|
||||
::view-transition-new(hero-sky) {
|
||||
/* A gentle height/scale blend reads as expand/collapse rather than a swap. */
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* View Transitions — smooth cross-page navigation.
|
||||
*
|
||||
* Astro's <ClientRouter /> uses the native View Transitions API. Without the
|
||||
* rules below the browser falls back to a blunt default cross-fade that feels
|
||||
* harsh, and worse: the NEW page is snapshotted while its `.reveal` elements
|
||||
* are still at opacity:0 (the scroll-reveal observer hasn't fired yet), so all
|
||||
* content flashes out during the morph and re-appears after the swap. The two
|
||||
* fixes here:
|
||||
*
|
||||
* 1. `::view-transition-group(root)` gets a short, soft duration + ease so
|
||||
* the swap is quick and gentle instead of slow and jarring.
|
||||
* 2. `html:has(...)::view-transition-*` + an `astro:before-swap` class forces
|
||||
* every `.reveal` to `is-visible` DURING the transition — so the snapshot
|
||||
* of the incoming page shows its content, eliminating the flash. (The
|
||||
* reveal-init.ts observer still handles the normal scroll case.)
|
||||
*
|
||||
* Reduced-motion users get an instant swap (no animation).
|
||||
* ------------------------------------------------------------------------- */
|
||||
@view-transition {
|
||||
navigation: auto;
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 0.28s;
|
||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
/* During a transition, force all reveal content visible so the incoming page's
|
||||
* snapshot isn't blank. `.vt-transitioning` is toggled by the script below on
|
||||
* astro:before-swap and removed on astro:after-swap. */
|
||||
html.vt-transitioning .reveal {
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root),
|
||||
::view-transition-group(*) {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user