From 1cb9eef9354edfc2b329e0fdb1336f63dd36a560 Mon Sep 17 00:00:00 2001 From: Danila Khodjaef Date: Fri, 31 Jul 2026 18:47:15 +0300 Subject: [PATCH] Rework Byzantine hall as a late-antique basilica interior. Replace the flat gold-mosaic chapel with marble revetment walls, a Cosmatesque stone floor, and a coffered timber ceiling, matching Santa Sabina / San Vitale reference interiors. New procedural surfaces in galleryProceduralTextures.ts: - marble-revetment: book-matched veined panels in moulded travertine surrounds (each panel mirrored about its centre) - marble-opus-sectile: cut-stone bays with diagonal diamond inlays and corner triangles in porphyry, verd-antique, giallo, and grey - coffered-wood: recessed walnut coffers with lit beam chamfers Add a byzantine detail set to MovementHallDetails.tsx: engaged porphyry colonnettes with white-marble basket capitals and impost blocks carrying the ceiling, a marble revetment dado, and hanging brass polycandela. Colonnettes stay flush in the corner pockets so they do not clip the ~0.7m frame hang margin. Basilicas are lit by shafts from high windows rather than evenly flooded, but the shared hall lights carry a brightness floor that prevented dimming. Add an opt-in lightScale to the interior style that scales the scene, hall, track, and HDR environment lights. It defaults to 1, so all other halls are unchanged; only Byzantine opts down. Window fill lights are left unscaled so the daylight shafts still read. Co-Authored-By: Claude Opus 5 --- Documentation/basics.md | 8 +- client/src/components/MovementHallDetails.tsx | 102 +++++++ client/src/components/VirtualGallery.tsx | 22 +- client/src/data/movement-interior-styles.ts | 41 +-- client/src/utils/galleryProceduralTextures.ts | 264 +++++++++++++++++- 5 files changed, 400 insertions(+), 37 deletions(-) diff --git a/Documentation/basics.md b/Documentation/basics.md index 7311b99..90c8ad3 100644 --- a/Documentation/basics.md +++ b/Documentation/basics.md @@ -335,12 +335,12 @@ Enter from the home page by clicking a **movement name** on the movement flow. F | 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, 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 chapel, not Gothic stone | -| Wall materials | Hi-res **procedural textures** with normal maps (`galleryProceduralTextures.ts`): real-world stone, marble, wood panelling, brick, velvet, stucco — plus **single-colour painted walls** (`painted-lime`, `painted-oil-matte`, `painted-oil-satin`, `painted-emulsion`, `painted-flat`) tinted per movement for Renaissance salons through modern white cubes | +| 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 | +| 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 — plus **single-colour painted walls** (`painted-lime`, `painted-oil-matte`, `painted-oil-satin`, `painted-emulsion`, `painted-flat`) tinted per movement for Renaissance salons through modern white cubes | | Textures | Wall/floor/ceiling maps applied via `useTexturedMaterial`; movement **tints** drive painted-wall hue | | Windows | **Side walls only** — placed in gaps between frames when possible; if a wall is packed, high **clerestory** windows are still added so the hall keeps daylight (`computeSideWallWindows`) | | 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) | +| 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 | | 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 | @@ -376,6 +376,8 @@ 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. + **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. diff --git a/client/src/components/MovementHallDetails.tsx b/client/src/components/MovementHallDetails.tsx index 5d0569f..43bd15c 100644 --- a/client/src/components/MovementHallDetails.tsx +++ b/client/src/components/MovementHallDetails.tsx @@ -139,6 +139,106 @@ function MedievalDetails({ halfW, halfD }: Pick) { ); } +function ByzantineDetails({ halfW, halfD, trim }: Pick & { trim: string }) { + // Engaged porphyry colonnettes with Byzantine basket/impost capitals at the + // corners (flush — never proud of the ~0.7m frame hang margin), a marble + // revetment dado at spring-line height, and hanging brass polycandela in + // place of wall torches (a Byzantine basilica hangs its light, not sconces it). + const colonnettes = useMemo( + () => + [ + [-halfW + 0.08, -halfD + 0.08], + [halfW - 0.08, -halfD + 0.08], + [-halfW + 0.08, halfD - 0.08], + [halfW - 0.08, halfD - 0.08], + ] as [number, number][], + [halfW, halfD] + ); + + const lampPositions = useMemo( + () => + [ + [0, -halfD * 0.32], + [0, halfD * 0.18], + ] as [number, number][], + [halfD] + ); + + const dadoW = Math.max(0, halfW * 2 - 0.3); + const dadoD = Math.max(0, halfD * 2 - 0.3); + + return ( + + {/* Marble revetment dado — imperial banding course at chair-rail height */} + {[ + [0, -halfD + 0.07, dadoW, 0.1] as const, + [0, halfD - 0.07, dadoW, 0.1] as const, + [-halfW + 0.07, 0, 0.1, dadoD] as const, + [halfW - 0.07, 0, 0.1, dadoD] as const, + ].map(([x, z, w, d], i) => ( + + + + + + + + + + + ))} + + {/* Engaged colonnettes, basket capital, and impost block */} + {colonnettes.map(([x, z], i) => ( + + {/* Polished porphyry shaft — the imperial stone of the reference basilicas */} + + + + + + + + + {/* Basket capital in white marble, then an impost block carrying the ceiling */} + + + + + + + + + + ))} + + {/* Hanging polycandela — brass ring lamps suspended from the vault */} + {lampPositions.map(([x, z], i) => ( + + + + + + + + + + {Array.from({ length: 6 }, (_, j) => { + const a = (j / 6) * Math.PI * 2; + return ( + + + + + ); + })} + + + ))} + + ); +} + 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. @@ -226,6 +326,8 @@ export default function MovementHallDetails({ style, width, depth, halfW, halfD return ; case 'medieval': return ; + case 'byzantine': + return ; case 'neoclassical': return ; case 'classical': diff --git a/client/src/components/VirtualGallery.tsx b/client/src/components/VirtualGallery.tsx index 581154a..5c7ae0b 100644 --- a/client/src/components/VirtualGallery.tsx +++ b/client/src/components/VirtualGallery.tsx @@ -1484,6 +1484,7 @@ function ArtistHall({ ); const titleColor = interiorStyle?.titleColor ?? '#4a3020'; const warmLight = interiorStyle?.warmLight ?? '#fff5e8'; + const hallLightScale = interiorStyle?.lightScale ?? 1; const wallRoughness = interiorStyle ? 0.75 : 0.92; const wallMetalness = interiorStyle ? 0.08 : 0.06; @@ -1656,7 +1657,7 @@ function ArtistHall({ @@ -1736,14 +1737,14 @@ function ArtistHall({ )} @@ -2431,7 +2432,8 @@ export default function VirtualGallery(props: Props) { const sceneFog = interiorStyle?.fog ?? '#0d0906'; // Floor ambient so dark period styles (Gothic, Byzantine) stay readable without HDR IBL. // three r155+ physical lights need ~π× legacy intensity for similar brightness. - const ambientIntensity = Math.max(0.85, interiorStyle?.ambient ?? 0.55) * Math.PI; + const lightScale = interiorStyle?.lightScale ?? 1; + const ambientIntensity = Math.max(0.85, interiorStyle?.ambient ?? 0.55) * Math.PI * lightScale; const handleCanvasPointerDown = (e: React.PointerEvent) => { if (!active || showExitNav || e.button !== 0) return; @@ -2546,22 +2548,22 @@ export default function VirtualGallery(props: Props) { {/* Guaranteed fill so walls never disappear if HDR Environment fails. */} diff --git a/client/src/data/movement-interior-styles.ts b/client/src/data/movement-interior-styles.ts index a3b09af..8eed6cc 100644 --- a/client/src/data/movement-interior-styles.ts +++ b/client/src/data/movement-interior-styles.ts @@ -52,7 +52,9 @@ export interface MovementInteriorStyle { doorWood: [string, string, string]; windows: GalleryWindowSpec[]; trackLights: number; - details: 'palazzo' | 'baroque' | 'medieval' | 'neoclassical' | 'salon' | 'modern' | 'classical' | 'museum' | 'industrial' | 'atelier'; + /** Scales the shared hall/scene lights. <1 for deliberately dim period interiors. */ + lightScale: number; + details: 'palazzo' | 'baroque' | 'medieval' | 'byzantine' | 'neoclassical' | 'salon' | 'modern' | 'classical' | 'museum' | 'industrial' | 'atelier'; } function windows(...specs: GalleryWindowSpec[]): GalleryWindowSpec[] { @@ -85,6 +87,7 @@ function mk( doorWood: opts.doorWood ?? ['#3d2818', '#4e3624', '#261a10'], windows: opts.windows, trackLights: opts.trackLights ?? 0.8, + lightScale: opts.lightScale ?? 1, details: opts.details, }; } @@ -114,25 +117,29 @@ const BY_MOVEMENT_ID: Record = { ), 28: mk( 'byzantine-sanctuary', - 'Byzantine chapel', - 'Gold mosaic walls · amber light through arched windows', - { wall: 'mosaic-byzantine', ceiling: 'gilded-stucco', floor: 'marble-checker' }, - { wall: '#d4af37', ceiling: '#c8a840', floor: '#ddd8cc', trim: '#b8860b' }, + 'Byzantine basilica', + 'Marble revetment · coffered timber ceiling · Cosmatesque stone floor', + { wall: 'marble-revetment', ceiling: 'coffered-wood', floor: 'marble-opus-sectile' }, + { wall: '#a89880', ceiling: '#6a4526', floor: '#a09079', trim: '#8a6440' }, { - details: 'medieval', - titleColor: '#f0e8c0', - ambient: 0.48, - warmLight: '#ffd898', - sunLight: '#ffe8b0', - fog: '#0a0806', - background: '#060504', + details: 'byzantine', + titleColor: '#e8d8b0', + ambient: 0.44, + warmLight: '#ffdca8', + sunLight: '#ffdca0', + fog: '#100b07', + background: '#0a0705', windows: windows( - { wall: 'back', x: 0, y: 2.8, width: 1.0, height: 2.2, style: 'roman-arch', lightColor: '#ffb860', lightIntensity: 2.4 }, - { wall: 'left', x: -1, y: 2.6, width: 0.7, height: 1.8, style: 'roman-arch', lightColor: '#ffb860', lightIntensity: 1.8 }, - { wall: 'right', x: 1, y: 2.6, width: 0.7, height: 1.8, style: 'roman-arch', lightColor: '#ffb860', lightIntensity: 1.8 } + { wall: 'back', x: 0, y: 3.1, width: 0.9, height: 1.9, style: 'roman-arch', lightColor: '#ffd28a', lightIntensity: 3.4 }, + { wall: 'left', x: -3.5, y: 3.2, width: 0.7, height: 1.6, style: 'roman-arch', lightColor: '#ffd28a', lightIntensity: 3.0 }, + { wall: 'left', x: 3.5, y: 3.2, width: 0.7, height: 1.6, style: 'roman-arch', lightColor: '#ffd28a', lightIntensity: 3.0 }, + { wall: 'right', x: -3.5, y: 3.2, width: 0.7, height: 1.6, style: 'roman-arch', lightColor: '#ffd28a', lightIntensity: 3.0 }, + { wall: 'right', x: 3.5, y: 3.2, width: 0.7, height: 1.6, style: 'roman-arch', lightColor: '#ffd28a', lightIntensity: 3.0 } ), - trackLights: 0.5, - doorWood: ['#3a3020', '#4a3830', '#2a2018'], + trackLights: 0.35, + // Basilica interiors are lit by shafts from high windows, not evenly flooded. + lightScale: 0.3, + doorWood: ['#3a2416', '#4c3220', '#241608'], } ), 29: mk( diff --git a/client/src/utils/galleryProceduralTextures.ts b/client/src/utils/galleryProceduralTextures.ts index 5e3350c..7eb7d26 100644 --- a/client/src/utils/galleryProceduralTextures.ts +++ b/client/src/utils/galleryProceduralTextures.ts @@ -8,6 +8,9 @@ export type SurfaceTextureKind = | 'marble-veined-carrara' | 'marble-veined-emerald' | 'marble-checker' + | 'marble-opus-sectile' + | 'marble-revetment' + | 'coffered-wood' | 'limestone' | 'sandstone' | 'rough-stone' @@ -244,6 +247,43 @@ function marbleVeined(ctx: CanvasRenderingContext2D, size: number, base: string, noiseOverlay(ctx, size, 0.035, seed + 1); } +/** Veined marble confined to one slab rect — for revetment panels and inlay. */ +function marbleSlab( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + w: number, + h: number, + base: string, + vein: string, + seed: number +) { + const rand = seeded(seed); + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + ctx.fillStyle = base; + ctx.fillRect(x, y, w, h); + const [vr, vg, vb] = hexToRgb(vein); + const strands = 10 + Math.floor(w / 24); + for (let i = 0; i < strands; i++) { + ctx.strokeStyle = `rgba(${vr},${vg},${vb},${0.1 + rand() * 0.3})`; + ctx.lineWidth = 0.6 + rand() * 3.4; + ctx.beginPath(); + let px = x + rand() * w; + let py = y + rand() * h; + ctx.moveTo(px, py); + for (let s = 0; s < 9; s++) { + px += (rand() - 0.35) * w * 0.28; + py += (rand() - 0.5) * h * 0.22; + ctx.quadraticCurveTo(px - w * 0.05, py + h * 0.04, px, py); + } + ctx.stroke(); + } + ctx.restore(); +} + function woodPanelSurface( ctx: CanvasRenderingContext2D, size: number, @@ -346,6 +386,161 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData { noiseOverlay(ctx, size, 0.03, 103); break; } + case 'marble-opus-sectile': { + // Cosmatesque paving: cut-stone squares with diagonally-set inlays and + // corner triangles in porphyry, verd-antique, and giallo over travertine. + const stones = [ + ['#6a3c42', '#4c2830'], + ['#47554b', '#333f38'], + ['#8a7752', '#66573c'], + ['#4e4a45', '#363330'], + ]; + fill(ctx, size, '#7d6e58'); + const grid = 2; + const cell = size / grid; + for (let row = 0; row < grid; row++) { + for (let col = 0; col < grid; col++) { + const x = col * cell; + const y = row * cell; + const rand = seeded(610 + row * grid + col); + // Travertine ground slab for this bay + marbleSlab(ctx, x, y, cell, cell, '#8f7f66', '#6d5f4c', 620 + row * grid + col); + + const pick = stones[(row * 3 + col * 5 + Math.floor(rand() * 2)) % stones.length]; + const inset = cell * 0.045; + // Corner triangles + ctx.fillStyle = pick[1]; + const corners: [number, number, number, number, number, number][] = [ + [x + inset, y + inset, x + cell * 0.5, y + inset, x + inset, y + cell * 0.5], + [x + cell - inset, y + inset, x + cell * 0.5, y + inset, x + cell - inset, y + cell * 0.5], + [x + inset, y + cell - inset, x + cell * 0.5, y + cell - inset, x + inset, y + cell * 0.5], + [x + cell - inset, y + cell - inset, x + cell * 0.5, y + cell - inset, x + cell - inset, y + cell * 0.5], + ]; + for (const [ax, ay, bx, by, cx2, cy2] of corners) { + ctx.beginPath(); + ctx.moveTo(ax, ay); + ctx.lineTo(bx, by); + ctx.lineTo(cx2, cy2); + ctx.closePath(); + ctx.fill(); + } + // Diamond inlay set on the diagonal + const cx = x + cell / 2; + const cy = y + cell / 2; + const r = cell * 0.34; + ctx.save(); + ctx.beginPath(); + ctx.moveTo(cx, cy - r); + ctx.lineTo(cx + r, cy); + ctx.lineTo(cx, cy + r); + ctx.lineTo(cx - r, cy); + ctx.closePath(); + ctx.clip(); + marbleSlab(ctx, cx - r, cy - r, r * 2, r * 2, pick[0], pick[1], 640 + row * grid + col); + ctx.restore(); + ctx.strokeStyle = 'rgba(40,30,22,0.45)'; + ctx.lineWidth = 1.4; + ctx.beginPath(); + ctx.moveTo(cx, cy - r); + ctx.lineTo(cx + r, cy); + ctx.lineTo(cx, cy + r); + ctx.lineTo(cx - r, cy); + ctx.closePath(); + ctx.stroke(); + // Cut joint around the bay + ctx.strokeStyle = 'rgba(40,30,22,0.5)'; + ctx.lineWidth = 2; + ctx.strokeRect(x + 1, y + 1, cell - 2, cell - 2); + } + } + noiseOverlay(ctx, size, 0.03, 601); + break; + } + case 'marble-revetment': { + // Basilica wall cladding: book-matched marble panels in carved stone + // surrounds — Santa Sabina / San Vitale, no mosaic. + fill(ctx, size, '#a8977c'); + const cols = 2; + const rows = 2; + const pw = size / cols; + const ph = size / rows; + const fields = [ + ['#5e6d64', '#33423a'], + ['#8a7a5e', '#5a4c36'], + ['#5e6d64', '#33423a'], + ['#6f5f56', '#453a34'], + ]; + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const x = col * pw; + const y = row * ph; + // Travertine surround + marbleSlab(ctx, x, y, pw, ph, '#b0a084', '#8a7658', 660 + row * cols + col); + const m = pw * 0.09; + const iw = pw - m * 2; + const ih = ph - m * 2; + const field = fields[(row * cols + col) % fields.length]; + // Book-matched pair: the same slab mirrored about the panel centre + const half = iw / 2; + marbleSlab(ctx, x + m, y + m, half, ih, field[0], field[1], 670 + row * cols + col); + ctx.save(); + ctx.translate(x + m + iw, y + m); + ctx.scale(-1, 1); + ctx.drawImage(ctx.canvas, x + m, y + m, half, ih, 0, 0, half, ih); + ctx.restore(); + // Moulded frame around the panel + ctx.strokeStyle = 'rgba(60,44,30,0.55)'; + ctx.lineWidth = 3; + ctx.strokeRect(x + m, y + m, iw, ih); + ctx.strokeStyle = 'rgba(255,244,222,0.35)'; + ctx.lineWidth = 2; + ctx.strokeRect(x + m - 4, y + m - 4, iw + 8, ih + 8); + } + } + // Horizontal string course between panel registers + ctx.fillStyle = 'rgba(90,68,44,0.35)'; + ctx.fillRect(0, ph - 3, size, 6); + noiseOverlay(ctx, size, 0.04, 680); + break; + } + case 'coffered-wood': { + // Open-timber coffered basilica ceiling: recessed dark walnut boxes with + // lit chamfers so the grid reads as depth, not a flat pattern. + fill(ctx, size, '#2e1e12'); + const n = 6; + const cell = size / n; + for (let row = 0; row < n; row++) { + for (let col = 0; col < n; col++) { + const x = col * cell; + const y = row * cell; + const beam = cell * 0.14; + // Beam faces catch light from below + ctx.fillStyle = '#6a4a2c'; + ctx.fillRect(x, y, cell, beam); + ctx.fillRect(x, y, beam, cell); + ctx.fillStyle = '#4a3018'; + ctx.fillRect(x + cell - beam, y, beam, cell); + ctx.fillRect(x, y + cell - beam, cell, beam); + // Recessed coffer pan + const px = x + beam; + const py = y + beam; + const pwid = cell - beam * 2; + const phei = cell - beam * 2; + const grad = ctx.createLinearGradient(px, py, px, py + phei); + grad.addColorStop(0, '#22150c'); + grad.addColorStop(1, '#3c2616'); + ctx.fillStyle = grad; + ctx.fillRect(px, py, pwid, phei); + // Central gilt rosette boss + ctx.fillStyle = '#8a6a2a'; + ctx.beginPath(); + ctx.arc(px + pwid / 2, py + phei / 2, Math.min(pwid, phei) * 0.13, 0, Math.PI * 2); + ctx.fill(); + } + } + noiseOverlay(ctx, size, 0.05, 690); + break; + } case 'limestone': fill(ctx, size, '#ddd4c4'); for (let y = 0; y < size; y += 4) { @@ -460,15 +655,59 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData { } break; case 'mosaic-byzantine': { - fill(ctx, size, '#1a1814'); - const cell = size / 32; - const colors = ['#d4af37', '#8a3020', '#2060a0', '#f0ece0', '#408040']; - for (let y = 0; y < size; y += cell) { - for (let x = 0; x < size; x += cell) { - ctx.fillStyle = colors[(x / cell + y / cell) % colors.length]; - ctx.fillRect(x + 1, y + 1, cell - 2, cell - 2); + // Gold-ground tessera field (gold-leaf glass, individually jittered) with + // sparse inlaid accent tesserae and a meander border course — Hagia Sophia / + // Ravenna style, not a flat multicolor checker. + fill(ctx, size, '#1a1408'); + const grid = 32; + const cell = size / grid; + const rand = seeded(777); + const accents: [number, number, number][] = [ + [32, 52, 98], + [126, 30, 44], + [28, 78, 58], + [18, 16, 22], + ]; + for (let gy = 0; gy < grid; gy++) { + for (let gx = 0; gx < grid; gx++) { + const x = gx * cell; + const y = gy * cell; + const roll = rand(); + let r: number, g: number, b: number; + if (roll < 0.05) { + [r, g, b] = accents[Math.floor(rand() * accents.length)]; + } else { + const j = (rand() - 0.5) * 50; + r = 214 + j; + g = 170 + j * 0.72; + b = 58 + j * 0.35; + } + ctx.fillStyle = rgb(r, g, b); + const pad = 1 + rand() * 1.4; + ctx.fillRect(x + pad, y + pad, cell - pad * 2, cell - pad * 2); } } + // Dark grout between tesserae reads as individual glass/gold tiles. + ctx.strokeStyle = 'rgba(16,10,4,0.4)'; + ctx.lineWidth = 1; + for (let i = 0; i <= grid; i++) { + ctx.beginPath(); + ctx.moveTo(i * cell, 0); + ctx.lineTo(i * cell, size); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, i * cell); + ctx.lineTo(size, i * cell); + ctx.stroke(); + } + // Meander (key-pattern) register band dividing the panel, in navy and gold. + const bandRow = Math.floor(grid * 0.47); + for (let gx = 0; gx < grid; gx++) { + const on = Math.floor(gx / 2) % 2 === 0; + ctx.fillStyle = on ? '#0f2a52' : '#e0b840'; + ctx.fillRect(gx * cell, bandRow * cell, cell, cell * 0.5); + } + noiseOverlay(ctx, size, 0.045, 778); break; } case 'mosaic-roman': { @@ -611,6 +850,9 @@ const ROUGHNESS: Partial> = { 'marble-veined-carrara': 0.16, 'marble-veined-emerald': 0.18, 'marble-checker': 0.14, + 'marble-opus-sectile': 0.2, + 'marble-revetment': 0.34, + 'coffered-wood': 0.72, 'gilded-stucco': 0.22, 'velvet-crimson': 0.92, 'velvet-navy': 0.92, @@ -638,6 +880,8 @@ const ROUGHNESS: Partial> = { const METALNESS: Partial> = { 'marble-white': 0.08, 'marble-veined-carrara': 0.1, + 'marble-opus-sectile': 0.14, + 'marble-revetment': 0.1, 'gilded-stucco': 0.65, 'concrete-polished': 0.12, 'terrazzo': 0.15, @@ -647,6 +891,9 @@ const METALNESS: Partial> = { const METERS: Partial> = { 'marble-checker': 2.4, + 'marble-opus-sectile': 5.0, + 'marble-revetment': 4.4, + 'coffered-wood': 4.2, flagstone: 3.0, 'mosaic-byzantine': 1.8, 'mosaic-roman': 2.0, @@ -669,6 +916,9 @@ const NORMAL_SCALE: Partial> = { 'painted-lime': 0.62, 'marble-white': 0.75, 'marble-veined-carrara': 0.8, + 'marble-opus-sectile': 0.55, + 'marble-revetment': 0.6, + 'coffered-wood': 1.0, 'velvet-crimson': 0.5, 'velvet-navy': 0.5, 'velvet-emerald': 0.5,