Files
Art-gallery/client/src/utils/parquetFloorTexture.ts
T
Danila KhodjaefandCursor 019ce4e136 Improve 3D gallery and debug checkup workflow for image curation.
Add parquet floor, museum-style exit, movement-tinted walls, and gold/black
frames with gallery sync after Fix it. Debug panel gets Checked and Fix it
buttons; documentation and image-fetch reliability updates included.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-20 22:05:24 +03:00

117 lines
3.6 KiB
TypeScript

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;