← back to Dw War Room

src/showroom/ProceduralMaps.ts

189 lines

// ProceduralMaps.ts — Canvas-based procedural normal and roughness maps
// Zero external file dependencies — all generated at runtime
import * as THREE from 'three';

/**
 * Generate a brick wall normal map using canvas.
 * Creates realistic mortar lines and brick surface variation.
 */
export function generateBrickNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Failed to create 2d canvas context');

  // Base neutral normal (pointing straight out: RGB 128,128,255)
  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  const brickW = width / 8;
  const brickH = height / 16;
  const mortarW = 3;

  for (let row = 0; row < 16; row++) {
    const offset = (row % 2) * (brickW / 2); // stagger rows
    for (let col = -1; col < 9; col++) {
      const x = col * brickW + offset;
      const y = row * brickH;

      // Mortar lines (recessed = normal pointing inward)
      // Horizontal mortar
      ctx.fillStyle = 'rgb(128, 100, 255)'; // slight downward normal
      ctx.fillRect(0, y, width, mortarW);

      // Vertical mortar
      ctx.fillStyle = 'rgb(100, 128, 255)'; // slight leftward normal
      ctx.fillRect(x, y, mortarW, brickH);

      // Brick surface noise (subtle variation)
      for (let i = 0; i < 20; i++) {
        const nx = x + Math.random() * brickW;
        const ny = y + mortarW + Math.random() * (brickH - mortarW * 2);
        const r = 128 + (Math.random() - 0.5) * 12;
        const g = 128 + (Math.random() - 0.5) * 12;
        ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
        ctx.fillRect(nx, ny, 3 + Math.random() * 6, 2 + Math.random() * 4);
      }
    }
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  tex.repeat.set(2, 2);
  return tex;
}

/**
 * Generate a polished concrete normal map.
 * Subtle micro-texture with occasional aggregate patches.
 */
export function generateConcreteNormalMap(width = 1024, height = 1024): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Failed to create 2d canvas context');

  // Base neutral normal
  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  // Micro-texture noise — polished concrete has subtle surface variation
  for (let i = 0; i < 5000; i++) {
    const x = Math.random() * width;
    const y = Math.random() * height;
    const r = 128 + (Math.random() - 0.5) * 6;
    const g = 128 + (Math.random() - 0.5) * 6;
    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
    ctx.fillRect(x, y, 1 + Math.random() * 3, 1 + Math.random() * 3);
  }

  // Aggregate patches (larger bumps)
  for (let i = 0; i < 80; i++) {
    const x = Math.random() * width;
    const y = Math.random() * height;
    const radius = 4 + Math.random() * 12;
    const r = 128 + (Math.random() - 0.5) * 18;
    const g = 128 + (Math.random() - 0.5) * 18;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
    ctx.fill();
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  tex.repeat.set(3, 3);
  return tex;
}

/**
 * Generate a brushed aluminum normal map.
 * Directional brushing lines for metallic surfaces.
 */
export function generateBrushedAluminumNormalMap(width = 512, height = 512): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Failed to create 2d canvas context');

  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  // Horizontal brush strokes
  for (let y = 0; y < height; y++) {
    const r = 128 + (Math.random() - 0.5) * 4;
    const g = 128 + Math.sin(y * 0.3) * 3 + (Math.random() - 0.5) * 2;
    ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
    ctx.fillRect(0, y, width, 1);
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  return tex;
}

/**
 * Generate a wood grain normal map for shelving.
 */
export function generateWoodNormalMap(width = 512, height = 512): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Failed to create 2d canvas context');

  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  // Wood grain lines (vertical, slightly wavy)
  for (let x = 0; x < width; x += 4 + Math.random() * 8) {
    ctx.beginPath();
    ctx.moveTo(x, 0);
    for (let y = 0; y < height; y += 10) {
      ctx.lineTo(x + Math.sin(y * 0.02 + x * 0.1) * 3, y);
    }
    const r = 128 + (Math.random() - 0.5) * 10;
    ctx.strokeStyle = `rgb(${r}, 128, 255)`;
    ctx.lineWidth = 1 + Math.random() * 2;
    ctx.stroke();
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  return tex;
}

/**
 * Generate a fabric/paper subtle normal map for wallcovering product panels.
 * Very subtle — just enough to catch light at grazing angles.
 */
export function generateFabricNormalMap(width = 256, height = 256): THREE.CanvasTexture {
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Failed to create 2d canvas context');

  ctx.fillStyle = 'rgb(128, 128, 255)';
  ctx.fillRect(0, 0, width, height);

  // Woven texture: alternating horizontal and vertical micro-lines
  for (let y = 0; y < height; y += 2) {
    for (let x = 0; x < width; x += 2) {
      const horizontal = (y / 2) % 2 === 0;
      const r = 128 + (horizontal ? 2 : -2) + (Math.random() - 0.5) * 3;
      const g = 128 + (horizontal ? -2 : 2) + (Math.random() - 0.5) * 3;
      ctx.fillStyle = `rgb(${r}, ${g}, 255)`;
      ctx.fillRect(x, y, 2, 2);
    }
  }

  const tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  tex.repeat.set(4, 4);
  return tex;
}