# Art Gallery — architecture basics Interactive virtual museum spanning art history: zoomable timeline with event guides, branching movement flow, 3D gallery halls, painting influence graphs, and art-history annotations on detail pages. See [README.md](../README.md) for quick start. ## Concept The app is organised as a **drill-down hierarchy**: 1. **Timeline** — historical eras (Ancient → Contemporary) with definite or fuzzy date boundaries. 2. **Movement flow** — art movements as curved SVG streams on the same year axis; documented predecessor→successor branches; portrait thumbnails placed along each stream. 3. **3D gallery** — one personal hall per artist *or* a **movement gallery** (click a movement name on the flow diagram): period-themed interiors, chronological wings of up to ~55 works, side-wall hang only. 4. **Painting detail** — full work in the centre, *Influenced By* on the left, *Influenced* on the right (paintings, artists, or movements), optional **art-history notes** with image markers, prev/next catalog browsing, optional fullscreen, link to artist biography. 5. **Artist biography** — portrait, lifespan, movement, and Wikipedia-sourced intro text (`bio_short` / `bio_full`). With debug mode on, the same image-audit panel as painting detail (portrait search, **Checked** / **Fix it** / **More** / **Clear** / **Upload**). 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 ```text Gallery/ ├── server/ # Express API, DB pool, image service ├── client/ # React/Vite frontend │ ├── src/ # Source (components, pages, 3D scene) │ │ ├── components/VirtualGallery.tsx # 3D hall (artist + movement modes) │ │ ├── components/GalleryWindows.tsx # Side-wall daylight windows (movement) │ │ ├── components/HallPassage.tsx # Open archway between movement wings │ │ ├── components/MovementHallDetails.tsx # Period architectural details │ │ ├── data/movement-interior-styles.ts # Per-movement interior themes │ │ ├── utils/movementHallLayout.ts # Wing split + window gap placement │ │ ├── utils/galleryProceduralTextures.ts # Hi-res wall/floor textures │ │ ├── components/PaintingDetail.tsx # Detail view + debug panel │ │ ├── components/ArtistBio.tsx # Biography + portrait debug panel │ │ ├── components/DebugSearchResultsModal.tsx # “More” search picker (20 results) │ │ ├── components/Timeline.tsx # Era bar, year ticks, event markers │ │ ├── components/TimelineEventGuides.tsx # Event vertical guides into movement flow │ │ ├── components/MovementBands.tsx # Movement flow (SVG streams + branches) │ │ ├── components/PaintingAnnotations.tsx # Art-history notes on painting detail │ │ ├── pages/CheckupPage.tsx # Image audit table │ │ ├── data/historical-events.ts # Timeline event markers (UI) │ │ ├── data/movement-lineage.ts # Curated movement predecessor links (UI) │ │ ├── utils/parquetFloorTexture.ts # Procedural parquet floor │ │ ├── utils/debugMode.ts # Debug mode + “Show more” localStorage prefs │ │ └── 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, checkup tools │ ├── fetch-artist-bios.js │ ├── expand-paintings.js │ ├── famous-paintings-data.js │ ├── fetch-missing-images.js │ ├── find-duplicate-paintings.js │ ├── audit-influence-duplicates.js │ ├── painter-palette-lib.js │ ├── import-painter-palette.js │ └── image-fetcher.js ├── Inputs/ # External datasets (e.g. PainterPalette.csv) ├── Output/ # Generated exports (e.g. paintings.csv) ├── data/images/ # Local portraits and paintings (+ thumbs/) ├── db/ # schema.sql, setup-admin.sql, migrate-*.sql ├── server/migrate.js # npm run migrate — schema + incremental migrations ├── deploy/ # Production nginx + systemd examples ├── Documentation/ # This folder └── .env # DB and port config (not committed) ``` ## Runtime modes ### Production-style (single process) ```bash npm run start:prod # build client + serve on PORT (default 3520) # or: npm run build && npm run start ``` Serves `/api/*`, `/images/*`, and the built SPA from `client/dist` if it exists. **Deployed URLs:** public http://gallery.mysuperlab.netcraze.pro · LAN http://192.168.10.70:3520 — see [setup.md](setup.md#production-deployment). ### Development (two processes) ```bash npm run dev:server # API on PORT from .env (3520 production, 3001 typical dev) npm run dev:client # Vite on :5173, proxies /api and /images to PORT ``` Use the Vite URL during frontend work for HMR. ## User navigation flow ```mermaid flowchart TD A[Home — timeline + movement flow] -->|scroll / drag / zoom| A A -->|click movement name| G[Movement gallery — 3D wings] G -->|click painting| D D -->|Back| G G -->|back door / Wings / Exit| H[Wing navigator] H -->|pick wing| G H -->|Exit to Timeline| A G -->|front arch / E| G G -->|Back to Timeline| 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`): timeline + movement flow sit in a shared `home-timeline-stack` so event guide lines can extend from the era bar down through the movement canvas. The movement flow compresses vertically so all movements in the visible year range fit without page scrolling. ### Timeline year labels Year ticks along the bottom of the era bar use **large, high-contrast** labels (bold cream text with shadow). The active range in the control row (e.g. `1400 CE — 1900 CE`) uses the same stronger styling. ### 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. **Vertical guides:** `TimelineEventGuides.tsx` draws faint gold lines (point events) or shaded bands (spans) from the marker row **down through the movement flow**, aligned to the same year scale. Guides are visual only (`pointer-events: none`); click-to-zoom stays on the markers in `Timeline.tsx`. ### 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 | | Click **movement name** (label on stream) | Open **movement gallery** for that movement | Artist portraits stop wheel/drag propagation so zooming over a face does not fight portrait clicks. Hovering a portrait highlights the artist’s lifespan on the era bar and brightens their segment on the movement stream. **Note:** Movement lineage is **frontend curation** for layout and labels — it is not stored in PostgreSQL. Painting influence links live in **`painting_influence_sources`** (paintings, artists, or movements as sources). The API reads that table for detail panels, hall navigation, and `has_influence_links`. The legacy **`painting_influences`** table is still written in parallel when curators add painting-to-painting edges but is not queried for display. ## Virtual gallery (3D halls) The 3D scene supports two modes in `VirtualGallery.tsx`: **artist halls** (personal catalog) and **movement galleries** (full movement collection, chronological). ### Artist 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 in **one row per wall**; room **depth grows** when the catalog is large | | Corridor layout | **15+ paintings:** short back wall (up to 8 works), remaining works on extended **left/right** side walls — a long gallery corridor | | Wall order | On each wall, left → right: **later works on the left**, **earlier works on the right**; undated works sort toward the left | | Floor | Herringbone **parquet** procedural texture (`parquetFloorTexture.ts`) | | Wall tint | Gallery walls blend the artist’s **movement colour** into cream plaster tones | | Frame finish | **Unchecked** works: black moulding; **Reviewed** (`checkup_checked`): bright gold moulding at **double width** | | 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 | | Museum exit | Front-wall **double doors** with transom, brass hardware, sconces, marble threshold, and warm vestibule glow | | 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 | | After image fix | Debug **Fix it** updates the gallery session, busts texture cache (`?v=N`), and returns to the hall with the new image and gold frame | **Controls (artist hall):** | 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 **`painting_influence_sources`** (painting and artist sources). Empty lists mean no influence edges are recorded yet for that artist — run `npm run update-influences` or extend `art-influences-data.js`. ### Movement galleries Enter from the home page by clicking a **movement name** on the movement flow (`MovementBands.tsx` → `GET /api/movements/:id/gallery`). | Rule | Implementation | |------|----------------| | One gallery per movement | All paintings by artists in that movement, sorted chronologically | | Wings | Catalog split into wings of up to **55 works** (`movementHallLayout.ts`); large movements (e.g. Baroque) use multiple wings | | Paintings on walls | **Left and right walls only** — back wall reserved for exit, front for passage to the next wing | | Wall order | Along each side wall: **later works on the left**, **earlier on the right** (same convention as artist halls) | | Frame captions | **Year · artist** label below each frame | | Period interior | Each of the 26 seeded movements maps to a unique style in `movement-interior-styles.ts` (Italian palazzo, Baroque palace, NYC loft, white cube, etc.) | | Wall materials | Hi-res **procedural textures** with normal maps (`galleryProceduralTextures.ts`): real-world stone, marble, wood panelling, brick, velvet, stucco — plus **single-colour painted walls** (`painted-lime`, `painted-oil-matte`, `painted-oil-satin`, `painted-emulsion`, `painted-flat`) tinted per movement for Renaissance salons through modern white cubes | | Textures | Wall/floor/ceiling maps applied via `useTexturedMaterial`; movement **tints** drive painted-wall hue | | Windows | **Side walls only** — placed in gaps between frames (high on the wall, no overlap with paintings); style matches the movement era | | Lighting | Daylight from windows + ceiling track lights + ambient/sun fill | | Back wall | **Exit double doors** → **Wing navigator** (jump to any wing) or **Exit to Timeline** | | Front wall | Open **“Next wing →”** archway when a later wing exists; walk through or press `E` when near | | Influence lamps | Same golden lamps as artist halls when `has_influence_links` is true | | Missing images | Draped canvas cover in frame | | Detail return | Hall stays mounted; camera preserved on **Back to Timeline** / **Back to Gallery** | **Controls (movement gallery):** | Input | Action | |-------|--------| | Walk / turn / drag | Same as artist hall | | Click painting | Open detail view (returns to the same wing) | | Back wall / `E` / **Wings / Exit →** | Open wing navigator | | Front archway / `E` (when near) | Advance to the **next chronological wing** | Movement galleries do **not** use the predecessor/successor influence picker — that remains artist-hall only. ### Shared 3D behaviour **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 an **artist** hall to link disk files. Movement galleries load painting lists from the API without a separate preload step. While a texture is loading, the frame shows the canvas cover instead of a white placeholder. ## Painting detail view Opened from the 3D hall (artist or movement wing — 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 (full work visible, letterboxed), artist portraits, or movement swatches — position in catalog (e.g. `3 of 12`) | | **Art history notes** | Numbered markers on the image (when positioned) plus a note list below — short citations from Gombrich, museum catalogs, Wikipedia, etc. (`painting_annotations` table) | | **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** / **← Back to Timeline** | Return to the hall or movement wing 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). - Side-panel influence images use **`object-fit: contain`** so tall or wide works are not cropped (dark letterbox background). - The 3D hall stays mounted in the background while detail is open so nothing is lost on return. ### Debug mode (developer) When **Debug mode** is enabled from the home header, painting detail and artist biography show a bottom-left panel with image search preview and action buttons. See [Developer tools (image audit)](#developer-tools-image-audit). Next to the toggle, **Show more** (checkbox, persisted in `localStorage`) opens the **More** search-results modal automatically whenever you open a painting or artist bio while debug mode is on. ## 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. - **Movement galleries** complement artist halls: full movement corpus in period-themed wings, entered from the flow diagram. - **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 in **`painting_influence_sources`** (directed links from paintings to source paintings, artists, or movements), with optional period fields and citation metadata. Sources include curated scholarship (`art-influences-data.js`) and **PainterPalette** (`discovered_via = painter-palette`). Legacy `painting_influences` mirrors painting-to-painting edges for scripts only. ## Developer tools (image audit) Optional workflow for curating local image files — not part of the public visitor experience. | Feature | Where | Purpose | |---------|--------|---------| | **Debug mode** | Home header toggle (`client/src/utils/debugMode.ts`) | Persists in `localStorage`; enables debug panel on painting detail and artist bio | | **Show more** | Home header checkbox (same util) | When debug mode is on, auto-opens the **More** modal on each painting / bio page load | | **Checkup page** | Home header → **Checkup** (`CheckupPage.tsx`) | Full-catalog table: gallery vs detail thumbnails, search, fix, review flags | | **Debug panel** | Painting detail or artist bio (bottom-left, when debug mode on) | Search preview + action buttons (six on painting detail, five on artist bio) | ### Debug panel (painting detail and artist bio) When debug mode is on, a panel at the bottom-left shows the image search query, a preview when a result is found, and action buttons in two or three rows: | Button | Painting detail | Artist bio | |--------|-----------------|------------| | **Checked** | Sets `checkup_checked` via `PATCH …/checkup-flags` | Same for artist portrait flags | | **Fix it** | Replaces local image from top search result | Replaces portrait | | **More** | Modal with up to **20** results (resolution shown when known) | Same | | **Clear** | Deletes files, clears DB paths, empty frame | Clears portrait slot | | **Upload** | Local file picker → disk + thumbnail | Local file → portrait | | **Remove entry** | **Painting detail only** — deletes row from DB, removes image files, refreshes 3D gallery, navigates to next/previous work in catalog (or back to gallery if last work). No confirmation dialog. | — | After **Fix it**, **More**, **Upload**, or **Clear**, the main view, gallery textures (paintings), and timeline portrait (artists) update without a full page reload. **Remove entry** refetches artist (and movement gallery when relevant) from the API and remounts the 3D hall so the deleted frame disappears immediately. Reviewed portraits show a gold border on the bio page; reviewed paintings use gold frames in the 3D hall. **Back to Gallery** returns to the live hall session, not a stale snapshot. Influence side-panel thumbnails use **letterboxing** (`object-fit: contain`) so full compositions are visible. ### Checkup page **Columns:** Gallery and Detail thumbnails, Search (reference image), Fix (replace local file), **Reviewed** (`checkup_checked`), **Fixed** (`checkup_fixed`). **Search visible** runs image search only for rows currently shown after text/filter — not automatically on page load. Fixing an image sets **Fixed** and **Reviewed**. Run `npm run migrate:checkup-flags`, `npm run migrate:artist-checkup-flags`, and `npm run migrate:painting-annotations` once on existing databases. Load notes with `npm run update-painting-annotations` (add `--wikipedia` for overview lines from Wikipedia intro text). After server code changes, restart `npm run start` (or `npm run dev:server`) so new routes (e.g. clear, upload, delete painting, portrait debug, annotations) are registered. JSON body limit for uploads is **20 MB** (`express.json` in `server/index.js`); individual files are capped at **15 MB** after decode. See [API.md](API.md#developer-image-audit) and [data-and-images.md](data-and-images.md#duplicate-paintings). ## Related docs | Document | Contents | |----------|----------| | [setup.md](setup.md) | Install, database, npm scripts | | [DB_structure.md](DB_structure.md) | Tables and relationships | | [API.md](API.md) | REST endpoints | | [data-and-images.md](data-and-images.md) | Image pipeline and seeding |