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);
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
// Dense field tuned for ~40% constellation coverage on any slice. Stars
|
// Dense field tuned for ~40% constellation coverage on any slice. Stars
|
||||||
// live in a sky disk larger than the viewport so rotation never reveals
|
// live in a sky disk large enough that rotation never reveals an empty
|
||||||
// an empty edge. Radius = distance to the far viewport corner.
|
// edge. The disk is centered on the PIVOT (the top-right corner) so the
|
||||||
const cx = width / 2;
|
// whole field rotates around that corner like a star chart turning about
|
||||||
const cy = height / 2;
|
// its corner mount; maxR reaches the far (bottom-left) corner.
|
||||||
const maxR = Math.sqrt(cx * cx + cy * cy) + 40;
|
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 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) => {
|
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.
|
// Uniform-ish distribution across the disk via sqrt on radius.
|
||||||
const rr = maxR * Math.sqrt(Math.random());
|
const rr = maxR * Math.sqrt(Math.random());
|
||||||
const a = Math.random() * Math.PI * 2;
|
const a = Math.random() * Math.PI * 2;
|
||||||
@@ -178,8 +180,9 @@ export default function AnimatedBackground({
|
|||||||
angle += ROT_SPEED * dt; // whole sky rotates together
|
angle += ROT_SPEED * dt; // whole sky rotates together
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
const cx = width / 2;
|
// Pivot = top-right corner (matches the disk center used in resize()).
|
||||||
const cy = height / 2;
|
const cx = width;
|
||||||
|
const cy = 0;
|
||||||
const cosA = Math.cos(angle);
|
const cosA = Math.cos(angle);
|
||||||
const sinA = Math.sin(angle);
|
const sinA = Math.sin(angle);
|
||||||
|
|
||||||
|
|||||||
@@ -94,14 +94,16 @@ export default function ConstellationField({
|
|||||||
canvas.height = Math.floor(height * dpr);
|
canvas.height = Math.floor(height * dpr);
|
||||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
const cx = width / 2;
|
// Pivot = top-right corner. The disk is centered there so the field
|
||||||
const cy = height / 2;
|
// rotates around that corner; maxR reaches the far (bottom-left) corner.
|
||||||
const maxR = Math.sqrt(cx * cx + cy * cy) + 40;
|
const cx = width;
|
||||||
// Tuned for ~40% constellation coverage. One star per ~6500 px^2, scaled
|
const cy = 0;
|
||||||
// by density; ~14% anchors for a rich constellation backbone.
|
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(
|
const count = Math.min(
|
||||||
260,
|
360,
|
||||||
Math.floor(((Math.PI * maxR * maxR) / 6500) * density),
|
Math.floor(((Math.PI * maxR * maxR) / 4500) * density),
|
||||||
);
|
);
|
||||||
stars = Array.from({ length: count }, (_, i) => {
|
stars = Array.from({ length: count }, (_, i) => {
|
||||||
const anchor = i % 7 === 0;
|
const anchor = i % 7 === 0;
|
||||||
@@ -143,8 +145,9 @@ export default function ConstellationField({
|
|||||||
angle += ROT_SPEED * dt;
|
angle += ROT_SPEED * dt;
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
const cx = width / 2;
|
// Pivot = top-right corner (matches the disk center used in resize()).
|
||||||
const cy = height / 2;
|
const cx = width;
|
||||||
|
const cy = 0;
|
||||||
const cosA = Math.cos(angle);
|
const cosA = Math.cos(angle);
|
||||||
const sinA = Math.sin(angle);
|
const sinA = Math.sin(angle);
|
||||||
const project = (rx: number, ry: number): [number, number] => [
|
const project = (rx: number, ry: number): [number, number] => [
|
||||||
|
|||||||
@@ -18,12 +18,21 @@ import { navGroups, siteIdentity } from '@lib/site';
|
|||||||
const path = Astro.url.pathname;
|
const path = Astro.url.pathname;
|
||||||
const isActive = (href: string) =>
|
const isActive = (href: string) =>
|
||||||
href === '/' ? path === '/' : path === href || path.startsWith(href + '/');
|
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
|
<header
|
||||||
id="site-nav"
|
id="site-nav"
|
||||||
data-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>
|
<div class="border-b border-transparent transition-colors duration-300" data-nav-inner>
|
||||||
<nav
|
<nav
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ const {
|
|||||||
ogImage,
|
ogImage,
|
||||||
} = Astro.props;
|
} = 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
|
const fullTitle = title
|
||||||
? `${title} | ${siteIdentity.name}`
|
? `${title} | ${siteIdentity.name}`
|
||||||
: `${siteIdentity.name} — Developer. Maker. Storyteller.`;
|
: `${siteIdentity.name} — Developer. Maker. Storyteller.`;
|
||||||
@@ -95,14 +99,28 @@ const ogImageUrl = ogImage
|
|||||||
import '@/scripts/reveal-init';
|
import '@/scripts/reveal-init';
|
||||||
</script>
|
</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 />
|
<ClientRouter />
|
||||||
</head>
|
</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)">
|
<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
|
Skip to content
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<NavBar />
|
<NavBar isHome={isHome} />
|
||||||
|
|
||||||
<main id="main">
|
<main id="main">
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ alt: 'Resin crafting in progress',
|
|||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<!-- ===================== HERO ===================== -->
|
<!-- ===================== 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" />
|
<AnimatedBackground client:idle transitionName="sky" />
|
||||||
|
|
||||||
<div class="container-prose relative z-10 flex flex-col items-start gap-8 py-20">
|
<div class="container-prose relative z-10 flex flex-col items-start gap-8 py-20">
|
||||||
|
|||||||
@@ -308,3 +308,71 @@
|
|||||||
.vt-sky {
|
.vt-sky {
|
||||||
view-transition-name: 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