diff --git a/.forge/features/homepage-memory/spec.md b/.forge/features/homepage-memory/spec.md new file mode 100644 index 000000000..404506ee4 --- /dev/null +++ b/.forge/features/homepage-memory/spec.md @@ -0,0 +1,73 @@ +# KHIX Homepage Memory Optimization Spec + +Status: Approved + +## User-facing purpose + +The public Knight Hacks IX homepage should retain its illustrated, animated +experience while loading quickly and remaining responsive throughout a visit. +Scrolling through the full page or leaving it open must not cause browser memory +to climb toward gigabyte-scale usage. + +## Users / actors + +- Prospective hackers, sponsors, speakers, and community members visiting the + public homepage on desktop or mobile. +- Visitors who prefer reduced motion or use a lower-powered device. + +## User-visible interface + +- The full `/` homepage: hero, About, Tracks, Speakers, Sponsors, Team, FAQ, and + footer. +- The existing parallax, gallery, speaker carousel, waterfall, ambient creature, + cave-note, and Lenny interactions remain recognizable. +- Reduced-motion visitors receive complete static artwork without decorative + autoplay motion. + +## Scope + +### In scope + +- Replace memory-intensive decorative animation delivery with bounded, + viewport-aware media while preserving transparent artwork and motion. +- Serve static raster artwork at dimensions appropriate to its rendered size. +- Defer below-the-fold media and data until it is near the viewport. +- Pause timers, media, and CSS animation when their section or browser tab is not + active. +- Reuse browser audio resources across cave-note interactions. +- Improve initial hero discovery and paint without changing the visual hierarchy. + +### Out of scope + +- Portal, dashboard, application, credits, and other non-homepage routes. +- Changes to event copy, sponsor data, team membership, or authentication. +- A visual redesign or removal of the KHIX illustrated theme. +- CDN deployment or changes to external asset storage. + +## Vocabulary + +- `ambient media`: decorative looping motion that is not required to understand + or operate the page. +- `viewport-aware`: active only while the relevant section is visible or close to + visible. +- `static fallback`: the existing non-animated art shown when motion is disabled + or video playback is unavailable. + +## Acceptance criteria + +- The homepage keeps its layered hero, moving water, atmospheric motion, gallery, + speaker carousel, interactive tracks, cave notes, and blinking Lenny. +- Animated assets no longer retain every full-resolution WebP frame in browser + image memory. +- Images with small rendered dimensions do not download/decode their original + multi-megapixel sources. +- Below-the-fold autoplay work is idle before the visitor approaches it and + pauses after it leaves the viewport or the document becomes hidden. +- Repeated cave-note interactions share one audio context. +- Desktop and mobile layouts remain visually coherent at current breakpoints. +- The page remains fully usable with `prefers-reduced-motion: reduce`. + +## Open questions + +- None. The user explicitly requested a broad homepage pass that preserves or + improves visual quality while dramatically improving performance. diff --git a/.forge/features/homepage-memory/srd.md b/.forge/features/homepage-memory/srd.md new file mode 100644 index 000000000..f2a7bc04f --- /dev/null +++ b/.forge/features/homepage-memory/srd.md @@ -0,0 +1,88 @@ +# KHIX Homepage Memory Optimization SRD + +Status: Approved + +## Technical purpose + +Bound the KHIX homepage's decoded media, compositor, DOM-animation, timer, and +audio costs without reducing the site's art direction. Issue #562 supplies the +initial suspects; profiling the full homepage determines the implementation. + +## Relevant principles + +- `docs/agentic-development/forge-engineering-principles.md#react-and-nextjs-principles` +- `docs/agentic-development/forge-engineering-principles.md#readability-and-colocation` +- `docs/agentic-development/forge-engineering-principles.md#testing-principles` +- `docs/agentic-development/frontend-design-skill.md` + +## Access policy + +The homepage remains public and unauthenticated. This work adds no logged-in or +permission-based behavior. + +## Architecture / data flow + +- Changes stay in `apps/2026` and its public assets. +- Decorative animated WebPs are transcoded to transparent VP9 WebM. Existing + static scene layers remain underneath as fallbacks. +- A small app-local ambient-video component owns intersection, document + visibility, and reduced-motion playback lifecycle. +- Homepage components independently pause section-specific timers or CSS + animation when inactive. +- No shared package or new dependency is required. + +## tRPC/API behavior + +No API contract changes. The existing public team-roster request keeps its +contract but begins only when the Team section approaches the viewport. + +## Validation + +No new data validation. Media elements retain accessible fallbacks, and all +decorative video remains hidden from assistive technology. + +## Data / migration / compatibility + +- No database or migration changes. +- Transparent WebM is an enhancement over existing static scene art. Browsers + that cannot play it still render the static fallback. +- Rollback consists of restoring the prior WebP references and component + lifecycles. + +## Discord integration + +None. + +## Configurability review + +Would this require a developer change next year? + +- Answer: Only when yearly artwork itself changes, as it already does. +- The media lifecycle component is generic within the 2026 app; event and roster + content remain untouched. + +## React / frontend constraints + +- Keep `page.tsx` server-side and isolate browser lifecycle behavior in focused + client components/hooks. +- Preserve server-rendered initial hero art so LCP discovery does not depend on + hydration. +- Set explicit responsive `sizes` and use Next image optimization for raster + images that do not need original-file delivery. +- Do not keep permanent `will-change` hints on large inactive surfaces. +- Honor reduced motion and document visibility in every autoplay lifecycle. +- Maintain existing semantic controls, labels, focus behavior, and static art. + +## Testing / verification strategy + +- Static regression tests cover viewport-activity rules where practical. +- Run `pnpm --filter=@forge/2026 test`, `lint`, `typecheck`, and `build`. +- Run `pnpm analyze:react:changed` for the meaningful React changes. +- Compare production-mode Lighthouse desktop/mobile reports before and after. +- Inspect the live and local page at desktop and mobile breakpoints, including + reduced motion and interactive controls. +- Verify generated media metadata, transparency, dimensions, and file sizes. + +## Open questions + +- None. diff --git a/.forge/features/homepage-memory/status.md b/.forge/features/homepage-memory/status.md new file mode 100644 index 000000000..8a6da3902 --- /dev/null +++ b/.forge/features/homepage-memory/status.md @@ -0,0 +1,72 @@ +# KHIX Homepage Memory Optimization Status + +Current phase: Complete + +## Decision log + +- 2026-09-11: User requested a broad homepage optimization rather than a literal + checklist implementation of issue #562. Visual quality and interactions must + be preserved or improved while performance improves dramatically. +- 2026-09-11: Scope is limited to `apps/2026` homepage behavior and assets; no + shared packages, dependencies, data, auth, or portal behavior will change. +- 2026-09-11: Use transparent VP9 video plus existing static fallbacks for large + decorative animation. Make all autoplay work viewport-, visibility-, and + reduced-motion-aware. + +## Open questions + +- None. + +## Task list + +- [x] Complete reverse-prompting for `spec.md` from issue #562 and user scope. +- [x] Complete reverse-prompting for `srd.md`. +- [x] Complete reverse-prompting for `test-cases.md`. +- [x] Record user approval of the broad performance/quality direction. +- [x] Finish desktop and mobile baseline profiling. +- [x] Generate and verify optimized transparent video assets. +- [x] Implement responsive image and media lifecycle changes. +- [x] Implement viewport-aware animation, timer, data, and audio lifecycles. +- [x] Run scoped validation and the 2026 app formatting gate. +- [x] Complete desktop/mobile/browser lifecycle verification. + +## Validation / commands + +- `pnpm install --frozen-lockfile`: passed. +- `pnpm forge:feature homepage-memory "KHIX Homepage Memory Optimization"`: + first attempt failed because dependencies were absent; passed after install. +- Live desktop Lighthouse baseline: performance 62, LCP 11.7 s, 87 requests, + 19,334 KiB transferred, with about 7,116 KiB estimated image-delivery savings. +- Observed Chrome memory on the same full-page test fell from approximately + 1.3 GB to 450 MB after the rendering and media lifecycle changes. +- Media inspection: the nine largest static/animated suspects represent roughly + 1.34 GiB of worst-case RGBA frame data; `extended-front.webp` alone is + 3840x12000 (175.8 MiB decoded), and the animated WebPs contain 5-20 frames. +- Final desktop Lighthouse: performance 87, LCP 2.3 s, 57 requests, and 2.30 + MiB transferred. Compared with production, transfer fell 87.8%, LCP fell + 80.2%, and the performance score rose 25 points. +- Final mobile Lighthouse: performance 63, LCP 7.5 s, 53 requests, and 1.30 + MiB transferred. Compared with production, transfer fell 88.1%, LCP fell + 64.6%, total blocking time fell 64.6%, and the score rose 21 points. +- `pnpm --filter=@forge/2026 test`: passed (14 tests). +- `pnpm --filter=@forge/2026 typecheck`: passed with a 4 GiB Node heap after + the default 2 GiB process exhausted its heap. +- `pnpm analyze:react:changed`: passed (0 failures). +- Production `@forge/2026` build: passed with local non-secret environment + placeholders. +- In-app browser checks: desktop and mobile composition preserved, no + horizontal overflow, no runtime console errors, responsive video sources + selected correctly, and only near-viewport video/ambient work became active. +- Final architecture follow-up: replaced the desktop hero's nine decoded image + layers with one AVIF composition and one merged transparent ambient video; + replaced 154 animated waterfall DOM nodes with one viewport-bound canvas; + and limited the waterfall to one active, centered video stream at a time. +- Final in-app browser hero sample: 567 DOM nodes, one active 1280x720 ambient + video, no active canvas, and approximately 12.1 MiB of decoded in-DOM imagery. +- `pnpm --filter @forge/2026 format`: passed. + +## Links + +- PRs: +- Issues: https://github.com/KnightHacks/forge/issues/562 +- Discord/thread context: diff --git a/.forge/features/homepage-memory/test-cases.md b/.forge/features/homepage-memory/test-cases.md new file mode 100644 index 000000000..1d2ebeee7 --- /dev/null +++ b/.forge/features/homepage-memory/test-cases.md @@ -0,0 +1,153 @@ +# KHIX Homepage Memory Optimization Test Cases + +Status: Approved + +## Scope + +Homepage media loading, autoplay lifecycle, responsive image delivery, cave-note +audio reuse, and visual/interactive regressions. Portal and non-homepage routes +are excluded. + +## Test placement plan + +- App-level lifecycle tests in `apps/2026` where behavior can be isolated. +- Browser and Lighthouse verification against the production build. +- Commands: `pnpm --filter=@forge/2026 test`, `lint`, `typecheck`, `build`, plus + `pnpm analyze:react:changed`. + +## Test cases + +### TC-001: Initial hero is discoverable before hydration + +Setup: + +- Load `/` on a fresh desktop or mobile navigation. + +Action: + +- Inspect the initial document and first visual paint. + +Expected observations: + +- A viewport-appropriate base hero image is present immediately. +- Only that critical base image receives high fetch priority. +- The remaining layers appear without a layout shift. + +### TC-002: Ambient animation uses bounded video decoding + +Setup: + +- Load `/` with normal motion preferences. + +Action: + +- Visit the hero, waterfall, and footer regions. + +Expected observations: + +- Water and Lenny motion remain visible through transparent looping video. +- The previous animated WebP resources are not requested. +- Existing static layers remain visible if video is unavailable. + +### TC-003: Ambient work follows viewport and tab activity + +Setup: + +- Load `/` with normal motion preferences. + +Action: + +- Scroll each animated section in and out of range, then hide and restore the + document. + +Expected observations: + +- Video, decorative CSS animation, gallery rotation, and speaker rotation run + only when relevant and pause while offscreen or hidden. +- User controls still work immediately when a section is visible. + +### TC-004: Static raster images are responsive + +Setup: + +- Load `/` at desktop and mobile widths. + +Action: + +- Inspect requested image URLs and intrinsic dimensions after scrolling through + the page. + +Expected observations: + +- Hero layers, track characters/leaves, speaker art, and other small rendered + rasters use responsive image variants instead of full original dimensions. +- Artwork remains crisp at its rendered size. + +### TC-005: Cave-note audio resources are reused + +Setup: + +- Load the FAQ and activate multiple gemstone/hotspot notes. + +Action: + +- Trigger notes repeatedly, including overlapping notes. + +Expected observations: + +- Notes remain audible and responsive. +- A single reusable audio context is used, and completed node graphs disconnect. + +### TC-006: Interactive homepage behavior is preserved + +Setup: + +- Load `/` and scroll through the full page. + +Action: + +- Use hero/apply navigation, gallery controls, tracks reveal, speaker controls, + sponsor/team links, FAQ categories/questions, and footer links. + +Expected observations: + +- Controls remain keyboard accessible and visually coherent. +- No horizontal overflow or material layout shift is introduced. + +## Negative / regression cases + +### TC-NEG-001: Reduced-motion preference + +Setup: + +- Enable `prefers-reduced-motion: reduce` before loading `/`. + +Action: + +- Scroll through the full page and use all controls. + +Expected observations: + +- Decorative video and CSS autoplay motion remain paused/hidden. +- Static hero, waterfall, speaker, and Lenny artwork remains complete. +- User-triggered controls continue to work without animated transitions where + applicable. + +### TC-NEG-002: Video playback is unavailable + +Setup: + +- Simulate an unsupported or failed WebM source. + +Action: + +- Visit each video-enhanced section. + +Expected observations: + +- The underlying static art prevents blank or broken scene regions. +- No unhandled promise rejection is logged. + +## Open questions + +- None. diff --git a/apps/2026/public/media/homepage/extended-front-mobile.webp b/apps/2026/public/media/homepage/extended-front-mobile.webp new file mode 100644 index 000000000..9ccb0ed74 Binary files /dev/null and b/apps/2026/public/media/homepage/extended-front-mobile.webp differ diff --git a/apps/2026/public/media/homepage/extended-front.webp b/apps/2026/public/media/homepage/extended-front.webp new file mode 100644 index 000000000..b34c10bab Binary files /dev/null and b/apps/2026/public/media/homepage/extended-front.webp differ diff --git a/apps/2026/public/media/homepage/faq-static/cave-ceiling-primary.webp b/apps/2026/public/media/homepage/faq-static/cave-ceiling-primary.webp new file mode 100644 index 000000000..4896279b8 Binary files /dev/null and b/apps/2026/public/media/homepage/faq-static/cave-ceiling-primary.webp differ diff --git a/apps/2026/public/media/homepage/faq-static/cave-ceiling-secondary.webp b/apps/2026/public/media/homepage/faq-static/cave-ceiling-secondary.webp new file mode 100644 index 000000000..7430c413a Binary files /dev/null and b/apps/2026/public/media/homepage/faq-static/cave-ceiling-secondary.webp differ diff --git a/apps/2026/public/media/homepage/hero-ambient-combined.mov b/apps/2026/public/media/homepage/hero-ambient-combined.mov new file mode 100644 index 000000000..b2b8228f0 Binary files /dev/null and b/apps/2026/public/media/homepage/hero-ambient-combined.mov differ diff --git a/apps/2026/public/media/homepage/hero-ambient-combined.webm b/apps/2026/public/media/homepage/hero-ambient-combined.webm new file mode 100644 index 000000000..202362496 Binary files /dev/null and b/apps/2026/public/media/homepage/hero-ambient-combined.webm differ diff --git a/apps/2026/public/media/homepage/hero-static/hero-desktop-composite.avif b/apps/2026/public/media/homepage/hero-static/hero-desktop-composite.avif new file mode 100644 index 000000000..c98fd5cc9 Binary files /dev/null and b/apps/2026/public/media/homepage/hero-static/hero-desktop-composite.avif differ diff --git a/apps/2026/public/media/homepage/hero-static/hero-mobile-7bg.webp b/apps/2026/public/media/homepage/hero-static/hero-mobile-7bg.webp new file mode 100644 index 000000000..cbaceb356 Binary files /dev/null and b/apps/2026/public/media/homepage/hero-static/hero-mobile-7bg.webp differ diff --git a/apps/2026/public/media/homepage/hero-static/hero-mobile-composite.webp b/apps/2026/public/media/homepage/hero-static/hero-mobile-composite.webp new file mode 100644 index 000000000..dfa6a7a76 Binary files /dev/null and b/apps/2026/public/media/homepage/hero-static/hero-mobile-composite.webp differ diff --git a/apps/2026/public/media/homepage/lenny-blink-poster.webp b/apps/2026/public/media/homepage/lenny-blink-poster.webp new file mode 100644 index 000000000..a194786b8 Binary files /dev/null and b/apps/2026/public/media/homepage/lenny-blink-poster.webp differ diff --git a/apps/2026/public/media/homepage/lenny-blink.mov b/apps/2026/public/media/homepage/lenny-blink.mov new file mode 100644 index 000000000..ff4e5d763 Binary files /dev/null and b/apps/2026/public/media/homepage/lenny-blink.mov differ diff --git a/apps/2026/public/media/homepage/lenny-blink.webm b/apps/2026/public/media/homepage/lenny-blink.webm new file mode 100644 index 000000000..e0a68a3a0 Binary files /dev/null and b/apps/2026/public/media/homepage/lenny-blink.webm differ diff --git a/apps/2026/public/media/homepage/sponsor-impressink.webp b/apps/2026/public/media/homepage/sponsor-impressink.webp new file mode 100644 index 000000000..cf0e4877c Binary files /dev/null and b/apps/2026/public/media/homepage/sponsor-impressink.webp differ diff --git a/apps/2026/public/media/homepage/waterfall-one-balanced.mov b/apps/2026/public/media/homepage/waterfall-one-balanced.mov new file mode 100644 index 000000000..8ec5b3533 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-one-balanced.mov differ diff --git a/apps/2026/public/media/homepage/waterfall-one-balanced.webm b/apps/2026/public/media/homepage/waterfall-one-balanced.webm new file mode 100644 index 000000000..90bf5290c Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-one-balanced.webm differ diff --git a/apps/2026/public/media/homepage/waterfall-one-mobile.mov b/apps/2026/public/media/homepage/waterfall-one-mobile.mov new file mode 100644 index 000000000..19cffd016 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-one-mobile.mov differ diff --git a/apps/2026/public/media/homepage/waterfall-one-mobile.webm b/apps/2026/public/media/homepage/waterfall-one-mobile.webm new file mode 100644 index 000000000..876a8fce9 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-one-mobile.webm differ diff --git a/apps/2026/public/media/homepage/waterfall-three-balanced.mov b/apps/2026/public/media/homepage/waterfall-three-balanced.mov new file mode 100644 index 000000000..464fa2455 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-three-balanced.mov differ diff --git a/apps/2026/public/media/homepage/waterfall-three-balanced.webm b/apps/2026/public/media/homepage/waterfall-three-balanced.webm new file mode 100644 index 000000000..830934ebc Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-three-balanced.webm differ diff --git a/apps/2026/public/media/homepage/waterfall-three-mobile.mov b/apps/2026/public/media/homepage/waterfall-three-mobile.mov new file mode 100644 index 000000000..56cf40388 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-three-mobile.mov differ diff --git a/apps/2026/public/media/homepage/waterfall-three-mobile.webm b/apps/2026/public/media/homepage/waterfall-three-mobile.webm new file mode 100644 index 000000000..a27e8e8a7 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-three-mobile.webm differ diff --git a/apps/2026/public/media/homepage/waterfall-two-balanced.mov b/apps/2026/public/media/homepage/waterfall-two-balanced.mov new file mode 100644 index 000000000..b3079be17 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-two-balanced.mov differ diff --git a/apps/2026/public/media/homepage/waterfall-two-balanced.webm b/apps/2026/public/media/homepage/waterfall-two-balanced.webm new file mode 100644 index 000000000..a3513c633 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-two-balanced.webm differ diff --git a/apps/2026/public/media/homepage/waterfall-two-mobile.mov b/apps/2026/public/media/homepage/waterfall-two-mobile.mov new file mode 100644 index 000000000..e86f14590 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-two-mobile.mov differ diff --git a/apps/2026/public/media/homepage/waterfall-two-mobile.webm b/apps/2026/public/media/homepage/waterfall-two-mobile.webm new file mode 100644 index 000000000..45806a731 Binary files /dev/null and b/apps/2026/public/media/homepage/waterfall-two-mobile.webm differ diff --git a/apps/2026/src/app/_components/AmbientVideo.tsx b/apps/2026/src/app/_components/AmbientVideo.tsx new file mode 100644 index 000000000..746b931a4 --- /dev/null +++ b/apps/2026/src/app/_components/AmbientVideo.tsx @@ -0,0 +1,79 @@ +"use client"; + +import type { CSSProperties } from "react"; +import { useEffect } from "react"; + +import type { AmbientVideoSource } from "./ambientVideoSources"; +import { useViewportActivity } from "./useViewportActivity"; + +interface AmbientVideoProps { + className?: string; + poster?: string; + preload?: "auto" | "metadata" | "none"; + rootMargin?: string; + sources: readonly AmbientVideoSource[]; + style?: CSSProperties; +} + +/** Renders decorative video that loads and plays only while its region is active. */ +export function AmbientVideo({ + className, + poster, + preload = "none", + rootMargin, + sources, + style, +}: AmbientVideoProps) { + const [videoRef, isActive] = useViewportActivity({ + rootMargin, + }); + + useEffect(() => { + const video = videoRef.current; + + if (!video) return; + + if (!isActive) { + video.pause(); + + // Reset the media element after it leaves the viewport so decoded frame + // buffers are released instead of accumulating as visitors scroll. + video.load(); + + return; + } + + // Autoplay can still be rejected by browser policy or power-saving mode. + // Static artwork underneath every ambient video is the visual fallback. + void video.play().catch(() => undefined); + }, [isActive, videoRef]); + + return ( + + ); +} diff --git a/apps/2026/src/app/_components/ambientVideoSources.ts b/apps/2026/src/app/_components/ambientVideoSources.ts new file mode 100644 index 000000000..a7e030456 --- /dev/null +++ b/apps/2026/src/app/_components/ambientVideoSources.ts @@ -0,0 +1,33 @@ +export interface AmbientVideoSource { + media?: string; + src: string; + type: string; +} + +/** Builds cross-browser WebM and HEVC-alpha sources for transparent video. */ +export function getTransparentVideoSources(srcBase: string) { + return [ + { + src: `${srcBase}.mov`, + type: 'video/quicktime; codecs="hvc1"', + }, + { + src: `${srcBase}.webm`, + type: 'video/webm; codecs="vp9"', + }, + ] satisfies AmbientVideoSource[]; +} + +/** Builds mobile and desktop transparent-video sources with media queries. */ +export function getResponsiveTransparentVideoSources(srcBase: string) { + return [ + ...getTransparentVideoSources(`${srcBase}-mobile`).map((source) => ({ + ...source, + media: "(max-width: 760px)", + })), + ...getTransparentVideoSources(`${srcBase}-desktop`).map((source) => ({ + ...source, + media: "(min-width: 761px)", + })), + ] satisfies AmbientVideoSource[]; +} diff --git a/apps/2026/src/app/_components/footer/CaveBugs.module.css b/apps/2026/src/app/_components/footer/CaveBugs.module.css index 4b06a85a8..6ed0d0b73 100644 --- a/apps/2026/src/app/_components/footer/CaveBugs.module.css +++ b/apps/2026/src/app/_components/footer/CaveBugs.module.css @@ -20,12 +20,13 @@ top: calc( var(--band-top) + var(--bug-band) * (var(--band-bottom) - var(--band-top)) ); - left: -12%; + left: 0; width: clamp(2.4rem, 4.5vw, 3.4rem); - transform: scaleX(calc(var(--bug-dir) * var(--bug-scale))) - scaleY(var(--bug-scale)); - will-change: left; + transform: translate3d(-100%, 0, 0) + scaleX(calc(var(--bug-dir) * var(--bug-scale))) scaleY(var(--bug-scale)); + will-change: auto; animation: crawl var(--bug-duration) linear var(--bug-delay) infinite; + animation-play-state: paused; } .bug[data-direction="rtl"] { @@ -58,6 +59,7 @@ .bugBob { display: block; animation: bob 2.4s ease-in-out calc(var(--bug-delay) / 2) infinite alternate; + animation-play-state: paused; } .bugSvg { @@ -102,20 +104,31 @@ } .legsA { - animation: scuttle 0.36s ease-in-out infinite alternate; + transform: rotate(-3deg); } .legsB { - animation: scuttle 0.36s ease-in-out infinite alternate-reverse; + transform: rotate(3deg); +} + +.swarm[data-active="true"] .bug, +.swarm[data-active="true"] .bugBob { + animation-play-state: running; +} + +.swarm[data-active="true"] .bug { + will-change: transform; } @keyframes crawl { from { - left: -12%; + transform: translate3d(-100%, 0, 0) + scaleX(calc(var(--bug-dir) * var(--bug-scale))) scaleY(var(--bug-scale)); } to { - left: 112%; + transform: translate3d(100vw, 0, 0) + scaleX(calc(var(--bug-dir) * var(--bug-scale))) scaleY(var(--bug-scale)); } } @@ -166,6 +179,8 @@ /* Park a few visible so it still reads as a cave floor of critters. */ .bug { left: clamp(1rem, 8vw, 6rem); + transform: scaleX(calc(var(--bug-dir) * var(--bug-scale))) + scaleY(var(--bug-scale)); } .bug[data-direction="rtl"] { diff --git a/apps/2026/src/app/_components/footer/CaveBugs.tsx b/apps/2026/src/app/_components/footer/CaveBugs.tsx index 52194af4c..272c87627 100644 --- a/apps/2026/src/app/_components/footer/CaveBugs.tsx +++ b/apps/2026/src/app/_components/footer/CaveBugs.tsx @@ -1,5 +1,8 @@ +"use client"; + import type { CSSProperties } from "react"; +import { useViewportActivity } from "../useViewportActivity"; import styles from "./CaveBugs.module.css"; type BugColor = "cyan" | "purple" | "yellow" | "green"; @@ -22,65 +25,68 @@ interface BugConfig { // A loose scattering of critters at different depths, speeds, and tints so the // swarm never looks like it marches in lockstep. -const BUGS: BugConfig[] = [ - { - color: "cyan", - band: 0.75, - scale: 1.05, - duration: 26, - delay: -4, - direction: "ltr", - }, - { - color: "purple", - band: 0.31, - scale: 0.8, - duration: 34, - delay: -18, - direction: "rtl", - }, - { - color: "yellow", - band: 1, - scale: 0.9, - duration: 22, - delay: -11, - direction: "ltr", - }, - { - color: "green", - band: 0.5, - scale: 1.15, - duration: 38, - delay: -30, - direction: "rtl", - }, - { - color: "cyan", - band: 0, - scale: 0.7, - duration: 30, - delay: -8, - direction: "rtl", - }, - { - color: "yellow", - band: 0.16, - scale: 1, - duration: 28, - delay: -21, - direction: "ltr", - }, - { - color: "purple", - band: 0.94, - scale: 0.85, - duration: 24, - delay: -14, - direction: "ltr", - }, -]; +const BUGS = ( + [ + { + color: "cyan", + band: 0.75, + scale: 1.05, + duration: 26, + delay: -4, + direction: "ltr", + }, + { + color: "purple", + band: 0.31, + scale: 0.8, + duration: 34, + delay: -18, + direction: "rtl", + }, + { + color: "yellow", + band: 1, + scale: 0.9, + duration: 22, + delay: -11, + direction: "ltr", + }, + { + color: "green", + band: 0.5, + scale: 1.15, + duration: 38, + delay: -30, + direction: "rtl", + }, + { + color: "cyan", + band: 0, + scale: 0.7, + duration: 30, + delay: -8, + direction: "rtl", + }, + { + color: "yellow", + band: 0.16, + scale: 1, + duration: 28, + delay: -21, + direction: "ltr", + }, + { + color: "purple", + band: 0.94, + scale: 0.85, + duration: 24, + delay: -14, + direction: "ltr", + }, + ] satisfies BugConfig[] +).slice(0, 4); +/** Renders one deterministic ambient cave bug from its motion configuration. */ function Bug({ config }: { config: BugConfig }) { const style = { "--bug-band": config.band, @@ -140,9 +146,17 @@ function Bug({ config }: { config: BugConfig }) { ); } +/** Renders the reduced, viewport-aware set of animated footer cave bugs. */ export function CaveBugs() { + const [swarmRef, isSwarmActive] = useViewportActivity(); + return ( -