Support artist and movement influence links with web discovery, a developer checkup table with gallery/detail thumbnails, and debug image search with fix-it workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
13 KiB
Art Gallery — architecture basics
Interactive virtual museum spanning art history: zoomable timeline, branching movement flow, 3D gallery halls, and painting influence graphs. See README.md for quick start.
Concept
The app is organised as a drill-down hierarchy:
- Timeline — historical eras (Ancient → Contemporary) with definite or fuzzy date boundaries.
- Movement flow — art movements as curved SVG streams on the same year axis; documented predecessor→successor branches; portrait thumbnails placed along each stream.
- 3D gallery — one personal hall per artist; paintings on the walls, open centre, single exit for influence-based navigation.
- Painting detail — full work in the centre, Influenced By on the left, Influenced on the right (paintings, artists, or movements), prev/next catalog browsing, optional fullscreen, link to artist biography.
- Artist biography — portrait, lifespan, movement, and Wikipedia-sourced intro text (
bio_short/bio_full).
All artwork images are stored locally under data/images/ — the UI never hot-links to Wikipedia or Commons at runtime (except optional on-demand fetch when a file is missing).
Stack
| Layer | Choice | Role |
|---|---|---|
| API | Node.js + Express 5 | REST API, static image serving, optional SPA hosting from client/dist |
| Database | PostgreSQL | Eras, movements, artists, paintings, influence edges |
| Frontend | React 19 + Vite 8 | SPA routing and UI |
| 3D | Three.js via @react-three/fiber, @react-three/drei |
Virtual gallery navigation |
| Data ingestion | Node scripts | Wikipedia summaries, Commons images, curated influence data |
Repository layout
Gallery/
├── server/ # Express API, DB pool, image service
├── client/ # React/Vite frontend
│ ├── src/ # Source (components, pages, 3D scene)
│ │ ├── components/MovementBands.tsx # Movement flow (SVG streams + branches)
│ │ ├── data/historical-events.ts # Timeline event markers (UI)
│ │ ├── data/movement-lineage.ts # Curated movement predecessor links (UI)
│ │ └── utils/timelineView.ts # Shared zoom/pan math for timeline + movements
│ └── dist/ # Production build (served by API when present)
├── scripts/ # Seed, bios, catalog expansion, image fetch
│ ├── fetch-artist-bios.js
│ ├── expand-paintings.js
│ ├── famous-paintings-data.js
│ ├── fetch-missing-images.js
│ └── image-fetcher.js
├── data/images/ # Local portraits and paintings (+ thumbs/)
├── db/ # SQL schema and migrations (when present)
├── Documentation/ # This folder
└── .env # DB and port config (not committed)
Runtime modes
Production-style (single process)
npm run server # http://localhost:3001
Serves /api/*, /images/*, and the built SPA from client/dist if it exists.
Development (two processes)
npm run dev:server # API on :3001
npm run dev:client # Vite on :5173, proxies /api and /images
Use the Vite URL during frontend work for HMR.
User navigation flow
flowchart TD
A[Home — timeline + movement flow] -->|scroll / drag / zoom| A
A -->|click portrait| B[Artist bio — Wikipedia text]
B -->|Enter Gallery| C[Artist hall — 3D]
C -->|click painting| D[Painting detail + influences]
D -->|prev / next| D
D -->|click centre image| F[Fullscreen lightbox]
F -->|close| D
D -->|Back| C
C -->|exit doorway / E key| E[Path picker]
E -->|predecessors| C
E -->|successors| C
D -->|influence thumbnail| D
D -->|artist link| B
B -->|Back| A
C -->|Back| A
Timeline and movement flow
The home page shows two linked views over the same year window (viewStart / viewEnd in HomePage.tsx):
| View | Component | Purpose |
|---|---|---|
| Era bar | Timeline.tsx |
Historical eras, major event markers, click-to-zoom |
| Movement flow | MovementBands.tsx |
Curved streams per movement, lineage branches, artist portraits |
Both views share zoom/pan behaviour via client/src/utils/timelineView.ts. The home page uses a fixed viewport (100vh): the movement flow compresses vertically so all movements in the visible year range fit without page scrolling.
Timeline controls
| Input | Action |
|---|---|
| Click an era band | Zoom to that historical period |
| Click an event marker | Zoom to that event (or war span) |
| Scroll wheel | Zoom toward cursor |
| Drag centre | Pan the year range |
| Drag left / right handle | Trim view start / end |
| + / − / ⟲ buttons | Zoom in, zoom out, reset to full range |
Historical event markers
Major world events appear on the era bar as pin markers (single years) or shaded spans (e.g. World War I, World War II). Data lives in client/src/data/historical-events.ts — not in PostgreSQL. Labels appear when zoomed in enough; tooltips always show name and dates. Edit HISTORICAL_EVENTS and rebuild the client to add or change markers.
Movement flow
Each visible movement is drawn as a portrait-width curved stream (~54 px stroke, compressed when many rows are visible) from its start year to its end year.
| Feature | Implementation |
|---|---|
| Lineage layout | client/src/data/movement-lineage.ts — curated predecessor→successor pairs (Met / ArtStory / museum essays); multiple parents allowed |
| Vertical depth | Successor movements sit on rows below their deepest parent; sibling movements at the same depth are spread into lanes to limit overlap |
| Branch connectors | Smooth curves from the centre of a parent stream to the centre of each child stream (siblings fan out along the parent’s length) |
| Visual blending | Path-aligned SVG gradients with transparent fades at stream ends and branch junctions; streams draw on top of branches so overlap brightness stays uniform |
| Filtering | Same rule as the API: only movements with at least one artist active in the visible year range |
| Viewport layout | Row height and stream width scale from measured canvas size so every visible movement row fits in the remaining screen space |
Artists on movement streams
Each artist appears as a portrait circle on their movement’s stream row:
| Feature | Behaviour |
|---|---|
| Placement | Portrait at the midpoint of birth–death years |
| Lifespan line | Hidden by default; hover the portrait to show a horizontal line from birth year to death year |
| Timeline span | The lifespan line follows the full timeline axis (clamped to the current zoom), even when it extends beyond the movement band |
| Stacking | Artists in the same movement who would overlap are placed on separate lanes within the stream band |
| Colour | Each lane gets a slightly shifted tint derived from the movement colour; portrait border matches its line |
Movement flow controls
| Input | Action |
|---|---|
| Scroll wheel on flow canvas | Zoom (same range as timeline) |
| Drag on flow canvas | Pan |
| Click portrait | Open artist biography |
Artist portraits stop wheel/drag propagation so zooming over a face does not fight portrait clicks.
Note: Movement lineage is frontend curation for layout and labels — it is not stored in PostgreSQL. Painting influence links (painting_influence_sources, plus legacy painting_influences for hall navigation) are separate and drive the 3D exit picker and detail panels.
Virtual gallery (3D halls)
Each artist has exactly one hall. The hall is a rectangular room sized to fit their catalog:
| Rule | Implementation |
|---|---|
| One hall per artist | VirtualGallery.tsx builds a single room from that artist’s paintings |
| Catalog depth | Most artists target ≥ 6 notable works via npm run expand-catalog and famous-paintings-data.js; some masters have larger museum dumps |
| Paintings on walls | Works hang on the back, left, and right walls; the room grows and uses multiple rows when the catalog is large (e.g. 75+ works) |
| Wall order | On each wall, left → right: later works on the left, earlier works on the right; undated works sort toward the left |
| Influence lamps | A golden picture light appears above frames whose work has any influence-graph edge (has_influence_links from the API) |
| Eye-level viewing | Frame centres sit at eye height (~1.65 m); the camera stays level with the floor (no pitch up/down) |
| Open centre | Floor and ceiling only — no columns, pedestals, or other centre objects |
| Single exit | One doorway on the front wall; walk to it or click it |
| Hall-to-hall travel | Exit opens a panel: Predecessors (left) and Successors (right), each grouped by art movement |
| Missing images | Works without a local file show a draped canvas cover in the frame (not a blank white rectangle) |
| Detail view return | Opening a painting close-up keeps the 3D hall mounted in the background so position and view direction are preserved when you go back |
Controls:
| Input | Action |
|---|---|
W / ↑ |
Walk forward |
S / ↓ |
Walk back |
A / ← / Q |
Turn left |
D / → |
Turn right |
| Mouse drag | Look left / right (same direction as keyboard turns) |
| Click painting | Open detail view |
Exit doorway / E / Exit → header button |
Open path picker |
Predecessors and successors come from the painting influence graph (painting_influences → other artists). Empty lists mean no influence edges are recorded yet for that artist — run npm run update-influences or extend seed data.
3D images use locally cached files only (galleryImageUrl in client/src/api/client.ts). Remote fetches are too slow for realtime WebGL textures; call POST /api/artists/:id/preload-images before entering a hall to link disk files. While a texture is loading, the frame shows the canvas cover instead of a white placeholder.
Painting detail view
Opened from the 3D hall (click a frame) or from influence thumbnails on another work’s detail page.
| Layer | What you see |
|---|---|
| Detail | Centre image, Influenced By (left) and Influenced (right) — painting thumbnails, artist portraits, or movement swatches — position in catalog (e.g. 3 of 12) |
| Fullscreen | Click the centre image; Escape or click anywhere to return to detail only |
Controls:
| Input | Action |
|---|---|
‹ / › beside image |
Previous / next work by the same artist (chronological order) |
← / → |
Same as prev / next (disabled while fullscreen is open) |
| Click centre image | Open fullscreen lightbox |
| Click influence thumbnail | Open that work’s detail (different artist allowed) |
| Click influence artist portrait | Open that artist’s 3D gallery hall |
| ← Back to Gallery | Return to the hall you entered from — 3D camera position is preserved |
| About {artist} | Open artist biography |
Navigation rules:
- Catalog browsing (‹ › / arrow keys) walks the current artist’s works earliest → latest. It does not change the back target: after browsing several works, Back to Gallery still returns directly to the hall.
- Influence links push a new detail layer; Back from an influenced work returns to the painting you came from (and from there back to the gallery if applicable).
- The 3D hall stays mounted in the background while detail is open so nothing is lost on return.
Key design decisions
- Timeline bounds derive from the earliest art movement start year, not ancient-era metadata alone, so the default view opens where catalogued content begins.
- Movement filtering on zoom only shows movements that have at least one artist active in the visible year range.
- Movement lineage (
movement-lineage.ts) documents art-historical predecessor→successor links for the flow diagram; extend that file to add or correct branches. - One hall per artist keeps navigation predictable: enter from the timeline or bio, leave via the single exit or back button.
- Influence-based hall links connect artists through documented painting relationships, grouped by movement at the exit.
- 3D gallery images use locally cached files only; slow remote fetches would break realtime rendering.
- Influence data is stored as directed links from paintings to sources (another painting, an artist, or a movement), with optional period fields and citation metadata (source author, quote, URL).
Related docs
| Document | Contents |
|---|---|
| setup.md | Install, database, npm scripts |
| DB_structure.md | Tables and relationships |
| API.md | REST endpoints |
| data-and-images.md | Image pipeline and seeding |