Repository navigation
feat: animated, procedurally generated page background - #183
Conversation
Replace the per-page, hand-placed background shapes with a single `<Background />` mounted in the root layout and shared by every page. The background is split into screen-tall segments covering the whole page (footer included), each filled with the same composition as the Figma designs: a teal star overlapping blue blobs crossed by a looper, plus a counterweight on the opposite side. Layouts are generated from a seed derived from the pathname, so every page gets its own variation that stays the same across reloads. Animations (CSS only, disabled with prefers-reduced-motion): - scroll parallax via scroll-driven `view()` timelines, with a different depth per shape so they move relative to each other - passive drift, sway and breathing with desynced sine-like periods - the looper is now rendered inline from a single curve, and its fan slowly opens and closes - on client navigation shapes glide to the new page's layout Remove the page-specific shape files, the unused glow SVGs and ConditionalGlobalShapes; `Shape` is kept for in-component decorations.
There was a problem hiding this comment.
🔇 Additional comments: 2
package.json: 1
61-61: LGTM!src/styles/background.css: 1
69-69: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift
⚠️ Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.Move shape navigation transitions to transforms.
When navigation changes a long page’s scene, every shape transitions
left,top,width, andheightfor 1.5 seconds. These properties require repeated layout work, although the shapes do not affect surrounding content. Use a transform-based placement and size layer while keeping parallax and drift on their separate layers. Based on learnings, prefer transform or opacity when an animation does not need to change surrounding layout.Source: Learnings
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
1aac042e-173a-4cbf-923a-6afba2ec37ce
⛔ Files ignored due to path filters: 5
src/assets/shapes/hero-glow.svgis excluded by!**/*.svgsrc/assets/shapes/matricole-glow-1.svgis excluded by!**/*.svgsrc/assets/shapes/projects-glow.svgis excluded by!**/*.svgsrc/assets/shapes/resources-glow-2.svgis excluded by!**/*.svgsrc/assets/shapes/resources-top-glow.svgis excluded by!**/*.svg
📒 Files selected for processing: 29
package.jsonsrc/app/associations/page.tsxsrc/app/associations/shapes.tsxsrc/app/collabora/page.tsxsrc/app/collabora/shapes.tsxsrc/app/contact/page.tsxsrc/app/faqs/page.tsxsrc/app/groups/layout.tsxsrc/app/groups/shapes.tsxsrc/app/guides/layout.tsxsrc/app/layout.tsxsrc/app/matricole/guida/page.tsxsrc/app/matricole/page.tsxsrc/app/matricole/shapes.tsxsrc/app/page.tsxsrc/app/projects/page.tsxsrc/app/projects/shapes.tsxsrc/components/background/index.tsxsrc/components/background/looper.tsxsrc/components/background/scene.tssrc/components/conditional-global-shapes.tsxsrc/components/footer/index.tsxsrc/components/footer/shapes.tsxsrc/components/home/hero.tsxsrc/components/home/shapes.tsxsrc/components/resources/shapes.tsxsrc/components/shapes.tsxsrc/styles/background.csssrc/styles/globals.css
💤 Files with no reviewable changes: 13
- src/components/resources/shapes.tsx
- src/app/guides/layout.tsx
- src/app/projects/shapes.tsx
- src/components/home/shapes.tsx
- src/app/associations/shapes.tsx
- src/app/matricole/guida/page.tsx
- src/app/matricole/shapes.tsx
- src/components/conditional-global-shapes.tsx
- src/app/contact/page.tsx
- src/components/footer/shapes.tsx
- src/app/collabora/shapes.tsx
- src/app/groups/shapes.tsx
- src/components/home/hero.tsx
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.
The CSS version ran at ~10 FPS in Firefox-based browsers and behaved differently across engines (no scroll-driven animations in Firefox). - Blobs are now small pre-rasterized WebP images instead of SVGs with a 50px gaussian blur, which Firefox re-runs whenever the image is drawn at a new scale. - The looper is no longer an inline SVG with animated paths (repainted on the CPU every frame in Firefox): each line is an element showing the same rasterized curve, rotated around its pivot, so the GPU only ever transforms a single texture. - Every animation now runs from one requestAnimationFrame loop with plain sine math, so it looks the same in every browser. It reads all positions before writing transforms, skips off-screen shapes, only promotes on-screen ones with will-change, and eases in after hydration so the first frame doesn't jump. - More scroll feedback: stronger parallax, a scroll-driven rotation, smoothed scroll for some inertia, and the loopers' fan now opens from tight (~4.4°) to wide (~7.5°) as they cross the viewport.
Navigating kept the same shape elements and transitioned them to the new page's layout, which looked like shapes flying around the screen. The scene is now remounted for every page, its first frame is applied synchronously before paint, and scroll smoothing is skipped right after mounting so the scroll reset done by the navigation doesn't sweep the parallax. The fade-in of the motion only runs on the first page load.
Port the animated background to the pages added on main (about, chi-siamo, join, storia, team, projects/[id], collabora): drop their per-page shapes files, `ShapeLayer`/`FigmaShape` backgrounds and the team hero glow. The shapes drawn over the collabora and join photos stay as static decorations, now defined in their components via `PhotoShapes`.
What
Reworks the decorative background. The hand-placed, per-page
shapes.tsxfiles are replaced with a single<Background />in the root layout, so every page uses the same components and behaves the same way.The look matches the existing design. Each screen-tall segment of the page gets a teal star overlapping blue blobs, crossed by a looper, plus a smaller counterweight on the other side. Sides alternate between segments. Positions, sizes and rotations come from a seed derived from the pathname, so each page gets its own variation that doesn't change between reloads.
Animations
Everything runs from a single
requestAnimationFrameloop (src/components/background/animate.ts) using plain sine math, so it behaves the same in every browser. It's disabled underprefers-reduced-motion.Performance
The first version, using CSS animations and inline SVG, ran at about 10 FPS in Firefox-based browsers. The likely causes, both fixed by design:
feGaussianBlur, which Firefox re-runs whenever an SVG image is drawn at a new scale. They are now small pre-rasterized WebPs (15–19KB each, 256px). They're so blurry that upscaling is invisible.looper-line.webp, 5KB), rotated around its pivot. The GPU only transforms one shared texture, and nothing gets repainted. I checked the static result against the original SVG with an overlay, and they match.transform. It reads every position before writing anything, to avoid a style flush per shape. It skips off-screen shapes and only setswill-changeon the visible ones, because Firefox ignoreswill-changepast a memory budget and these shapes are huge.Cleanup
ConditionalGlobalShapes, the guides layout that only mounted it, and the page-specific glow SVGs.relativewrappers that only existed to host shape layers.Shapeis kept, now limited to 4 variants, for the in-component decorations (dialog, card hover, accordion). It now uses the raster blobs too, loaded eagerly.Notes
ResizeObserver. Server rendering produces the first 2 segments, and the rest is added after hydration, below the fold. Motion fades in over the first 1.5s, so the first animated frame doesn't jump from the server-rendered layout.src/components/background/scene.ts. The global constants (scroll inertia, intro duration) are inanimate.ts. Global scale and segment height are insrc/styles/background.css.Testing
pnpm check,pnpm typecheckandpnpm buildpass.