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
@@ -14,6 +14,9 @@ export type SurfaceTextureKind =
|
||||
| 'limestone'
|
||||
| 'sandstone'
|
||||
| 'rough-stone'
|
||||
| 'gothic-ashlar'
|
||||
| 'gothic-vault'
|
||||
| 'gothic-stone-floor'
|
||||
| 'basalt'
|
||||
| 'stucco-cream'
|
||||
| 'stucco-terracotta'
|
||||
@@ -247,6 +250,52 @@ function marbleVeined(ctx: CanvasRenderingContext2D, size: number, base: string,
|
||||
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. */
|
||||
function marbleSlab(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
@@ -572,6 +621,235 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
|
||||
fill(ctx, size, '#3a3834');
|
||||
noiseOverlay(ctx, size, 0.15, 107);
|
||||
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':
|
||||
stuccoSurface(ctx, size, '#f0e8d8', 108);
|
||||
break;
|
||||
@@ -842,6 +1120,11 @@ function normalStrengthFor(kind: SurfaceTextureKind): number {
|
||||
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 === '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;
|
||||
}
|
||||
|
||||
@@ -853,6 +1136,9 @@ const ROUGHNESS: Partial<Record<SurfaceTextureKind, number>> = {
|
||||
'marble-opus-sectile': 0.2,
|
||||
'marble-revetment': 0.34,
|
||||
'coffered-wood': 0.72,
|
||||
'gothic-ashlar': 0.84,
|
||||
'gothic-vault': 0.9,
|
||||
'gothic-stone-floor': 0.56,
|
||||
'gilded-stucco': 0.22,
|
||||
'velvet-crimson': 0.92,
|
||||
'velvet-navy': 0.92,
|
||||
@@ -894,6 +1180,10 @@ const METERS: Partial<Record<SurfaceTextureKind, number>> = {
|
||||
'marble-opus-sectile': 5.0,
|
||||
'marble-revetment': 4.4,
|
||||
'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,
|
||||
'mosaic-byzantine': 1.8,
|
||||
'mosaic-roman': 2.0,
|
||||
@@ -919,6 +1209,9 @@ const NORMAL_SCALE: Partial<Record<SurfaceTextureKind, number>> = {
|
||||
'marble-opus-sectile': 0.55,
|
||||
'marble-revetment': 0.6,
|
||||
'coffered-wood': 1.0,
|
||||
'gothic-ashlar': 1.0,
|
||||
'gothic-vault': 0.85,
|
||||
'gothic-stone-floor': 0.45,
|
||||
'velvet-crimson': 0.5,
|
||||
'velvet-navy': 0.5,
|
||||
'velvet-emerald': 0.5,
|
||||
|
||||
Reference in New Issue
Block a user