diff --git a/src/components/hero/AnimatedBackground.tsx b/src/components/hero/AnimatedBackground.tsx index dfd0429..1f8d47f 100644 --- a/src/components/hero/AnimatedBackground.tsx +++ b/src/components/hero/AnimatedBackground.tsx @@ -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); diff --git a/src/components/mystic/ConstellationField.tsx b/src/components/mystic/ConstellationField.tsx index f05d94e..726788e 100644 --- a/src/components/mystic/ConstellationField.tsx +++ b/src/components/mystic/ConstellationField.tsx @@ -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] => [ diff --git a/src/components/nav/NavBar.astro b/src/components/nav/NavBar.astro index 65d1ab5..e8e3a01 100644 --- a/src/components/nav/NavBar.astro +++ b/src/components/nav/NavBar.astro @@ -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'; ---