Add production deployment, photorealistic movement walls, and fix duplicate influence links.

Configure hosting for gallery.mysuperlab.netcraze.pro and LAN access, enhance movement gallery textures with period-appropriate painted materials, dedupe influenced-by API responses via painting_influence_sources, and refresh several painting image files.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-06-21 23:00:13 +03:00
co-authored by Cursor
parent df29848d89
commit 4eead54062
161 changed files with 668 additions and 187 deletions
+1
View File
@@ -1165,6 +1165,7 @@ function GalleryFloor({
<meshStandardMaterial
map={texture.map}
normalMap={texture.normalMap}
normalScale={new THREE.Vector2(texture.normalScale, texture.normalScale)}
roughness={texture.roughness}
metalness={texture.metalness}
envMapIntensity={0.65}
+30 -30
View File
@@ -160,8 +160,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
30: mk(
'early-renaissance-palazzo',
'Florentine palazzo',
'Stucco salone · terracotta accents · arched windows',
{ wall: 'stucco-cream', ceiling: 'fresco-worn', floor: 'terracotta-tiles' },
'Lime-washed walls · terracotta accents · arched windows',
{ wall: 'painted-lime', wallSide: 'stucco-cream', ceiling: 'fresco-worn', floor: 'terracotta-tiles' },
{ wall: '#f4ebe0', ceiling: '#faf6ee', floor: '#c89070', trim: '#c9a227' },
{
details: 'palazzo',
@@ -249,8 +249,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
35: mk(
'rococo-salon',
'Rococo salon',
'Pastel stucco · gilt trim · chevron parquet',
{ wall: 'pastel-plaster', ceiling: 'silk-pale', floor: 'parquet-chevrons' },
'Pastel painted walls · gilt trim · chevron parquet',
{ wall: 'painted-oil-satin', ceiling: 'silk-pale', floor: 'parquet-chevrons' },
{ wall: '#e8dce8', ceiling: '#faf6f0', floor: '#c8a882', trim: '#d4af37' },
{
details: 'salon',
@@ -266,8 +266,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
36: mk(
'neoclassical-museum',
'Neoclassical museum',
'White marble · coffered ceiling · skylit salon',
{ wall: 'marble-white', ceiling: 'plaster-white', floor: 'marble-veined-carrara' },
'White painted walls · coffered ceiling · skylit salon',
{ wall: 'painted-flat', ceiling: 'plaster-white', floor: 'marble-veined-carrara' },
{ wall: '#f2f0ec', ceiling: '#fafafa', floor: '#eceae6', trim: '#b8b0a4' },
{
details: 'neoclassical',
@@ -302,8 +302,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
38: mk(
'realist-bourgeois',
'Realist picture gallery',
'Warm plaster · bourgeois salon · oak parquet',
{ wall: 'plaster-warm', ceiling: 'plaster-warm', floor: 'parquet-herringbone' },
'Warm painted walls · bourgeois salon · oak parquet',
{ wall: 'painted-emulsion', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
{ wall: '#e8e2d8', ceiling: '#f5f0e8', floor: '#a08060', trim: '#8a7050' },
{
details: 'salon',
@@ -316,8 +316,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
39: mk(
'impressionist-salon',
'Impressionist salon',
'North-light skylight · pale walls · herringbone floor',
{ wall: 'plaster-warm', ceiling: 'plaster-white', floor: 'parquet-herringbone' },
'North-light skylight · pale painted walls · herringbone floor',
{ wall: 'painted-emulsion', ceiling: 'painted-flat', floor: 'parquet-herringbone' },
{ wall: '#f0ebe3', ceiling: '#fafafa', floor: '#c8a882', trim: '#c9a96e' },
{
details: 'salon',
@@ -333,8 +333,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
40: mk(
'post-impressionist-atelier',
'Montmartre atelier',
'Studio walls · large north window · worn floorboards',
{ wall: 'plaster-warm', ceiling: 'plaster-warm', floor: 'parquet-herringbone' },
'Painted studio walls · large north window · worn floorboards',
{ wall: 'painted-emulsion', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
{ wall: '#e8e0d0', ceiling: '#f0ebe0', floor: '#9a7858', trim: '#8a6848' },
{
details: 'atelier',
@@ -347,8 +347,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
41: mk(
'symbolist-chamber',
'Symbolist chamber',
'Deep emerald walls · amber window glow · dark parquet',
{ wall: 'velvet-emerald', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
'Deep green painted walls · amber window glow · dark parquet',
{ wall: 'painted-oil-matte', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
{ wall: '#1a4030', ceiling: '#1a1814', floor: '#4a3828', trim: '#8a7050' },
{
details: 'salon',
@@ -365,8 +365,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
42: mk(
'art-nouveau-salon',
'Art Nouveau salon',
'Organic plaster · stained glass · terrazzo floor',
{ wall: 'silk-pale', ceiling: 'silk-gold', floor: 'terrazzo' },
'Painted plaster · stained glass · terrazzo floor',
{ wall: 'painted-oil-satin', ceiling: 'silk-gold', floor: 'terrazzo' },
{ wall: '#f0ece4', ceiling: '#d8c890', floor: '#d8d0c4', trim: '#6a9868' },
{
details: 'salon',
@@ -381,8 +381,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
43: mk(
'fauvist-studio',
'Fauvist studio',
'Bold warm plaster · flooded with color and light',
{ wall: 'stucco-terracotta', wallSide: 'silk-gold', ceiling: 'plaster-warm', floor: 'parquet-herringbone' },
'Bold painted walls · flooded with color and light',
{ wall: 'painted-oil-matte', wallSide: 'painted-oil-matte', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
{ wall: '#e89060', wallSide: '#e8c860', ceiling: '#f8f0e0', floor: '#a07048', trim: '#c04020' },
{
details: 'atelier',
@@ -413,8 +413,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
45: mk(
'cubist-studio',
'Cubist studio',
'Paris atelier · factory windows · raw plaster',
{ wall: 'plaster-warm', ceiling: 'plaster-white', floor: 'parquet-herringbone' },
'Paris atelier · factory windows · painted plaster',
{ wall: 'painted-emulsion', ceiling: 'painted-flat', floor: 'parquet-herringbone' },
{ wall: '#e8e4dc', ceiling: '#f5f5f5', floor: '#9a8870', trim: '#888888' },
{
details: 'atelier',
@@ -447,8 +447,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
47: mk(
'suprematist-gallery',
'Suprematist white cube',
'Pure white volume · geometric light · polished floor',
{ wall: 'white-plaster', ceiling: 'white-plaster', floor: 'concrete-polished' },
'Matte white paint · geometric light · polished floor',
{ wall: 'painted-flat', ceiling: 'painted-flat', floor: 'concrete-polished' },
{ wall: '#ffffff', ceiling: '#ffffff', floor: '#e0e0e0', trim: '#cccccc' },
{
details: 'modern',
@@ -485,8 +485,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
49: mk(
'dada-salon',
'Dada salon',
'Eclectic bourgeois room · mismatched light sources',
{ wall: 'plaster-warm', wallSide: 'brick', ceiling: 'plaster-warm', floor: 'parquet-herringbone' },
'Eclectic bourgeois room · painted walls and exposed brick',
{ wall: 'painted-emulsion', wallSide: 'brick', ceiling: 'painted-emulsion', floor: 'parquet-herringbone' },
{ wall: '#e8dcc8', wallSide: '#8a5040', ceiling: '#f0ebe0', floor: '#8a6848', trim: '#6a4830' },
{
details: 'salon',
@@ -501,8 +501,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
50: mk(
'surrealist-interior',
'Surrealist interior',
'Bourgeois wallpaper tones · uncanny warm light',
{ wall: 'velvet-navy', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
'Dark painted walls · uncanny warm light',
{ wall: 'painted-oil-matte', ceiling: 'dark-plaster', floor: 'parquet-herringbone' },
{ wall: '#1a2848', ceiling: '#2a2428', floor: '#5a4838', trim: '#8a7050' },
{
details: 'salon',
@@ -520,8 +520,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
51: mk(
'abstract-expressionist-loft',
'NYC loft',
'Raw brick and plaster · north-light factory windows',
{ wall: 'brick', wallSide: 'plaster-white', ceiling: 'concrete-raw', floor: 'industrial-floor' },
'Raw brick and painted plaster · north-light factory windows',
{ wall: 'brick', wallSide: 'painted-flat', ceiling: 'concrete-raw', floor: 'industrial-floor' },
{ wall: '#8a5040', wallSide: '#f5f5f5', ceiling: '#989898', floor: '#888880', trim: '#606060' },
{
details: 'industrial',
@@ -536,8 +536,8 @@ const BY_MOVEMENT_ID: Record<number, MovementInteriorStyle> = {
52: mk(
'pop-art-gallery',
'Pop Art gallery',
'White cube · fluorescent daylight · polished concrete',
{ wall: 'studio-white', ceiling: 'studio-white', floor: 'concrete-polished' },
'White painted walls · fluorescent daylight · polished concrete',
{ wall: 'painted-flat', ceiling: 'painted-flat', floor: 'concrete-polished' },
{ wall: '#ffffff', ceiling: '#ffffff', floor: '#d8d8d8', trim: '#cccccc' },
{
details: 'modern',
+2 -1
View File
@@ -18,10 +18,11 @@ export function useTexturedMaterial(
return new THREE.MeshStandardMaterial({
map: surf.map,
normalMap: surf.normalMap,
normalScale: new THREE.Vector2(surf.normalScale, surf.normalScale),
color: tint,
roughness: surf.roughness,
metalness: surf.metalness,
envMapIntensity: 0.65,
envMapIntensity: kind.startsWith('painted-') ? 0.45 : 0.65,
});
}, [kind, tint, spanW, spanH]);
+298 -72
View File
@@ -41,7 +41,12 @@ export type SurfaceTextureKind =
| 'dark-plaster'
| 'pastel-plaster'
| 'white-plaster'
| 'studio-white';
| 'studio-white'
| 'painted-lime'
| 'painted-oil-matte'
| 'painted-oil-satin'
| 'painted-emulsion'
| 'painted-flat';
export interface SurfaceTextureSet {
map: THREE.CanvasTexture;
@@ -49,6 +54,7 @@ export interface SurfaceTextureSet {
roughness: number;
metalness: number;
metersPerRepeat: number;
normalScale: number;
}
const cache = new Map<string, SurfaceTextureSet>();
@@ -94,28 +100,207 @@ function noiseOverlay(ctx: CanvasRenderingContext2D, size: number, alpha: number
ctx.putImageData(img, 0, 0);
}
/** Fractal value noise for organic surface variation. */
function fbm(x: number, y: number, seed: number, octaves = 5): number {
let value = 0;
let amp = 0.5;
let freq = 1;
for (let i = 0; i < octaves; i++) {
value +=
amp *
(Math.sin(x * freq * 12.9898 + seed * 0.137 + i * 1.7) *
Math.cos(y * freq * 78.233 + seed * 0.311 + i * 2.3) *
0.5 +
0.5);
amp *= 0.52;
freq *= 2.05;
}
return value;
}
function paintNeutralBase(ctx: CanvasRenderingContext2D, size: number) {
fill(ctx, size, '#f8f8f6');
}
type PaintFinish = 'lime' | 'oil-matte' | 'oil-satin' | 'emulsion' | 'flat';
/** Single-colour painted wall — neutral base so movement tint drives the hue. */
function paintedWallSurface(
ctx: CanvasRenderingContext2D,
size: number,
finish: PaintFinish,
seed: number
) {
paintNeutralBase(ctx, size);
const img = ctx.getImageData(0, 0, size, size);
const d = img.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const u = x / size;
const v = y / size;
let lum = 0;
if (finish === 'lime') {
lum += (fbm(u * 3.2, v * 3.2, seed) - 0.5) * 0.14;
lum += (fbm(u * 8, v * 8, seed + 17) - 0.5) * 0.06;
lum += Math.sin(v * 40 + fbm(u, 0, seed + 3) * 8) * 0.012;
} else if (finish === 'oil-matte') {
lum += Math.sin(v * 55 + fbm(u * 2, 0, seed) * 4) * 0.018;
lum += (fbm(u * 18, v * 18, seed + 5) - 0.5) * 0.04;
lum += (fbm(u * 60, v * 60, seed + 9) - 0.5) * 0.025;
} else if (finish === 'oil-satin') {
lum += Math.sin(v * 48 + fbm(u, 0, seed) * 3) * 0.014;
const sheen = Math.pow(Math.max(0, Math.sin(u * 28 + v * 22 + seed)), 3) * 0.06;
lum += sheen;
} else if (finish === 'emulsion') {
lum += Math.sin(v * (Math.PI * 2 * size) / 105) * 0.022;
lum += (fbm(u * 22, v * 22, seed + 11) - 0.5) * 0.035;
lum += (fbm(u * 70, v * 70, seed + 13) - 0.5) * 0.018;
} else {
lum += (fbm(u * 35, v * 35, seed + 19) - 0.5) * 0.028;
lum += (fbm(u * 90, v * 90, seed + 21) - 0.5) * 0.012;
}
const delta = lum * 255;
d[i] = Math.min(255, Math.max(0, d[i] + delta));
d[i + 1] = Math.min(255, Math.max(0, d[i + 1] + delta));
d[i + 2] = Math.min(255, Math.max(0, d[i + 2] + delta));
}
}
ctx.putImageData(img, 0, 0);
noiseOverlay(ctx, size, finish === 'flat' ? 0.012 : 0.022, seed + 99);
}
function plasterSurface(ctx: CanvasRenderingContext2D, size: number, base: string, seed: number) {
fill(ctx, size, base);
const rand = seeded(seed);
for (let pass = 0; pass < 55; pass++) {
const angle = rand() * Math.PI;
const len = size * (0.08 + rand() * 0.22);
const x0 = rand() * size;
const y0 = rand() * size;
ctx.strokeStyle = `rgba(0,0,0,${0.015 + rand() * 0.025})`;
ctx.lineWidth = 6 + rand() * 18;
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x0 + Math.cos(angle) * len, y0 + Math.sin(angle) * len);
ctx.stroke();
}
noiseOverlay(ctx, size, 0.045, seed + 1);
}
function stuccoSurface(ctx: CanvasRenderingContext2D, size: number, base: string, seed: number) {
fill(ctx, size, base);
const rand = seeded(seed);
for (let i = 0; i < 80; i++) {
const cx = rand() * size;
const cy = rand() * size;
const r = 20 + rand() * 60;
ctx.strokeStyle = `rgba(0,0,0,${0.02 + rand() * 0.04})`;
ctx.lineWidth = 2 + rand() * 4;
ctx.beginPath();
ctx.arc(cx, cy, r, rand() * Math.PI, rand() * Math.PI + Math.PI * 0.8);
ctx.stroke();
}
noiseOverlay(ctx, size, 0.07, seed + 2);
}
function marbleVeined(ctx: CanvasRenderingContext2D, size: number, base: string, vein: string, seed: number) {
fill(ctx, size, base);
const rand = seeded(seed);
for (let i = 0; i < 28; i++) {
ctx.strokeStyle = vein.replace(')', `,${0.06 + rand() * 0.14})`).replace('rgb', 'rgba');
if (vein.startsWith('#')) {
const [r, g, b] = hexToRgb(vein);
ctx.strokeStyle = `rgba(${r},${g},${b},${0.05 + rand() * 0.12})`;
}
ctx.lineWidth = 1 + rand() * 5;
for (let i = 0; i < 42; i++) {
const [r, g, b] = hexToRgb(vein.startsWith('#') ? vein : '#a8a098');
ctx.strokeStyle = `rgba(${r},${g},${b},${0.04 + rand() * 0.16})`;
ctx.lineWidth = 0.8 + rand() * 6;
ctx.beginPath();
let x = rand() * size;
let y = rand() * size;
ctx.moveTo(x, y);
for (let s = 0; s < 10; s++) {
x += (rand() - 0.5) * size * 0.18;
y += (rand() - 0.5) * size * 0.12;
ctx.lineTo(x, y);
for (let s = 0; s < 14; s++) {
x += (rand() - 0.5) * size * 0.14;
y += (rand() - 0.5) * size * 0.1;
const cx = (x + rand() * size * 0.05) / 2;
const cy = (y + rand() * size * 0.05) / 2;
ctx.quadraticCurveTo(cx, cy, x, y);
}
ctx.stroke();
}
noiseOverlay(ctx, size, 0.04, seed + 1);
{
const img = ctx.getImageData(0, 0, size, size);
const d = img.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const n = (fbm(x / 180, y / 180, seed + 50) - 0.5) * 12;
d[i] += n;
d[i + 1] += n;
d[i + 2] += n;
}
}
ctx.putImageData(img, 0, 0);
}
noiseOverlay(ctx, size, 0.035, seed + 1);
}
function woodPanelSurface(
ctx: CanvasRenderingContext2D,
size: number,
base: string,
seed: number,
plankCount = 14
) {
const plankH = size / plankCount;
for (let i = 0; i < plankCount; i++) {
const grad = ctx.createLinearGradient(0, i * plankH, size, i * plankH + plankH);
grad.addColorStop(0, shadeHex(base, -18));
grad.addColorStop(0.35, base);
grad.addColorStop(0.65, shadeHex(base, 8));
grad.addColorStop(1, shadeHex(base, -12));
ctx.fillStyle = grad;
ctx.fillRect(0, i * plankH, size, plankH - 2);
for (let g = 0; g < 16; g++) {
const gx = g * (size / 16);
ctx.strokeStyle = 'rgba(0,0,0,0.06)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(gx, i * plankH);
const wave = Math.sin(g * 0.8 + i + seed * 0.01) * size * 0.04;
ctx.bezierCurveTo(gx + size * 0.03, i * plankH + plankH * 0.3, gx - size * 0.02 + wave, i * plankH + plankH * 0.7, gx + wave, i * plankH + plankH);
ctx.stroke();
}
ctx.strokeStyle = 'rgba(0,0,0,0.28)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, i * plankH);
ctx.lineTo(size, i * plankH);
ctx.stroke();
}
noiseOverlay(ctx, size, 0.035, seed);
}
function velvetSurface(ctx: CanvasRenderingContext2D, size: number, base: string, seed: number) {
fill(ctx, size, base);
const img = ctx.getImageData(0, 0, size, size);
const d = img.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const weave =
Math.sin(x * 0.35) * Math.sin(y * 0.35) * 0.04 +
Math.sin(x * 0.12 + y * 0.08 + seed) * 0.025;
const pile = fbm(x / 40, y / 40, seed) * 0.08;
const delta = (weave + pile) * 255;
d[i] += delta;
d[i + 1] += delta;
d[i + 2] += delta;
}
}
ctx.putImageData(img, 0, 0);
noiseOverlay(ctx, size, 0.12, seed + 1);
}
function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
@@ -125,6 +310,21 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
const ctx = canvas.getContext('2d')!;
switch (kind) {
case 'painted-lime':
paintedWallSurface(ctx, size, 'lime', 501);
break;
case 'painted-oil-matte':
paintedWallSurface(ctx, size, 'oil-matte', 502);
break;
case 'painted-oil-satin':
paintedWallSurface(ctx, size, 'oil-satin', 503);
break;
case 'painted-emulsion':
paintedWallSurface(ctx, size, 'emulsion', 504);
break;
case 'painted-flat':
paintedWallSurface(ctx, size, 'flat', 505);
break;
case 'marble-white':
case 'marble-veined-carrara':
marbleVeined(ctx, size, '#ece8e0', '#a8a098', 101);
@@ -148,7 +348,14 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
}
case 'limestone':
fill(ctx, size, '#ddd4c4');
noiseOverlay(ctx, size, 0.08, 104);
for (let y = 0; y < size; y += 4) {
for (let x = 0; x < size; x += 4) {
const n = (fbm(x / 120, y / 120, 104) - 0.5) * 18;
ctx.fillStyle = rgb(221 + n, 212 + n, 196 + n);
ctx.fillRect(x, y, 4, 4);
}
}
noiseOverlay(ctx, size, 0.06, 104);
break;
case 'sandstone':
fill(ctx, size, '#c8b090');
@@ -171,42 +378,31 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
noiseOverlay(ctx, size, 0.15, 107);
break;
case 'stucco-cream':
fill(ctx, size, '#f0e8d8');
noiseOverlay(ctx, size, 0.06, 108);
stuccoSurface(ctx, size, '#f0e8d8', 108);
break;
case 'stucco-terracotta':
fill(ctx, size, '#c89070');
noiseOverlay(ctx, size, 0.08, 109);
stuccoSurface(ctx, size, '#c89070', 109);
break;
case 'plaster-white':
case 'white-plaster':
case 'studio-white':
fill(ctx, size, '#f8f8f6');
noiseOverlay(ctx, size, 0.035, 110);
plasterSurface(ctx, size, '#f8f8f6', 110);
break;
case 'plaster-warm':
fill(ctx, size, '#f2ebe0');
noiseOverlay(ctx, size, 0.045, 111);
case 'pastel-plaster':
plasterSurface(ctx, size, kind === 'pastel-plaster' ? '#ece4ec' : '#f2ebe0', 111);
break;
case 'dark-plaster':
fill(ctx, size, '#3a3230');
noiseOverlay(ctx, size, 0.07, 112);
break;
case 'pastel-plaster':
fill(ctx, size, '#e8dce8');
noiseOverlay(ctx, size, 0.05, 113);
plasterSurface(ctx, size, '#3a3230', 112);
break;
case 'velvet-crimson':
fill(ctx, size, '#5c1828');
noiseOverlay(ctx, size, 0.18, 114);
velvetSurface(ctx, size, '#5c1828', 114);
break;
case 'velvet-navy':
fill(ctx, size, '#1a2848');
noiseOverlay(ctx, size, 0.18, 115);
velvetSurface(ctx, size, '#1a2848', 115);
break;
case 'velvet-emerald':
fill(ctx, size, '#1a4030');
noiseOverlay(ctx, size, 0.18, 116);
velvetSurface(ctx, size, '#1a4030', 116);
break;
case 'silk-gold':
fill(ctx, size, '#d8c890');
@@ -217,34 +413,14 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
noiseOverlay(ctx, size, 0.05, 118);
break;
case 'oak-panel':
woodPanelSurface(ctx, size, '#8a6848', 119);
break;
case 'walnut-panel':
case 'dark-wood-panel': {
const base = kind === 'oak-panel' ? '#8a6848' : kind === 'walnut-panel' ? '#5a4030' : '#3a2818';
const plankH = size / 14;
for (let i = 0; i < 14; i++) {
const grad = ctx.createLinearGradient(0, i * plankH, size, i * plankH);
grad.addColorStop(0, shadeHex(base, -15));
grad.addColorStop(0.5, base);
grad.addColorStop(1, shadeHex(base, -10));
ctx.fillStyle = grad;
ctx.fillRect(0, i * plankH, size, plankH - 2);
ctx.strokeStyle = 'rgba(0,0,0,0.25)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, i * plankH);
ctx.lineTo(size, i * plankH);
ctx.stroke();
for (let g = 0; g < 12; g++) {
ctx.strokeStyle = 'rgba(0,0,0,0.08)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(g * (size / 12), i * plankH);
ctx.lineTo(g * (size / 12) + size * 0.08, i * plankH + plankH);
ctx.stroke();
}
}
woodPanelSurface(ctx, size, '#5a4030', 120);
break;
case 'dark-wood-panel':
woodPanelSurface(ctx, size, '#3a2818', 121);
break;
}
case 'parquet-herringbone':
case 'parquet-chevrons': {
const tones = ['#6b4a2e', '#735234', '#624428', '#7a5638'];
@@ -267,7 +443,7 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
const tile = size / 8;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
ctx.fillStyle = ['#b87050', '#c88058', '#a86048'][ (row + col) % 3];
ctx.fillStyle = ['#b87050', '#c88058', '#a86048'][(row + col) % 3];
ctx.fillRect(col * tile + 2, row * tile + 2, tile - 4, tile - 4);
}
}
@@ -289,7 +465,7 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
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.fillStyle = colors[(x / cell + y / cell) % colors.length];
ctx.fillRect(x + 1, y + 1, cell - 2, cell - 2);
}
}
@@ -301,7 +477,7 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
const colors = ['#8a7060', '#a88870', '#706050', '#d0c0a0'];
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.fillStyle = colors[(x / cell + y / cell) % colors.length];
ctx.fillRect(x + 1, y + 1, cell - 2, cell - 2);
}
}
@@ -347,22 +523,23 @@ function paintSurface(kind: SurfaceTextureKind, size: number): ImageData {
noiseOverlay(ctx, size, 0.12, 124);
break;
case 'gilded-stucco':
fill(ctx, size, '#d8c070');
noiseOverlay(ctx, size, 0.05, 125);
stuccoSurface(ctx, size, '#d8c070', 125);
break;
case 'fresco-worn':
fill(ctx, size, '#d0c8b8');
noiseOverlay(ctx, size, 0.09, 126);
plasterSurface(ctx, size, '#d0c8b8', 126);
break;
case 'brick': {
const bh = size / 16;
const bw = size / 8;
fill(ctx, size, '#6a4030');
fill(ctx, size, '#5a4030');
for (let row = 0; row < 16; row++) {
const offset = row % 2 ? bw / 2 : 0;
for (let col = -1; col < 9; col++) {
const rand = seeded(130 + row * 9 + col);
ctx.fillStyle = ['#8a5040', '#7a4838', '#9a5848'][(row + col) % 3];
ctx.fillRect(col * bw + offset, row * bh, bw - 3, bh - 3);
const inset = 2 + rand() * 2;
ctx.fillRect(col * bw + offset + inset, row * bh + inset, bw - inset * 2, bh - inset * 2);
}
}
break;
@@ -379,7 +556,7 @@ function imageDataToNormalMap(data: ImageData, size: number, strength = 2.5): Im
const out = new ImageData(size, size);
const src = data.data;
const dst = out.data;
const idx = (x: number, y: number) => ((y * size + x) * 4);
const idx = (x: number, y: number) => (y * size + x) * 4;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
@@ -419,6 +596,16 @@ function imageDataToTexture(data: ImageData): THREE.CanvasTexture {
return tex;
}
function normalStrengthFor(kind: SurfaceTextureKind): number {
if (kind.startsWith('painted-')) return 2.2;
if (kind.includes('velvet')) return 1.6;
if (kind.includes('marble')) return 3.4;
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;
return 2.5;
}
const ROUGHNESS: Partial<Record<SurfaceTextureKind, number>> = {
'marble-white': 0.18,
'marble-veined-carrara': 0.16,
@@ -431,7 +618,21 @@ const ROUGHNESS: Partial<Record<SurfaceTextureKind, number>> = {
'concrete-polished': 0.28,
'concrete-raw': 0.72,
'oak-panel': 0.55,
'walnut-panel': 0.58,
'dark-wood-panel': 0.62,
'parquet-herringbone': 0.42,
'painted-lime': 0.88,
'painted-oil-matte': 0.82,
'painted-oil-satin': 0.68,
'painted-emulsion': 0.86,
'painted-flat': 0.94,
'limestone': 0.78,
'stucco-cream': 0.8,
'stucco-terracotta': 0.78,
'plaster-white': 0.9,
'plaster-warm': 0.88,
'white-plaster': 0.92,
'studio-white': 0.94,
};
const METALNESS: Partial<Record<SurfaceTextureKind, number>> = {
@@ -440,17 +641,41 @@ const METALNESS: Partial<Record<SurfaceTextureKind, number>> = {
'gilded-stucco': 0.65,
'concrete-polished': 0.12,
'terrazzo': 0.15,
'painted-oil-satin': 0.06,
'painted-flat': 0.02,
};
const METERS: Partial<Record<SurfaceTextureKind, number>> = {
'marble-checker': 2.4,
'flagstone': 3.0,
flagstone: 3.0,
'mosaic-byzantine': 1.8,
'mosaic-roman': 2.0,
'parquet-herringbone': 1.8,
'parquet-chevrons': 1.8,
'concrete-raw': 4.0,
'brick': 2.5,
brick: 2.5,
'painted-lime': 3.2,
'painted-oil-matte': 3.0,
'painted-oil-satin': 3.0,
'painted-emulsion': 3.5,
'painted-flat': 4.0,
};
const NORMAL_SCALE: Partial<Record<SurfaceTextureKind, number>> = {
'painted-flat': 0.32,
'painted-emulsion': 0.48,
'painted-oil-satin': 0.42,
'painted-oil-matte': 0.55,
'painted-lime': 0.62,
'marble-white': 0.75,
'marble-veined-carrara': 0.8,
'velvet-crimson': 0.5,
'velvet-navy': 0.5,
'velvet-emerald': 0.5,
'oak-panel': 0.7,
'brick': 0.85,
'stucco-cream': 0.65,
'plaster-warm': 0.58,
};
export function getSurfaceTexture(kind: SurfaceTextureKind): SurfaceTextureSet {
@@ -458,7 +683,7 @@ export function getSurfaceTexture(kind: SurfaceTextureKind): SurfaceTextureSet {
if (cached) return cached;
const colorData = paintSurface(kind, TEXTURE_SIZE);
const normalData = imageDataToNormalMap(colorData, TEXTURE_SIZE, kind.includes('velvet') ? 1.2 : 2.8);
const normalData = imageDataToNormalMap(colorData, TEXTURE_SIZE, normalStrengthFor(kind));
const set: SurfaceTextureSet = {
map: imageDataToTexture(colorData),
@@ -466,6 +691,7 @@ export function getSurfaceTexture(kind: SurfaceTextureKind): SurfaceTextureSet {
roughness: ROUGHNESS[kind] ?? 0.75,
metalness: METALNESS[kind] ?? 0.04,
metersPerRepeat: METERS[kind] ?? 2.8,
normalScale: NORMAL_SCALE[kind] ?? 0.6,
};
cache.set(kind, set);
return set;
+15 -9
View File
@@ -1,13 +1,19 @@
import { defineConfig } from 'vite'
import path from 'node:path'
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:3001',
'/images': 'http://localhost:3001',
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, path.resolve(__dirname, '..'), '')
const apiTarget = `http://localhost:${env.PORT || '3001'}`
return {
plugins: [react()],
server: {
proxy: {
'/api': apiTarget,
'/images': apiTarget,
},
},
},
})
}
})