- )
-}
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.
-
+ 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.
+
+ )
+}
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 (
-
)
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 (
+