From 1b8351877479c3f024684b1820b10078cd7343ab Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:20:54 +0530 Subject: [PATCH 1/4] Replace bloom blur filters with gradient falloff --- src/app/globals.css | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 908d077..762a999 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -79,14 +79,19 @@ body { position: absolute; display: block; border-radius: 50%; - filter: blur(82px); mix-blend-mode: multiply; - background: radial-gradient(circle at 50% 50%, var(--accent), transparent 66%); + /* Softness lives in the gradient falloff itself — a `filter: blur()` here + forces the GPU to re-filter a ~52vw texture every animation frame. */ + background: radial-gradient( + circle at 50% 50%, + var(--accent), + color-mix(in oklab, var(--accent) 45%, transparent) 34%, + transparent 72% + ); will-change: transform; } .dark .aurora b { mix-blend-mode: screen; - filter: blur(72px); } .aurora .a1 { width: 52vw; @@ -149,9 +154,13 @@ body { position: absolute; display: block; border-radius: 50%; - filter: blur(90px); mix-blend-mode: multiply; - background: radial-gradient(circle at 50% 50%, var(--accent), transparent 64%); + background: radial-gradient( + circle at 50% 50%, + var(--accent), + color-mix(in oklab, var(--accent) 45%, transparent) 32%, + transparent 70% + ); will-change: transform; } .dark .section-glow b { From 7d6ce42818cb7021f57bfd8fcdff5df3270bfa79 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:21:27 +0530 Subject: [PATCH 2/4] Animate blueprint drift via transform, not background-position --- src/app/globals.css | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 762a999..76c1d1f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -129,11 +129,19 @@ body { inset: 0; z-index: 0; pointer-events: none; + overflow: hidden; + mask-image: radial-gradient(120% 100% at 50% 0%, #000 30%, transparent 78%); +} +/* Grid lives on an oversized child so drift can animate `transform` (composited) + instead of `background-position` (repaints every frame). */ +.blueprint::before { + content: ""; + position: absolute; + inset: -46px; background-image: linear-gradient(var(--grid-line) 1px, transparent 1px), linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); background-size: 46px 46px; - mask-image: radial-gradient(120% 100% at 50% 0%, #000 30%, transparent 78%); } .hero-net { position: absolute; @@ -205,7 +213,7 @@ body { .aurora .a3 { animation: bloom-3 15s ease-in-out infinite alternate; } - .blueprint { + .blueprint::before { animation: grid-drift 36s linear infinite; } } @@ -235,9 +243,7 @@ body { } @keyframes grid-drift { to { - background-position: - 46px 46px, - 46px 46px; + transform: translate(46px, 46px); } } From 065fee984d60385cfa45b11d24355d0a8a6c7fa8 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:21:48 +0530 Subject: [PATCH 3/4] Cache hero offsets instead of per-move rect reads --- src/components/hero-net.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/hero-net.tsx b/src/components/hero-net.tsx index 250225e..bb79193 100644 --- a/src/components/hero-net.tsx +++ b/src/components/hero-net.tsx @@ -35,6 +35,8 @@ export function HeroNet() { let W = 0; let H = 0; + let heroLeft = 0; + let heroTopDoc = 0; let nodes: Node[] = []; let rgb: [number, number, number] = [31, 157, 84]; let raf: number | null = null; @@ -51,6 +53,8 @@ export function HeroNet() { const r = hero.getBoundingClientRect(); W = Math.max(1, r.width); H = Math.max(1, r.height); + heroLeft = r.left; + heroTopDoc = r.top + window.scrollY; canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); canvas.style.width = `${W}px`; @@ -155,10 +159,11 @@ export function HeroNet() { draw(); }, 150); }; + // Uses offsets cached in build() — hero sits at a fixed document position, + // so a getBoundingClientRect() here would force layout on every move. const onMove = (e: PointerEvent) => { - const r = hero.getBoundingClientRect(); - mouse.x = e.clientX - r.left; - mouse.y = e.clientY - r.top; + mouse.x = e.clientX - heroLeft; + mouse.y = e.clientY - (heroTopDoc - window.scrollY); mouse.on = true; }; const onLeave = () => { From b08492a1ef510f87eab60aa5078e708ea15a8271 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:22:21 +0530 Subject: [PATCH 4/4] Batch hero-net canvas strokes by alpha bucket --- src/components/hero-net.tsx | 51 ++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/components/hero-net.tsx b/src/components/hero-net.tsx index bb79193..9853bc2 100644 --- a/src/components/hero-net.tsx +++ b/src/components/hero-net.tsx @@ -74,8 +74,31 @@ export function HeroNet() { } }; + // Segments are grouped into a few alpha buckets so each frame issues one + // stroke() per bucket instead of one per link (~300 at full density). + const BUCKETS = 8; + const linkBuckets: number[][] = Array.from({ length: BUCKETS }, () => []); + + const bucketFor = (t: number) => Math.min(BUCKETS - 1, Math.floor(t * BUCKETS)); + + const strokeBuckets = (maxAlpha: number) => { + for (let k = 0; k < BUCKETS; k++) { + const seg = linkBuckets[k]; + if (seg.length === 0) continue; + ctx.strokeStyle = rgba(((k + 0.5) / BUCKETS) * maxAlpha); + ctx.beginPath(); + for (let s = 0; s < seg.length; s += 4) { + ctx.moveTo(seg[s], seg[s + 1]); + ctx.lineTo(seg[s + 2], seg[s + 3]); + } + ctx.stroke(); + seg.length = 0; + } + }; + const draw = () => { ctx.clearRect(0, 0, W, H); + ctx.lineWidth = 1; for (let i = 0; i < nodes.length; i++) { const a = nodes[i]; @@ -85,16 +108,11 @@ export function HeroNet() { const dy = a.y - b.y; const d2 = dx * dx + dy * dy; if (d2 < LINK * LINK) { - const d = Math.sqrt(d2); - ctx.strokeStyle = rgba((1 - d / LINK) * 0.22); - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(a.x, a.y); - ctx.lineTo(b.x, b.y); - ctx.stroke(); + linkBuckets[bucketFor(1 - Math.sqrt(d2) / LINK)].push(a.x, a.y, b.x, b.y); } } } + strokeBuckets(0.22); if (mouse.on) { for (const a of nodes) { @@ -102,25 +120,22 @@ export function HeroNet() { const dy = a.y - mouse.y; const d2 = dx * dx + dy * dy; if (d2 < MOUSE_LINK * MOUSE_LINK) { - const d = Math.sqrt(d2); - ctx.strokeStyle = rgba((1 - d / MOUSE_LINK) * 0.5); - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(a.x, a.y); - ctx.lineTo(mouse.x, mouse.y); - ctx.stroke(); - a.vx += (dx > 0 ? -1 : 1) * 0.0009 * (1 - d / MOUSE_LINK); - a.vy += (dy > 0 ? -1 : 1) * 0.0009 * (1 - d / MOUSE_LINK); + const t = 1 - Math.sqrt(d2) / MOUSE_LINK; + linkBuckets[bucketFor(t)].push(a.x, a.y, mouse.x, mouse.y); + a.vx += (dx > 0 ? -1 : 1) * 0.0009 * t; + a.vy += (dy > 0 ? -1 : 1) * 0.0009 * t; } } + strokeBuckets(0.5); } ctx.fillStyle = rgba(0.55); + ctx.beginPath(); for (const a of nodes) { - ctx.beginPath(); + ctx.moveTo(a.x + a.r, a.y); ctx.arc(a.x, a.y, a.r, 0, Math.PI * 2); - ctx.fill(); } + ctx.fill(); }; const step = () => {