diff --git a/package.json b/package.json index a8fec72..ea07787 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "tw-animate-css": "^1.4.0", "typescript": "^7.0.2" }, - "packageManager": "pnpm@10.33.0", + "packageManager": "pnpm@10.34.7+sha512.ba98abc585539933375b5971f4bc6d6c73a1b4b7edc4a879e7d102fe5423d1e02309d67c60a11f04955093a96e521ad9c3b138cdd654ca5be563f378b3ae48bd", "engines": { "node": ">=24.5.2" } diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 4055131..8cab8cb 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -8,7 +8,6 @@ import { OrganizationChart } from "@/components/structure/organization-chart" import { Participation } from "@/components/structure/participation" import { Button } from "@/components/ui/button" import { Hero } from "@/components/ui/hero" -import { StructureShapes } from "./shapes" export const metadata: Metadata = { title: "Struttura", @@ -23,71 +22,50 @@ const titleClass = export default function StructurePage() { return (
-
- -
- -
-
-
- - -
-
- -
-

- Il Direttivo in carica -

-
- -

- Il Direttivo viene eletto ogni due anni dall’Assemblea dei Soci tra i soci dell’associazione. -

-
-
-
-
- -
-

- Come funziona l’associazione -

- -
-
-
- -
-

- Trova il tuo posto in PoliNetwork -

- -
-
-
- -
-

- Tutto ciò che c’è da sapere su PoliNetwork. -

-

- Scopri chi siamo, cosa facciamo e come entrare a far parte della nostra realtà. +

+ +
+ +
+

+ Il Direttivo in carica +

+
+ +

+ Il Direttivo viene eletto ogni due anni dall’Assemblea dei Soci tra i soci dell’associazione.

- -
-
-
- -
- -
-
+ + +
+

+ Come funziona l’associazione +

+ +
+
+

+ Trova il tuo posto in PoliNetwork +

+ +
+
+

+ Tutto ciò che c’è da sapere su PoliNetwork. +

+

+ Scopri chi siamo, cosa facciamo e come entrare a far parte della nostra realtà. +

+ +
+
+ +
) } diff --git a/src/app/about/shapes.tsx b/src/app/about/shapes.tsx deleted file mode 100644 index d1fe841..0000000 --- a/src/app/about/shapes.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import { FigmaShape, type FigmaShapePlacement, ShapeLayer } from "@/components/shapes" - -type StructureSection = "hero" | "diagram" | "board" | "association" | "participation" | "join" | "legal" - -// Original Figma layers, anchored to content sections to follow responsive reflow. -const layouts: Record<"desktop" | "mobile", Partial>> = { - desktop: { - hero: [ - { - variant: "big-teal", - x: 1034, - y: 411, - w: 974, - h: 974, - }, - { - variant: "small-blue", - x: 524.063, - y: 325.063, - w: 354.126, - h: 354.126, - matrix: [0.962952, -0.269672, 0.269672, 0.962952], - }, - { - variant: "small-blue", - x: 1347.063, - y: 643.063, - w: 354.126, - h: 354.126, - matrix: [0.956549, 0.291573, -0.291573, 0.956549], - }, - { - variant: "looper", - x: 1213.853, - y: 569.24, - w: 1609.706, - h: 1560.48, - }, - ], - board: [ - { - variant: "big-teal", - x: 371.5, - y: 393.5, - w: 531, - h: 531, - }, - { - variant: "big-blue", - x: 1048.74, - y: 212.413, - w: 961.091, - h: 961.091, - matrix: [0.896447, 0.443151, -0.443151, 0.896447], - }, - { - variant: "looper", - x: 686.145, - y: 47.876, - w: 1609.706, - h: 1560.48, - matrix: [0.341412, -0.939914, 0.939914, 0.341412], - }, - ], - association: [ - { - variant: "big-teal", - x: 26, - y: 487, - w: 974, - h: 974, - }, - { - variant: "big-teal", - x: 1710, - y: 487, - w: 974, - h: 974, - }, - ], - participation: [ - { - variant: "looper", - x: 767.228, - y: 6.001, - w: 1957.868, - h: 1897.994, - matrix: [-0.392771, 0.919636, -0.919636, -0.392771], - }, - ], - join: [ - { - variant: "big-blue", - x: 1012.929, - y: 583.929, - w: 589.394, - h: 589.394, - matrix: [0.965926, -0.258819, 0.258819, 0.965926], - }, - { - variant: "big-teal", - x: 725, - y: 515, - w: 690, - h: 690, - }, - ], - legal: [ - { - variant: "looper", - x: 677.351, - y: 108.285, - w: 1609.706, - h: 1560.48, - matrix: [-0.908395, 0.418112, -0.418112, -0.908395], - }, - ], - }, - mobile: { - hero: [ - { - variant: "big-teal", - x: 275.5, - y: 295.5, - w: 401, - h: 401, - }, - { - variant: "small-blue", - x: 75.063, - y: 272.063, - w: 354.126, - h: 354.126, - }, - { - variant: "small-blue", - x: 359.5, - y: 495.5, - w: 233, - h: 233, - }, - { - variant: "looper", - x: 247.062, - y: 197.977, - w: 891, - h: 866, - matrix: [0.542465, 0.840078, -0.840078, 0.542465], - }, - ], - diagram: [ - { - variant: "big-teal", - x: -25.5, - y: 250.5, - w: 401, - h: 401, - }, - { - variant: "big-teal", - x: 331.5, - y: 493.5, - w: 401, - h: 401, - }, - { - variant: "looper", - x: 150.273, - y: 415.9, - w: 891, - h: 866, - matrix: [-0.996255, 0.086469, -0.086469, -0.996255], - }, - ], - board: [ - { - variant: "big-teal", - x: 312.5, - y: 311.5, - w: 401, - h: 401, - }, - { - variant: "small-blue", - x: 75.5, - y: 30.5, - w: 233, - h: 233, - }, - ], - association: [ - { - variant: "big-teal", - x: 220.5, - y: 334.5, - w: 401, - h: 401, - }, - { - variant: "looper", - x: 261.925, - y: 523.285, - w: 891, - h: 866, - matrix: [-0.011115, 0.999938, -0.999938, -0.011115], - }, - ], - participation: [ - { - variant: "big-teal", - x: 312.5, - y: 316.5, - w: 401, - h: 401, - }, - { - variant: "small-blue", - x: 118.063, - y: 730.063, - w: 354.126, - h: 354.126, - }, - { - variant: "looper", - x: 328.423, - y: 1356.32, - w: 891, - h: 866, - matrix: [0.104528, -0.994522, 0.994522, 0.104528], - }, - ], - join: [ - { - variant: "big-teal", - x: 392.5, - y: 104.5, - w: 401, - h: 401, - }, - { - variant: "small-blue", - x: -20.937, - y: 182.063, - w: 354.126, - h: 354.126, - }, - ], - legal: [ - { - variant: "big-teal", - x: 191.5, - y: 512.5, - w: 401, - h: 401, - }, - { - variant: "small-blue", - x: 197.063, - y: 366.063, - w: 354.126, - h: 354.126, - }, - { - variant: "looper", - x: 288.422, - y: 453.976, - w: 891, - h: 866, - matrix: [0.542465, 0.840078, -0.840078, 0.542465], - }, - ], - }, -} - -export function StructureShapes({ section }: { section: StructureSection }) { - return ( - - {(["desktop", "mobile"] as const).map((layout) => - (layouts[layout][section] ?? []).map((placement) => ( - - )) - )} - - ) -} diff --git a/src/app/associations/page.tsx b/src/app/associations/page.tsx index 58304b1..e4760a9 100644 --- a/src/app/associations/page.tsx +++ b/src/app/associations/page.tsx @@ -2,7 +2,6 @@ import type { Metadata } from "next" import { AssociationsList } from "@/components/associations/associations-list" import { Hero } from "@/components/ui/hero" import { getAllAssociations } from "@/queries/associations" -import { AssociationsShapes } from "./shapes" export const metadata: Metadata = { title: "Associazioni", @@ -15,8 +14,7 @@ export default async function AssociationsPage() { const associations = await getAllAssociations() return ( -
- +
diff --git a/src/app/associations/shapes.tsx b/src/app/associations/shapes.tsx deleted file mode 100644 index 1ffeb2d..0000000 --- a/src/app/associations/shapes.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Shape, ShapeLayer } from "@/components/shapes" - -export function AssociationsShapes() { - return ( - - {/* Mobile Shapes */} - - - - - - - - - - - {/* Desktop Shapes */} - {/* bottom-anchored: the list length (so
's height) is data-dependent, a fixed top would drift from the footer */} - - - - - - - - ) -} diff --git a/src/app/chi-siamo/page.tsx b/src/app/chi-siamo/page.tsx index e8a55e9..0fdae58 100644 --- a/src/app/chi-siamo/page.tsx +++ b/src/app/chi-siamo/page.tsx @@ -6,14 +6,6 @@ import { PathSection } from "@/components/chi-siamo/path-section" import { ProblemSection } from "@/components/chi-siamo/problem-section" import { SupportSection } from "@/components/chi-siamo/support-section" import { VisionSection } from "@/components/chi-siamo/vision-section" -import { - ChiSiamoAnswerShapes, - ChiSiamoHistoryShapes, - ChiSiamoNumbersShapes, - ChiSiamoPathShapes, - ChiSiamoProblemShapes, - ChiSiamoVisionShapes, -} from "./shapes" export const metadata: Metadata = { title: "Su di noi", @@ -23,30 +15,12 @@ export const metadata: Metadata = { export default function ChiSiamoPage() { return (
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+ + + + + +
) diff --git a/src/app/chi-siamo/shapes.tsx b/src/app/chi-siamo/shapes.tsx deleted file mode 100644 index 3c16ff8..0000000 --- a/src/app/chi-siamo/shapes.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Shape, ShapeLayer } from "@/components/shapes" - -/** - * `left` offsets are `calc(50% + Npx)`, relative to the horizontal center of - * the 1728px (desktop) Figma frame — see src/components/home/shapes.tsx. - * - * Each section's shapes are `top`-anchored to that section's heading (the - * section's top edge): `top = figmaY - figmaHeadingY`. The vision shapes are - * instead anchored to the page top, since that section starts at the top of - * `
`, where page y = Figma y. - * - * The Figma file only has a desktop frame for this page, so there are no - * mobile shapes yet. - */ - -export function ChiSiamoVisionShapes() { - return ( - - {/* Desktop Shapes */} - - - - - - ) -} - -export function ChiSiamoProblemShapes() { - return ( - - {/* Desktop Shapes */} - - - - ) -} - -export function ChiSiamoAnswerShapes() { - return ( - - {/* Desktop Shapes */} - - - ) -} - -export function ChiSiamoPathShapes() { - return ( - - {/* Desktop Shapes */} - - - - - ) -} - -export function ChiSiamoNumbersShapes() { - return ( - - {/* Desktop Shapes */} - - - - - ) -} - -export function ChiSiamoHistoryShapes() { - return ( - - {/* Desktop Shapes */} - - - ) -} diff --git a/src/app/collabora/page.tsx b/src/app/collabora/page.tsx index b4598a5..a2889aa 100644 --- a/src/app/collabora/page.tsx +++ b/src/app/collabora/page.tsx @@ -7,16 +7,6 @@ import { ContactSection } from "@/components/collabora/contact-section" import { sectionContainerClass } from "@/components/collabora/section" import { Hero } from "@/components/ui/hero" import { cn } from "@/lib/utils" -import { - CollaboraAboutShapes, - CollaboraAssociationPhotoShapes, - CollaboraAssociationShapes, - CollaboraCollaborationShapes, - CollaboraCompanyPhotoShapes, - CollaboraCompanyShapes, - CollaboraContactShapes, - CollaboraHeroShapes, -} from "./shapes" export const metadata: Metadata = { title: "Collabora con Noi", @@ -27,37 +17,19 @@ export const metadata: Metadata = { export default function CollaboraPage() { return (
-
- - {/* Title at y=262 (mobile) / 286 (desktop) as in Figma; Hero adds 16px / 56px above it */} -
- -
-
-
- - -
-
- - -
-
- - } /> -
-
- - } /> -
-
- - -
+ {/* Title at y=262 (mobile) / 286 (desktop) as in Figma; Hero adds 16px / 56px above it */} +
+ +
+ + + + +
) } diff --git a/src/app/collabora/shapes.tsx b/src/app/collabora/shapes.tsx deleted file mode 100644 index 05e07cd..0000000 --- a/src/app/collabora/shapes.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { FigmaShape, type FigmaShapePlacement, ShapeLayer } from "@/components/shapes" - -/** - * Decorative shapes of the "Collabora con Noi" Figma frames (desktop node 5248:13597, 1728px wide; mobile node - * 5246:13012, 402px wide). Values are the Figma layers' centers, sizes and rotations, in paint order. - * - * Background shapes are grouped by the section that contains their center and `y` is measured from that section's - * top, so they stay next to their section however the copy reflows. `x` and the size scale with the page width. - * - * Shapes Figma draws over the photos are anchored to the photo itself (see `PhotoShapes`). - */ - -type Placement = FigmaShapePlacement - -const DESKTOP_WIDTH = 1728 -const MOBILE_WIDTH = 402 - -function SectionShapes({ desktop = [], mobile = [] }: { desktop?: Placement[]; mobile?: Placement[] }) { - return ( - - {mobile.map((shape) => ( - - ))} - {desktop.map((shape) => ( - - ))} - - ) -} - -const ROTATE_MINUS_34 = [0.828, -0.56, 0.56, 0.828] as const -const ROTATE_MINUS_98 = [-0.132, -0.991, 0.991, -0.132] as const -const ROTATE_MINUS_45 = [Math.SQRT1_2, -Math.SQRT1_2, Math.SQRT1_2, Math.SQRT1_2] as const - -export function CollaboraHeroShapes() { - return ( - - ) -} - -export function CollaboraAboutShapes() { - return ( - - ) -} - -export function CollaboraCollaborationShapes() { - return ( - - ) -} - -export function CollaboraAssociationShapes() { - return ( - - ) -} - -export function CollaboraCompanyShapes() { - return ( - - ) -} - -export function CollaboraContactShapes() { - return ( - - ) -} - -/** - * Shapes drawn over a photo, positioned in the photo's own Figma frame (611px wide on desktop, 354px on mobile) so - * they follow it at every size. - */ -function PhotoShapes({ - desktop, - mobile, -}: { - desktop: { width: number; height: number; shapes: Placement[] } - mobile: { width: number; height: number; shapes: Placement[] } -}) { - return ( - <> - {mobile.shapes.map((shape) => ( - - ))} - {desktop.shapes.map((shape) => ( - - ))} - - ) -} - -export function CollaboraAssociationPhotoShapes() { - return ( - - ) -} - -export function CollaboraCompanyPhotoShapes() { - return ( - - ) -} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index 54ce541..256adc9 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -1,5 +1,4 @@ import type { Metadata } from "next" -import { ConditionalGlobalShapes } from "@/components/conditional-global-shapes" import { CollaboraSection } from "@/components/contact/collabora-section" import { EmailSection } from "@/components/contact/email-section" import { GroupsSection } from "@/components/contact/groups-section" @@ -13,7 +12,6 @@ export const metadata: Metadata = { export default function Contact() { return (
- - +
diff --git a/src/app/groups/layout.tsx b/src/app/groups/layout.tsx index b82d45d..c0623a4 100644 --- a/src/app/groups/layout.tsx +++ b/src/app/groups/layout.tsx @@ -1,14 +1,10 @@ import type { ReactNode } from "react" import { ReportFab } from "@/components/groups/report/fab" -import { GroupsShapes } from "./shapes" export default function GroupsLayout({ children }: { children: ReactNode }) { return ( <> -
- - {children} -
+
{children}
) diff --git a/src/app/groups/shapes.tsx b/src/app/groups/shapes.tsx deleted file mode 100644 index 84e4083..0000000 --- a/src/app/groups/shapes.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Shape, ShapeLayer } from "@/components/shapes" - -export function GroupsShapes() { - return ( - - {/* Mobile Shapes */} - - - - - - - - {/* Desktop Shapes */} - - - - - - - - ) -} diff --git a/src/app/guides/layout.tsx b/src/app/guides/layout.tsx deleted file mode 100644 index 2692944..0000000 --- a/src/app/guides/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { ReactNode } from "react" -import { ConditionalGlobalShapes } from "@/components/conditional-global-shapes" - -export default function GuidesLayout({ children }: { children: ReactNode }) { - return ( - <> - - {children} - - ) -} diff --git a/src/app/join/page.tsx b/src/app/join/page.tsx index 6e24834..c22fc5c 100644 --- a/src/app/join/page.tsx +++ b/src/app/join/page.tsx @@ -4,7 +4,6 @@ import { Introduction } from "@/components/join/introduction" import { MembershipSection } from "@/components/join/membership" import { TeamsSection } from "@/components/join/teams" import { Hero } from "@/components/ui/hero" -import { JoinPhotoShapes, JoinShapes } from "./shapes" export const metadata: Metadata = { title: "Unisciti a noi", @@ -15,32 +14,17 @@ export const metadata: Metadata = { export default function JoinPage() { return (
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
- - } /> -
+
+ +
+ + + +
) } diff --git a/src/app/join/shapes.tsx b/src/app/join/shapes.tsx deleted file mode 100644 index 2303d3d..0000000 --- a/src/app/join/shapes.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { FigmaShape, type FigmaShapePlacement, ShapeLayer } from "@/components/shapes" - -type JoinSection = "hero" | "intro" | "admin" | "team" | "membership" - -// Original Figma layer transforms, anchored to each section as its content reflows. -const layouts: Record<"desktop" | "mobile", Record> = { - desktop: { - hero: [ - { variant: "big-teal", x: 864, y: 199, w: 974, h: 974, matrix: [1, 0, 0, 1] }, - { variant: "big-blue", x: 1740.603, y: 169.599, w: 616.16, h: 616.16, matrix: [1, -0.006, 0.006, 1] }, - { variant: "small-blue", x: 35.063, y: 40.063, w: 354.126, h: 354.126, matrix: [1, 0, 0, 1] }, - { - variant: "looper", - x: 890.098, - y: 150.508, - w: 1246.093, - h: 1207.653, - matrix: [0.9659, -0.2588, 0.2588, 0.9659], - }, - ], - intro: [ - { variant: "big-teal", x: 72, y: 49, w: 746, h: 746, matrix: [1, 0, 0, 1] }, - { variant: "big-blue", x: 1631.454, y: 182.454, w: 616.16, h: 616.16, matrix: [0.4229, -0.9062, 0.9062, 0.4229] }, - { variant: "small-blue", x: -12.937, y: 214.063, w: 354.126, h: 354.126, matrix: [1, 0, 0, 1] }, - { variant: "small-blue", x: 937.063, y: 584.063, w: 354.126, h: 354.126, matrix: [1, 0, 0, 1] }, - { - variant: "looper", - x: -98.299, - y: 458.503, - w: 1090.912, - h: 1057.259, - matrix: [-0.5916, -0.8062, 0.8062, -0.5916], - }, - ], - admin: [ - { variant: "big-teal", x: 159.5, y: 1077.5, w: 675, h: 675, matrix: [1, 0, 0, 1] }, - { variant: "big-teal", x: 1583, y: 626, w: 664, h: 664, matrix: [1, 0, 0, 1] }, - { variant: "big-blue", x: -32.083, y: 170.919, w: 616.16, h: 616.16, matrix: [-0.275, -0.9614, 0.9614, -0.275] }, - { variant: "small-blue", x: 1019.967, y: 1036.968, w: 221, h: 221, matrix: [0.9731, -0.2302, 0.2302, 0.9731] }, - { - variant: "small-blue", - x: 27.612, - y: 940.612, - w: 354.126, - h: 354.126, - matrix: [0.5243, -0.8515, 0.8515, 0.5243], - }, - { variant: "looper", x: 1794.453, y: 502.487, w: 1246.093, h: 1207.653, matrix: [0.5, 0.866, -0.866, 0.5] }, - { - variant: "looper", - x: -32.232, - y: 1031.157, - w: 1246.093, - h: 1207.653, - matrix: [0.1468, -0.9892, 0.9892, 0.1468], - }, - ], - team: [ - { variant: "big-teal", x: -18.5, y: 685.5, w: 741, h: 741, matrix: [1, 0, 0, 1] }, - { variant: "big-blue", x: 1696.443, y: 502.443, w: 616.16, h: 616.16, matrix: [0.8537, -0.5208, 0.5208, 0.8537] }, - { - variant: "looper", - x: -61, - y: 611.597, - w: 830.216, - h: 804.605, - matrix: [-Math.SQRT1_2, -Math.SQRT1_2, Math.SQRT1_2, -Math.SQRT1_2], - }, - ], - membership: [ - { variant: "big-blue", x: -179.592, y: 61.408, w: 554.027, h: 554.027, matrix: [0.866, -0.5, 0.5, 0.866] }, - { - variant: "big-blue", - x: 928.663, - y: 239.663, - w: 487.353, - h: 487.353, - matrix: [0.9656, -0.2601, 0.2601, 0.9656], - }, - ], - }, - mobile: { - hero: [ - { variant: "big-teal", x: 195.5, y: 97.524, w: 571, h: 571, matrix: [1, 0, 0, 1] }, - { variant: "looper", x: 219.393, y: 168.269, w: 730.628, h: 708.09, matrix: [0.8581, -0.5135, 0.5135, 0.8581] }, - { variant: "small-blue", x: 0, y: 40, w: 222, h: 222, matrix: [1, 0, 0, 1] }, - { variant: "small-blue", x: 0, y: 651, w: 222, h: 222, matrix: [1, 0, 0, 1] }, - ], - intro: [ - { variant: "big-teal", x: 339.5, y: 441.5, w: 571, h: 571, matrix: [1, 0, 0, 1] }, - { variant: "looper", x: 455.27, y: 408.394, w: 730.628, h: 708.09, matrix: [0.5, 0.866, -0.866, 0.5] }, - { variant: "small-blue", x: 9, y: 624, w: 222, h: 222, matrix: [1, 0, 0, 1] }, - ], - admin: [ - { variant: "big-teal", x: -33.5, y: 391.5, w: 571, h: 571, matrix: [1, 0, 0, 1] }, - { variant: "big-teal", x: -20.366, y: 1077.633, w: 444, h: 444, matrix: [0.7877, -0.616, 0.616, 0.7877] }, - { variant: "looper", x: -106.469, y: 430.5, w: 730.628, h: 708.09, matrix: [-0.2588, -0.9659, 0.9659, -0.2588] }, - { variant: "small-blue", x: 269, y: 677, w: 200, h: 200, matrix: [1, 0, 0, 1] }, - { variant: "small-blue", x: 377.5, y: 1339.5, w: 265, h: 265, matrix: [1, 0, 0, 1] }, - ], - team: [ - { variant: "big-teal", x: -20.366, y: 491.633, w: 444, h: 444, matrix: [0.7877, -0.616, 0.616, 0.7877] }, - { variant: "looper", x: -184.934, y: 453.92, w: 730.628, h: 708.09, matrix: [0.4581, -0.8889, 0.8889, 0.4581] }, - ], - membership: [ - { variant: "big-teal", x: 373.634, y: 331.633, w: 444, h: 444, matrix: [0.7877, -0.616, 0.616, 0.7877] }, - { variant: "small-blue", x: -31.5, y: 607.5, w: 265, h: 265, matrix: [1, 0, 0, 1] }, - { variant: "small-blue", x: 373.5, y: 893.5, w: 265, h: 265, matrix: [1, 0, 0, 1] }, - ], - }, -} -const photoLayers: Record<"desktop" | "mobile", FigmaShapePlacement[]> = { - desktop: [ - { variant: "small-blue", x: 593.712, y: 189, w: 312, h: 312, matrix: [0.9403, -0.3404, 0.3404, 0.9403] }, - { variant: "small-blue", x: -35.053, y: 60.843, w: 372.538, h: 372.538, matrix: [0.9403, -0.3404, 0.3404, 0.9403] }, - { variant: "looper", x: 89.691, y: 137.058, w: 718.112, h: 695.96, matrix: [0.2588, -0.9659, -0.9659, -0.2588] }, - ], - mobile: [ - { - variant: "small-blue", - x: 313.296, - y: 140.296, - w: 148.818, - h: 148.818, - matrix: [0.9403, -0.3404, 0.3404, 0.9403], - }, - { variant: "small-blue", x: -6.214, y: 24.787, w: 177.693, h: 177.693, matrix: [0.9403, -0.3404, 0.3404, 0.9403] }, - { variant: "looper", x: 39.651, y: 77.385, w: 342.525, h: 331.959, matrix: [0.2588, -0.9659, -0.9659, -0.2588] }, - ], -} - -export function JoinShapes({ section }: { section: JoinSection }) { - return ( - - {(["mobile", "desktop"] as const).map((layout) => ( -
- {layouts[layout][section].map((shape, index) => ( - - ))} -
- ))} -
- ) -} - -export function JoinPhotoShapes() { - return ( -
- {(["mobile", "desktop"] as const).map((layout) => ( -
- {photoLayers[layout].map((shape, index) => ( - - ))} -
- ))} -
- ) -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3dd9300..62e5cb9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,3 +1,4 @@ +import { Background } from "@/components/background" import { ConditionalFooter } from "@/components/conditional-footer" import { Footer } from "@/components/footer" import { Header } from "@/components/header/header" @@ -73,8 +74,8 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac // storageKey="polinetwork_darkmode_temp" // TODO: enable when dark mode design is ready disableTransitionOnChange > - {/* */} -
+
+
{children} diff --git a/src/app/matricole/guida/page.tsx b/src/app/matricole/guida/page.tsx index 729e925..91334bf 100644 --- a/src/app/matricole/guida/page.tsx +++ b/src/app/matricole/guida/page.tsx @@ -1,7 +1,6 @@ import type { Metadata } from "next" import { FiDownload } from "react-icons/fi" import { CardSplit } from "@/components/card-split" -import { ResourcesHeroShapes } from "@/components/resources/shapes" import { Button } from "@/components/ui/button" import { getLatestGuidaMatricola } from "@/queries/guida-matricola" @@ -24,7 +23,6 @@ export default async function GuidaMatricolaPage() { return (
-

Guida della Matricola diff --git a/src/app/matricole/page.tsx b/src/app/matricole/page.tsx index 7b7575a..9d1e36d 100644 --- a/src/app/matricole/page.tsx +++ b/src/app/matricole/page.tsx @@ -2,8 +2,6 @@ import type { Metadata } from "next" import { FAQsPage } from "@/components/matricole/faqs" import { MatricoleGuides } from "@/components/matricole/guides" import { MatricoleIntro } from "@/components/matricole/intro" -import { ResourcesHeroShapes } from "@/components/resources/shapes" -import { MatricoleFAQsShapes, MatricoleGuidesShapes } from "./shapes" export const metadata: Metadata = { title: "Matricole", @@ -13,18 +11,9 @@ export const metadata: Metadata = { export default function MatricolePage() { return (
-
- - -
-
- - -
-
- - -
+ + +
) } diff --git a/src/app/matricole/shapes.tsx b/src/app/matricole/shapes.tsx deleted file mode 100644 index 9372982..0000000 --- a/src/app/matricole/shapes.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Shape, ShapeLayer } from "@/components/shapes" - -/** - * `left` offsets are `calc(50% + Npx)`, relative to the horizontal center of - * the 1728px-wide (desktop) / 402px-wide (mobile) Figma reference frames — - * see src/components/home/shapes.tsx for why. - * - * `top` is relative to the top of the section each component is mounted in - * (MatricoleGuides, FAQsPage), matching how the section - * boundaries are grouped in the Figma file, not to the page as a whole. - */ - -export function MatricoleGuidesShapes() { - return ( - - {/* Mobile Shapes */} - - - - - - - - - - - - - - {/* Desktop Shapes */} - - - - - - - ) -} - -export function MatricoleFAQsShapes() { - return ( - - {/* Mobile Shapes (this section has no shapes on desktop) */} - - - - - ) -} diff --git a/src/app/page.tsx b/src/app/page.tsx index 66b8b37..0296908 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,20 +2,13 @@ import { AboutUs } from "@/components/home/about-us" import { Hero } from "@/components/home/hero" import { Materials } from "@/components/home/materials" import { Projects } from "@/components/home/projects" -import { HomeMaterialsShapes, HomeProjectsShapes } from "@/components/home/shapes" export default function Home() { return (
-
- - -
-
- - -
+ +
) diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx index fed6fa4..3e02366 100644 --- a/src/app/projects/[id]/page.tsx +++ b/src/app/projects/[id]/page.tsx @@ -6,7 +6,6 @@ import { FiArrowLeft, FiArrowUpRight, FiUploadCloud } from "react-icons/fi" import { Glass } from "@/components/glass" import { GradientIcon } from "@/components/gradient-icon" import { getProjectResources, resourceDetails } from "@/components/projects/resources" -import { Shape, ShapeLayer } from "@/components/shapes" import { Button } from "@/components/ui/button" import { Card, CardContent, CardTitle } from "@/components/ui/card" import { cn } from "@/lib/utils" @@ -40,47 +39,42 @@ export default async function ProjectPage({ params }: Props) { return (
-
- - - -
- - Tutti i progetti - - - {project.logo ? ( - // Project logos are app icons with their own background and corners: shown as they are, not framed - - ) : ( - - - +
+ + Tutti i progetti + + + {project.logo ? ( + // Project logos are app icons with their own background and corners: shown as they are, not framed + + ) : ( + + + + )} +
+ {project.category in categoryLabels && ( +

{categoryLabels[project.category]}

)} -
- {project.category in categoryLabels && ( -

{categoryLabels[project.category]}

- )} -

- {project.title} -

-

- {project.descriptionIt} -

-
- -
-
+

+ {project.title} +

+

+ {project.descriptionIt} +

+

+ + {resources.length > 0 && (
-
- - -
-
- - -
-
- - -
-
- - -
+ + + +
) } diff --git a/src/app/projects/shapes.tsx b/src/app/projects/shapes.tsx deleted file mode 100644 index 297b464..0000000 --- a/src/app/projects/shapes.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Shape, ShapeLayer } from "@/components/shapes" - -/** - * `left` offsets below are expressed as `calc(50% + Npx)`, i.e. relative to the - * horizontal center of the section rather than its left edge. The projects - * content is itself center-anchored (not width-capped), so anchoring shapes to - * the edge would make them drift away from the content on screens wider or - * narrower than the 1728px/402px Figma reference frames. - * - * `top` is relative to the top of the section each component is mounted in - * (CommunityNews, Collection, Upload, Deprecated), not to the page. Every - * section on this page uses `min-h-screen` (fluid height, depends on the viewport) - * — each shape below is therefore scoped and `top`-anchored to the section it - * visually belongs to, rather than using cumulative page offsets. - */ - -export function CommunityNewsShapes() { - return ( - - {/* Mobile Shapes */} - - - - - - - - {/* Desktop Shapes */} - - - - - - - ) -} - -export function CollectionShapes() { - return ( - - {/* Mobile Shapes */} - - - - {/* Desktop Shapes */} - - - - - ) -} - -export function UploadShapes() { - return ( - - {/* Mobile Shapes */} - - - - {/* Desktop Shapes */} - - - - - ) -} - -export function DeprecatedShapes() { - return ( - - {/* Mobile Shapes */} - - - - - {/* Desktop Shapes */} - - - - - ) -} diff --git a/src/app/storia/page.tsx b/src/app/storia/page.tsx index f5ebf38..dc4fc1c 100644 --- a/src/app/storia/page.tsx +++ b/src/app/storia/page.tsx @@ -11,15 +11,6 @@ import { Organization } from "@/components/storia/organization" import { HistoryProjects } from "@/components/storia/projects" import { Thanks } from "@/components/storia/thanks" import { Chapters, Timeline } from "@/components/storia/timeline" -import { - HistoryBoardsShapes, - HistoryChaptersShapes, - HistoryHeroShapes, - HistoryIntroShapes, - HistoryProjectsShapes, - HistoryThanksShapes, - HistoryTimelineShapes, -} from "./shapes" export const metadata: Metadata = { title: "La nostra storia", @@ -29,78 +20,59 @@ export const metadata: Metadata = { export default function HistoryPage() { return ( -
- +
-
-
- - -

- Una domanda semplice, una grande idea. -

-

- Tutto è cominciato nel 2016, quando un futuro studente cercava uno spazio per entrare in contatto con - i propri compagni e trovare risposte alle prime domande sulla vita universitaria. -

-

- Da questa esigenza è nato un progetto collettivo, che nel tempo si è trasformato in una{" "} - rete capillare costruita dagli studenti, per gli studenti. -

- - } - classNames={{ - section: "mx-auto max-w-330 gap-12 px-6 md:px-6 xl:flex-row xl:items-center [&>div>h2]:typo-title-large", - textDiv: "min-w-0 xl:w-1/2 xl:items-start", - title: "text-balance text-blue-secondary xl:text-start", - descriptionDiv: "xl:text-start", - contentDiv: "min-w-0 xl:w-1/2", - }} - > -
- - -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+
+ +

+ Una domanda semplice, una grande idea. +

+

+ Tutto è cominciato nel 2016, quando un futuro studente cercava uno spazio per entrare in contatto con i + propri compagni e trovare risposte alle prime domande sulla vita universitaria. +

+

+ Da questa esigenza è nato un progetto collettivo, che nel tempo si è trasformato in una{" "} + rete capillare costruita dagli studenti, per gli studenti. +

+ + } + classNames={{ + section: "mx-auto max-w-330 gap-12 px-6 md:px-6 xl:flex-row xl:items-center [&>div>h2]:typo-title-large", + textDiv: "min-w-0 xl:w-1/2 xl:items-start", + title: "text-balance text-blue-secondary xl:text-start", + descriptionDiv: "xl:text-start", + contentDiv: "min-w-0 xl:w-1/2", + }} + > +
+ + +
+
+ + + + + - - - - - - ) -} - -export function HistoryIntroShapes() { - return ( - - - - ) -} - -export function HistoryTimelineShapes() { - return ( - - - - - ) -} - -export function HistoryChaptersShapes() { - return ( - - - - - - - ) -} - -export function HistoryBoardsShapes() { - return ( - - - - ) -} - -export function HistoryProjectsShapes() { - return ( - - - - - - ) -} - -export function HistoryThanksShapes() { - return ( - - - - ) -} diff --git a/src/app/team/page.tsx b/src/app/team/page.tsx index 7d6e3af..df96657 100644 --- a/src/app/team/page.tsx +++ b/src/app/team/page.tsx @@ -5,7 +5,6 @@ import { applicationHref, teams } from "@/components/team/constants" import { TeamSection } from "@/components/team/team-section" import { Button } from "@/components/ui/button" import { Hero } from "@/components/ui/hero" -import { TeamHeroShapes, TeamSectionShapes } from "./shapes" export const metadata: Metadata = { title: "I Team", @@ -15,21 +14,15 @@ export const metadata: Metadata = { export default function TeamPage() { return (
-
- -
- -
-
+
+ +
{teams.map((team) => ( -
- - -
+ ))}
diff --git a/src/app/team/shapes.tsx b/src/app/team/shapes.tsx deleted file mode 100644 index dab2b36..0000000 --- a/src/app/team/shapes.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import Image from "next/image" -import heroGlow from "@/assets/shapes/team/hero-glow.svg" -import { FigmaShape, type FigmaShapePlacement, ShapeLayer } from "@/components/shapes" - -export function TeamHeroShapes() { - return ( - - - - - ) -} - -// Original Figma layers, anchored to their sections so they follow responsive content reflow. -const placements: Record = { - it: [ - { variant: "big-teal", x: 11, y: 1, w: 1164, h: 1164 }, - { variant: "small-blue", x: -13, y: -306, w: 424, h: 424, matrix: [0.7649, 0.6441, -0.6441, 0.7649] }, - { variant: "looper", x: 373, y: -13, w: 1924, h: 1866 }, - ], - "design-marketing": [ - { variant: "big-teal", x: 1587, y: -177, w: 662, h: 662 }, - { variant: "small-blue", x: 1442, y: -612, w: 354, h: 354, matrix: [0.8875, -0.4608, 0.4608, 0.8875] }, - { variant: "small-blue", x: -82, y: -240, w: 502, h: 502, matrix: [0.8875, -0.4608, 0.4608, 0.8875] }, - ], - hr: [ - { variant: "big-teal", x: 0, y: 559, w: 974, h: 974 }, - { variant: "small-blue", x: 310, y: 869, w: 354.126, h: 354.126 }, - ], - "events-partnership": [ - { variant: "small-blue", x: 1423, y: 432, w: 781, h: 781, matrix: [0.951, -0.3093, 0.3093, 0.951] }, - { variant: "big-teal", x: 1086, y: 627, w: 705, h: 705, matrix: [0.928, 0.3727, -0.3727, 0.928] }, - ], -} - -export function TeamSectionShapes({ section }: { section: string }) { - return ( - - {(placements[section] ?? []).map((placement, index) => ( - - ))} - - ) -} diff --git a/src/assets/shapes/big-blue.svg b/src/assets/shapes/big-blue.svg deleted file mode 100644 index 354ec86..0000000 --- a/src/assets/shapes/big-blue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/shapes/big-blue.webp b/src/assets/shapes/big-blue.webp new file mode 100644 index 0000000..c4af504 Binary files /dev/null and b/src/assets/shapes/big-blue.webp differ diff --git a/src/assets/shapes/big-teal.svg b/src/assets/shapes/big-teal.svg deleted file mode 100644 index ffce061..0000000 --- a/src/assets/shapes/big-teal.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/shapes/big-teal.webp b/src/assets/shapes/big-teal.webp new file mode 100644 index 0000000..15e3be0 Binary files /dev/null and b/src/assets/shapes/big-teal.webp differ diff --git a/src/assets/shapes/hero-glow.svg b/src/assets/shapes/hero-glow.svg deleted file mode 100644 index 98a159b..0000000 --- a/src/assets/shapes/hero-glow.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/shapes/looper-line.webp b/src/assets/shapes/looper-line.webp new file mode 100644 index 0000000..a1ef9e9 Binary files /dev/null and b/src/assets/shapes/looper-line.webp differ diff --git a/src/assets/shapes/matricole-glow-1.svg b/src/assets/shapes/matricole-glow-1.svg deleted file mode 100644 index 1c9425e..0000000 --- a/src/assets/shapes/matricole-glow-1.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/assets/shapes/projects-glow.svg b/src/assets/shapes/projects-glow.svg deleted file mode 100644 index 07df19c..0000000 --- a/src/assets/shapes/projects-glow.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/assets/shapes/resources-glow-2.svg b/src/assets/shapes/resources-glow-2.svg deleted file mode 100644 index 23e7c87..0000000 --- a/src/assets/shapes/resources-glow-2.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/assets/shapes/resources-top-glow.svg b/src/assets/shapes/resources-top-glow.svg deleted file mode 100644 index 36e654d..0000000 --- a/src/assets/shapes/resources-top-glow.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/shapes/small-blue.svg b/src/assets/shapes/small-blue.svg deleted file mode 100644 index feedfff..0000000 --- a/src/assets/shapes/small-blue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/shapes/small-blue.webp b/src/assets/shapes/small-blue.webp new file mode 100644 index 0000000..3e58846 Binary files /dev/null and b/src/assets/shapes/small-blue.webp differ diff --git a/src/assets/shapes/team/hero-glow.svg b/src/assets/shapes/team/hero-glow.svg deleted file mode 100644 index 364ef2b..0000000 --- a/src/assets/shapes/team/hero-glow.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/background/animate.ts b/src/components/background/animate.ts new file mode 100644 index 0000000..10ab304 --- /dev/null +++ b/src/components/background/animate.ts @@ -0,0 +1,89 @@ +import { lineAngle } from "./looper" +import type { SceneShape } from "./scene" + +/** Shapes further than this (px) from the viewport are not updated */ +const MARGIN = 200 +/** Time constant (s) of the scroll smoothing, gives scroll-driven motion some inertia */ +const SCROLL_INERTIA = 0.15 +/** Duration (ms) of the fade-in of the motion, so the first frame doesn't jump from the server-rendered layout */ +const INTRO = 1500 +/** For this long (ms) after a scene starts the scroll isn't smoothed: navigating or restoring the scroll position jumps */ +const SETTLE = 300 + +/** The intro only plays once, on the first page load: later pages are rendered on the client, directly in place */ +let introStart: number | undefined + +const wave = (t: number, period: number, phase: number) => Math.sin(2 * Math.PI * (t / period + phase)) +const smoothstep = (x: number) => { + const c = Math.min(1, Math.max(0, x)) + return c * c * (3 - 2 * c) +} + +/** + * Animates the background from a single `requestAnimationFrame` loop, the + * same way in every browser. Each shape gets a passive drift/sway/breathing + * and, based on how far it has travelled through the viewport, a parallax + * offset and a small rotation. Loopers also open their fan while scrolling + * past them. Only `transform`s are touched, so nothing is repainted. + * + * `scene` children must match the order of `getShapes()`, each being + * `layout box > motion > content`. The first frame is rendered synchronously, + * so new shapes never show up out of place. + */ +export function animateScene(scene: HTMLElement, getShapes: () => SceneShape[]) { + let scroll = window.scrollY + let last = performance.now() + const start = last + introStart ??= start + const introFrom = introStart + + const tick = (now: number) => { + frame = requestAnimationFrame(tick) + const dt = Math.min(0.1, (now - last) / 1000) + last = now + if (now - start < SETTLE) scroll = window.scrollY + else scroll += (window.scrollY - scroll) * (1 - Math.exp(-dt / SCROLL_INERTIA)) + + const t = now / 1000 + const intro = smoothstep((now - introFrom) / INTRO) + const viewport = window.innerHeight + const sceneTop = scene.getBoundingClientRect().top + window.scrollY - scroll + const boxes = scene.children as HTMLCollectionOf + // read every position before writing any transform, interleaving them would force a style flush per shape + const tops = Array.from(boxes, (box) => sceneTop + box.offsetTop) + const heights = Array.from(boxes, (box) => box.offsetHeight) + + for (const [i, shape] of getShapes().entries()) { + const motion = boxes[i]?.firstElementChild as HTMLElement | null | undefined + const top = tops[i] + const height = heights[i] + if (!motion || top === undefined || height === undefined) break + + const visible = top < viewport + MARGIN && top + height > -MARGIN + // promote only what's on screen: these are huge, and Firefox ignores will-change over a memory budget + motion.style.willChange = visible ? "transform" : "" + if (!visible) continue + // 0 when the shape enters from the bottom of the viewport, 1 when it leaves from the top + const progress = Math.min(1, Math.max(0, (viewport - top) / (viewport + height))) + + const x = shape.driftX * wave(t, shape.periodX, shape.phase) + const y = shape.driftY * wave(t, shape.periodY, shape.phase + 0.25) + shape.depth * (1 - 2 * progress) + const rotate = shape.sway * wave(t, shape.periodSway, shape.phase + 0.5) + shape.spin * (progress - 0.5) + const scale = shape.breathe * wave(t, shape.periodY, shape.phase + 0.25) + motion.style.transform = `translate3d(${x * intro}px, ${y * intro}px, 0) rotate(${rotate * intro}deg) scale(${1 + scale * intro})` + + if (shape.variant === "looper") { + // tight when entering the viewport, wide when leaving it + const spread = shape.spread * (2 * smoothstep(progress) - 1) + shape.fan * wave(t, shape.periodFan, shape.phase) + const lines = motion.firstElementChild?.children ?? [] + for (let j = 0; j < lines.length; j++) { + ;(lines[j] as HTMLElement).style.transform = `rotate(${lineAngle(j, spread * intro)}deg)` + } + } + } + } + + let frame = 0 + tick(start) + return () => cancelAnimationFrame(frame) +} diff --git a/src/components/background/index.tsx b/src/components/background/index.tsx new file mode 100644 index 0000000..76b6125 --- /dev/null +++ b/src/components/background/index.tsx @@ -0,0 +1,85 @@ +"use client" + +import { usePathname } from "next/navigation" +import { useEffect, useLayoutEffect, useRef, useState } from "react" +import { Shape } from "@/components/shapes" +import { animateScene } from "./animate" +import { LOOPER_ASPECT, Looper } from "./looper" +import { type SceneShape, sceneSegment } from "./scene" + +/** Rendered on the server, before the page height can be measured */ +const INITIAL_SEGMENTS = 2 + +/** + * Animated decorative background shared by every page. It covers the whole + * page (footer included) and is split into `--bg-segment` tall segments, each + * filled with shapes from `sceneSegment` and animated by `animateScene`. + * Every page gets a fresh scene, rendered directly in its final place. + */ +export function Background() { + const pathname = usePathname() + return +} + +function Scene({ pathname }: { pathname: string }) { + const ref = useRef(null) + const [segments, setSegments] = useState(INITIAL_SEGMENTS) + const shapes = Array.from({ length: segments }, (_, i) => sceneSegment(pathname, i)).flat() + const shapesRef = useRef(shapes) + + useEffect(() => { + shapesRef.current = shapes + }) + + useEffect(() => { + const el = ref.current + if (!el) return + const observer = new ResizeObserver(() => { + const segment = Number.parseFloat(getComputedStyle(el).getPropertyValue("--bg-segment")) + setSegments(Math.max(1, Math.ceil(el.offsetHeight / segment))) + }) + observer.observe(el) + return () => observer.disconnect() + }, []) + + // a layout effect, so the first frame is applied before the new page is painted + useLayoutEffect(() => { + const el = ref.current + if (!el || matchMedia("(prefers-reduced-motion: reduce)").matches) return + return animateScene(el, () => shapesRef.current) + }, []) + + return ( +
+ {shapes.map((shape) => ( + + ))} +
+ ) +} + +function SceneItem({ shape }: { shape: SceneShape }) { + const looper = shape.variant === "looper" + const width = shape.size + const height = looper ? shape.size * LOOPER_ASPECT : shape.size + + return ( +
+
+ {looper ? ( + + ) : ( + + )} +
+
+ ) +} diff --git a/src/components/background/looper.tsx b/src/components/background/looper.tsx new file mode 100644 index 0000000..0e48ca0 --- /dev/null +++ b/src/components/background/looper.tsx @@ -0,0 +1,73 @@ +import type { CSSProperties } from "react" +import line from "@/assets/shapes/looper-line.webp" +import { cn } from "@/lib/utils" + +/** + * Animatable version of `assets/shapes/looper.svg`. + * + * The original is 19 copies of the same curve, each rotated 6° more than the + * previous one around its own pivot. Here every line is an element showing + * the same rasterized curve (a single texture for the GPU, so rotating lines + * never triggers a repaint), rotated around its pivot. `lineAngle` adds an + * extra rotation proportional to the distance from the middle line, which + * opens and closes the fan. + */ +const PIVOTS = [ + [814, 799], + [811, 785], + [808, 771], + [805, 757], + [801, 744], + [797, 731], + [792, 718], + [787, 706], + [782, 694], + [776, 682], + [770, 671], + [764, 660], + [757, 650], + [750, 639], + [743, 630], + [735, 620], + [728, 610], + [719, 601], + [710, 592], +] as const + +const LAST = PIVOTS.length - 1 +const MIDDLE = LAST / 2 + +/** Size of the original SVG and box of `looper-line.webp` (the curve plus its stroke) in it */ +const VIEW = { width: 1610, height: 1562 } +const LINE = { x: 425, y: -3, width: 254, height: 1520 } + +export const LOOPER_ASPECT = VIEW.height / VIEW.width + +/** Rotation of the line `i`, `spread` is the extra angle between consecutive lines */ +export const lineAngle = (i: number, spread: number) => (i - LAST) * 6 + (i - MIDDLE) * spread + +const percent = (value: number) => `${value * 100}%` + +export function Looper({ className, style }: { className?: string; style?: CSSProperties }) { + return ( +
+ {PIVOTS.map(([x, y], i) => ( + + ))} +
+ ) +} diff --git a/src/components/background/scene.ts b/src/components/background/scene.ts new file mode 100644 index 0000000..4b2102f --- /dev/null +++ b/src/components/background/scene.ts @@ -0,0 +1,107 @@ +import type { BlobVariant } from "@/components/shapes" + +export type SceneShape = { + id: string + variant: BlobVariant | "looper" + /** Horizontal center, from -1 (left edge) to 1 (right edge) of the page */ + x: number + /** Vertical center, in segments from the top of the page */ + y: number + /** Width, in `--bg-unit`s (≈px on a 1728px wide screen) */ + size: number + /** Static rotation, in degrees */ + rotate: number + /** Scroll: parallax offset in px (negative is further away, slower than the page) and rotation in degrees */ + depth: number + spin: number + /** Passive motion: drift amplitude (px), sway (deg), breathing scale, their periods (s) and phase (0-1) */ + driftX: number + driftY: number + sway: number + breathe: number + periodX: number + periodY: number + periodSway: number + phase: number + /** Looper only: extra angle between lines (deg) driven by scroll, and its passive oscillation */ + spread: number + fan: number + periodFan: number +} + +/** FNV-1a, to turn the pathname into a seed */ +function hash(str: string) { + let h = 0x811c9dc5 + for (let i = 0; i < str.length; i++) h = Math.imul(h ^ str.charCodeAt(i), 0x01000193) + return h >>> 0 +} + +/** mulberry32, a tiny seeded PRNG */ +function prng(seed: number) { + let a = seed + return () => { + a = (a + 0x6d2b79f5) | 0 + let t = Math.imul(a ^ (a >>> 15), 1 | a) + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} + +/** + * Deterministically generates the shapes of one segment (a screen-ish tall + * slice) of the page background. Every segment has the same composition as + * the original Figma backgrounds: a teal star overlapping a couple of blue + * blobs on one side, crossed by a looper, and a smaller counterweight on the + * other side. Sides alternate between segments, everything else is jittered + * from a seed derived from the pathname and the segment index, so each page + * gets its own (but stable across reloads) variation. + */ +export function sceneSegment(pathname: string, index: number): SceneShape[] { + const seed = hash(pathname) + const rand = prng(seed ^ Math.imul(index + 1, 0x9e3779b1)) + const between = (min: number, max: number) => min + rand() * (max - min) + const side = (seed + index) % 2 ? 1 : -1 + + const shapes: SceneShape[] = [] + const add = (variant: SceneShape["variant"], x: number, y: number, size: number, depth: [number, number]) => { + const looper = variant === "looper" + shapes.push({ + id: `${index}-${shapes.length}`, + variant, + x, + y: index + y, + size, + rotate: between(0, 360), + depth: between(...depth), + spin: looper ? between(6, 10) : between(-8, 8), + driftX: between(12, 32), + driftY: between(12, 32), + sway: looper ? between(2, 4) : between(5, 12), + breathe: looper ? 0 : between(0.02, 0.05), + periodX: between(34, 58), + periodY: between(38, 62), + periodSway: between(58, 94), + phase: between(0, 1), + spread: looper ? between(1, 1.3) : 0, + fan: looper ? between(0.25, 0.4) : 0, + periodFan: between(22, 38), + }) + } + + // main cluster: the blobs overlap, so the looper's white lines show on top of them + const cx = side * between(0.35, 0.8) + const cy = between(0.15, 0.45) + add("big-blue", cx + side * between(-0.1, 0.15), cy - between(0, 0.2), between(650, 900), [-140, -60]) + add("big-teal", cx, cy, between(800, 1050), [-70, 30]) + add("small-blue", cx - side * between(0.1, 0.25), cy + between(0.05, 0.2), between(380, 520), [100, 200]) + add("looper", cx + side * between(-0.3, 0.1), cy + between(-0.05, 0.2), between(1400, 1800), [-240, -140]) + + // counterweight on the other side + const ox = -side * between(0.55, 0.95) + const oy = between(0.6, 0.9) + add(rand() < 0.5 ? "big-blue" : "big-teal", ox, oy, between(550, 850), [-120, -20]) + if (rand() < 0.5) + add("small-blue", ox + side * between(0.1, 0.3), oy - between(0, 0.15), between(250, 400), [120, 220]) + + return shapes +} diff --git a/src/components/collabora/association-section.tsx b/src/components/collabora/association-section.tsx index 705e9d5..1e2fd86 100644 --- a/src/components/collabora/association-section.tsx +++ b/src/components/collabora/association-section.tsx @@ -1,9 +1,9 @@ -import type { ReactNode } from "react" import associationsImage from "@/assets/images/collabora_associations.jpg" +import { PhotoShapes } from "@/components/shapes" import { PhotoSection } from "./photo-section" import { Highlight } from "./section" -export function AssociationSection({ photoDecorations }: { photoDecorations: ReactNode }) { +export function AssociationSection() { return ( + } className="py-30 md:py-37.5" >

diff --git a/src/components/collabora/company-section.tsx b/src/components/collabora/company-section.tsx index 55723cf..d04247b 100644 --- a/src/components/collabora/company-section.tsx +++ b/src/components/collabora/company-section.tsx @@ -1,16 +1,37 @@ -import type { ReactNode } from "react" import companyImage from "@/assets/images/collabora_company.jpg" +import { PhotoShapes } from "@/components/shapes" import { PhotoSection } from "./photo-section" import { Highlight } from "./section" -export function CompanySection({ photoDecorations }: { photoDecorations: ReactNode }) { +export function CompanySection() { return ( + } photoFirst className="py-30 md:py-31.25" > diff --git a/src/components/collabora/photo-section.tsx b/src/components/collabora/photo-section.tsx index dc53590..52c5184 100644 --- a/src/components/collabora/photo-section.tsx +++ b/src/components/collabora/photo-section.tsx @@ -54,9 +54,7 @@ export function PhotoSection({ className={cn("absolute max-w-none", imageClassName)} />

-
- {decorations} -
+ {decorations}
diff --git a/src/components/conditional-global-shapes.tsx b/src/components/conditional-global-shapes.tsx deleted file mode 100644 index d034b7e..0000000 --- a/src/components/conditional-global-shapes.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client" - -import { usePathname } from "next/navigation" -import { Shape } from "@/components/shapes" - -const HIDDEN_EXACT = ["/"] - -/** - * Generic top-of-page decorative shapes, reused on every page that doesn't - * define its own (e.g. the homepage, which has a Figma-accurate composition). - */ -export function ConditionalGlobalShapes() { - const pathname = usePathname() - if (HIDDEN_EXACT.includes(pathname)) return null - - return ( -
-
- - - - -
-
- ) -} diff --git a/src/components/footer/index.tsx b/src/components/footer/index.tsx index 7135c8b..58568b4 100644 --- a/src/components/footer/index.tsx +++ b/src/components/footer/index.tsx @@ -4,12 +4,10 @@ import { FiFacebook, FiGithub, FiInstagram, FiLinkedin } from "react-icons/fi" import discord from "@/assets/icons/discord.svg" import telegram from "@/assets/icons/telegram.svg" import { CardMultipleIcons } from "../card-multiple-icons" -import { FooterShapes } from "./shapes" export function Footer() { return ( -
- +

Keep in touch!

diff --git a/src/components/footer/shapes.tsx b/src/components/footer/shapes.tsx deleted file mode 100644 index 018629d..0000000 --- a/src/components/footer/shapes.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Shape, ShapeLayer } from "@/components/shapes" - -export function FooterShapes() { - return ( - - {/* Desktop Shapes */} - - - - - ) -} diff --git a/src/components/home/hero.tsx b/src/components/home/hero.tsx index 1e9ff50..6f28b27 100644 --- a/src/components/home/hero.tsx +++ b/src/components/home/hero.tsx @@ -2,12 +2,10 @@ import Link from "next/link" import { FiNavigation, FiUserPlus } from "react-icons/fi" import { Button } from "../ui/button" import { GroupSearch } from "./group-search" -import { HomeHeroShapes } from "./shapes" export function Hero() { return (
-

Trova gruppi, risorse e supporto tra gli studenti del Polimi diff --git a/src/components/home/shapes.tsx b/src/components/home/shapes.tsx deleted file mode 100644 index 8307868..0000000 --- a/src/components/home/shapes.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Shape, ShapeLayer } from "@/components/shapes" - -/** - * `left` offsets below are expressed as `calc(50% + Npx)`, i.e. relative to the - * horizontal center of the section rather than its left edge. The homepage - * content is itself center-anchored (not width-capped), so anchoring shapes to - * the edge would make them drift away from the content on screens wider or - * narrower than the 1728px/402px Figma reference frames. - * - * `top` is relative to the top of the section each component is mounted in - * (Materials or Projects), not to the page. Materials and Projects/AboutUs - * used to share one absolute layer with cumulative offsets, but AboutUs uses - * `min-h-screen` (fluid height, depends on the viewport) — if a shape ever - * needs to sit inside AboutUs, mount it scoped to that section too, the same - * way this file already splits Materials from Projects. Otherwise any - * mismatch between a fluid section's real height and Figma's fixed height - * assumption compounds into every shape positioned after it (e.g. shapes - * drifting away from their section when the browser is zoomed out — see - * src/app/projects/shapes.tsx, which had exactly this bug). - */ - -export function HomeHeroShapes() { - return ( - - {/* Mobile Shapes */} - - - - - {/* Desktop Shapes */} - - - - ) -} - -export function HomeMaterialsShapes() { - return ( - - {/* Mobile Shapes */} - - - - - - - - {/* Desktop Shapes */} - - - - - ) -} - -export function HomeProjectsShapes() { - return ( - - {/* Mobile Shapes */} - - - - - {/* Desktop Shapes */} - - - - ) -} diff --git a/src/components/join/membership.tsx b/src/components/join/membership.tsx index 6b724f4..0d7a82e 100644 --- a/src/components/join/membership.tsx +++ b/src/components/join/membership.tsx @@ -1,12 +1,12 @@ import Image from "next/image" -import type { ReactNode } from "react" import externalLink from "@/assets/icons/join/external-link.svg" import membersPhoto from "@/assets/images/join/members.webp" import { Highlight, SectionTitle } from "@/components/collabora/section" +import { PhotoShapes } from "@/components/shapes" import { cn } from "@/lib/utils" import { membershipSteps } from "./constants" -export function MembershipSection({ photoDecorations }: { photoDecorations: ReactNode }) { +export function MembershipSection() { return (

- {photoDecorations} +
) diff --git a/src/components/resources/shapes.tsx b/src/components/resources/shapes.tsx deleted file mode 100644 index 33c0144..0000000 --- a/src/components/resources/shapes.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Shape, ShapeLayer } from "@/components/shapes" - -/** - * Hero background shared by the Matricole and FAQs pages (same Figma design). - */ - -export function ResourcesHeroShapes() { - return ( - - {/* Mobile Shapes */} - - - - - - {/* Desktop Shapes */} - - - - - - ) -} diff --git a/src/components/shapes.tsx b/src/components/shapes.tsx index 71201c8..2b05968 100644 --- a/src/components/shapes.tsx +++ b/src/components/shapes.tsx @@ -1,85 +1,64 @@ import Image from "next/image" -import bigBlueSvg from "@/assets/shapes/big-blue.svg" -import bigTealSvg from "@/assets/shapes/big-teal.svg" -import heroGlowSvg from "@/assets/shapes/hero-glow.svg" -import looperSvg from "@/assets/shapes/looper.svg" -import matricoleGlow1Svg from "@/assets/shapes/matricole-glow-1.svg" -import projectsGlowSvg from "@/assets/shapes/projects-glow.svg" -import resourcesGlow2Svg from "@/assets/shapes/resources-glow-2.svg" -import resourcesTopGlowSvg from "@/assets/shapes/resources-top-glow.svg" -import smallBlueSvg from "@/assets/shapes/small-blue.svg" +import type { CSSProperties } from "react" +import bigBlue from "@/assets/shapes/big-blue.webp" +import bigTeal from "@/assets/shapes/big-teal.webp" +import looper from "@/assets/shapes/looper.svg" +import smallBlue from "@/assets/shapes/small-blue.webp" import { cn } from "@/lib/utils" -export type ShapeVariant = - | "big-blue" - | "big-teal" - | "small-blue" - | "hero-glow" - | "projects-glow" //TODO: remove these page specific shapes - | "matricole-glow-1" - | "resources-glow-2" - | "resources-top-glow" - | "looper" +/** + * The blobs are rasterized from the Figma SVGs (a path with a 50px gaussian + * blur): Firefox re-runs the blur every time an SVG image is drawn at a new + * scale, which made animating them unbearably slow. Being so blurry, a small + * bitmap upscaled looks the same. + */ +const SOURCES = { + "big-blue": bigBlue, + "big-teal": bigTeal, + "small-blue": smallBlue, + looper, +} + +export type ShapeVariant = keyof typeof SOURCES +export type BlobVariant = Exclude export type ShapeProps = { variant: ShapeVariant className?: string - style?: React.CSSProperties + style?: CSSProperties } -export const Shape: React.FC = ({ variant, className, style }) => { - const getShapeSrc = () => { - switch (variant) { - case "big-blue": - return bigBlueSvg - case "big-teal": - return bigTealSvg - case "small-blue": - return smallBlueSvg - case "hero-glow": - return heroGlowSvg - case "projects-glow": - return projectsGlowSvg - case "matricole-glow-1": - return matricoleGlow1Svg - case "resources-glow-2": - return resourcesGlow2Svg - case "resources-top-glow": - return resourcesTopGlowSvg - case "looper": - return looperSvg - default: - return "" - } - } - - return ( - - ) -} - -type FigmaShapeVariant = Extract +/** + * Static decorative shape. The page background is animated and generated in + * `@/components/background`, this is for decorations inside components. + */ +export const Shape: React.FC = ({ variant, className, style }) => ( + +) /** Layer size of the blurred shapes' master components in Figma (the looper has no blur). */ -const FIGMA_SHAPE_MASTER_SIZE: Record = { +const FIGMA_SHAPE_MASTER_SIZE: Record = { "big-blue": 1062.24, "big-teal": 974, "small-blue": 354.126, looper: null, } -/** The masters' layer blur: 100px (a 50px standard deviation), which the SVGs include as 100px of bleed per side. */ +/** The masters' layer blur: 100px (a 50px standard deviation), which the images include as 100px of bleed per side. */ const FIGMA_SHAPE_BLUR = 50 const FIGMA_SHAPE_BLEED = 100 export type FigmaShapePlacement = { - variant: FigmaShapeVariant + variant: ShapeVariant /** Center x of the Figma layer, in px from the left edge of the reference box. */ x: number /** Center y of the Figma layer, in px from the top edge of the reference box. */ @@ -92,27 +71,21 @@ export type FigmaShapePlacement = { matrix?: readonly [number, number, number, number] } -export type FigmaShapeProps = FigmaShapePlacement & { - /** Figma width of the reference box (e.g. 1728 for a desktop frame, 402 for mobile, or a photo's width). */ +type FigmaShapeProps = FigmaShapePlacement & { + /** Figma size of the reference box (e.g. a photo): position and size are percentages of it. */ refWidth: number - /** - * Figma height of the reference box. When set, `y` is a percentage of the box height (use it when the box has a - * fixed aspect ratio, e.g. a photo). Otherwise `y` is in px, so shapes keep their offset from the box top - * regardless of how text reflows. - */ - refHeight?: number + refHeight: number className?: string } /** - * Places a `` from its Figma geometry. Horizontal position and size are percentages of the reference box - * width, so the composition scales with it (on 2K/4K screens the background grows with the viewport instead of - * leaving empty bands at the sides). Must be inside a `@container` as wide as the reference box. + * Places a `` from its Figma geometry, scaling with the reference box. Must be inside a `@container` as wide + * as the box. * - * Resizing an instance in Figma doesn't scale its blur, while scaling the SVG does: shapes smaller than their + * Resizing an instance in Figma doesn't scale its blur, while scaling the image does: shapes smaller than their * master get the missing blur from a CSS filter (Gaussian blurs add up in quadrature). */ -export function FigmaShape({ variant, x, y, w, h, matrix, refWidth, refHeight, className }: FigmaShapeProps) { +function FigmaShape({ variant, x, y, w, h, matrix, refWidth, refHeight, className }: FigmaShapeProps) { const master = FIGMA_SHAPE_MASTER_SIZE[variant] const ratio = master ? w / master : 1 const bleed = master ? FIGMA_SHAPE_BLEED * ratio : 0 @@ -126,7 +99,7 @@ export function FigmaShape({ variant, x, y, w, h, matrix, refWidth, refHeight, c className={className} style={{ left: `${(x / refWidth) * 100}%`, - top: refHeight ? `${(y / refHeight) * 100}%` : `${y}px`, + top: `${(y / refHeight) * 100}%`, width: `${(width / refWidth) * 100}%`, height: "auto", aspectRatio: `${width} / ${height}`, @@ -137,32 +110,24 @@ export function FigmaShape({ variant, x, y, w, h, matrix, refWidth, refHeight, c ) } -export type ShapeLayerProps = { - children: React.ReactNode - className?: string -} +export type PhotoShapesLayout = { width: number; height: number; shapes: FigmaShapePlacement[] } -/** - * Wraps a group of ``s positioned behind a section. `overflow-x-clip` - * (not `overflow-hidden`/`overflow-x-hidden`) is required: pairing it with - * `overflow-y-visible` clips horizontal bleed only — CSS forces the "visible" - * axis to compute as `auto` (which still clips, just via an invisible - * scrollbar) whenever the other axis is `hidden`/`auto`/`scroll`, but not - * when it's `clip`, since `clip` doesn't create a scroll container. This lets - * shapes bleed vertically into the next section (as several are designed to) - * without shapes positioned off-canvas horizontally causing a page-wide - * horizontal scrollbar. - * - * Don't add `overflow-hidden` here to stop shapes from lengthening the page: - * that also cuts them off at the section's bottom edge (e.g. before the - * footer). The page wrapper in app/layout.tsx is `overflow-clip`, so shapes - * can cross into the footer but anything past the end of the page is clipped. - */ -export const ShapeLayer: React.FC = ({ children, className }) => ( -
- {children} -
-) +/** Static shapes Figma draws over a photo, laid out on the photo's mobile and desktop frames. */ +export function PhotoShapes({ mobile, desktop }: { mobile: PhotoShapesLayout; desktop: PhotoShapesLayout }) { + return ( +
+ {(["mobile", "desktop"] as const).map((layout) => { + const { width, height, shapes } = layout === "mobile" ? mobile : desktop + return shapes.map((shape) => ( + + )) + })} +
+ ) +} diff --git a/src/styles/background.css b/src/styles/background.css new file mode 100644 index 0000000..683a2b7 --- /dev/null +++ b/src/styles/background.css @@ -0,0 +1,12 @@ +/* Animated page background, see src/components/background */ + +.shape-scene { + /* scales shapes with the screen, ≈1px on the 1440-1728px wide Figma frames */ + --bg-unit: clamp(0.5px, 0.064vw, 1.1px); + /* height of the slices of page the background is generated in */ + --bg-segment: 900px; + + @media (width >= 48rem) { + --bg-segment: 1000px; + } +} diff --git a/src/styles/globals.css b/src/styles/globals.css index 2560003..98dafe8 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -4,6 +4,7 @@ @import "./sidebar.css"; @import "./charts.css"; @import "./animations.css"; +@import "./background.css"; @custom-variant dark (&:is(.dark *));