import * as THREE from 'three'; /** Warm oak tones with subtle variation — museum-style herringbone parquet. */ const OAK_TONES = ['#6b4a2e', '#735234', '#624428', '#7a5638', '#5c3f26', '#6e4d30']; function shade(hex: string, amount: number): string { const n = parseInt(hex.slice(1), 16); const r = Math.min(255, Math.max(0, ((n >> 16) & 255) + amount)); const g = Math.min(255, Math.max(0, ((n >> 8) & 255) + amount)); const b = Math.min(255, Math.max(0, (n & 255) + amount)); return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`; } function drawPlank( ctx: CanvasRenderingContext2D, cx: number, cy: number, length: number, width: number, angle: number, color: string ) { ctx.save(); ctx.translate(cx, cy); ctx.rotate(angle); const grad = ctx.createLinearGradient(-length / 2, 0, length / 2, 0); grad.addColorStop(0, shade(color, -12)); grad.addColorStop(0.35, color); grad.addColorStop(0.65, shade(color, 6)); grad.addColorStop(1, shade(color, -8)); ctx.fillStyle = grad; ctx.fillRect(-length / 2, -width / 2, length, width); // Fine grain lines ctx.strokeStyle = 'rgba(0,0,0,0.12)'; ctx.lineWidth = 0.6; for (let g = -length / 2 + 4; g < length / 2; g += 7) { ctx.beginPath(); ctx.moveTo(g, -width / 2 + 1); ctx.lineTo(g + 3, width / 2 - 1); ctx.stroke(); } // Plank edge / grout ctx.strokeStyle = 'rgba(18,10,4,0.55)'; ctx.lineWidth = 1.1; ctx.strokeRect(-length / 2, -width / 2, length, width); ctx.restore(); } function paintHerringbone(ctx: CanvasRenderingContext2D, size: number) { ctx.fillStyle = '#2a1810'; ctx.fillRect(0, 0, size, size); const plankLen = 56; const plankW = 14; const step = plankLen * 0.707; // cos(45°) spacing for herringbone unit const cols = Math.ceil(size / step) + 3; const rows = Math.ceil(size / step) + 3; for (let row = -1; row < rows; row++) { for (let col = -1; col < cols; col++) { const px = col * step; const py = row * step; const tone = OAK_TONES[(row * 5 + col * 7 + size) % OAK_TONES.length]; if ((row + col) % 2 === 0) { drawPlank(ctx, px, py, plankLen, plankW, Math.PI / 4, tone); drawPlank(ctx, px + step * 0.5, py + step * 0.5, plankLen, plankW, Math.PI / 4, shade(tone, -4)); } else { drawPlank(ctx, px + step * 0.25, py + step * 0.25, plankLen, plankW, -Math.PI / 4, tone); drawPlank(ctx, px + step * 0.75, py + step * 0.75, plankLen, plankW, -Math.PI / 4, shade(tone, 5)); } } } // Soft polish sheen const sheen = ctx.createLinearGradient(0, 0, size, size); sheen.addColorStop(0, 'rgba(255,240,210,0.06)'); sheen.addColorStop(0.5, 'rgba(255,255,255,0)'); sheen.addColorStop(1, 'rgba(255,230,200,0.04)'); ctx.fillStyle = sheen; ctx.fillRect(0, 0, size, size); } let cachedTexture: THREE.CanvasTexture | null = null; /** Seamless herringbone parquet — shared across gallery halls. */ export function createParquetFloorTexture(): THREE.CanvasTexture { if (cachedTexture) return cachedTexture; const size = 512; const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Could not create parquet canvas'); paintHerringbone(ctx, size); const texture = new THREE.CanvasTexture(canvas); texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.colorSpace = THREE.SRGBColorSpace; texture.anisotropy = 8; texture.needsUpdate = true; cachedTexture = texture; return texture; } /** Meters covered by one texture repeat — tune for plank scale in the hall. */ export const PARQUET_METERS_PER_TILE = 2.8;