Rework Gothic hall as a stone nave interior.
Add gothic ashlar/vault/floor textures, compound piers with transverse ribs, proper lancet arches, textured door-flanking panels, and lightScale for shaft-lit naves. Docs updated in basics and data-and-images. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
1cb9eef935
commit
f9b0fb0496
@@ -336,11 +336,13 @@ 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) |
|
| Wall order | Same U-shaped hang as artist halls (left → end → right) |
|
||||||
| Frame captions | **Year · artist** label below each frame |
|
| 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 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 |
|
| 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 |
|
| 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`) |
|
| 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 |
|
| 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 |
|
| Period details | `MovementHallDetails.tsx` — classical / neoclassical use **shallow engaged corner pilasters** (never freestanding mid-hall or proud corner shafts that cover frames); `byzantine` adds engaged porphyry colonnettes with basket capitals, a marble revetment dado, and hanging brass polycandela; `gothic` adds bay-spaced compound piers with vault springers, transverse ribs arching across the nave, and a moulded string course — all flush to the side walls |
|
||||||
| Back wall | Single wing: solid display wall. Multi-wing: **Exit double doors** → **Wing navigator** or **Exit to Timeline** |
|
| 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 |
|
| 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 |
|
| Influence lamps | Same golden upside-down emissive fixtures as artist halls when `has_influence_links` is true |
|
||||||
|
|||||||
@@ -278,7 +278,9 @@ To add or fix a branch, edit `MOVEMENT_LINEAGE` in that file and rebuild the cli
|
|||||||
|
|
||||||
## Movement gallery interiors (frontend)
|
## 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.
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -69,10 +69,56 @@ function WindowFrame({
|
|||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{arch && style === 'gothic-lancet' && (
|
{arch && style === 'gothic-lancet' && (
|
||||||
<mesh position={[0, height / 2 + 0.15, -depth / 2 + 0.02]}>
|
<>
|
||||||
<coneGeometry args={[width / 2 + frameW, 0.5, 4]} />
|
{/* Two-centred arch head built from short chords */}
|
||||||
<meshStandardMaterial color={trimColor} roughness={0.4} metalness={0.35} />
|
{Array.from({ length: 14 }, (_, i) => {
|
||||||
</mesh>
|
const segs = 14;
|
||||||
|
const rise = width * 0.62;
|
||||||
|
const pt = (t: number): [number, number] => {
|
||||||
|
const x = (t - 0.5) * (width + frameW * 2);
|
||||||
|
const y = Math.pow(Math.cos((t - 0.5) * Math.PI), 0.72) * rise;
|
||||||
|
return [x, y];
|
||||||
|
};
|
||||||
|
const [x0, y0] = pt(i / segs);
|
||||||
|
const [x1, y1] = pt((i + 1) / segs);
|
||||||
|
const len = Math.hypot(x1 - x0, y1 - y0);
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
key={i}
|
||||||
|
position={[(x0 + x1) / 2, height / 2 + (y0 + y1) / 2, -depth / 2 + 0.02]}
|
||||||
|
rotation={[0, 0, Math.atan2(y1 - y0, x1 - x0)]}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[len * 1.15, frameW * 1.4, depth]} />
|
||||||
|
<meshStandardMaterial color={trimColor} roughness={0.55} metalness={0.15} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* Glazed spandrel filling the arch head */}
|
||||||
|
<mesh position={[0, height / 2 + width * 0.16, 0.01]}>
|
||||||
|
<planeGeometry args={[width * 0.72, width * 0.5]} />
|
||||||
|
<meshStandardMaterial
|
||||||
|
color="#9fc4ef"
|
||||||
|
emissive="#9fc4ef"
|
||||||
|
emissiveIntensity={0.6}
|
||||||
|
toneMapped={false}
|
||||||
|
transparent
|
||||||
|
opacity={0.8}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
{/* Mullions and transoms */}
|
||||||
|
{[-width / 4, width / 4].map((ox) => (
|
||||||
|
<mesh key={ox} position={[ox, 0, -depth / 2 + 0.03]}>
|
||||||
|
<boxGeometry args={[0.045, height, depth]} />
|
||||||
|
<meshStandardMaterial color={trimColor} roughness={0.55} metalness={0.15} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
{[height / 3, -height / 6].map((oy) => (
|
||||||
|
<mesh key={oy} position={[0, oy, -depth / 2 + 0.03]}>
|
||||||
|
<boxGeometry args={[width, 0.035, depth]} />
|
||||||
|
<meshStandardMaterial color={trimColor} roughness={0.55} metalness={0.15} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{style === 'roman-arch' && (
|
{style === 'roman-arch' && (
|
||||||
|
|||||||
@@ -100,41 +100,94 @@ function BaroqueDetails({ width, depth, halfW, halfD, trim }: Props & { trim: st
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MedievalDetails({ halfW, halfD }: Pick<Props, 'halfW' | 'halfD'>) {
|
function GothicDetails({ width, depth, halfW, halfD, trim }: Props & { trim: string }) {
|
||||||
const torchPositions = useMemo(
|
// 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.
|
||||||
[-halfW + 0.2, -halfD * 0.5],
|
const bays = useMemo(() => {
|
||||||
[halfW - 0.2, -halfD * 0.5],
|
const spacing = 4.2;
|
||||||
[-halfW + 0.2, halfD * 0.3],
|
const count = Math.max(2, Math.min(9, Math.floor(depth / spacing)));
|
||||||
[halfW - 0.2, halfD * 0.3],
|
const step = depth / (count + 1);
|
||||||
] as [number, number][],
|
return Array.from({ length: count }, (_, i) => -halfD + step * (i + 1));
|
||||||
[halfW, halfD]
|
}, [depth, halfD]);
|
||||||
);
|
|
||||||
|
const shaftHeight = 3.5;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{torchPositions.map(([x, z], i) => (
|
{bays.map((z, i) => (
|
||||||
<group key={i} position={[x, 2.2, z]}>
|
<group key={i}>
|
||||||
<mesh>
|
{[-1, 1].map((side) => (
|
||||||
<boxGeometry args={[0.08, 0.35, 0.12]} />
|
<group key={side} position={[side * (halfW - 0.09), 0, z]}>
|
||||||
<meshStandardMaterial color="#3a3028" roughness={0.9} />
|
{/* Compound pier: a heavier centre shaft flanked by colonnettes */}
|
||||||
</mesh>
|
<mesh position={[0, shaftHeight / 2, 0]}>
|
||||||
<pointLight position={[0, 0.15, 0.08]} intensity={0.65} distance={5} color="#ff9830" />
|
<cylinderGeometry args={[0.1, 0.115, shaftHeight, 12]} />
|
||||||
<mesh position={[0, 0.2, 0.06]}>
|
<meshStandardMaterial color="#a89e88" roughness={0.78} metalness={0.04} />
|
||||||
<sphereGeometry args={[0.06, 8, 8]} />
|
</mesh>
|
||||||
<meshStandardMaterial color="#ffb040" emissive="#ff8010" emissiveIntensity={0.8} toneMapped={false} />
|
{[-0.17, 0.17].map((dz) => (
|
||||||
</mesh>
|
<mesh key={dz} position={[0, shaftHeight / 2, dz]}>
|
||||||
|
<cylinderGeometry args={[0.052, 0.06, shaftHeight, 10]} />
|
||||||
|
<meshStandardMaterial color="#9e9482" roughness={0.82} metalness={0.03} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
{/* Moulded base and foliate capital */}
|
||||||
|
<mesh position={[0, 0.1, 0]}>
|
||||||
|
<boxGeometry args={[0.28, 0.2, 0.52]} />
|
||||||
|
<meshStandardMaterial color="#978d79" roughness={0.85} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, shaftHeight + 0.12, 0]}>
|
||||||
|
<boxGeometry args={[0.3, 0.24, 0.56]} />
|
||||||
|
<meshStandardMaterial color="#b3a892" roughness={0.7} metalness={0.05} />
|
||||||
|
</mesh>
|
||||||
|
{/* Vault springer resting on the capital */}
|
||||||
|
<mesh position={[0, shaftHeight + 0.36, 0]} rotation={[0, 0, Math.PI / 4]}>
|
||||||
|
<boxGeometry args={[0.17, 0.17, 0.46]} />
|
||||||
|
<meshStandardMaterial color={trim} roughness={0.72} metalness={0.06} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Transverse rib arching across the nave between opposite piers */}
|
||||||
|
<group position={[0, shaftHeight + 0.42, z]}>
|
||||||
|
{Array.from({ length: 9 }, (_, s) => {
|
||||||
|
const segs = 9;
|
||||||
|
const t0 = s / segs;
|
||||||
|
const t1 = (s + 1) / segs;
|
||||||
|
const xAt = (t: number) => -halfW + 0.09 + t * (halfW - 0.09) * 2;
|
||||||
|
const yAt = (t: number) => Math.sin(Math.PI * t) * 0.34;
|
||||||
|
const x0 = xAt(t0);
|
||||||
|
const x1 = xAt(t1);
|
||||||
|
const y0 = yAt(t0);
|
||||||
|
const y1 = yAt(t1);
|
||||||
|
const len = Math.hypot(x1 - x0, y1 - y0);
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
key={s}
|
||||||
|
position={[(x0 + x1) / 2, (y0 + y1) / 2, 0]}
|
||||||
|
rotation={[0, 0, Math.atan2(y1 - y0, x1 - x0)]}
|
||||||
|
>
|
||||||
|
<boxGeometry args={[len * 1.06, 0.13, 0.16]} />
|
||||||
|
<meshStandardMaterial color="#b0a58f" roughness={0.74} metalness={0.05} />
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Rough stone courses */}
|
{/* Moulded string course running the length of both side walls */}
|
||||||
{[-halfD + 0.08, halfD - 0.08].map((z, i) => (
|
{[-1, 1].map((side) => (
|
||||||
<mesh key={i} position={[0, 1.5, z]}>
|
<mesh key={side} position={[side * (halfW - 0.05), 2.62, 0]}>
|
||||||
<boxGeometry args={[0.04, 3, 0.04]} />
|
<boxGeometry args={[0.12, 0.1, depth - 0.2]} />
|
||||||
<meshStandardMaterial color="#6a6458" roughness={0.98} />
|
<meshStandardMaterial color="#b3a892" roughness={0.7} metalness={0.05} />
|
||||||
</mesh>
|
</mesh>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Chancel-style cornice on the end wall */}
|
||||||
|
<mesh position={[0, 3.96, -halfD + 0.1]}>
|
||||||
|
<boxGeometry args={[Math.max(2.4, width - 0.4), 0.16, 0.18]} />
|
||||||
|
<meshStandardMaterial color="#b0a58f" roughness={0.7} metalness={0.05} />
|
||||||
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -324,8 +377,8 @@ export default function MovementHallDetails({ style, width, depth, halfW, halfD
|
|||||||
return <PalazzoDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
|
return <PalazzoDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
|
||||||
case 'baroque':
|
case 'baroque':
|
||||||
return <BaroqueDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
|
return <BaroqueDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
|
||||||
case 'medieval':
|
case 'gothic':
|
||||||
return <MedievalDetails halfW={halfW} halfD={halfD} />;
|
return <GothicDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
|
||||||
case 'byzantine':
|
case 'byzantine':
|
||||||
return <ByzantineDetails halfW={halfW} halfD={halfD} trim={trim} />;
|
return <ByzantineDetails halfW={halfW} halfD={halfD} trim={trim} />;
|
||||||
case 'neoclassical':
|
case 'neoclassical':
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
import { galleryImageUrlCandidates, imageUrl, api } from '../api/client';
|
import { galleryImageUrlCandidates, imageUrl, api } from '../api/client';
|
||||||
import { comparePaintingsChronological, paintingHasCuratorNotes, paintingHasInfluenceLinks, paintingWallCaption } from '../utils/paintingUtils';
|
import { comparePaintingsChronological, paintingHasCuratorNotes, paintingHasInfluenceLinks, paintingWallCaption } from '../utils/paintingUtils';
|
||||||
import { cloneSurfaceTexture, getSurfaceTexture } from '../utils/galleryProceduralTextures';
|
import { cloneSurfaceTexture, getSurfaceTexture, type SurfaceTextureKind } from '../utils/galleryProceduralTextures';
|
||||||
import { resolveMovementInteriorStyle, type MovementInteriorStyle, type GalleryWindowSpec } from '../data/movement-interior-styles';
|
import { resolveMovementInteriorStyle, type MovementInteriorStyle, type GalleryWindowSpec } from '../data/movement-interior-styles';
|
||||||
import { useTexturedMaterial } from '../hooks/useTexturedMaterial';
|
import { useTexturedMaterial } from '../hooks/useTexturedMaterial';
|
||||||
import MovementHallDetails from './MovementHallDetails';
|
import MovementHallDetails from './MovementHallDetails';
|
||||||
@@ -1059,13 +1059,31 @@ function GalleryWall({
|
|||||||
size,
|
size,
|
||||||
rotation = [0, 0, 0],
|
rotation = [0, 0, 0],
|
||||||
color = '#ebe4d8',
|
color = '#ebe4d8',
|
||||||
|
kind,
|
||||||
|
tint,
|
||||||
}: {
|
}: {
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
size: [number, number];
|
size: [number, number];
|
||||||
rotation?: [number, number, number];
|
rotation?: [number, number, number];
|
||||||
color?: string;
|
color?: string;
|
||||||
|
/** When set, the panel is textured like the rest of the hall instead of flat colour. */
|
||||||
|
kind?: SurfaceTextureKind;
|
||||||
|
tint?: string;
|
||||||
}) {
|
}) {
|
||||||
const [w, h] = size;
|
const [w, h] = size;
|
||||||
|
// Door-flanking panels would otherwise read as flat single-colour blocks
|
||||||
|
// beside fully textured walls.
|
||||||
|
if (kind) {
|
||||||
|
return (
|
||||||
|
<TexturedWall
|
||||||
|
kind={kind}
|
||||||
|
tint={tint ?? color}
|
||||||
|
position={position}
|
||||||
|
size={[w, h, WALL_THICKNESS]}
|
||||||
|
rotation={rotation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<mesh position={position} rotation={rotation} renderOrder={0}>
|
<mesh position={position} rotation={rotation} renderOrder={0}>
|
||||||
<boxGeometry args={[w, h, WALL_THICKNESS]} />
|
<boxGeometry args={[w, h, WALL_THICKNESS]} />
|
||||||
@@ -1563,16 +1581,22 @@ function ArtistHall({
|
|||||||
position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]}
|
position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]}
|
||||||
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
|
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
|
||||||
color={walls.main}
|
color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall}
|
||||||
/>
|
/>
|
||||||
<GalleryWall
|
<GalleryWall
|
||||||
position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]}
|
position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]}
|
||||||
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
|
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
|
||||||
color={walls.main}
|
color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall}
|
||||||
/>
|
/>
|
||||||
<GalleryWall
|
<GalleryWall
|
||||||
position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, halfD]}
|
position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, halfD]}
|
||||||
size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]}
|
size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]}
|
||||||
color={walls.main}
|
color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall}
|
||||||
/>
|
/>
|
||||||
<HallPassage
|
<HallPassage
|
||||||
position={[0, 0, halfD - WALL_THICKNESS / 2 - 0.02]}
|
position={[0, 0, halfD - WALL_THICKNESS / 2 - 0.02]}
|
||||||
@@ -1584,12 +1608,20 @@ function ArtistHall({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : movementMode && endWallHasDoor ? (
|
) : movementMode && endWallHasDoor ? (
|
||||||
<GalleryWall position={[0, WALL_HEIGHT / 2, halfD]} size={[width, WALL_HEIGHT]} color={walls.main} />
|
<GalleryWall position={[0, WALL_HEIGHT / 2, halfD]} size={[width, WALL_HEIGHT]} color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<GalleryWall position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]} size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]} color={walls.main} />
|
<GalleryWall position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]} size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]} color={walls.main}
|
||||||
<GalleryWall position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]} size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]} color={walls.main} />
|
kind={interiorStyle?.surfaces.wall}
|
||||||
<GalleryWall position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, halfD]} size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]} color={walls.main} />
|
tint={interiorStyle?.tints.wall} />
|
||||||
|
<GalleryWall position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]} size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]} color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall} />
|
||||||
|
<GalleryWall position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, halfD]} size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]} color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall} />
|
||||||
<ExitPortal
|
<ExitPortal
|
||||||
position={[0, 0, halfD - WALL_THICKNESS / 2 - 0.02]}
|
position={[0, 0, halfD - WALL_THICKNESS / 2 - 0.02]}
|
||||||
active={nearExit}
|
active={nearExit}
|
||||||
@@ -1608,16 +1640,22 @@ function ArtistHall({
|
|||||||
position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, -halfD]}
|
position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, -halfD]}
|
||||||
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
|
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
|
||||||
color={walls.main}
|
color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall}
|
||||||
/>
|
/>
|
||||||
<GalleryWall
|
<GalleryWall
|
||||||
position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, -halfD]}
|
position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, -halfD]}
|
||||||
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
|
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
|
||||||
color={walls.main}
|
color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall}
|
||||||
/>
|
/>
|
||||||
<GalleryWall
|
<GalleryWall
|
||||||
position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, -halfD]}
|
position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, -halfD]}
|
||||||
size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]}
|
size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]}
|
||||||
color={walls.main}
|
color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall}
|
||||||
/>
|
/>
|
||||||
<group position={[0, 0, -halfD + WALL_THICKNESS / 2 + 0.02]} rotation={[0, Math.PI, 0]}>
|
<group position={[0, 0, -halfD + WALL_THICKNESS / 2 + 0.02]} rotation={[0, Math.PI, 0]}>
|
||||||
<ExitPortal
|
<ExitPortal
|
||||||
@@ -1639,7 +1677,9 @@ function ArtistHall({
|
|||||||
size={[width, WALL_HEIGHT, WALL_THICKNESS]}
|
size={[width, WALL_HEIGHT, WALL_THICKNESS]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<GalleryWall position={[0, WALL_HEIGHT / 2, -halfD]} size={[width, WALL_HEIGHT]} color={walls.main} />
|
<GalleryWall position={[0, WALL_HEIGHT / 2, -halfD]} size={[width, WALL_HEIGHT]} color={walls.main}
|
||||||
|
kind={interiorStyle?.surfaces.wall}
|
||||||
|
tint={interiorStyle?.tints.wall} />
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export interface MovementInteriorStyle {
|
|||||||
trackLights: number;
|
trackLights: number;
|
||||||
/** Scales the shared hall/scene lights. <1 for deliberately dim period interiors. */
|
/** Scales the shared hall/scene lights. <1 for deliberately dim period interiors. */
|
||||||
lightScale: number;
|
lightScale: number;
|
||||||
details: 'palazzo' | 'baroque' | 'medieval' | 'byzantine' | 'neoclassical' | 'salon' | 'modern' | 'classical' | 'museum' | 'industrial' | 'atelier';
|
details: 'palazzo' | 'baroque' | 'gothic' | 'byzantine' | 'neoclassical' | 'salon' | 'modern' | 'classical' | 'museum' | 'industrial' | 'atelier';
|
||||||
}
|
}
|
||||||
|
|
||||||
function windows(...specs: GalleryWindowSpec[]): GalleryWindowSpec[] {
|
function windows(...specs: GalleryWindowSpec[]): GalleryWindowSpec[] {
|
||||||
@@ -144,26 +144,30 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
|
|||||||
),
|
),
|
||||||
29: mk(
|
29: mk(
|
||||||
'gothic-cathedral',
|
'gothic-cathedral',
|
||||||
'Gothic hall',
|
'Gothic nave',
|
||||||
'Stone vault · tall lancet windows · flagstone floor',
|
'Blind-arcaded ashlar · ribbed stone vault · worn flagstones',
|
||||||
{ wall: 'rough-stone', ceiling: 'basalt', floor: 'flagstone' },
|
{ wall: 'gothic-ashlar', ceiling: 'gothic-vault', floor: 'gothic-stone-floor' },
|
||||||
{ wall: '#8a8478', ceiling: '#3a3630', floor: '#6a6458', trim: '#5c5648' },
|
{ wall: '#8d8471', ceiling: '#6d6555', floor: '#6f6759', trim: '#7d7159' },
|
||||||
{
|
{
|
||||||
details: 'medieval',
|
details: 'gothic',
|
||||||
titleColor: '#e8dcc8',
|
titleColor: '#efe4cc',
|
||||||
ambient: 0.72,
|
ambient: 0.6,
|
||||||
warmLight: '#e8d8c0',
|
warmLight: '#ffe6bc',
|
||||||
sunLight: '#d0e8ff',
|
sunLight: '#dbe8ff',
|
||||||
// Keep atmosphere dark, but not pure void (walls must stay readable).
|
// Cool shadowed stone, but never a pure void — walls must stay readable.
|
||||||
fog: '#1a1816',
|
fog: '#1b1a18',
|
||||||
background: '#141210',
|
background: '#131211',
|
||||||
windows: windows(
|
windows: windows(
|
||||||
{ wall: 'back', x: -2, y: 2.8, width: 0.8, height: 2.8, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 3.5 },
|
{ wall: 'back', x: -2, y: 2.8, width: 0.8, height: 2.8, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 3.8 },
|
||||||
{ wall: 'back', x: 2, y: 2.8, width: 0.8, height: 2.8, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 3.5 },
|
{ wall: 'back', x: 2, y: 2.8, width: 0.8, height: 2.8, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 3.8 },
|
||||||
{ wall: 'left', x: 0, y: 3.0, width: 0.9, height: 2.6, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 2.8 },
|
{ wall: 'left', x: -4.5, y: 3.0, width: 0.9, height: 2.6, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 3.2 },
|
||||||
{ wall: 'right', x: 0, y: 3.0, width: 0.9, height: 2.6, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 2.8 }
|
{ wall: 'left', x: 4.5, y: 3.0, width: 0.9, height: 2.6, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 3.2 },
|
||||||
|
{ wall: 'right', x: -4.5, y: 3.0, width: 0.9, height: 2.6, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 3.2 },
|
||||||
|
{ wall: 'right', x: 4.5, y: 3.0, width: 0.9, height: 2.6, style: 'gothic-lancet', lightColor: '#c8e0ff', lightIntensity: 3.2 }
|
||||||
),
|
),
|
||||||
trackLights: 0.85,
|
trackLights: 0.5,
|
||||||
|
// Naves are lit by window shafts against shadowed stone, not flooded.
|
||||||
|
lightScale: 0.26,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
30: mk(
|
30: mk(
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export type SurfaceTextureKind =
|
|||||||
| 'limestone'
|
| 'limestone'
|
||||||
| 'sandstone'
|
| 'sandstone'
|
||||||
| 'rough-stone'
|
| 'rough-stone'
|
||||||
|
| 'gothic-ashlar'
|
||||||
|
| 'gothic-vault'
|
||||||
|
| 'gothic-stone-floor'
|
||||||
| 'basalt'
|
| 'basalt'
|
||||||
| 'stucco-cream'
|
| 'stucco-cream'
|
||||||
| 'stucco-terracotta'
|
| 'stucco-terracotta'
|
||||||
@@ -247,6 +250,52 @@ function marbleVeined(ctx: CanvasRenderingContext2D, size: number, base: string,
|
|||||||
noiseOverlay(ctx, size, 0.035, seed + 1);
|
noiseOverlay(ctx, size, 0.035, seed + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two-centred (pointed) arch path from the springing line up to the apex.
|
||||||
|
* `springY` is the bottom of the curve, `apexY` the crown — canvas y grows down.
|
||||||
|
*/
|
||||||
|
function pointedArchPath(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
cx: number,
|
||||||
|
halfW: number,
|
||||||
|
springY: number,
|
||||||
|
apexY: number
|
||||||
|
) {
|
||||||
|
const shoulder = apexY + (springY - apexY) * 0.28;
|
||||||
|
ctx.moveTo(cx - halfW, springY);
|
||||||
|
ctx.quadraticCurveTo(cx - halfW, shoulder, cx, apexY);
|
||||||
|
ctx.quadraticCurveTo(cx + halfW, shoulder, cx + halfW, springY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Coursed ashlar masonry — staggered blocks with cut joints and per-block tone. */
|
||||||
|
function ashlarCourses(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
size: number,
|
||||||
|
base: string,
|
||||||
|
seed: number,
|
||||||
|
courses = 12
|
||||||
|
) {
|
||||||
|
fill(ctx, size, base);
|
||||||
|
const [br, bg, bb] = hexToRgb(base);
|
||||||
|
const ch = size / courses;
|
||||||
|
const bw = size / 6;
|
||||||
|
for (let row = 0; row < courses; row++) {
|
||||||
|
const offset = row % 2 ? bw / 2 : 0;
|
||||||
|
for (let col = -1; col < 7; col++) {
|
||||||
|
const rand = seeded(seed + row * 11 + col * 3);
|
||||||
|
const t = (rand() - 0.5) * 22;
|
||||||
|
ctx.fillStyle = rgb(br + t, bg + t, bb + t * 0.9);
|
||||||
|
ctx.fillRect(col * bw + offset, row * ch, bw - 1.5, ch - 1.5);
|
||||||
|
}
|
||||||
|
// Recessed bed joint with a lit lower lip
|
||||||
|
ctx.fillStyle = 'rgba(60,52,40,0.35)';
|
||||||
|
ctx.fillRect(0, row * ch + ch - 1.5, size, 1.5);
|
||||||
|
ctx.fillStyle = 'rgba(255,250,235,0.16)';
|
||||||
|
ctx.fillRect(0, row * ch, size, 1);
|
||||||
|
}
|
||||||
|
noiseOverlay(ctx, size, 0.05, seed + 1);
|
||||||
|
}
|
||||||
|
|
||||||
/** Veined marble confined to one slab rect — for revetment panels and inlay. */
|
/** Veined marble confined to one slab rect — for revetment panels and inlay. */
|
||||||
function marbleSlab(
|
function marbleSlab(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
@@ -572,6 +621,235 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
|
|||||||
fill(ctx, size, '#3a3834');
|
fill(ctx, size, '#3a3834');
|
||||||
noiseOverlay(ctx, size, 0.15, 107);
|
noiseOverlay(ctx, size, 0.15, 107);
|
||||||
break;
|
break;
|
||||||
|
case 'gothic-ashlar': {
|
||||||
|
// One tile spans a full wall height: plinth, tall blind arcade, string
|
||||||
|
// course, triforium gallery, cornice — so the wall reads as architecture
|
||||||
|
// rather than a flat stone panel. Relief comes through the normal map.
|
||||||
|
ashlarCourses(ctx, size, '#c9bda6', 210, 13);
|
||||||
|
|
||||||
|
const stringCourse = (y: number, h: number) => {
|
||||||
|
ctx.fillStyle = 'rgba(250,244,228,0.5)';
|
||||||
|
ctx.fillRect(0, y, size, h * 0.45);
|
||||||
|
ctx.fillStyle = 'rgba(74,64,50,0.45)';
|
||||||
|
ctx.fillRect(0, y + h * 0.45, size, h * 0.55);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Plinth and cornice
|
||||||
|
ctx.fillStyle = 'rgba(60,52,40,0.22)';
|
||||||
|
ctx.fillRect(0, size * 0.9, size, size * 0.1);
|
||||||
|
stringCourse(size * 0.885, size * 0.022);
|
||||||
|
stringCourse(size * 0.055, size * 0.028);
|
||||||
|
stringCourse(size * 0.375, size * 0.024);
|
||||||
|
|
||||||
|
// Tall blind arcade — 3 bays across the tile
|
||||||
|
const bays = 3;
|
||||||
|
const bayW = size / bays;
|
||||||
|
for (let i = 0; i < bays; i++) {
|
||||||
|
const cx = bayW * (i + 0.5);
|
||||||
|
const halfW = bayW * 0.36;
|
||||||
|
const springY = size * 0.66;
|
||||||
|
const apexY = size * 0.43;
|
||||||
|
|
||||||
|
// Recessed panel behind the arch
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
pointedArchPath(ctx, cx, halfW, springY, apexY);
|
||||||
|
ctx.lineTo(cx + halfW, size * 0.885);
|
||||||
|
ctx.lineTo(cx - halfW, size * 0.885);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.clip();
|
||||||
|
ctx.fillStyle = 'rgba(52,44,34,0.34)';
|
||||||
|
ctx.fillRect(cx - halfW, apexY, halfW * 2, size);
|
||||||
|
const shade = ctx.createLinearGradient(cx - halfW, 0, cx + halfW, 0);
|
||||||
|
shade.addColorStop(0, 'rgba(20,16,12,0.4)');
|
||||||
|
shade.addColorStop(0.45, 'rgba(20,16,12,0)');
|
||||||
|
shade.addColorStop(1, 'rgba(20,16,12,0.28)');
|
||||||
|
ctx.fillStyle = shade;
|
||||||
|
ctx.fillRect(cx - halfW, apexY, halfW * 2, size);
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Arch moulding: lit outer roll, dark soffit
|
||||||
|
ctx.beginPath();
|
||||||
|
pointedArchPath(ctx, cx, halfW + size * 0.016, springY, apexY - size * 0.02);
|
||||||
|
ctx.strokeStyle = 'rgba(252,246,230,0.62)';
|
||||||
|
ctx.lineWidth = size * 0.016;
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
pointedArchPath(ctx, cx, halfW, springY, apexY);
|
||||||
|
ctx.strokeStyle = 'rgba(58,48,36,0.5)';
|
||||||
|
ctx.lineWidth = size * 0.008;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Colonnette shafts carrying the arch
|
||||||
|
for (const sx of [cx - halfW, cx + halfW]) {
|
||||||
|
ctx.fillStyle = 'rgba(250,244,228,0.5)';
|
||||||
|
ctx.fillRect(sx - size * 0.011, springY, size * 0.014, size * 0.225);
|
||||||
|
ctx.fillStyle = 'rgba(58,48,36,0.42)';
|
||||||
|
ctx.fillRect(sx + size * 0.003, springY, size * 0.005, size * 0.225);
|
||||||
|
// Moulded capital and base
|
||||||
|
ctx.fillStyle = 'rgba(252,246,230,0.62)';
|
||||||
|
ctx.fillRect(sx - size * 0.019, springY - size * 0.016, size * 0.038, size * 0.016);
|
||||||
|
ctx.fillRect(sx - size * 0.017, size * 0.868, size * 0.034, size * 0.017);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triforium gallery — paired small arches per bay
|
||||||
|
for (let i = 0; i < bays; i++) {
|
||||||
|
const bayCx = bayW * (i + 0.5);
|
||||||
|
for (const sub of [-1, 1]) {
|
||||||
|
const cx = bayCx + sub * bayW * 0.19;
|
||||||
|
const halfW = bayW * 0.13;
|
||||||
|
const springY = size * 0.29;
|
||||||
|
const apexY = size * 0.135;
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
pointedArchPath(ctx, cx, halfW, springY, apexY);
|
||||||
|
ctx.lineTo(cx + halfW, size * 0.345);
|
||||||
|
ctx.lineTo(cx - halfW, size * 0.345);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.clip();
|
||||||
|
ctx.fillStyle = 'rgba(28,24,18,0.55)';
|
||||||
|
ctx.fillRect(cx - halfW, apexY, halfW * 2, size * 0.3);
|
||||||
|
ctx.restore();
|
||||||
|
ctx.beginPath();
|
||||||
|
pointedArchPath(ctx, cx, halfW + size * 0.009, springY, apexY - size * 0.012);
|
||||||
|
ctx.strokeStyle = 'rgba(252,246,230,0.58)';
|
||||||
|
ctx.lineWidth = size * 0.009;
|
||||||
|
ctx.stroke();
|
||||||
|
for (const sx of [cx - halfW, cx + halfW]) {
|
||||||
|
ctx.fillStyle = 'rgba(250,244,228,0.5)';
|
||||||
|
ctx.fillRect(sx - size * 0.006, springY, size * 0.008, size * 0.055);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
noiseOverlay(ctx, size, 0.035, 211);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'gothic-vault': {
|
||||||
|
// Quadripartite rib vault with tiercerons, seen from below: webbing
|
||||||
|
// panels lift toward a gilded central boss.
|
||||||
|
fill(ctx, size, '#cec4ac');
|
||||||
|
const c = size / 2;
|
||||||
|
const glow = ctx.createRadialGradient(c, c, size * 0.05, c, c, size * 0.62);
|
||||||
|
glow.addColorStop(0, 'rgba(255,250,236,0.55)');
|
||||||
|
glow.addColorStop(1, 'rgba(38,32,25,0.72)');
|
||||||
|
ctx.fillStyle = glow;
|
||||||
|
ctx.fillRect(0, 0, size, size);
|
||||||
|
for (let y = 0; y < size; y += 4) {
|
||||||
|
for (let x = 0; x < size; x += 4) {
|
||||||
|
const n = (fbm(x / 90, y / 90, 221) - 0.5) * 14;
|
||||||
|
ctx.fillStyle = `rgba(${210 + n | 0},${200 + n | 0},${176 + n | 0},0.35)`;
|
||||||
|
ctx.fillRect(x, y, 4, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rib = (x0: number, y0: number, x1: number, y1: number, w: number) => {
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.strokeStyle = 'rgba(48,40,30,0.5)';
|
||||||
|
ctx.lineWidth = w * 1.55;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x0, y0);
|
||||||
|
ctx.lineTo(x1, y1);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.strokeStyle = 'rgba(236,228,206,0.95)';
|
||||||
|
ctx.lineWidth = w;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x0, y0);
|
||||||
|
ctx.lineTo(x1, y1);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.strokeStyle = 'rgba(255,252,242,0.75)';
|
||||||
|
ctx.lineWidth = w * 0.32;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x0, y0);
|
||||||
|
ctx.lineTo(x1, y1);
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
const major = size * 0.044;
|
||||||
|
const minor = size * 0.026;
|
||||||
|
// Transverse and wall ribs around the bay
|
||||||
|
rib(0, 0, size, 0, major);
|
||||||
|
rib(0, size, size, size, major);
|
||||||
|
rib(0, 0, 0, size, major);
|
||||||
|
rib(size, 0, size, size, major);
|
||||||
|
// Diagonal cross ribs
|
||||||
|
rib(0, 0, size, size, major);
|
||||||
|
rib(size, 0, 0, size, major);
|
||||||
|
// Tiercerons springing to the edge midpoints
|
||||||
|
rib(0, 0, c, 0, minor);
|
||||||
|
rib(0, 0, 0, c, minor);
|
||||||
|
rib(size, 0, c, 0, minor);
|
||||||
|
rib(size, 0, size, c, minor);
|
||||||
|
rib(0, size, c, size, minor);
|
||||||
|
rib(0, size, 0, c, minor);
|
||||||
|
rib(size, size, c, size, minor);
|
||||||
|
rib(size, size, size, c, minor);
|
||||||
|
// Ridge ribs
|
||||||
|
rib(c, 0, c, size, minor);
|
||||||
|
rib(0, c, size, c, minor);
|
||||||
|
|
||||||
|
// Carved boss at the crown
|
||||||
|
ctx.fillStyle = 'rgba(46,38,28,0.55)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(c, c, size * 0.052, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = '#c9a94e';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(c, c, size * 0.042, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = 'rgba(255,246,216,0.75)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(c - size * 0.008, c - size * 0.008, size * 0.02, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
noiseOverlay(ctx, size, 0.03, 222);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'gothic-stone-floor': {
|
||||||
|
// Worn limestone flags with dark diamond cabochons at the slab corners.
|
||||||
|
fill(ctx, size, '#a89c86');
|
||||||
|
const n = 4;
|
||||||
|
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 rand = seeded(230 + row * n + col);
|
||||||
|
const t = (rand() - 0.5) * 26;
|
||||||
|
ctx.fillStyle = rgb(178 + t, 168 + t, 146 + t);
|
||||||
|
ctx.fillRect(x + 2, y + 2, cell - 4, cell - 4);
|
||||||
|
// Footfall polish toward the slab centre
|
||||||
|
const wear = ctx.createRadialGradient(
|
||||||
|
x + cell / 2, y + cell / 2, cell * 0.05,
|
||||||
|
x + cell / 2, y + cell / 2, cell * 0.55
|
||||||
|
);
|
||||||
|
wear.addColorStop(0, 'rgba(226,218,198,0.35)');
|
||||||
|
wear.addColorStop(1, 'rgba(226,218,198,0)');
|
||||||
|
ctx.fillStyle = wear;
|
||||||
|
ctx.fillRect(x + 2, y + 2, cell - 4, cell - 4);
|
||||||
|
ctx.strokeStyle = 'rgba(58,50,40,0.45)';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.strokeRect(x + 2, y + 2, cell - 4, cell - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dark cabochons where four slabs meet
|
||||||
|
for (let row = 1; row < n; row++) {
|
||||||
|
for (let col = 1; col < n; col++) {
|
||||||
|
const cx = col * cell;
|
||||||
|
const cy = row * cell;
|
||||||
|
const r = cell * 0.085;
|
||||||
|
ctx.fillStyle = '#3c3a3e';
|
||||||
|
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.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
noiseOverlay(ctx, size, 0.05, 231);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'stucco-cream':
|
case 'stucco-cream':
|
||||||
stuccoSurface(ctx, size, '#f0e8d8', 108);
|
stuccoSurface(ctx, size, '#f0e8d8', 108);
|
||||||
break;
|
break;
|
||||||
@@ -842,6 +1120,11 @@ function normalStrengthFor(kind: SurfaceTextureKind): number {
|
|||||||
if (kind.includes('stucco') || kind.includes('plaster')) return 2.6;
|
if (kind.includes('stucco') || kind.includes('plaster')) return 2.6;
|
||||||
if (kind.includes('panel') || kind.includes('oak') || kind.includes('walnut')) return 2.8;
|
if (kind.includes('panel') || kind.includes('oak') || kind.includes('walnut')) return 2.8;
|
||||||
if (kind === 'brick' || kind === 'rough-stone') return 3.2;
|
if (kind === 'brick' || kind === 'rough-stone') return 3.2;
|
||||||
|
// Arcading and rib mouldings are drawn as light/dark bands — lean on the
|
||||||
|
// derived normals so they read as carved relief, not painted-on lines.
|
||||||
|
if (kind === 'gothic-ashlar') return 4.2;
|
||||||
|
if (kind === 'gothic-vault') return 3.6;
|
||||||
|
if (kind === 'gothic-stone-floor') return 2.4;
|
||||||
return 2.5;
|
return 2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,6 +1136,9 @@ const ROUGHNESS: Partial<Record<SurfaceTextureKind, number>> = {
|
|||||||
'marble-opus-sectile': 0.2,
|
'marble-opus-sectile': 0.2,
|
||||||
'marble-revetment': 0.34,
|
'marble-revetment': 0.34,
|
||||||
'coffered-wood': 0.72,
|
'coffered-wood': 0.72,
|
||||||
|
'gothic-ashlar': 0.84,
|
||||||
|
'gothic-vault': 0.9,
|
||||||
|
'gothic-stone-floor': 0.56,
|
||||||
'gilded-stucco': 0.22,
|
'gilded-stucco': 0.22,
|
||||||
'velvet-crimson': 0.92,
|
'velvet-crimson': 0.92,
|
||||||
'velvet-navy': 0.92,
|
'velvet-navy': 0.92,
|
||||||
@@ -894,6 +1180,10 @@ const METERS: Partial<Record<SurfaceTextureKind, number>> = {
|
|||||||
'marble-opus-sectile': 5.0,
|
'marble-opus-sectile': 5.0,
|
||||||
'marble-revetment': 4.4,
|
'marble-revetment': 4.4,
|
||||||
'coffered-wood': 4.2,
|
'coffered-wood': 4.2,
|
||||||
|
// Matches WALL_HEIGHT so one tile = one full storey of arcading.
|
||||||
|
'gothic-ashlar': 4.2,
|
||||||
|
'gothic-vault': 5.5,
|
||||||
|
'gothic-stone-floor': 3.2,
|
||||||
flagstone: 3.0,
|
flagstone: 3.0,
|
||||||
'mosaic-byzantine': 1.8,
|
'mosaic-byzantine': 1.8,
|
||||||
'mosaic-roman': 2.0,
|
'mosaic-roman': 2.0,
|
||||||
@@ -919,6 +1209,9 @@ const NORMAL_SCALE: Partial<Record<SurfaceTextureKind, number>> = {
|
|||||||
'marble-opus-sectile': 0.55,
|
'marble-opus-sectile': 0.55,
|
||||||
'marble-revetment': 0.6,
|
'marble-revetment': 0.6,
|
||||||
'coffered-wood': 1.0,
|
'coffered-wood': 1.0,
|
||||||
|
'gothic-ashlar': 1.0,
|
||||||
|
'gothic-vault': 0.85,
|
||||||
|
'gothic-stone-floor': 0.45,
|
||||||
'velvet-crimson': 0.5,
|
'velvet-crimson': 0.5,
|
||||||
'velvet-navy': 0.5,
|
'velvet-navy': 0.5,
|
||||||
'velvet-emerald': 0.5,
|
'velvet-emerald': 0.5,
|
||||||
|
|||||||
Reference in New Issue
Block a user