Files
Art-gallery/Documentation/basics.md
T

47 KiB
Raw Blame History

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 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; classic left→right or vertical bottom→up layout (header link).
  2. Movement flow — art movements as SVG streams on the same year axis; documented predecessor→successor branches (classic); portrait thumbnails along each stream.
  3. 3D gallery — one personal hall per artist, a movement gallery (click a movement name → artist filter → hall), or a guided tour hall (timeline → Tours): period-themed or tour wings of up to ~55 works, U-shaped hang (left → end wall → right).
  4. Painting detail — full work in the centre, Influenced By on the left, Influenced on the right (paintings, artists, or movements), optional curator notes and 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).

Catalog search — on the timeline home page, the header search bar (CatalogSearchBar.tsx) finds artists, paintings, and movements by name and metadata (year, movement, Wikipedia title). Type at least 2 characters (300 ms debounce); results group into Artists, Movements, and Paintings with thumbnails. Keyboard: / to move, Enter to open, Escape to close. Choosing a result opens the artist gallery, movement gallery, or painting detail. Paintings opened from search show ← Back to Timeline and return to the home timeline (full year range), not the previous view.

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/VirtualGallery.tsx  # 3D hall (artist + movement modes; WebGL context-loss recovery)
│   │   ├── components/GalleryLoadingMarker.tsx  # Loading spinner overlay/banner (catalog, portraits, halls)
│   │   ├── 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/Timeline.tsx            # Classic horizontal era bar
│   │   ├── components/VerticalTimeline.tsx    # Bottom-up vertical era rail
│   │   ├── components/MovementBands.tsx   # Movement flow (SVG streams + branches)
│   │   ├── components/VerticalMovementBands.tsx  # Bottom-up movement streams
│   │   ├── components/TimelineEventGuides.tsx  # Event vertical guides into movement flow
│   │   ├── components/CatalogSearchBar.tsx  # Timeline header catalog search
│   │   ├── components/PaintingAnnotations.tsx  # Art-history notes on painting detail
│   │   ├── pages/CheckupPage.tsx            # Image audit table
│   │   ├── pages/TranslationsPage.tsx       # Russian translation review
│   │   ├── pages/InfluencesPage.tsx         # Influence links CRUD + import wizard
│   │   ├── pages/ToursPage.tsx              # Guided tour editor
│   │   ├── pages/UsersPage.tsx              # Staff accounts
│   │   ├── pages/AuditPage.tsx              # Admin curator activity reports
│   │   ├── components/ToursPopup.tsx        # Public published-tours modal
│   │   ├── i18n/                            # react-i18next bootstrap
│   │   └── locales/{en,ru}/                 # UI chrome strings
│   │   ├── 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
│   ├── seed-wikipedia.js
│   ├── seed-catalog-data.js
│   ├── sync-image-paths.js
│   ├── fetch-artist-images.js
│   ├── 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 & influence workbooks (PainterPalette, book extracts, …)
├── Output/              # Generated exports (CSV dumps, scratch extracts) — gitignored
├── data/images/         # Local portraits and paintings (+ thumbs/)
├── db/                  # schema.sql, setup-admin.sql, migrate-*.sql
├── server/migrate.js    # npm run dev:migrate — schema + incremental migrations
├── deploy/              # Legacy nginx + systemd examples (optional)
├── infra/docker/        # Production Dockerfile, TrueNAS compose, deploy scripts
├── Documentation/       # This folder
│   └── environments.md  # Dev/prod URLs, DB split, sync, deploy
└── .env                 # DB and port config (not committed)

Runtime modes

Default: develop and test on dev (gallery_dev, devgallery URL). Production is updated on a scheduled release (~weekly), not on every edit. See environments.md — Development-first workflow.

Public development (dev:web) — primary

npm run dev:web   # Vite :5173 + API :3451 — https://devgallery.mysuperlab.netcraze.pro

Uses database gallery_dev on the same PostgreSQL host.

Production (TrueNAS Docker) — scheduled releases

Production runs in gallery-web on TrueNAS port 5173, database gallery_prod, public URL https://gallery.mysuperlab.netcraze.pro. Images: /mnt/BasePool/Applications/Gallery/data/images (SMB share Gallery). Deploy via npm run prod:docker:publish and the promote checklist in environments.md.

Production-style single process (local)

npm run prod:start   # build client + serve on PORT from .env

Serves /api/*, /images/*, and the built SPA from client/dist.

Local HMR (two processes)

npm run dev:server   # API on PORT from .env
npm run dev:client   # Vite on :5173, proxies /api and /images

Use for fast frontend iteration without Keenetic. Legacy nginx config in deploy/nginx-gallery.conf proxied the public domain to Vite :5173.

User navigation flow

flowchart TD
  A[Home — timeline + movement flow] -->|scroll / drag / zoom| A
  A -->|catalog search| S[Search results dropdown]
  S -->|artist| C
  S -->|movement| G
  S -->|painting| D
  A -->|click movement name| G[Movement gallery — 3D wings]
  G -->|click painting| D
  D -->|Back to Gallery| 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 to Gallery| 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| C
  C -->|Back to Timeline| A
  D -->|Back to Timeline when opened from search| A

Back navigation

Control Behaviour
← Back to Timeline (3D gallery header, movement Exit to Timeline) Always returns to the home timeline: unmounts the hall, clears the gallery session, resets timeline zoom to the full catalog year range
← Back to Gallery (painting detail from a hall) Returns to the same hall session — camera position and wing are preserved
← Back to Timeline (painting detail opened from catalog search) Returns to the home timeline (same as gallery Back to Timeline)
← Back (artist bio) Returns to wherever you opened bio from (usually the artist hall)

Implementation: goToTimelineHome() in HomePage.tsx — do not use the browser Back button; it is not wired to app navigation.

Timeline and movement flow

The home page shows two linked views over the same year window (viewStart / viewEnd in HomePage.tsx). A header link switches between layouts:

Layout Era rail Movement flow Time direction
Classic (default) Timeline.tsx (top bar) MovementBands.tsx Left → right
Vertical VerticalTimeline.tsx (left rail) VerticalMovementBands.tsx (streams + lineage branches; no portraits) Bottom → top
View Component Purpose
Era bar / rail Timeline.tsx / VerticalTimeline.tsx Historical eras, major event markers, click-to-zoom
Movement flow MovementBands.tsx / VerticalMovementBands.tsx Streams per movement, artist portraits (classic also draws lineage branches)

Both layouts share zoom/pan behaviour via client/src/utils/timelineView.ts (zoomTimelineView, panTimelineView, chooseTimelineTickInterval, createViewChangeScheduler). The home page uses a fixed viewport (100vh). Classic stacks timeline above movements; vertical places the year rail beside the flow (home-timeline-stack-vertical).

Catalog search (timeline header)

CatalogSearchBar.tsx calls GET /api/search?q=… (public, no login). The dropdown is stacked above the timeline (z-index on .site-header) so results are not hidden by movement bands.

UX Detail
Minimum query 2 characters after trim
Debounce 300 ms
Result groups Artists, Movements, Paintings (with thumb or movement colour swatch)
Open artist Preload images → artist 3D hall
Open movement Movement gallery (wing 1)
Open painting Painting detail with returnTo: timelineBack to Timeline
Keyboard / highlight, Enter open, Escape close

Run npm run dev:migrate:search once on existing databases before first use, or rely on npm run dev:migrate (includes migrate-search.sql). See API.md — GET /api/search.

Timeline data loading

On first visit, HomePage.tsx fetches the full catalog once:

  1. GET /api/bounds — initialise the year range.
  2. GET /api/timeline?start=…&end=… — all eras and movements for that range.
  3. GET /api/artists?timeline=1 — lightweight artist rows (portraits, lifespan, movement colour; no full biography text).

Pan, zoom, and era/event click-to-zoom only update local viewStart / viewEnd state. MovementBands.tsx and Timeline.tsx filter what is visible for the current window — they do not trigger new API calls.

Loading indicators (GalleryLoadingMarker.tsx) keep the user informed while data is still arriving:

Marker When
Overlay “Loading art history…” Until the first catalog fetch (bounds + timeline + artists) completes
Bottom banner “Loading portraits…” While artist portrait thumbnails are still downloading on the movement flow (timeline stays interactive)
Overlay “Opening artist/movement gallery…” / “Loading artists…” Between clicking a portrait/movement and the 3D hall (or artist-filter modal) data being ready
Overlay “Loading gallery…” While the 3D canvas initializes and door/hall shaders warm up after the hall opens (HDR Environment and painting textures continue in the background)
Overlay “Loading paintings…” Brief counter while wall painting textures start downloading; large halls (e.g. Byzantine) keep loading after the overlay dismisses — a slow image no longer permanently blanks the frame
Overlay “Restoring gallery…” Briefly after WebGL context loss while the canvas remounts

View updates are batched to one commit per animation frame via createViewChangeScheduler() in timelineView.ts (HomePage.tsxhandleViewChange), so rapid scroll-wheel events do not flood React with separate renders.

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.

Label density is dynamic: chooseTimelineTickInterval() in timelineView.ts picks a “nice” step (1, 2, 5, 10, … 5000 years) from the visible span and measured bar width so labels stay ~76 px apart. Zoomed-out overviews show fewer dates; zooming in reveals finer steps automatically.

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) in a solid vivid colour 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 lanes Movements share a horizontal lane only when one ends at least 10 years before the next starts (in the current zoom); closer or overlapping spans stack into extra rows (assignTemporalLanes). Lane choice minimizes vertical branch length: children prefer parent lanes (same row when years allow), corridor clearing (up to 20 passes) evicts unrelated streams preferring rows above the parent/child strip, then re-attracts the child toward the parent
Branch connectors Smooth curves from fan-out points along a parent stream to the left edge (start) of each child stream; origin X is always strictly left of the target (time-forward only — never right→left); stroke thickness matches the target (child) band height; color gradients from parent → child at constant opacity; a stream-shaped mask hides branch ink under movements so translucent overlaps do not brighten the bands; pan/zoom layout changes animate (streams + branches chase new geometry) so shifts stay trackable
Band thickness Stream height is proportional to influence_link_count (edges on paintings by artists in that movement): thicker bands for denser influence graphs; lane rows share vertical space weighted by the thickest band in each lane
Filtering A movement is drawn when its span overlaps the visible year range and it has at least one catalogued artist — artists whose lifespan falls outside the window still keep their movement visible (their portraits simply do not render). Filtered client-side after initial load
Viewport layout Row height and stream width scale from measured canvas size so every visible movement row fits in the remaining screen space
Name labels On-band movement name only. Label placement priority: (1) center of movement if label fits and no portrait collision; (2) right end inside band borders; (3) overflow right or left (up to 100% outside the band border), choosing the side with less transition-line overlap; (4) adjacent free gap before/after the band; (5) hidden until hover. Collision checks exclude the movement's own portraits (labels may overlap their own band's artists). Movements with overflow labels are packed onto exclusive horizontal lanes before vertical stacking so they never share a row with other movements. Minimum movement height = 125% of label height

Artists on movement streams

Each artist appears as a portrait circle on their movements stream row:

Feature Behaviour
Placement Portrait at the midpoint of birthdeath 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 anywhere on flow canvas Zoom (same year range as timeline; works over portraits and labels too)
Drag on flow canvas Pan
Click portrait Open artist biography
Click movement name (label on stream) Open the artist filter modal for that movement, then the movement gallery

Zoom/pan performance: The movement canvas uses a capture-phase wheel listener so scroll zoom works even when the cursor is over a portrait. While scrolling or dragging, a short interaction mode (~120 ms after the last input) draws simplified solid SVG strokes and temporarily hides portrait/label DOM so zoom stays responsive; full stream styling and portraits return when you stop.

Only mousedown on portraits and movement labels stops propagation (so drag-to-pan does not start when clicking them). Hovering a portrait highlights the artists 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.

The 3D scene supports three modes in VirtualGallery.tsx: artist halls (personal catalog), movement galleries (full movement collection, chronological), and guided tours (curator-ordered stops — tours.md).

Shared wall hang (all modes): visit order is a U-shapeleft wall, then the far/end wall ahead when entering, then the right wall. The first work hangs near the entrance on the left (immediately left of the opening view); the last hangs near the entrance on the right. With fewer than three works, only left/right are used. Artist halls use chronological order; movement wings use chronological order within each wing; tours use stop order.

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 artists paintings
Catalog depth Most artists target ≥ 6 notable works via npm run dev:expand-catalog and famous-paintings-data.js; some masters have larger museum dumps
Paintings on walls Works hang on left, far/end, and right walls in one row per wall; room depth and width grow with the catalog
Wall order Chronological U-path: first third left (entrance → end), middle third end wall, last third right (end → entrance)
Floor Herringbone parquet procedural texture (parquetFloorTexture.ts)
Wall tint Gallery walls blend the artists 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 marks works with any influence-graph edge (has_influence_links from the API). All lamps in a hall share one rail height: 40 cm above the tallest allocated frame in that hall (so they line up regardless of canvas size). Fixture is mounted upside down and is emissive-only (no per-frame lights — see light budget below)
Curator-note plates A small brass plate hangs beneath frames that have non-empty curator_notes
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 freestanding columns or pedestals in the walkway
Museum exit Front-wall double doors with transom, brass hardware, sconces, marble threshold, and warm vestibule glow
Shadows Disabled — no Canvas shadow maps / castShadow (performance; flat lighting only)
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 dev:update-influences or extend art-influences-data.js.

Movement galleries

Enter from the home page by clicking a movement name on the movement flow. First a centered artist filter modal (ArtistFilterModal.tsx) loads GET /api/movements/:id/artists-summary (portrait, lifespan, painting count). All artists are selected by default; deselect any to exclude their works, then Enter gallery. The client loads GET /api/movements/:id/gallery and filters paintings to the selected artist IDs before opening VirtualGallery.

Rule Implementation
One gallery per movement Paintings by selected 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, end, and right — single-wing halls keep the far wall solid (full span); multi-wing halls hang end-wall works on panels beside the back exit
Wall order Same U-shaped hang as artist halls (left → end → right)
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, Byzantine basilica, NYC loft, white cube, etc.). Style keys use a legacy id map (DB_ID_TO_STYLE_KEY: gallery ids 126 → authored keys 2752, with Northern/High Renaissance swapped) so Byzantine gets the basilica, not Gothic stone
Wall materials Hi-res procedural textures with normal maps (galleryProceduralTextures.ts): real-world stone, marble, wood panelling, brick, velvet, stucco, marble revetment (framed book-matched panels), Cosmatesque paving (marble-opus-sectile), coffered timber ceilings, blind-arcaded ashlar (gothic-ashlar) and ribbed vaulting (gothic-vault) — 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
Architecture in texture Period halls that need arcades, vaults, or panelling draw that relief into the texture rather than adding geometry: gothic-ashlar paints a full storey (plinth → blind arcade → string course → triforium → cornice) into one tile whose metersPerRepeat equals WALL_HEIGHT, so it maps once vertically onto the wall; gothic-vault paints a quadripartite bay with tiercerons and a boss. Light/dark banding plus a high normalStrengthFor() makes it read as carved stone, keeping the mesh and light budget flat
Door-flanking panels Wall segments beside exit doors and passages take the hall's own wall texture (GalleryWall accepts kind/tint) instead of rendering as flat single-colour blocks next to textured walls
Textures Wall/floor/ceiling maps applied via useTexturedMaterial; movement tints drive painted-wall hue
Windows Side walls only — placed in gaps between frames when possible; if a wall is packed, high clerestory windows are still added so the hall keeps daylight (computeSideWallWindows). Styles are drawn in GalleryWindows.tsx; gothic-lancet builds a two-centred arch head from chords with a glazed spandrel, mullions, and transoms
Lighting Shared hall lights only (ambient / hemisphere / directional + capped ceiling track spots + one fill per window). See light budget below
Period details MovementHallDetails.tsx — classical / neoclassical use shallow engaged corner pilasters (never freestanding mid-hall or proud corner shafts that cover frames); byzantine adds engaged porphyry colonnettes with basket capitals, a marble revetment dado, and hanging brass polycandela; gothic adds bay-spaced compound piers with vault springers, transverse ribs arching across the nave, and a moulded string course — all flush to the side walls
Back wall Single wing: solid display wall. Multi-wing: Exit double doorsWing navigator or Exit to Timeline
Front / entrance wall Single wing: Exit double doors (leave the way you entered). Multi-wing: “Next wing →” archway when a later wing exists
Influence lamps Same golden upside-down emissive fixtures as artist halls when has_influence_links is true (shared rail: 40 cm above the tallest frame in the wing)
Missing images Draped canvas cover in frame
Detail return Back to Gallery from painting detail returns to the same wing with camera preserved; Back to Timeline exits the hall entirely

Controls (movement gallery):

Input Action
Walk / turn / drag Same as artist hall
Click painting Open detail view (returns to the same wing)
Entrance / back door / E / Wings / Exit → Open wing navigator (or exit on single-wing halls)
Front archway / E (when near, multi-wing) Advance to the next chronological wing

Movement galleries do not use the predecessor/successor influence picker — that remains artist-hall only.

Guided tour halls

Enter from the home page Tours popup (GET /api/tours/:id). Layout reuses the movement winged hall (mode: 'tour' in VirtualGallery.tsx).

Rule Implementation
Visit order Curator sort_order on tour_stops (not chronological)
Wings Same ~55-per-wing split as movements; order preserved across wings
Wall hang Same U-shaped hang as other halls (left → end → right)
Detail Tour stop text panel; walks tour stops
Exit Wing navigator / Exit to Timeline (no influence picker)

Full guide: tours.md.

Shared 3D behaviour

Light budget (critical): Three.js forward shading cannot compile MeshStandardMaterial with dozens of dynamic lights. Large wings used to add one spotLight per painting (plus influence lamps), which pushed the scene to ~100200 lights — walls, floor, and ceiling then rendered as a black void while painting canvases (MeshBasicMaterial) still showed. Halls therefore use shared lighting only: ambient / hemisphere / directional fill, a small set of ceiling track spots (GalleryTrackLights, capped), and one window spot each. Painting canvases stay unlit (MeshBasicMaterial); frames and influence fixtures use emissive accents instead of per-frame lights. Intensities are scaled for three.js physical lights (post-r155).

Shared lights carry a brightness floor so dark period styles never render as a black void. A style that is meant to be dim (a basilica lit by window shafts, not flooded) sets lightScale in movement-interior-styles.ts — it multiplies the scene ambient/hemisphere/directional/fill lights, the hall point lights, the ceiling track spots, and the HDR environmentIntensity. It defaults to 1, so only styles that opt in are affected. Window fill lights are deliberately not scaled, so the daylight shafts still read against the darker room.

Period architecture: Freestanding columns must not sit in the walkway or in the corner pocket in front of frames (hang margin is only ~0.7 m from each wall end). Classical / neoclassical details use engaged corner pilasters flush to the walls (MovementHallDetails.tsx).

Surface textures: Procedural colour maps use sRGB; normal maps use NoColorSpace (galleryProceduralTextures.ts). Wrong colour space on normals breaks wall shading.

3D images prefer local thumbnail files (galleryImageUrlCandidates in client/src/api/client.ts: thumb → GET /api/paintings/:id/image?size=thumb; full originals are not used for hall frames — they can be tens of MB and made large halls take ~1 minute). Remote Wikipedia fetches are too slow for realtime WebGL textures. Entering an artist or movement hall fires POST /api/artists/:id/preload-images or POST /api/movements/:id/preload-images in the background (does not block hall open). Texture downloads are queued (max 8 parallel). While a texture is loading, the frame shows the canvas cover instead of a white placeholder. A per-image deadline only releases the boot overlay counter — it does not permanently blank the frame if the download finishes later.

Boot overlay (VirtualGallery.tsx): the center shows “Loading gallery…” until the WebGL canvas is ready and a short gl.compileAsync warm-up finishes (so entrance doors / passages do not hitch on the first turn). HDR Environment loads in a Suspense boundary without blocking the overlay. Painting textures keep loading in the background. The 3D hall stays mounted while painting detail or bio overlays are open; returning remounts the canvas when the hall becomes active again.

WebGL context-loss recovery: on some GPUs/drivers (notably certain Chrome setups) the browser can drop the WebGL context right after entering a hall, which would otherwise leave a permanent dark window. VirtualGallery.tsx listens for webglcontextlost / webglcontextrestored, calls preventDefault() so the browser can restore the context, and remounts the <Canvas> with a fresh context (a “Restoring gallery…” overlay shows briefly). The network-loaded HDR Environment map is wrapped in an error boundary so, if it fails to load, the hall still renders without reflections instead of unmounting the whole scene.

Painting detail view

Opened from the 3D hall (artist, movement, or tour wing — click a frame) or from influence thumbnails on another works 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)
Tour notes When opened from a guided tour: stop text panel under the image (English body from tour_stops)
Curator notes Public editorial text on the painting (paintings.curator_notes); visitors see it when non-empty; curators edit inline on the detail page
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 in the current catalog — artist chronology, movement chronology, or tour stop order
/ Same as prev / next (disabled while fullscreen is open)
Click centre image Open fullscreen lightbox
Click influence thumbnail Open that works detail (different artist allowed)
Click influence artist portrait Open that artists 3D gallery hall
← Back to Gallery Return to the hall or movement wing you entered from — 3D camera position is preserved
← Back to Timeline Return to the home timeline (from search result, or from the 3D gallery header / movement Exit to Timeline) — hall unmounts, timeline zoom resets
About {artist} Open artist biography

Navigation rules:

  • Catalog browsing ( / arrow keys) walks the active catalog (artist / movement chronology, or tour stops). 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)

Curator login required. Debug tools are hidden until you sign in from the home header (Curator login). After login, enable Debug mode from the same header area.

When debug mode is on, painting detail and artist biography show a bottom-left panel with image search preview and action buttons. See 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.
  • Timeline catalog loads once from the API; pan/zoom is client-side only, with per-frame batching via createViewChangeScheduler().
  • Movement flow interaction uses simplified SVG and hides portrait DOM during active scroll/drag so zoom stays responsive over dense portrait fields.
  • Movement filtering on zoom shows movements whose span overlaps the visible year range and that have at least one catalogued artist; a movement (e.g. Byzantine or Gothic viewed at 3111231 CE) still appears even when all its artists lived outside the current window.
  • 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.
  • Guided tours add curator-ordered winged halls with stop text on painting detail — tours.md.
  • Wall hang is shared: first work on the left at the entrance, last on the right.
  • Influence-based hall links connect artists through documented painting relationships, grouped by movement at the exit.
  • 3D gallery images use local thumbnails for hall frames (not multi-MB originals). POST /api/artists/:id/preload-images / POST /api/movements/:id/preload-images run in the background when entering a hall (public routes — link disk files and regenerate missing thumbs).
  • 3D gallery session stays mounted while painting detail or bio overlays are open; returning to the hall remounts the WebGL canvas when it becomes active again.
  • 3D gallery resilience: a lost WebGL context is recovered by remounting the canvas with a fresh context (rather than showing a dark window), and the HDR environment map is isolated behind an error boundary so its failure never blanks the scene.
  • Loading feedback: GalleryLoadingMarker surfaces catalog load, portrait download, gallery entry, painting-texture / GPU warm-up, Environment settle, and context-restore states so the user always knows work is still in progress.
  • 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.

User roles and access

Role Who Can do
user Anonymous visitor (default) Browse timeline, movement flow, 3D artist/movement halls, painting detail, artist bios, images
curator Named staff account Public browse + tools allowed by their permission flags
admin Named staff account All curator tools + Users management + Activity audit reports

Permission flags: images, checkup, curator_notes, translations, influences, tours, users. Admins always have every flag.

Staff sign in via Curator login in the site header (individual username/password). Sessions use an HTTP-only cookie (gallery.sid). The UI shows only tools the account may use; the server enforces the same rules (401 without a session, 403 without permission).

Admins create and manage accounts on the Users page (UsersPage.tsx / /api/users). Bootstrap the first admin with CURATOR_* env vars + npm run dev:migrate (or npm run dev:reset-curator).

Mutating actions are appended to curator_audit_log with user_id, action, target id, optional JSON details, and client IP. Admins browse reports on the Activity page (AuditPage.tsx / /api/audit); the API reads whatever DB the server is connected to (DB_NAME: gallery_dev on dev, gallery_prod on prod). See DB_structure.md.

Developer tools (image audit)

Staff workflow for reviewing and fixing local image files (requires images permission) — not part of the public visitor experience.

Feature Where Purpose
Catalog search Timeline header (all visitors) Find artists, paintings, movements; GET /api/search; navigate to gallery or detail
Curator login Home header (guests) Username + password modal; unlocks permitted tools
Debug mode Home header toggle (images) Persists in localStorage; enables debug panel on painting detail and artist bio
Show more Home header checkbox (images, when debug on) Auto-opens the More modal on each painting / bio page load
Checkup page Home header → Checkup (checkup) Full-catalog table: gallery vs detail thumbnails, search, fix, review flags
Translations Home header → Translations (translations) Review/publish Russian entity_translations
Influences Home header → Influences (influences) List/CRUD influence edges, CSV/JSON/XLSX import wizard, neighborhood graph — influence-import.md
Tour editor Home header → Tour editor (tours) Create/publish guided tours and stop text — tours.md
Users Home header → Users (users / admin) Create staff accounts, roles, permissions, reset passwords, disable accounts
Activity Home header → Activity (admin only) Curator audit reports: filters, summary, dated action log from curator_audit_log (env DB)
Tours Home header → Tours (everyone) Open published tours in a 3D hall — tours.md
Logout Home header (staff) Ends session; hides staff tools
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 full image from search result; regenerates painting thumb (~400px JPEG) from that file Replaces portrait; regenerates timeline thumb (256px)
More Modal with up to 20 results (resolution shown when known); thumb regenerated from chosen full image Same
Clear Deletes files, clears DB paths, empty frame Clears portrait slot
Upload Local file picker (DebugUploadButton) → full image + auto-generated painting thumb; full-page Loading… overlay; hides current image and pauses search/fix until upload finishes Local file → portrait + auto-generated portrait thumb; same upload overlay behaviour
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. Painting and portrait thumbs under data/images/*/thumbs/ are rebuilt on the server whenever a curator replaces the full image. Remove entry refetches artist (and movement gallery when relevant) from the API and remounts the 3D hall so the deleted frame disappears immediately.

Pressing Upload clears the debug search preview and closes More before the file picker opens. While uploading, Fix it, More, and Checked are disabled and the main painting/portrait is hidden behind a centered loading overlay.

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 dev:migrate:checkup-flags, npm run dev:migrate:artist-checkup-flags, and npm run dev:migrate:painting-annotations once on existing databases. Load notes with npm run dev:update-painting-annotations (add --wikipedia for overview lines from Wikipedia intro text). After server code changes, restart npm run dev:start (or npm run dev:server) so new routes 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.

Server-side auth lives in server/middleware/session.js, server/middleware/auth.js, server/routes/auth.js, and server/audit-log.js. Client auth context: client/src/context/AuthContext.tsx.

See API.md and data-and-images.md.

Document Contents
setup.md Install, database, npm scripts
deploy-dev-to-prod.md Release runbook + one-command devtoprod:release
harmonize-dev-prod.md Incremental dev ↔ prod merge (catalog DB + images)
DB_structure.md Tables and relationships
API.md REST endpoints
influence-import.md Curator Influences tool — import wizard, CRUD, graph
tours.md Guided tours — editor, public popup, 3D tour hall
i18n-russian.md Russian UI + entity_translations
data-and-images.md Image pipeline and seeding