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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1853222e01
commit
1cb9eef935
@@ -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<Record<SurfaceTextureKind, number>> = {
|
||||
'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<Record<SurfaceTextureKind, number>> = {
|
||||
const METALNESS: Partial<Record<SurfaceTextureKind, number>> = {
|
||||
'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<Record<SurfaceTextureKind, number>> = {
|
||||
|
||||
const METERS: Partial<Record<SurfaceTextureKind, number>> = {
|
||||
'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<Record<SurfaceTextureKind, number>> = {
|
||||
'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,
|
||||
|
||||
Reference in New Issue
Block a user