diff --git a/Documentation/basics.md b/Documentation/basics.md index e301693..f014b5b 100644 --- a/Documentation/basics.md +++ b/Documentation/basics.md @@ -37,7 +37,8 @@ Gallery/ │ │ ├── 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 +│ │ ├── components/MovementHallDetails.tsx # Period architecture dispatcher (Gothic + Byzantine reference) +│ │ ├── components/hall-details/ # One period interior per movement + shared primitives │ │ ├── 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 @@ -353,13 +354,14 @@ Enter from the home page by clicking a **movement name** on the movement flow. F | 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 **1–26** → authored keys **27–52**, with Northern/High Renaissance swapped) so Byzantine gets the basilica, not Gothic stone | +| Period architecture | Each style names a `details` kind (26 of them) that selects one component under `components/hall-details/` — the hall is built from the architecture of the **same period as the movement** (Roman entablature, Brunelleschi arcade, Flemish beam ceiling, Horta whiplash brackets, Rodchenko lattice, 1960s troffer grid). Gothic and Byzantine remain the reference implementations in `MovementHallDetails.tsx`; the geometry budget every module works to is documented in `hall-details/geometry.ts` | | 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 | +| Period details | `MovementHallDetails.tsx` dispatches to one module per movement. `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. Every other movement follows the same rules in `components/hall-details/`: orders and piers stay **flush to the side walls** (≤ 0.17 m proud), freestanding masses only in the corner pockets, and no more than two added lights per hall — see `hall-details/geometry.ts` | | Back wall | Single wing: solid display wall. Multi-wing: **Exit double doors** → **Wing 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) | @@ -395,7 +397,11 @@ Full guide: [tours.md](tours.md). **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 ~100–200 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. +Shared lights carry a **brightness floor** so dark period styles never render as a black void. That floor is high enough to blow out a pale wall, so **`lightScale`** in `movement-interior-styles.ts` is the knob that holds a hall back — it multiplies the scene ambient/hemisphere/directional/fill lights, the hall point lights, the ceiling track spots, and the HDR `environmentIntensity`. Window fill lights are deliberately **not** scaled, so daylight shafts still read against the darker room. + +It defaults to **1**, but most halls now set it: Gothic uses **0.26**, Byzantine **0.30**, and the 19 formerly pale interiors (Roman through Pop Art) also use **0.30**. Only the five halls that are already dark by albedo — Symbolist, Expressionist, Surrealist, Baroque, Romantic — stay at 1, since scaling their lights would crush walls that already sit at 0.15–0.27 luminance. + +**Wall brightness is albedo × `lightScale`.** Gallery walls are authored well below white for that reason: the reworked interiors sit around **0.50–0.56** wall luminance (0.66 for the two white-cube halls), landing at an effective 0.14–0.20 — the range Gothic and Byzantine established. Reach for `lightScale` before darkening a tint further; the two multiply, and dimming the hall dims the paintings with it. **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`). diff --git a/Documentation/data-and-images.md b/Documentation/data-and-images.md index 65b18f6..62ee326 100644 --- a/Documentation/data-and-images.md +++ b/Documentation/data-and-images.md @@ -278,9 +278,38 @@ To add or fix a branch, edit `MOVEMENT_LINEAGE` in that file and rebuild the cli ## Movement gallery interiors (frontend) -Separate from movement lineage layout, `client/src/data/movement-interior-styles.ts` defines a **unique 3D interior** for each seeded art movement (26 styles): wall/floor/ceiling textures, trim colours, window style, and architectural details (pilasters, coffered ceilings, etc.). Textures are generated procedurally in `client/src/utils/galleryProceduralTextures.ts` (colour maps in sRGB, normal maps in linear/`NoColorSpace`). Period mesh details live in `client/src/components/MovementHallDetails.tsx` — classical / neoclassical use **shallow engaged corner pilasters** so shafts never cover frames; Byzantine adds porphyry colonnettes / polycandela; Gothic (`details: 'gothic'`) adds bay-spaced compound piers with vault springers and transverse ribs flush to the side walls. +Separate from movement lineage layout, `client/src/data/movement-interior-styles.ts` defines a **unique 3D interior** for each seeded art movement (26 styles): wall/floor/ceiling textures, trim colours, window style, and the `details` kind that selects its architecture. Textures are generated procedurally in `client/src/utils/galleryProceduralTextures.ts` (colour maps in sRGB, normal maps in linear/`NoColorSpace`). -Gothic surfaces include **`gothic-ashlar`** (full-storey blind arcade mapped once vertically via `metersPerRepeat === WALL_HEIGHT`), **`gothic-vault`** (quadripartite rib bay), and **`gothic-stone-floor`**. Door-flanking wall panels use the hall wall texture via `GalleryWall` `kind`/`tint` so they match the textured walls. Deliberately dim naves set **`lightScale`** (Byzantine / Gothic) so shared lights stay soft while window shafts stay bright. +### Period architecture per movement + +Every movement is built from the architecture of **its own period** — no two movements share an interior. `client/src/components/MovementHallDetails.tsx` dispatches on `style.details` (`MovementDetailKind`, 26 values) to one component: + +| Module | Movements | +|--------|-----------| +| `MovementHallDetails.tsx` (reference implementations) | `gothic` — bay-spaced compound piers, vault springers, transverse ribs · `byzantine` — porphyry colonnettes, basket capitals, revetment dado, hanging polycandela | +| `hall-details/classical.tsx` | `roman` (Tuscan half-columns, dentil entablature, Pompeian socle) · `quattrocento` (pietra serena pilasters, blind arcade, tondi) · `cinquecento` (paired fluted Corinthian pilasters, Pantheon coffers) | +| `hall-details/courtly.tsx` | `flemish` (linenfold wainscot, beam-and-corbel ceiling) · `mannerist` (herm pilasters, banded rustication, broken cornice) · `baroque` (solomonic corner columns, modillions, velvet field panels) · `rococo` (boiserie, rocaille cartouches, painted cove) · `neoclassical` (fluted Ionic order, meander frieze) | +| `hall-details/nineteenth.tsx` | `gothic-revival` · `bourgeois` · `north-light` · `paris-atelier` · `symbolist` · `art-nouveau` | +| `hall-details/modernism.tsx` | `fauvist` · `expressionist` · `cubist-atelier` · `futurist` · `suprematist` · `constructivist` · `dada` · `surrealist` · `loft` · `white-cube` | + +`hall-details/primitives.tsx` holds the wall-flush building blocks (`WallBand`, `CorniceRing`, `FlutedShaft`, capitals, `CofferedCeiling`, `HangingFixture`, `WallSconce`); `hall-details/geometry.ts` holds the shared hooks and the **geometry budget** every module has to obey, because frames hang 0.23 m proud of the wall face with a 0.7 m clear margin at each wall end: + +- relief below the frame line stays within **0.11 m** proud; friezes and cornices above 2.6 m within **0.17 m** +- freestanding masses only inside the **0.34 m corner pocket**; nothing under 2.6 m within 1.45 m of a wall centre (doorways) +- the end wall carries the hall title between **3.4 m and 3.9 m** — keep it clear +- side-wall windows are placed at runtime between 2.4 m and 3.9 m, so panels, arches and roundels up there are filtered through **`useWallClearance`** (the hall passes its computed windows into `MovementHallDetails`) +- at most **two added lights per hall**, so hall materials stay inside the WebGL light limits the shared track lighting already budgets for + +Gothic surfaces include **`gothic-ashlar`** (full-storey blind arcade mapped once vertically via `metersPerRepeat === WALL_HEIGHT`), **`gothic-vault`** (quadripartite rib bay), and **`gothic-stone-floor`**. Door-flanking wall panels use the hall wall texture via `GalleryWall` `kind`/`tint` so they match the textured walls. + +### Wall brightness + +Halls are lit by a shared rig with a high brightness floor, so a pale wall tint clips to flat white and loses its texture. Two settings control this together, and they **multiply**: + +- **tints** — gallery walls are authored around **0.50–0.56** relative luminance (0.66 for the Suprematist and Pop white cubes, 0.15–0.27 for the deliberately dark halls). Nothing sits near white. +- **`lightScale`** — Gothic **0.26**, Byzantine **0.30**, and every formerly pale interior **0.30**; the already-dark halls keep **1**. Window fills are not scaled, so daylight shafts stay bright. + +When a hall reads too bright, prefer `lightScale`: it keeps the material colour believable (Byzantine's warm stone is a genuine `#a89880`, simply lit dimly) and avoids greying tints toward neutral. Remember that dimming a hall dims its paintings too — the lighting is shared, with no per-painting spots. Wing layout (up to 55 works per wing, U-shaped hang, window gap placement with packed-wall **clerestory** fallback) lives in `client/src/utils/movementHallLayout.ts`. Visit order fills the **left wall** (first work at the entrance), then the **far/end wall**, then the **right** (last work at the entrance). The same hang applies to artist halls and guided tours. Hall lighting is **shared** (no per-painting spotlights) so `MeshStandardMaterial` walls stay within WebGL light limits — see [basics.md](basics.md) § Shared 3D behaviour. To change a movement’s look, edit its entry in `movement-interior-styles.ts` and rebuild the client. diff --git a/client/src/components/MovementHallDetails.tsx b/client/src/components/MovementHallDetails.tsx index 9dfef1a..5500a07 100644 --- a/client/src/components/MovementHallDetails.tsx +++ b/client/src/components/MovementHallDetails.tsx @@ -1,6 +1,38 @@ import { useMemo } from 'react'; -import * as THREE from 'three'; -import type { MovementInteriorStyle } from '../data/movement-interior-styles'; +import type { MovementInteriorStyle, GalleryWindowSpec } from '../data/movement-interior-styles'; +import type { RoomDims } from './hall-details/geometry'; +import { + CinquecentoDetails, + QuattrocentoDetails, + RomanAtriumDetails, +} from './hall-details/classical'; +import { + BaroqueStateDetails, + FlemishDetails, + ManneristDetails, + NeoclassicalDetails, + RococoDetails, +} from './hall-details/courtly'; +import { + ArtNouveauDetails, + BourgeoisSalonDetails, + GothicRevivalDetails, + NorthLightSalonDetails, + ParisAtelierDetails, + SymbolistDetails, +} from './hall-details/nineteenth'; +import { + ConstructivistDetails, + CubistAtelierDetails, + DadaDetails, + ExpressionistDetails, + FauvistDetails, + FuturistDetails, + LoftDetails, + SuprematistDetails, + SurrealistDetails, + WhiteCubeDetails, +} from './hall-details/modernism'; interface Props { style: MovementInteriorStyle; @@ -8,99 +40,11 @@ interface Props { depth: number; halfW: number; halfD: number; + /** Runtime window placement, so upper-wall relief can step around openings. */ + windows?: GalleryWindowSpec[]; } -function PalazzoDetails({ width, depth, halfW, halfD, trim }: Props & { trim: string }) { - const pilasterPositions = useMemo( - () => - [ - [-halfW + 0.35, -halfD + 0.35], - [halfW - 0.35, -halfD + 0.35], - [-halfW + 0.35, halfD - 0.35], - [halfW - 0.35, halfD - 0.35], - ] as [number, number][], - [halfW, halfD] - ); - - return ( - - {pilasterPositions.map(([x, z], i) => ( - - - - - - - - - - - ))} - - {/* Wainscoting rail */} - {[ - [0, -halfD + 0.09, width, 0.12] as const, - [0, halfD - 0.09, width, 0.12] as const, - [-halfW + 0.09, 0, 0.12, depth] as const, - [halfW - 0.09, 0, 0.12, depth] as const, - ].map(([x, z, w, d], i) => ( - - - - - ))} - - {/* Coffered ceiling */} - {Array.from({ length: Math.min(6, Math.floor(width / 2.5)) }, (_, col) => - Array.from({ length: Math.min(5, Math.floor(depth / 2.8)) }, (_, row) => { - const cx = -halfW + 1.4 + col * 2.4; - const cz = -halfD + 1.5 + row * 2.6; - return ( - - - - - ); - }) - )} - - ); -} - -function BaroqueDetails({ width, depth, halfW, halfD, trim }: Props & { trim: string }) { - return ( - - {/* Gilded cornice ring */} - {[ - [0, -halfD + 0.06, width, 0.1] as const, - [0, halfD - 0.06, width, 0.1] as const, - [-halfW + 0.06, 0, 0.1, depth] as const, - [halfW - 0.06, 0, 0.1, depth] as const, - ].map(([x, z, w, d], i) => ( - - - - - ))} - - {/* Wall panels */} - {[-halfW + 0.12, halfW - 0.12].map((x, i) => ( - - - - - ))} - - - - - - - - ); -} - -function GothicDetails({ width, depth, halfW, halfD, trim }: Props & { trim: string }) { +function GothicDetails({ width, depth, halfW, halfD, trim }: Omit & { trim: string }) { // Compound piers along the side walls, spaced by bay, each rising into the // vault springer. Shafts stay flush against the wall (0.16 m proud) so they // never intrude on the ~0.7 m frame hang margin. @@ -292,102 +236,75 @@ function ByzantineDetails({ halfW, halfD, trim }: Pick ); } -function NeoclassicalDetails({ halfW, halfD, trim }: Pick & { trim: string }) { - // Shallow engaged corner pilasters only. Freestanding columns (~0.5m into the - // room) overlap the outermost frames — hang margin is only ~0.7m from each end. - const pilasters = useMemo( - () => - [ - [-halfW + 0.06, -halfD + 0.06], - [halfW - 0.06, -halfD + 0.06], - [-halfW + 0.06, halfD - 0.06], - [halfW - 0.06, halfD - 0.06], - ] as [number, number][], - [halfW, halfD] - ); - - return ( - - {pilasters.map(([x, z], i) => ( - - - - - - - - - - - ))} - {/* Cornice above wall title (title sits ~y 3.75) */} - - - - - - ); -} - -function ClassicalDetails({ halfW, halfD, trim }: Pick & { trim: string }) { - // Engaged corner shafts only — anything proud of the corner pocket covers frames. - const columns = useMemo( - () => - [ - [-halfW + 0.06, -halfD + 0.06], - [halfW - 0.06, -halfD + 0.06], - [-halfW + 0.06, halfD - 0.06], - [halfW - 0.06, halfD - 0.06], - ] as [number, number][], - [halfW, halfD] - ); - - return ( - - {columns.map(([x, z], i) => ( - - - - - - - - - - - ))} - - ); -} - -function SalonDetails({ trim }: { trim: string }) { - return ( - - - - - ); -} - -export default function MovementHallDetails({ style, width, depth, halfW, halfD }: Props) { +/** + * Period architecture for a movement hall. + * + * Every movement has its own interior, built from the architecture of the same + * period as the movement itself. The Gothic nave and Byzantine basilica below + * are the reference implementations; everything else lives in + * `hall-details/`, sharing the geometry budget documented in + * `hall-details/primitives.tsx`. + */ +export default function MovementHallDetails({ style, width, depth, halfW, halfD, windows }: Props) { const trim = style.tints.trim; + const room: RoomDims = useMemo( + () => ({ width, depth, halfW, halfD }), + [width, depth, halfW, halfD] + ); + const period = { room, trim, windows }; switch (style.details) { - case 'palazzo': - return ; - case 'baroque': - return ; case 'gothic': - return ; + return ; case 'byzantine': return ; + case 'roman': + return ; + case 'quattrocento': + return ; + case 'cinquecento': + return ; + case 'flemish': + return ; + case 'mannerist': + return ; + case 'baroque': + return ; + case 'rococo': + return ; case 'neoclassical': - return ; - case 'classical': - return ; - case 'salon': - return ; - default: - return null; + return ; + case 'gothic-revival': + return ; + case 'bourgeois': + return ; + case 'north-light': + return ; + case 'paris-atelier': + return ; + case 'symbolist': + return ; + case 'art-nouveau': + return ; + case 'fauvist': + return ; + case 'expressionist': + return ; + case 'cubist-atelier': + return ; + case 'futurist': + return ; + case 'suprematist': + return ; + case 'constructivist': + return ; + case 'dada': + return ; + case 'surrealist': + return ; + case 'loft': + return ; + case 'white-cube': + return ; } } diff --git a/client/src/components/VirtualGallery.tsx b/client/src/components/VirtualGallery.tsx index d7c4529..34e311f 100644 --- a/client/src/components/VirtualGallery.tsx +++ b/client/src/components/VirtualGallery.tsx @@ -15,7 +15,12 @@ import type { import { galleryImageUrlCandidates, imageUrl, api } from '../api/client'; import { comparePaintingsChronological, paintingHasCuratorNotes, paintingHasInfluenceLinks, paintingWallCaption } from '../utils/paintingUtils'; import { cloneSurfaceTexture, getSurfaceTexture, type SurfaceTextureKind } from '../utils/galleryProceduralTextures'; -import { resolveMovementInteriorStyle, type MovementInteriorStyle, type GalleryWindowSpec } from '../data/movement-interior-styles'; +import { + resolveMovementInteriorStyle, + type MovementDetailKind, + type MovementInteriorStyle, + type GalleryWindowSpec, +} from '../data/movement-interior-styles'; import { useTexturedMaterial } from '../hooks/useTexturedMaterial'; import MovementHallDetails from './MovementHallDetails'; import GalleryWindows, { GalleryTrackLights } from './GalleryWindows'; @@ -84,6 +89,15 @@ interface TourGalleryProps extends BaseGalleryProps { type Props = ArtistGalleryProps | MovementGalleryProps | TourGalleryProps; +/** Period interiors built from steel, concrete or a white cube read better under the city HDR. */ +const CITY_ENVIRONMENT_DETAILS = new Set([ + 'futurist', + 'suprematist', + 'constructivist', + 'loft', + 'white-cube', +]); + const WALL_HEIGHT = 4.2; const WALL_THICKNESS = 0.18; const MOUNT_OFFSET = 0.16; @@ -1739,7 +1753,14 @@ function ArtistHall({ {interiorStyle && ( - + )} diff --git a/client/src/components/hall-details/classical.tsx b/client/src/components/hall-details/classical.tsx new file mode 100644 index 0000000..c2343c8 --- /dev/null +++ b/client/src/components/hall-details/classical.tsx @@ -0,0 +1,271 @@ +import { + CofferedCeiling, + CorniceRing, + FlutedShaft, + HangingFixture, + RoundArch, + ShaftBase, + TuscanCapital, + WallBand, +} from './primitives'; +import { + shade, + useBayZ, + useCorners, + useRepeat, + useWallClearance, + type PeriodProps, +} from './geometry'; + +/** + * Roman atrium — Ancient / Classical. + * + * Engaged Tuscan half-columns on a bay rhythm carry a compressed but complete + * entablature (architrave, plain frieze, dentil course, projecting cornice). + * Below the frame line the walls carry the Pompeian red socle of a Fourth Style + * room; a bronze polycandelon hangs over the centre in place of ceiling lights. + */ +export function RomanAtriumDetails({ room, trim }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 4.0, 9); + const corners = useCorners(room.halfW, room.halfD, 0.1); + const stone = '#e6dcc6'; + const shadowStone = shade(stone, -0.14); + const socle = '#7c3227'; + + const dentils = useRepeat(room.depth, 0.46, 30); + + return ( + + {/* Pompeian socle with a black skirting course */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + + ))} + + {/* Engaged half-columns on the bay rhythm */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + + + + ))} + + ))} + + {/* Entablature: architrave, frieze and dentils along the colonnaded walls */} + {(['left', 'right'] as const).map((wall) => ( + + + + {dentils.map((z, i) => ( + + + + + ))} + + ))} + + {/* Projecting cornice returning around the whole room */} + + + {/* Corner antae — square pilasters closing the order at each corner */} + {corners.map(([x, z], i) => ( + + + + + ))} + + + + ); +} + +/** + * Florentine palazzo — Early Renaissance. + * + * Brunelleschi's grammar: flat pietra serena pilasters set against lime plaster, + * a blind arcade of semicircular arches springing between them, glazed roundels + * in the spandrels, and a painted-and-beamed quattrocento ceiling. + */ +export function QuattrocentoDetails({ room, trim, windows }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 3.8, 9); + const clearLeft = useWallClearance(windows, 'left'); + const clearRight = useWallClearance(windows, 'right'); + const serena = '#8b8f8a'; + const serenaLight = shade(serena, 0.16); + const step = bays.length > 1 ? bays[1] - bays[0] : room.depth / 2; + + return ( + + {/* Pietra serena dado with a moulded cap */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + ))} + + {/* Flat pilasters carrying the arcade */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + + + + + + {/* Simplified Corinthian capital */} + + + + + + + + + + ))} + + ))} + + {/* Blind arcade between the pilasters, plus a roundel in each spandrel */} + {bays.slice(0, -1).map((z, i) => { + const mid = z + step / 2; + return ( + + {([-1, 1] as const).map((side) => ( + + + {/* Glazed tondo, only where no window claims the bay */} + {(side < 0 ? clearLeft : clearRight)(mid, 0.22) && ( + + + + + )} + + ))} + + ); + })} + + {/* Painted beam ceiling over the arcade */} + + + + ); +} + +/** + * Roman palazzo — High Renaissance. + * + * Bramante's cinquecento order: paired fluted Corinthian pilasters on a wide + * bay, a full marble entablature, panelled dado, and a rosetted coffer field + * modelled on the Pantheon soffit. + */ +export function CinquecentoDetails({ room, trim }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 5.2, 6); + const marble = '#efe9dd'; + const grey = shade(marble, -0.16); + + return ( + + {/* Panelled marble dado */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + + ))} + + {/* Paired fluted pilasters */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => + [-0.19, 0.19].map((dz) => ( + + + + + + + + + + + + + + + )) + )} + + ))} + + {/* Full entablature on the pilastered walls, cornice all round */} + {(['left', 'right'] as const).map((wall) => ( + + + + + ))} + + + + + ); +} diff --git a/client/src/components/hall-details/courtly.tsx b/client/src/components/hall-details/courtly.tsx new file mode 100644 index 0000000..706f923 --- /dev/null +++ b/client/src/components/hall-details/courtly.tsx @@ -0,0 +1,415 @@ +import { + CeilingBeams, + CeilingRose, + CofferedCeiling, + CorniceRing, + FlutedShaft, + HangingFixture, + IonicCapital, + ShaftBase, + WallBand, +} from './primitives'; +import { + shade, + useBayZ, + useCorners, + useRepeat, + useWallClearance, + type PeriodProps, +} from './geometry'; + +/** + * Flemish panel hall — Northern Renaissance. + * + * Oak linenfold wainscot under a moulded cap rail, carved corbels carrying an + * exposed beam-and-joist ceiling, and the brass ring chandelier that hangs in + * the background of half the period's interiors. + */ +export function FlemishDetails({ room, trim }: PeriodProps) { + const beams = useBayZ(room.depth, room.halfD, 2.3, 10); + const oak = '#6f4a2c'; + const oakLight = shade(oak, 0.2); + + const folds = useRepeat(room.depth, 0.55, 34); + + const backFolds = useRepeat(room.width, 0.55, 34); + + return ( + + {/* Wainscot ground, cap rail and skirting */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + + ))} + + {/* Linenfold ribs across the wainscot */} + {(['left', 'right'] as const).map((wall) => + folds.map((z, i) => ( + + + + + )) + )} + {backFolds.map((x, i) => ( + + + + + ))} + + {/* Carved corbels carrying the beams */} + {beams.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + ))} + + ))} + + + + + + + ); +} + +/** + * Mannerist gallery — Mannerism. + * + * The order used against itself: herm pilasters that taper the wrong way, + * banded rustication blocking the shafts, oversized scroll consoles, and a + * cornice deliberately broken over each bay. + */ +export function ManneristDetails({ room, trim, windows }: PeriodProps) { + const clearLeft = useWallClearance(windows, 'left'); + const clearRight = useWallClearance(windows, 'right'); + const bays = useBayZ(room.depth, room.halfD, 4.4, 7); + const stone = '#ddc9ae'; + const dark = shade(stone, -0.34); + const bands = [0.55, 1.55, 2.35, 3.05]; + + return ( + + {/* Heavy blocked socle */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + {/* Herm pilasters — wider at the shoulder than at the foot */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + + {/* Rustication blocks interrupting the shaft */} + {bands.map((y) => ( + + + + + ))} + {/* Scroll console instead of a capital */} + + + + + + + + + + ))} + + ))} + + {/* Broken cornice — segments that stop short of each bay */} + {(['left', 'right'] as const).map((wall) => ( + + + + + ))} + + + {/* Cartouches slung between the bays, clear of the window openings */} + {bays.slice(0, -1).map((z, i) => { + const mid = (z + bays[i + 1]) / 2; + return ( + + {([-1, 1] as const) + .filter((side) => (side < 0 ? clearLeft : clearRight)(mid, 0.24)) + .map((side) => ( + + + + + + + + + + + ))} + + ); + })} + + ); +} + +/** + * Baroque state gallery — Baroque. + * + * Twisted solomonic half-columns at the corners, gilt-framed velvet field + * panels between the bays, a modillion cornice, and a crystal-and-gilt + * chandelier over the centre of the room. + */ +export function BaroqueStateDetails({ room, trim, windows }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 4.2, 8); + const corners = useCorners(room.halfW, room.halfD, 0.16); + const clearLeft = useWallClearance(windows, 'left'); + const clearRight = useWallClearance(windows, 'right'); + const gilt = trim; + + const modillions = useRepeat(room.depth, 0.86, 26); + + return ( + + {/* Gilt-edged velvet field panels between the bays, clear of the windows */} + {bays.slice(0, -1).map((z, i) => { + const mid = (z + bays[i + 1]) / 2; + const w = Math.min(1.5, Math.max(0.7, (bays[i + 1] - z) * 0.4)); + return ( + + {([-1, 1] as const) + .filter((side) => (side < 0 ? clearLeft : clearRight)(mid, w / 2 + 0.1)) + .map((side) => ( + + + + + + + + + + + ))} + + ); + })} + + {/* Solomonic corner columns — twist read as stacked, rotated drums */} + {corners.map(([x, z], i) => ( + + + {Array.from({ length: 14 }, (_, d) => ( + + + + + ))} + + + + + + ))} + + {/* Modillion brackets under a heavy gilt cornice */} + {(['left', 'right'] as const).map((wall) => + modillions.map((z, i) => ( + + + + + )) + )} + + + + + + ); +} + +/** + * Rococo salon — Rococo. + * + * Boiserie panelling with rounded corner arcs, asymmetric rocaille cartouches, + * a deep painted cove instead of a cornice, and a light gilt lustre. Everything + * is kept thin and pale: the period's relief is shallow by design. + */ +export function RococoDetails({ room, trim }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 3.4, 9); + const gilt = trim; + const boiserie = '#efe6ee'; + + return ( + + {/* Boiserie dado with a moulded chair rail */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + + ))} + + {/* Panel styles with quarter-round corners, and a rocaille cartouche above */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + + + + + + + + + + + + + ))} + + ))} + + {/* Painted cove: two stacked, slightly stepped bands reading as a curve */} + + + + + + + + ); +} + +/** + * Neoclassical museum — Neoclassicism. + * + * Schinkel's museum grammar: fluted Ionic pilasters on a wide bay, a Greek-key + * meander frieze, a dentilled cornice, and a plain coffered soffit around the + * lantern. Deliberately cool and flat next to the Baroque hall. + */ +export function NeoclassicalDetails({ room, trim }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 4.8, 6); + const stone = '#f1eee8'; + const shadowStone = shade(stone, -0.14); + + const meander = useRepeat(room.depth, 0.72, 24); + + return ( + + {/* Plinth course under the hang */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + ))} + + {/* Fluted Ionic pilasters */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + + + + ))} + + ))} + + {/* Meander frieze — alternating key blocks read as a running fret */} + {(['left', 'right'] as const).map((wall) => ( + + + {meander.map((z, i) => ( + + + + + + + + + + + ))} + + ))} + + + + + + + ); +} diff --git a/client/src/components/hall-details/geometry.ts b/client/src/components/hall-details/geometry.ts new file mode 100644 index 0000000..a1f0ec0 --- /dev/null +++ b/client/src/components/hall-details/geometry.ts @@ -0,0 +1,109 @@ +import { useMemo } from 'react'; +import type { GalleryWindowSpec } from '../../data/movement-interior-styles'; + +/** + * Shared geometry budget for every period interior. + * + * Movement halls hang frames 0.23 m proud of the inner wall face + * (`MOUNT_OFFSET` + `WALL_STANDOFF`), centred at eye height with tops around + * 2.4 m, and each wall keeps a 0.7 m clear margin at both ends + * (`WALL_PADDING` in `movementHallLayout.ts`). Period architecture therefore + * has to stay inside these limits, exactly as the Gothic nave and Byzantine + * basilica do: + * + * - relief below the frame line: at most 0.11 m proud of the wall face + * - friezes and cornices: 2.6 m and above, at most 0.17 m proud + * - freestanding masses: only inside the 0.34 m corner pocket at a corner + * - doorways: nothing below 2.6 m within 1.45 m of a wall centre + * - the end wall carries the hall title from 3.4 m to 3.9 m — keep it clear + * - side-wall windows are placed at runtime between 2.4 m and 3.9 m, so + * anything wider than a mullion up there goes through `useWallClearance` + * - added lights: at most two per hall, so hall materials stay inside the + * WebGL light limits the shared track lighting already budgets for + */ +/** Half of WALL_THICKNESS — distance from a wall's centre plane to its inner face. */ +export const WALL_INNER = 0.09; +/** Highest a detail can sit before it breaks the ceiling plane at 4.2 m. */ +export const CEILING_Y = 4.13; +/** Sink bands this far into the wall so they never z-fight with the surface. */ +export const EMBED = 0.02; + +export type WallId = 'back' | 'front' | 'left' | 'right'; + +export interface RoomDims { + width: number; + depth: number; + halfW: number; + halfD: number; +} + +/** Every period module takes the same three inputs. */ +export interface PeriodProps { + room: RoomDims; + /** The movement's trim colour, already blended with its accent. */ + trim: string; + /** Windows the hall placed at runtime, so upper-wall relief can avoid them. */ + windows?: GalleryWindowSpec[]; +} + +/** Lighten (amount > 0) or darken (amount < 0) a hex colour. */ +export function shade(hex: string, amount: number): string { + const n = parseInt(hex.replace('#', ''), 16); + const ch = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((c) => + Math.max(0, Math.min(255, Math.round(amount >= 0 ? c + (255 - c) * amount : c * (1 + amount)))) + ); + return `#${((ch[0] << 16) | (ch[1] << 8) | ch[2]).toString(16).padStart(6, '0')}`; +} + +/** Bay rhythm along the depth axis — the spacing that drives piers and beams. */ +export function useBayZ(depth: number, halfD: number, spacing = 4.2, max = 9): number[] { + return useMemo(() => { + const count = Math.max(2, Math.min(max, Math.floor(depth / spacing))); + const step = depth / (count + 1); + return Array.from({ length: count }, (_, i) => -halfD + step * (i + 1)); + }, [depth, halfD, spacing, max]); +} + +/** + * Evenly spaced positions centred on a wall, for the repeating small members — + * dentils, linenfold ribs, modillions, stencil motifs, quatrefoils. + */ +export function useRepeat(span: number, step: number, max: number): number[] { + return useMemo(() => { + const count = Math.min(max, Math.max(3, Math.floor((span - 0.6) / step))); + const run = (count - 1) * step; + return Array.from({ length: count }, (_, i) => -run / 2 + i * step); + }, [span, step, max]); +} + +/** + * Side-wall windows are placed at runtime in whatever gaps the hang leaves, so + * any panel, arch or roundel a period module paints across the upper wall has + * to step around them. Returns a predicate: true when the span is clear. + */ +export function useWallClearance( + windows: GalleryWindowSpec[] | undefined, + wall: 'left' | 'right' | 'back' +) { + return useMemo(() => { + const spans = (windows ?? []) + .filter((w) => w.wall === wall) + .map((w) => [w.x - w.width / 2 - 0.18, w.x + w.width / 2 + 0.18] as [number, number]); + return (pos: number, halfSpan = 0.2) => + !spans.some(([a, b]) => pos + halfSpan > a && pos - halfSpan < b); + }, [windows, wall]); +} + +/** The four corner columns of the room, each inside its hang-margin pocket. */ +export function useCorners(halfW: number, halfD: number, inset = 0.08): [number, number][] { + return useMemo( + () => + [ + [-halfW + inset, -halfD + inset], + [halfW - inset, -halfD + inset], + [-halfW + inset, halfD - inset], + [halfW - inset, halfD - inset], + ] as [number, number][], + [halfW, halfD, inset] + ); +} diff --git a/client/src/components/hall-details/modernism.tsx b/client/src/components/hall-details/modernism.tsx new file mode 100644 index 0000000..6b6626c --- /dev/null +++ b/client/src/components/hall-details/modernism.tsx @@ -0,0 +1,717 @@ +import { useMemo } from 'react'; +import { + CeilingBeams, + CorniceRing, + HangingFixture, + SideRails, + WallBand, +} from './primitives'; +import { + shade, + useBayZ, + useCorners, + useRepeat, + type PeriodProps, +} from './geometry'; + +/** + * Fauvist studio — Fauvism. + * + * The Collioure summer room: close-set whitewashed rafters, plank shutters + * folded back against the wall beside every window, and a plain painted rail. + * The architecture stays vernacular so the wall colour does the work. + */ +export function FauvistDetails({ room, trim, windows }: PeriodProps) { + const rafters = useBayZ(room.depth, room.halfD, 1.5, 14); + const timber = '#d8c8a8'; + + const shutters = useMemo( + () => + (windows ?? []) + .filter((w) => w.wall === 'left' || w.wall === 'right') + .flatMap((w) => + [-1, 1].map((dir) => ({ + wall: w.wall as 'left' | 'right', + z: w.x + dir * (w.width / 2 + w.width * 0.28), + y: w.y, + h: w.height, + w: w.width * 0.5, + })) + ), + [windows] + ); + + return ( + + + + + {/* Folded-back plank shutters flanking the windows */} + {shutters.map((s, i) => ( + + + + + + {[-0.3, 0, 0.3].map((t) => ( + + + + + ))} + + ))} + + + + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + ); +} + +/** + * Expressionist room — Expressionism. + * + * Die Brücke's carved-plank interior pushed towards the raked sets of the + * period's cinema: battens canted at alternating angles above the hang, a + * zig-zag frieze, and ceiling members that refuse to run square. + */ +export function ExpressionistDetails({ room, trim }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 2.2, 11); + const plank = '#5a3f27'; + const light = shade(plank, 0.3); + + return ( + + {/* Canted battens, alternating lean */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + ))} + + ))} + + {/* Zig-zag frieze under the ceiling */} + {(['left', 'right'] as const).map((wall) => + bays.map((z, i) => ( + + + + + )) + )} + + {/* Ceiling members deliberately off-square */} + {bays + .filter((_, i) => i % 3 === 0) + .map((z, i) => ( + + + + + ))} + + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + ); +} + +/** + * Cubist studio — Cubism. + * + * The Bateau-Lavoir atelier: slim cast-iron columns in the corners, riveted tie + * rods across the room, and a cornice broken into faceted plaster planes — the + * building itself analysed into flat facets. + */ +export function CubistAtelierDetails({ room, trim }: PeriodProps) { + const corners = useCorners(room.halfW, room.halfD, 0.24); + const ties = useBayZ(room.depth, room.halfD, 3.6, 8); + const iron = '#4d4a45'; + const facet = '#dedad2'; + + const facets = useRepeat(room.depth, 1.15, 22); + + return ( + + {/* Cast-iron columns in the corner pockets */} + {corners.map(([x, z], i) => ( + + + + + + + + + + + + + + + ))} + + {/* Riveted tie rods spanning the room, with a turnbuckle at mid-span */} + {ties.map((z, i) => ( + + + + + + + + + + + ))} + + {/* Faceted cornice: flat planes tilted alternately */} + {(['left', 'right'] as const).map((wall) => + facets.map((z, i) => ( + + + + + )) + )} + + + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + ); +} + +/** + * Futurist loft — Futurism. + * + * Sant'Elia's Città Nuova reduced to one room: riveted steel stanchions, + * diagonal wind bracing across the upper wall, and a gantry rail running the + * length of the hall. Structure celebrated, nothing concealed. + */ +export function FuturistDetails({ room, trim }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 4.0, 8); + const steel = '#8f9298'; + const dark = shade(steel, -0.42); + + return ( + + {/* Riveted stanchions */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + + {[0.6, 1.7, 2.8, 3.8].map((y) => ( + + + + + ))} + + ))} + + ))} + + {/* Diagonal wind bracing between the stanchions, above the hang */} + {bays.slice(0, -1).map((z, i) => { + const mid = (z + bays[i + 1]) / 2; + const run = bays[i + 1] - z; + return ( + + {[-1, 1].map((side) => + [-1, 1].map((dir) => ( + + + + + )) + )} + + ); + })} + + {/* Gantry rail and its brackets */} + + + + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + ); +} + +/** + * Suprematist white cube — Suprematism. + * + * The 0,10 room: a bare white shell, a shelf slung across the upper corner + * where Malevich hung the Black Square in the icon position, and thin black and + * red geometric bands drifting off the axis of the walls. + */ +export function SuprematistDetails({ room, trim }: PeriodProps) { + const bars = useMemo(() => { + const seeds = [-0.62, -0.24, 0.18, 0.55]; + return seeds.map((t, i) => ({ + z: t, + y: 3.0 + (i % 3) * 0.28, + len: 0.9 + (i % 2) * 0.7, + tilt: i % 2 === 0 ? 0.22 : -0.34, + color: i % 3 === 0 ? '#c8302a' : '#141414', + })); + }, []); + + return ( + + {/* Icon-corner shelf, carried on two plain brackets */} + + + + + + {[-0.4, 0.4].map((t) => ( + + + + + ))} + + + {/* Drifting bands above the hang */} + {(['left', 'right'] as const).map((wall) => + bars.map((b, i) => ( + + + + + )) + )} + + {/* Shadow-gap reveal instead of a cornice */} + + + + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + ); +} + +/** + * Constructivist space — Constructivism. + * + * Rodchenko's lattice made architecture: red-painted diagonal trusses under the + * ceiling, angled Proun panels folded into the corners, and exposed steel ties. + * Every member is doing structural work and says so. + */ +export function ConstructivistDetails({ room, trim }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 2.8, 10); + const corners = useCorners(room.halfW, room.halfD, 0.3); + const red = '#b8342a'; + const steel = '#9a9a96'; + + return ( + + {/* Lattice truss under the ceiling — top chord, bottom chord, diagonals */} + + + {bays.map((z, i) => + [-0.9, 0.9].map((x) => ( + + + + + )) + )} + + {/* Proun panels folded into the corners */} + {corners.map(([x, z], i) => ( + + + + + + + + + + + ))} + + {/* Steel tie rails at the hang line and the skirting */} + + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + + ); +} + +/** + * Dada salon — Dada. + * + * A bourgeois room being dismantled: cornice fragments that stop and restart at + * different heights, a picture rail hung visibly out of level, and a service + * pipe left running across the wall where the moulding should be. + */ +export function DadaDetails({ room, trim }: PeriodProps) { + const fragments = useMemo( + () => + [ + { wall: 'left' as const, y: 4.04, span: 0.3, proud: 0.14 }, + { wall: 'left' as const, y: 3.78, span: 0.18, proud: 0.1 }, + { wall: 'right' as const, y: 4.12, span: 0.44, proud: 0.16 }, + { wall: 'right' as const, y: 3.9, span: 0.22, proud: 0.09 }, + { wall: 'back' as const, y: 4.06, span: 0.35, proud: 0.13 }, + ], + [] + ); + const plaster = '#e6dac4'; + const pipe = '#6a6259'; + + return ( + + {/* Cornice, in pieces */} + {fragments.map((f, i) => ( + + ))} + + {/* Picture rail, visibly out of level */} + {[-1, 1].map((side) => ( + + + + + ))} + + {/* Service pipe run, with a clumsy elbow into the ceiling */} + + + + + + + + + + {/* Skirting that changes its mind halfway along */} + + + + + + + ); +} + +/** + * Surrealist interior — Surrealism. + * + * The 1938 Exposition Internationale: sacks slung under the ceiling, drapery + * pulled across the cornice, and a door standing in a corner where no door can + * lead anywhere. The room is furnished as an image, not as a gallery. + */ +export function SurrealistDetails({ room, trim }: PeriodProps) { + const sacks = useBayZ(room.depth, room.halfD, 2.6, 9); + + return ( + + {/* Sacks hanging under the ceiling */} + {sacks.map((z, i) => + [-1, 0, 1].map((c) => ( + + + + + )) + )} + + {/* Drapery gathered along the top of the side walls */} + {(['left', 'right'] as const).map((wall) => + sacks.map((z, i) => ( + + + + + )) + )} + + {/* A door that leads nowhere, standing in the corner pocket */} + + + + + + + + + + + + + + + + + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + {/* A brazier glow in the opposite corner */} + + + + + + + + + + + + + ); +} + +/** + * NYC loft — Abstract Expressionism. + * + * A Tenth Street cast-iron building: fluted iron columns on their corbelled + * caps, a pressed-tin ceiling field, and the sprinkler main running the length + * of the room. Industrial fabric left exactly as the painters found it. + */ +export function LoftDetails({ room, trim }: PeriodProps) { + const corners = useCorners(room.halfW, room.halfD, 0.26); + const joists = useBayZ(room.depth, room.halfD, 1.7, 13); + const iron = '#5c5852'; + + return ( + + {/* Cast-iron columns with corbelled caps */} + {corners.map(([x, z], i) => ( + + + + + + + + + + + + + + + + + + + ))} + + {/* Pressed-tin ceiling field on close-set joists */} + + + + {/* Sprinkler main with drops */} + + + + + {joists + .filter((_, i) => i % 4 === 0) + .map((z, i) => ( + + + + + ))} + + {/* Steel skirting angle */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + ); +} + +/** + * Pop Art gallery — Pop Art. + * + * The 1960s dealer's room: a flat white shell, recessed fluorescent troffers in + * a ceiling grid, an aluminium reveal at the floor, and a shadow gap where a + * cornice used to be. The architecture is engineered to disappear. + */ +export function WhiteCubeDetails({ room, trim }: PeriodProps) { + const troffers = useMemo(() => { + const cols = Math.max(2, Math.min(3, Math.round(room.width / 4.5))); + const rows = Math.max(2, Math.min(6, Math.round(room.depth / 3.4))); + const out: [number, number][] = []; + for (let c = 0; c < cols; c++) { + for (let r = 0; r < rows; r++) { + out.push([ + -room.halfW + (room.width / (cols + 1)) * (c + 1), + -room.halfD + (room.depth / (rows + 1)) * (r + 1), + ]); + } + } + return out; + }, [room.width, room.depth, room.halfW, room.halfD]); + + return ( + + {/* Recessed fluorescent troffers */} + {troffers.map(([x, z], i) => ( + + + + + + + + + + + ))} + + {/* Ceiling grid tees between the fittings */} + z).filter((z, i, a) => a.indexOf(z) === i)} + axis="x" + width={0.05} + height={0.04} + y={4.1} + color="#d6d6d6" + roughness={0.5} + metalness={0.36} + /> + + {/* Shadow gap and aluminium skirting reveal */} + + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + ))} + + ); +} diff --git a/client/src/components/hall-details/nineteenth.tsx b/client/src/components/hall-details/nineteenth.tsx new file mode 100644 index 0000000..dd36caa --- /dev/null +++ b/client/src/components/hall-details/nineteenth.tsx @@ -0,0 +1,507 @@ +import { + CeilingBeams, + CeilingRose, + CorniceRing, + HangingFixture, + SideRails, + WallBand, + WallSconce, +} from './primitives'; +import { + shade, + useBayZ, + useRepeat, + useWallClearance, + type PeriodProps, +} from './geometry'; + +/** + * Romantic gallery — Romanticism (Gothic Revival). + * + * Pugin's revival vocabulary rather than a real nave: dark walnut linenfold + * panelling, a cusped blind arcade of pointed arches, a quatrefoil frieze, and + * gasoliers. The pointed arch here is applied ornament, which is exactly what + * separates the revival from the Gothic hall it quotes. + */ +export function GothicRevivalDetails({ room, trim, windows }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 3.8, 8); + const clearLeft = useWallClearance(windows, 'left'); + const clearRight = useWallClearance(windows, 'right'); + const walnut = '#4c3323'; + const walnutLight = shade(walnut, 0.22); + + const quatrefoils = useRepeat(room.depth, 0.95, 24); + + return ( + + {/* Panelled dado with a heavy moulded rail */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + + ))} + + {/* Colonnettes between the bays, carrying an applied pointed arcade */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + + + + + + + ))} + + ))} + + {/* Cusped pointed arches over each bay, stepping around the windows */} + {bays.slice(0, -1).map((z, i) => { + const mid = (z + bays[i + 1]) / 2; + // Capped so the raking limbs stay under the ceiling in deep halls. + const half = Math.min(1.1, (bays[i + 1] - z) / 2 - 0.1); + return ( + + {([-1, 1] as const) + .filter((side) => (side < 0 ? clearLeft : clearRight)(mid, half)) + .map((side) => ( + + {[-1, 1].map((dir) => ( + + + + + ))} + + + + + + ))} + + ); + })} + + {/* Quatrefoil frieze under the cornice */} + {(['left', 'right'] as const).map((wall) => + quatrefoils.map((z, i) => ( + + {[0, 1, 2, 3].map((q) => { + const a = (q / 4) * Math.PI * 2 + Math.PI / 4; + return ( + + + + + ); + })} + + )) + )} + + + + {[-1, 1].map((side) => ( + + ))} + + + + ); +} + +/** + * Realist picture gallery — Realism. + * + * The mid-century bourgeois interior the Realists exhibited against: a plaster + * dado and picture rail, run cornice with a moulded frieze, a ceiling rose, and + * gas brackets. Restrained, machine-made mouldings — no carving anywhere. + */ +export function BourgeoisSalonDetails({ room, trim }: PeriodProps) { + const plaster = '#efe8dc'; + const shadow = shade(plaster, -0.12); + + return ( + + {/* Dado, chair rail, skirting */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + + ))} + + {/* Picture rail just above the hang, as the period actually hung its walls */} + + + {/* Run cornice: cavetto, bead and a moulded frieze */} + + + + + + + {[-1, 1].map((side) => ( + + ))} + + + + ); +} + +/** + * Impressionist salon — Impressionism. + * + * The 1874 Boulevard des Capucines idea of a hung room: an iron-and-glass + * lantern overhead, a coved plaster cornice, and a velvet-faced picture rail + * with visible hanging rods above the frames. Nothing carved, everything pale. + */ +export function NorthLightSalonDetails({ room, trim }: PeriodProps) { + const iron = '#6d6a63'; + const plaster = '#f4f1ea'; + const trusses = useBayZ(room.depth, room.halfD, 2.4, 11); + + return ( + + {/* Skylight lantern: glazing bars across a shallow raised centre */} + + + + + + {[-1, 1].map((side) => ( + + + + + ))} + + {/* Picture rail with hanging rods dropping to the frame line */} + + + + {/* Coved cornice */} + + + + {/* Dark skirting so the pale walls sit on something */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + ))} + + ); +} + +/** + * Montmartre atelier — Post-Impressionism. + * + * A working painter's room rather than a gallery: a king-post roof truss under + * the slope, a cast-iron stove flue climbing the end wall, plain board skirting + * and a rough shelf rail carrying the studio's clutter line. + */ +export function ParisAtelierDetails({ room, trim }: PeriodProps) { + const trusses = useBayZ(room.depth, room.halfD, 3.1, 8); + const timber = '#8a6a48'; + const iron = '#3a3632'; + + return ( + + {/* Roof trusses: tie beam, king post and raking struts */} + {trusses.map((z, i) => { + // Struts run from the head of the king post down to the tie beam, and + // have to stay inside the 0.3 m the ceiling leaves above the beam. + const run = room.halfW - 0.6; + const rise = 0.16; + const strut = Math.hypot(run, rise); + return ( + + + + + + + + + + {[-1, 1].map((side) => ( + + + + + ))} + + ); + })} + + {/* Stove flue running up the end wall and out along the ceiling */} + + + + + + + + + + {/* Board skirting and a rough shelf rail above the hang */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + ))} + + {/* A single bare bulb on a flex, the way the ateliers were actually lit */} + + + + + + + + + + + ); +} + +/** + * Symbolist chamber — Symbolism. + * + * The aesthetic interior: a stencilled frieze of repeating motifs, a heavy + * portière rail, dark lacquered dado, and low amber sconces. Light is kept + * pooled and low, which is the whole point of the period's rooms. + */ +export function SymbolistDetails({ room, trim }: PeriodProps) { + const lacquer = '#12281f'; + const stencil = shade(trim, 0.1); + + const motifs = useRepeat(room.depth, 0.8, 26); + + return ( + + {/* Lacquered dado under a gilt bead */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + ))} + + {/* Stencilled frieze — a repeating lotus/flame motif under the cornice */} + {(['left', 'right'] as const).map((wall) => ( + + + {motifs.map((z, i) => ( + + + + + + + + + + + ))} + + ))} + + + + + {/* Portière rail across the end wall */} + + + {[-0.36, 0.36].map((t) => ( + + ))} + + + + ); +} + +/** + * Art Nouveau salon — Art Nouveau. + * + * Horta's structural line: whiplash brackets springing from the wall into the + * ceiling, tendrils running the cornice, and organic sconces. The curve is + * carried by the structure itself rather than applied as ornament. + */ +export function ArtNouveauDetails({ room, trim }: PeriodProps) { + const bays = useBayZ(room.depth, room.halfD, 3.0, 10); + const iron = shade(trim, -0.24); + + return ( + + {/* Whiplash brackets: a stem up the wall curling into the ceiling */} + {bays.map((z, i) => ( + + {[-1, 1].map((side) => ( + + + + + + {[0, 1, 2, 3].map((s) => { + const t = (s + 1) / 4; + return ( + + + + + ); + })} + {/* Bud terminal */} + + + + + + ))} + + ))} + + {/* Tendril cornice — a wave riding the top of every wall */} + + + + {/* Sinuous dado line */} + {(['left', 'right', 'back'] as const).map((wall) => ( + + + + + ))} + + {[-0.3, 0.3].map((t) => ( + + ))} + + + + ); +} diff --git a/client/src/components/hall-details/primitives.tsx b/client/src/components/hall-details/primitives.tsx new file mode 100644 index 0000000..3907d7c --- /dev/null +++ b/client/src/components/hall-details/primitives.tsx @@ -0,0 +1,513 @@ +import { useMemo } from 'react'; +import { CEILING_Y, EMBED, WALL_INNER, shade, type RoomDims, type WallId } from './geometry'; + +/** Wall-flush building blocks shared by the period interiors in this folder. */ +interface SurfaceProps { + color: string; + roughness?: number; + metalness?: number; + emissive?: string; + emissiveIntensity?: number; +} + +/** A moulding running the length of one wall, flush against its inner face. */ +export function WallBand({ + wall, + room, + y, + height, + proud = 0.1, + span = 1, + color, + roughness = 0.7, + metalness = 0.06, + emissive, + emissiveIntensity, +}: SurfaceProps & { + wall: WallId; + room: RoomDims; + y: number; + height: number; + proud?: number; + span?: number; +}) { + const alongZ = wall === 'left' || wall === 'right'; + const len = (alongZ ? room.depth : room.width) * span; + const thick = proud + EMBED * 2; + const off = WALL_INNER + proud / 2 - EMBED; + const args: [number, number, number] = alongZ ? [thick, height, len] : [len, height, thick]; + const pos: [number, number, number] = + wall === 'back' + ? [0, y, -room.halfD + off] + : wall === 'front' + ? [0, y, room.halfD - off] + : wall === 'left' + ? [-room.halfW + off, y, 0] + : [room.halfW - off, y, 0]; + + return ( + + + + + ); +} + +/** A moulding ring running right around the room at one height. */ +export function CorniceRing({ + room, + y, + height, + proud = 0.12, + walls = ['back', 'front', 'left', 'right'], + ...surface +}: SurfaceProps & { + room: RoomDims; + y: number; + height: number; + proud?: number; + walls?: WallId[]; +}) { + return ( + + {walls.map((wall) => ( + + ))} + + ); +} + +/** String course / picture rail on the two side walls only. */ +export function SideRails({ + room, + y, + height, + proud = 0.1, + ...surface +}: SurfaceProps & { room: RoomDims; y: number; height: number; proud?: number }) { + return ( + + {(['left', 'right'] as const).map((wall) => ( + + ))} + + ); +} + +/** + * Vertical shaft with carved flutes, read as alternating light stone and + * shadowed grooves. Sits flush: the caller places it against a wall face. + */ +export function FlutedShaft({ + height, + radius, + color, + flutes = 12, + taper = 0.9, + facing = 'x', +}: { + height: number; + radius: number; + color: string; + flutes?: number; + taper?: number; + /** Wall normal axis — grooves are only cut on the exposed half. */ + facing?: 'x' | 'z'; +}) { + const groove = shade(color, -0.28); + const arcs = useMemo( + () => Array.from({ length: flutes }, (_, i) => (i / flutes) * Math.PI * 2), + [flutes] + ); + return ( + + + + + + {arcs.map((a, i) => { + const dx = Math.cos(a) * radius * 0.94; + const dz = Math.sin(a) * radius * 0.94; + // Skip the grooves buried in the wall behind the shaft. + if (facing === 'x' ? dx > 0 : dz > 0) return null; + return ( + + + + + ); + })} + + ); +} + +export function TuscanCapital({ y, size, color }: { y: number; size: number; color: string }) { + return ( + + + + + + + + + + + ); +} + +/** Ionic capital — abacus over a pair of spiral volutes. */ +export function IonicCapital({ + y, + size, + color, + axis = 'z', +}: { + y: number; + size: number; + color: string; + /** Axis the volute pair spreads along. */ + axis?: 'x' | 'z'; +}) { + return ( + + + + + + {[-1, 1].map((s) => ( + + + + + ))} + + ); +} + +/** Moulded plinth under a shaft. */ +export function ShaftBase({ size, color }: { size: number; color: string }) { + return ( + + + + + + + + + + + ); +} + +/** Semicircular arch drawn as a chord of straight voussoir blocks. */ +export function RoundArch({ + span, + rise, + thickness = 0.14, + depth = 0.16, + color, + segments = 11, + roughness = 0.72, + metalness = 0.05, +}: { + span: number; + rise: number; + thickness?: number; + depth?: number; + color: string; + segments?: number; + roughness?: number; + metalness?: number; +}) { + const blocks = useMemo(() => { + const out: { x: number; y: number; len: number; rot: number }[] = []; + for (let s = 0; s < segments; s++) { + const t0 = s / segments; + const t1 = (s + 1) / segments; + const at = (t: number) => { + const a = Math.PI * (1 - t); + return { x: Math.cos(a) * (span / 2), y: Math.sin(a) * rise }; + }; + const p0 = at(t0); + const p1 = at(t1); + out.push({ + x: (p0.x + p1.x) / 2, + y: (p0.y + p1.y) / 2, + len: Math.hypot(p1.x - p0.x, p1.y - p0.y) * 1.08, + rot: Math.atan2(p1.y - p0.y, p1.x - p0.x), + }); + } + return out; + }, [span, rise, segments]); + + return ( + + {blocks.map((b, i) => ( + + + + + ))} + + ); +} + +/** Recessed coffer grid under the ceiling plane. */ +export function CofferedCeiling({ + room, + y = CEILING_Y, + cell = 2.3, + panelColor, + ribColor, + ribWidth = 0.16, + ribDrop = 0.12, + rosette, +}: { + room: RoomDims; + y?: number; + cell?: number; + panelColor: string; + ribColor: string; + ribWidth?: number; + ribDrop?: number; + rosette?: string; +}) { + const grid = useMemo(() => { + const cols = Math.max(2, Math.min(6, Math.round(room.width / cell))); + const rows = Math.max(2, Math.min(8, Math.round(room.depth / cell))); + const cw = room.width / cols; + const cd = room.depth / rows; + const cells: { x: number; z: number }[] = []; + for (let c = 0; c < cols; c++) { + for (let r = 0; r < rows; r++) { + cells.push({ x: -room.halfW + cw * (c + 0.5), z: -room.halfD + cd * (r + 0.5) }); + } + } + return { cells, cw, cd, cols, rows }; + }, [room.width, room.depth, room.halfW, room.halfD, cell]); + + return ( + + {grid.cells.map((c, i) => ( + + + + + + {rosette && ( + + + + + )} + + ))} + {/* Ribs between the coffers */} + {Array.from({ length: grid.cols + 1 }, (_, c) => ( + + + + + ))} + {Array.from({ length: grid.rows + 1 }, (_, r) => ( + + + + + ))} + + ); +} + +/** Exposed structural beams spanning the room. */ +export function CeilingBeams({ + room, + positions, + axis = 'x', + width = 0.22, + height = 0.3, + y = CEILING_Y - 0.16, + color, + roughness = 0.82, + metalness = 0.05, +}: SurfaceProps & { + room: RoomDims; + positions: number[]; + /** Axis the beams run along. */ + axis?: 'x' | 'z'; + width?: number; + height?: number; + y?: number; +}) { + return ( + + {positions.map((p, i) => ( + + + + + ))} + + ); +} + +/** Plaster ceiling rose, the anchor a salon chandelier hangs from. */ +export function CeilingRose({ + y = CEILING_Y, + radius = 1.0, + color, + rings = 3, +}: { + y?: number; + radius?: number; + color: string; + rings?: number; +}) { + return ( + + {Array.from({ length: rings }, (_, i) => { + const t = (i + 1) / rings; + return ( + + + + + ); + })} + + ); +} + +/** + * Hanging light fixture. `light` adds a real point light — keep at most two per + * hall so the shared track lighting stays inside the WebGL light budget. + */ +export function HangingFixture({ + position, + dropLength = 0.6, + radius = 0.42, + arms = 6, + metalColor, + flameColor = '#ffcf80', + glowColor = '#ff9830', + light = false, + lightColor = '#ffb060', + lightIntensity = 0.8, + lightDistance = 7, +}: { + position: [number, number, number]; + dropLength?: number; + radius?: number; + arms?: number; + metalColor: string; + flameColor?: string; + glowColor?: string; + light?: boolean; + lightColor?: string; + lightIntensity?: number; + lightDistance?: number; +}) { + const ringY = position[1] - dropLength; + return ( + + + + + + + + + + {Array.from({ length: arms }, (_, i) => { + const a = (i / arms) * Math.PI * 2; + return ( + + + + + ); + })} + {light && ( + + )} + + ); +} + +/** + * Wall light mounted above the frame line. Emissive only — sconces are for + * period character, not for lighting the hall. + */ +export function WallSconce({ + wall, + room, + along, + y = 2.86, + armColor, + glowColor = '#ffb45a', + shadeColor = '#ffdca8', +}: { + wall: 'left' | 'right' | 'back'; + room: RoomDims; + /** Offset along the wall from its centre. */ + along: number; + y?: number; + armColor: string; + glowColor?: string; + shadeColor?: string; +}) { + const pos: [number, number, number] = + wall === 'back' + ? [along, y, -room.halfD + WALL_INNER + 0.02] + : wall === 'left' + ? [-room.halfW + WALL_INNER + 0.02, y, along] + : [room.halfW - WALL_INNER - 0.02, y, along]; + const rot: [number, number, number] = + wall === 'back' ? [0, 0, 0] : wall === 'left' ? [0, Math.PI / 2, 0] : [0, -Math.PI / 2, 0]; + + return ( + + + + + + + + + + + + + + + ); +} diff --git a/client/src/data/movement-interior-styles.ts b/client/src/data/movement-interior-styles.ts index c2b4f69..2b7675a 100644 --- a/client/src/data/movement-interior-styles.ts +++ b/client/src/data/movement-interior-styles.ts @@ -54,9 +54,42 @@ export interface MovementInteriorStyle { trackLights: number; /** Scales the shared hall/scene lights. <1 for deliberately dim period interiors. */ lightScale: number; - details: 'palazzo' | 'baroque' | 'gothic' | 'byzantine' | 'neoclassical' | 'salon' | 'modern' | 'classical' | 'museum' | 'industrial' | 'atelier'; + /** + * Which period architecture the hall is built from. Every movement has its + * own — the interiors are not shared between movements — and each kind maps + * to one component under `components/hall-details/`. + */ + details: MovementDetailKind; } +export type MovementDetailKind = + | 'roman' + | 'byzantine' + | 'gothic' + | 'quattrocento' + | 'cinquecento' + | 'flemish' + | 'mannerist' + | 'baroque' + | 'rococo' + | 'neoclassical' + | 'gothic-revival' + | 'bourgeois' + | 'north-light' + | 'paris-atelier' + | 'symbolist' + | 'art-nouveau' + | 'fauvist' + | 'expressionist' + | 'cubist-atelier' + | 'futurist' + | 'suprematist' + | 'constructivist' + | 'dada' + | 'surrealist' + | 'loft' + | 'white-cube'; + function windows(...specs: GalleryWindowSpec[]): GalleryWindowSpec[] { return specs; } @@ -99,9 +132,12 @@ const BY_MOVEMENT_ID: Record = { 'Roman atrium', 'Marble-clad villa · mosaic floor · clerestory daylight', { wall: 'limestone', ceiling: 'plaster-warm', floor: 'mosaic-roman' }, - { wall: '#e8e0d0', ceiling: '#f5f0e8', floor: '#c8b898', trim: '#a89878' }, + { wall: '#8e8980', ceiling: '#9c9994', floor: '#c8b898', trim: '#a89878' }, { - details: 'classical', + details: 'roman', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#5a4830', ambient: 0.62, warmLight: '#fff8e8', @@ -175,9 +211,12 @@ const BY_MOVEMENT_ID: Record = { 'Florentine palazzo', 'Lime-washed walls · terracotta accents · arched windows', { wall: 'painted-lime', wallSide: 'stucco-cream', ceiling: 'fresco-worn', floor: 'terracotta-tiles' }, - { wall: '#f4ebe0', ceiling: '#faf6ee', floor: '#c89070', trim: '#c9a227' }, + { wall: '#89847e', ceiling: '#96948f', floor: '#c89070', trim: '#c9a227' }, { - details: 'palazzo', + details: 'quattrocento', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#6b4423', windows: windows( { wall: 'back', x: -2.2, y: 2.6, width: 1.3, height: 1.7, style: 'roman-arch', lightColor: '#fff4e0', lightIntensity: 3.0 }, @@ -191,9 +230,12 @@ const BY_MOVEMENT_ID: Record = { 'Roman palazzo', 'Marble and stucco · coffered ceiling · checker floor', { wall: 'marble-veined-carrara', wallSide: 'stucco-cream', ceiling: 'plaster-warm', floor: 'marble-checker' }, - { wall: '#f0ece4', wallSide: '#efe4d4', ceiling: '#faf6ee', floor: '#ddd8cc', trim: '#c9a227' }, + { wall: '#918f8a', wallSide: '#8f8980', ceiling: '#9c9994', floor: '#ddd8cc', trim: '#c9a227' }, { - details: 'palazzo', + details: 'cinquecento', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#6b4423', windows: windows( { wall: 'back', x: 0, y: 2.8, width: 2.8, height: 1.8, style: 'baroque-pair', lightColor: '#fff8f0', lightIntensity: 3.8 }, @@ -208,9 +250,12 @@ const BY_MOVEMENT_ID: Record = { 'Flemish panel hall', 'Oak wainscoting · leaded glass · herringbone floor', { wall: 'oak-panel', wallSide: 'plaster-warm', ceiling: 'dark-wood-panel', floor: 'parquet-herringbone' }, - { wall: '#8a6848', wallSide: '#f0ebe3', ceiling: '#3a2818', floor: '#8a6848', trim: '#5c4030' }, + { wall: '#8a6848', wallSide: '#8c8985', ceiling: '#3a2818', floor: '#8a6848', trim: '#5c4030' }, { - details: 'salon', + details: 'flemish', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#f0e8d8', warmLight: '#ffe8c8', windows: windows( @@ -226,9 +271,12 @@ const BY_MOVEMENT_ID: Record = { 'Mannerist gallery', 'Dramatic stucco · elongated windows · veined marble', { wall: 'stucco-terracotta', ceiling: 'gilded-stucco', floor: 'marble-veined-emerald' }, - { wall: '#d8b898', ceiling: '#d8c070', floor: '#dce8e0', trim: '#b8860b' }, + { wall: '#98816b', ceiling: '#a79456', floor: '#dce8e0', trim: '#b8860b' }, { - details: 'baroque', + details: 'mannerist', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#4a2818', windows: windows( { wall: 'back', x: -1.8, y: 2.7, width: 1.0, height: 2.2, style: 'roman-arch', lightColor: '#fff0d8', lightIntensity: 2.8 }, @@ -264,9 +312,12 @@ const BY_MOVEMENT_ID: Record = { 'Rococo salon', 'Pastel painted walls · gilt trim · chevron parquet', { wall: 'painted-oil-satin', ceiling: 'silk-pale', floor: 'parquet-chevrons' }, - { wall: '#e8dce8', ceiling: '#faf6f0', floor: '#c8a882', trim: '#d4af37' }, + { wall: '#918a91', ceiling: '#9b9995', floor: '#c8a882', trim: '#d4af37' }, { - details: 'salon', + details: 'rococo', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#6a4858', warmLight: '#fff0f0', windows: windows( @@ -281,9 +332,12 @@ const BY_MOVEMENT_ID: Record = { 'Neoclassical museum', 'White painted walls · coffered ceiling · skylit salon', { wall: 'painted-flat', ceiling: 'plaster-white', floor: 'marble-veined-carrara' }, - { wall: '#f2f0ec', ceiling: '#fafafa', floor: '#eceae6', trim: '#b8b0a4' }, + { wall: '#908f8d', ceiling: '#9e9e9e', floor: '#eceae6', trim: '#b8b0a4' }, { details: 'neoclassical', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#4a4844', ambient: 0.65, windows: windows( @@ -301,7 +355,7 @@ const BY_MOVEMENT_ID: Record = { { wall: 'walnut-panel', ceiling: 'dark-plaster', floor: 'parquet-herringbone' }, { wall: '#5a4030', ceiling: '#2a2420', floor: '#6a5040', trim: '#8a7050' }, { - details: 'salon', + details: 'gothic-revival', titleColor: '#e8dcc8', ambient: 0.5, warmLight: '#ffe0c0', @@ -317,9 +371,12 @@ const BY_MOVEMENT_ID: Record = { 'Realist picture gallery', 'Warm painted walls · bourgeois salon · oak parquet', { wall: 'painted-emulsion', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' }, - { wall: '#e8e2d8', ceiling: '#f5f0e8', floor: '#a08060', trim: '#8a7050' }, + { wall: '#88847e', ceiling: '#97948f', floor: '#a08060', trim: '#8a7050' }, { - details: 'salon', + details: 'bourgeois', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, windows: windows( { wall: 'back', x: 0, y: 2.5, width: 2.4, height: 1.6, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 3.2 }, { wall: 'left', x: 0, y: 2.7, width: 1.6, height: 1.3, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 2.4 } @@ -331,9 +388,12 @@ const BY_MOVEMENT_ID: Record = { 'Impressionist salon', 'North-light skylight · pale painted walls · herringbone floor', { wall: 'painted-emulsion', ceiling: 'painted-flat', floor: 'parquet-herringbone' }, - { wall: '#f0ebe3', ceiling: '#fafafa', floor: '#c8a882', trim: '#c9a96e' }, + { wall: '#8f8c87', ceiling: '#9e9e9e', floor: '#c8a882', trim: '#c9a96e' }, { - details: 'salon', + details: 'north-light', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, ambient: 0.68, windows: windows( { wall: 'ceiling', x: 0, y: 0, width: 5.0, height: 3.0, style: 'skylight', lightColor: '#ffffff', lightIntensity: 6.0 }, @@ -348,9 +408,12 @@ const BY_MOVEMENT_ID: Record = { 'Montmartre atelier', 'Painted studio walls · large north window · worn floorboards', { wall: 'painted-emulsion', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' }, - { wall: '#e8e0d0', ceiling: '#f0ebe0', floor: '#9a7858', trim: '#8a6848' }, + { wall: '#847f76', ceiling: '#918f88', floor: '#9a7858', trim: '#8a6848' }, { - details: 'atelier', + details: 'paris-atelier', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, windows: windows( { wall: 'left', x: 0, y: 2.4, width: 2.8, height: 2.0, style: 'factory', lightColor: '#f0f8ff', lightIntensity: 4.5 }, { wall: 'back', x: 0, y: 2.8, width: 1.0, height: 1.2, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 2.0 } @@ -364,7 +427,7 @@ const BY_MOVEMENT_ID: Record = { { wall: 'painted-oil-matte', ceiling: 'dark-plaster', floor: 'parquet-herringbone' }, { wall: '#1a4030', ceiling: '#1a1814', floor: '#4a3828', trim: '#8a7050' }, { - details: 'salon', + details: 'symbolist', titleColor: '#d8e8c8', ambient: 0.55, warmLight: '#ffd898', @@ -380,9 +443,12 @@ const BY_MOVEMENT_ID: Record = { 'Art Nouveau salon', 'Painted plaster · stained glass · terrazzo floor', { wall: 'painted-oil-satin', ceiling: 'silk-gold', floor: 'terrazzo' }, - { wall: '#f0ece4', ceiling: '#d8c890', floor: '#d8d0c4', trim: '#6a9868' }, + { wall: '#8f8c87', ceiling: '#a0946b', floor: '#d8d0c4', trim: '#6a9868' }, { - details: 'salon', + details: 'art-nouveau', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#3a5840', windows: windows( { wall: 'back', x: 0, y: 2.5, width: 2.2, height: 2.0, style: 'art-nouveau', lightColor: '#e8ffe8', lightIntensity: 3.2 }, @@ -396,9 +462,12 @@ const BY_MOVEMENT_ID: Record = { 'Fauvist studio', 'Bold painted walls · flooded with color and light', { wall: 'painted-oil-matte', wallSide: 'painted-oil-matte', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' }, - { wall: '#e89060', wallSide: '#e8c860', ceiling: '#f8f0e0', floor: '#a07048', trim: '#c04020' }, + { wall: '#e89060', wallSide: '#e8c860', ceiling: '#9e998e', floor: '#a07048', trim: '#c04020' }, { - details: 'atelier', + details: 'fauvist', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#402010', ambient: 0.7, windows: windows( @@ -414,7 +483,7 @@ const BY_MOVEMENT_ID: Record = { { wall: 'dark-wood-panel', ceiling: 'dark-plaster', floor: 'parquet-herringbone' }, { wall: '#3a2818', ceiling: '#2a2018', floor: '#5a4030', trim: '#8a6040' }, { - details: 'atelier', + details: 'expressionist', titleColor: '#f0d8b0', ambient: 0.52, windows: windows( @@ -428,9 +497,12 @@ const BY_MOVEMENT_ID: Record = { 'Cubist studio', 'Paris atelier · factory windows · painted plaster', { wall: 'painted-emulsion', ceiling: 'painted-flat', floor: 'parquet-herringbone' }, - { wall: '#e8e4dc', ceiling: '#f5f5f5', floor: '#9a8870', trim: '#888888' }, + { wall: '#8c8a85', ceiling: '#999999', floor: '#9a8870', trim: '#888888' }, { - details: 'atelier', + details: 'cubist-atelier', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, ambient: 0.65, windows: windows( { wall: 'left', x: 0, y: 2.5, width: 3.5, height: 2.4, style: 'factory', lightColor: '#f0f8ff', lightIntensity: 5.5 }, @@ -443,9 +515,12 @@ const BY_MOVEMENT_ID: Record = { 'Futurist loft', 'Steel and glass · industrial concrete · sweeping daylight', { wall: 'concrete-raw', ceiling: 'concrete-raw', floor: 'industrial-floor' }, - { wall: '#a8a8a8', ceiling: '#989898', floor: '#888880', trim: '#606060' }, + { wall: '#858585', ceiling: '#7a7a7a', floor: '#888880', trim: '#606060' }, { - details: 'industrial', + details: 'futurist', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#303030', ambient: 0.62, fog: '#181818', @@ -462,9 +537,12 @@ const BY_MOVEMENT_ID: Record = { 'Suprematist white cube', 'Matte white paint · geometric light · polished floor', { wall: 'painted-flat', ceiling: 'painted-flat', floor: 'concrete-polished' }, - { wall: '#ffffff', ceiling: '#ffffff', floor: '#e0e0e0', trim: '#cccccc' }, + { wall: '#a8a8a8', ceiling: '#b3b3b3', floor: '#e0e0e0', trim: '#cccccc' }, { - details: 'modern', + details: 'suprematist', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#222222', ambient: 0.72, fog: '#1a1a1a', @@ -482,9 +560,12 @@ const BY_MOVEMENT_ID: Record = { 'Constructivist space', 'Glass block · concrete · angular daylight', { wall: 'concrete-block', ceiling: 'concrete-raw', floor: 'concrete-polished' }, - { wall: '#b0b0b0', ceiling: '#a0a0a0', floor: '#c0c0c0', trim: '#808080' }, + { wall: '#858585', ceiling: '#7a7a7a', floor: '#c0c0c0', trim: '#808080' }, { - details: 'industrial', + details: 'constructivist', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#303030', ambient: 0.65, windows: windows( @@ -500,9 +581,12 @@ const BY_MOVEMENT_ID: Record = { 'Dada salon', 'Eclectic bourgeois room · painted walls and exposed brick', { wall: 'painted-emulsion', wallSide: 'brick', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' }, - { wall: '#e8dcc8', wallSide: '#8a5040', ceiling: '#f0ebe0', floor: '#8a6848', trim: '#6a4830' }, + { wall: '#8b8478', wallSide: '#8a5040', ceiling: '#97948d', floor: '#8a6848', trim: '#6a4830' }, { - details: 'salon', + details: 'dada', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, windows: windows( { wall: 'back', x: -1.5, y: 2.5, width: 1.0, height: 1.4, style: 'sash', lightColor: '#f0f4ff', lightIntensity: 2.5 }, { wall: 'back', x: 1.5, y: 2.6, width: 0.8, height: 1.8, style: 'gothic-lancet', lightColor: '#ffe8c0', lightIntensity: 2.0 }, @@ -518,7 +602,7 @@ const BY_MOVEMENT_ID: Record = { { wall: 'painted-oil-matte', ceiling: 'dark-plaster', floor: 'parquet-herringbone' }, { wall: '#1a2848', ceiling: '#2a2428', floor: '#5a4838', trim: '#8a7050' }, { - details: 'salon', + details: 'surrealist', titleColor: '#e8dcc8', ambient: 0.58, warmLight: '#ffd898', @@ -535,9 +619,12 @@ const BY_MOVEMENT_ID: Record = { 'NYC loft', 'Raw brick and painted plaster · north-light factory windows', { wall: 'brick', wallSide: 'painted-flat', ceiling: 'concrete-raw', floor: 'industrial-floor' }, - { wall: '#8a5040', wallSide: '#f5f5f5', ceiling: '#989898', floor: '#888880', trim: '#606060' }, + { wall: '#8a5040', wallSide: '#858585', ceiling: '#989898', floor: '#888880', trim: '#606060' }, { - details: 'industrial', + details: 'loft', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#303030', ambient: 0.65, windows: windows( @@ -551,9 +638,12 @@ const BY_MOVEMENT_ID: Record = { 'Pop Art gallery', 'White painted walls · fluorescent daylight · polished concrete', { wall: 'painted-flat', ceiling: 'painted-flat', floor: 'concrete-polished' }, - { wall: '#ffffff', ceiling: '#ffffff', floor: '#d8d8d8', trim: '#cccccc' }, + { wall: '#a8a8a8', ceiling: '#b3b3b3', floor: '#d8d8d8', trim: '#cccccc' }, { - details: 'modern', + details: 'white-cube', + // Lit like the basilica: shared lights held back so the walls read as + // surfaces rather than blowing out. Window shafts are not scaled. + lightScale: 0.3, titleColor: '#222222', ambient: 0.75, warmLight: '#ffffff',