[object Object]

← back to Model Arena

mood-board-studio: real products only — drop color chips, type tile, fake material chips

a236fbfa7829214b9a17c3ba743a2e3297b6240f · 2026-07-30 15:27:49 -0700 · Steve

Board is now a pure collage of 12 live DW products (2 per material, all
distinct SKUs via the rotating picker). Removed the synthetic tiles:
- color-chip palette swatches
- 'Living Room' type-pairing tile
- fake Brass/Marble/White Oak material chips

Replaced the hand-authored per-preset coordinate tables with a deterministic
staggered-collage generator (composeTilePositions now takes a count).
Sidebar style/palette reference and export chrome unchanged.

Verified: 12/12 real photos load, 0 console errors, export canvas clean (3.5MB PNG).

Files touched

Diff

commit a236fbfa7829214b9a17c3ba743a2e3297b6240f
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 30 15:27:49 2026 -0700

    mood-board-studio: real products only — drop color chips, type tile, fake material chips
    
    Board is now a pure collage of 12 live DW products (2 per material, all
    distinct SKUs via the rotating picker). Removed the synthetic tiles:
    - color-chip palette swatches
    - 'Living Room' type-pairing tile
    - fake Brass/Marble/White Oak material chips
    
    Replaced the hand-authored per-preset coordinate tables with a deterministic
    staggered-collage generator (composeTilePositions now takes a count).
    Sidebar style/palette reference and export chrome unchanged.
    
    Verified: 12/12 real photos load, 0 console errors, export canvas clean (3.5MB PNG).
---
 public/games/dw-mood-board-studio.html | 165 ++++++++++-----------------------
 1 file changed, 50 insertions(+), 115 deletions(-)

diff --git a/public/games/dw-mood-board-studio.html b/public/games/dw-mood-board-studio.html
index 9e92c42..7ca8ea2 100644
--- a/public/games/dw-mood-board-studio.html
+++ b/public/games/dw-mood-board-studio.html
@@ -1240,71 +1240,40 @@ const MATERIALS = [
 /* ============================================================
    LAYOUT COMPOSER — determines tile positions
    ============================================================ */
-function composeTilePositions(canvasW, canvasH, preset) {
-  // Returns array of {x, y, z} for: 6 swatches, 6 chips, 1 type, 3 materials
-  const pad = 24;
-  const sw = 160, sh = 160; // swatch
-  const cw = 80,  ch = 110; // chip
-  const tw = 220, th = 130; // type
-  const mw = 100, mh = 56;  // material
-
-  // Named layouts per preset — curated, intentional overlap
-  const layouts = {
-    grandmillennial: [
-      // swatches (6)
-      { x: 48,  y: 40  }, { x: 228, y: 24  }, { x: 410, y: 56  },
-      { x: 68,  y: 224 }, { x: 248, y: 210 }, { x: 430, y: 230 },
-      // chips (6)
-      { x: 620, y: 40  }, { x: 712, y: 40  }, { x: 804, y: 40  },
-      { x: 620, y: 162 }, { x: 712, y: 162 }, { x: 804, y: 162 },
-      // type tile
-      { x: 580, y: 300 },
-      // materials (3)
-      { x: 48,  y: 420 }, { x: 162, y: 420 }, { x: 276, y: 420 },
-    ],
-    coastal: [
-      { x: 32,  y: 32  }, { x: 212, y: 48  }, { x: 390, y: 28  },
-      { x: 52,  y: 214 }, { x: 232, y: 228 }, { x: 408, y: 210 },
-      { x: 616, y: 48  }, { x: 706, y: 56  }, { x: 796, y: 48  },
-      { x: 616, y: 170 }, { x: 706, y: 170 }, { x: 796, y: 170 },
-      { x: 570, y: 320 },
-      { x: 40,  y: 410 }, { x: 154, y: 418 }, { x: 268, y: 406 },
-    ],
-    moody: [
-      { x: 24,  y: 24  }, { x: 200, y: 40  }, { x: 376, y: 16  },
-      { x: 40,  y: 202 }, { x: 216, y: 218 }, { x: 392, y: 200 },
-      { x: 608, y: 32  }, { x: 702, y: 32  }, { x: 796, y: 32  },
-      { x: 608, y: 156 }, { x: 702, y: 156 }, { x: 796, y: 156 },
-      { x: 564, y: 296 },
-      { x: 24,  y: 400 }, { x: 138, y: 406 }, { x: 252, y: 396 },
-    ],
-    minimal: [
-      { x: 40,  y: 48  }, { x: 228, y: 32  }, { x: 418, y: 52  },
-      { x: 60,  y: 236 }, { x: 250, y: 220 }, { x: 440, y: 238 },
-      { x: 624, y: 52  }, { x: 716, y: 52  }, { x: 808, y: 52  },
-      { x: 624, y: 174 }, { x: 716, y: 174 }, { x: 808, y: 174 },
-      { x: 590, y: 306 },
-      { x: 48,  y: 428 }, { x: 162, y: 428 }, { x: 276, y: 428 },
-    ],
-    deco: [
-      { x: 36,  y: 36  }, { x: 214, y: 20  }, { x: 392, y: 40  },
-      { x: 54,  y: 220 }, { x: 232, y: 208 }, { x: 410, y: 224 },
-      { x: 612, y: 36  }, { x: 704, y: 36  }, { x: 796, y: 36  },
-      { x: 612, y: 158 }, { x: 704, y: 158 }, { x: 796, y: 158 },
-      { x: 574, y: 304 },
-      { x: 36,  y: 414 }, { x: 150, y: 414 }, { x: 264, y: 414 },
-    ],
-  };
-
-  const base = layouts[preset] || layouts.grandmillennial;
+function composeTilePositions(canvasW, canvasH, preset, count) {
+  // Real-products-only board: lay out `count` equal 160×160 product tiles in a
+  // staggered collage. Positions are deterministic per preset (stable across
+  // reshuffle) with a per-preset seed so each style scatters differently.
+  count = count || 12;
+  const pad = 22, sz = 160, gap = 26;
+  const cols = Math.max(3, Math.min(5, Math.floor((900 + gap) / (sz + gap)))); // ~4 @ 900
+  const rows = Math.ceil(count / cols);
+  const seedMap = { grandmillennial: 0, coastal: 1, moody: 2, minimal: 3, deco: 4 };
+  const seed = seedMap[preset] || 0;
+
+  const raw = [];
+  for (let i = 0; i < count; i++) {
+    const col = i % cols;
+    const row = Math.floor(i / cols);
+    let x = pad + col * (sz + gap);
+    let y = pad + row * (sz + gap);
+    // organic, deterministic jitter so it reads as a hand-pinned mood board
+    const jx = ((i * 53 + seed * 29) % 15) - 7;  // -7..7
+    const jy = ((i * 37 + seed * 13) % 11) - 5;  // -5..5
+    x += jx;
+    y += (col % 2 === 0 ? 14 : -8) + jy * 2;
+    raw.push({ x, y });
+  }
 
-  // Scale to actual canvas dimensions (base designed for ~940×500)
-  const scaleX = Math.min(1, (canvasW - pad*2) / 900);
-  const scaleY = Math.min(1, (canvasH - pad*2) / 500);
+  // Scale the base grid (pad + cols/rows of tiles) into the real canvas.
+  const baseW = pad * 2 + cols * sz + (cols - 1) * gap;
+  const baseH = pad * 2 + rows * sz + (rows - 1) * gap;
+  const scaleX = Math.min(1, (canvasW - pad) / baseW);
+  const scaleY = Math.min(1, (canvasH - pad) / baseH);
 
-  return base.map((p, i) => ({
-    x: Math.round(p.x * scaleX + pad * (1 - scaleX)),
-    y: Math.round(p.y * scaleY + pad * (1 - scaleY)),
+  return raw.map((p, i) => ({
+    x: Math.round(p.x * scaleX),
+    y: Math.round(p.y * scaleY),
     z: i + 1,
   }));
 }
@@ -1455,8 +1424,9 @@ function populateBoard(preset, animate) {
   const cW     = wrap.clientWidth;
   const cH     = wrap.clientHeight;
 
-  const data  = PRESETS[preset];
-  const positions = composeTilePositions(cW, cH, preset);
+  const data      = PRESETS[preset];
+  const prodTypes = buildProductTypes(data);
+  const positions = composeTilePositions(cW, cH, preset, prodTypes.length);
 
   // If animating (preset switch), fade out existing
   if (animate && tiles.length > 0) {
@@ -1467,12 +1437,12 @@ function populateBoard(preset, animate) {
     setTimeout(() => {
       canvas.innerHTML = '';
       tiles = [];
-      spawnTiles(data, positions, canvas, true);
+      spawnTiles(prodTypes, data.palette, positions, canvas, true);
     }, tiles.length * 40 + 350);
   } else {
     canvas.innerHTML = '';
     tiles = [];
-    spawnTiles(data, positions, canvas, animate);
+    spawnTiles(prodTypes, data.palette, positions, canvas, animate);
   }
 
   updateSidebar(preset);
@@ -1480,14 +1450,19 @@ function populateBoard(preset, animate) {
 
 let tileIdCounter = 0;
 
-function spawnTiles(data, positions, canvas, animate) {
-  const palette = data.palette;
-  const swOrder = data.swatches;
-  let posIdx    = 0;
+// Real-products-only board: build the list of material "slots" to fill with
+// live products. Two passes over the preset's 6 materials = 12 real SKUs
+// (pickProduct rotates, so every tile is a distinct product).
+function buildProductTypes(data) {
+  const order = data.swatches;      // 6 material types for this style
+  const out = [];
+  for (let pass = 0; pass < 2; pass++) for (const t of order) out.push(t);
+  return out;                        // 12 real product tiles
+}
 
-  // 6 swatch tiles
-  swOrder.forEach((type, i) => {
-    const pos = positions[posIdx++];
+function spawnTiles(prodTypes, palette, positions, canvas, animate) {
+  prodTypes.forEach((type, i) => {
+    const pos = positions[i] || positions[positions.length - 1];
     const id  = 'tile-' + (++tileIdCounter);
     const el  = createSwatchTile(type, palette, pos.x, pos.y, pos.z, id);
     if (animate) { el.style.opacity = '0'; }
@@ -1496,47 +1471,7 @@ function spawnTiles(data, positions, canvas, animate) {
     el.addEventListener('mousedown', () => selectTile(id), { passive: true });
     el.addEventListener('touchstart', () => selectTile(id), { passive: true });
     tiles.push({ id, el, type: 'swatch', x: pos.x, y: pos.y, z: pos.z });
-    if (animate) setTimeout(() => { el.classList.add('animating'); el.style.opacity = '1'; }, i * 60 + 20);
-  });
-
-  // 6 color chips
-  palette.forEach((c, i) => {
-    const pos = positions[posIdx++];
-    const id  = 'tile-' + (++tileIdCounter);
-    const el  = createColorChip(c, pos.x, pos.y, pos.z, id);
-    if (animate) { el.style.opacity = '0'; }
-    canvas.appendChild(el);
-    attachDragListeners(el);
-    el.addEventListener('mousedown', () => selectTile(id), { passive: true });
-    el.addEventListener('touchstart', () => selectTile(id), { passive: true });
-    tiles.push({ id, el, type: 'chip', x: pos.x, y: pos.y, z: pos.z });
-    if (animate) setTimeout(() => { el.classList.add('animating'); el.style.opacity = '1'; }, (i + 6) * 60 + 20);
-  });
-
-  // Type tile
-  const typePos = positions[posIdx++];
-  const typeId  = 'tile-' + (++tileIdCounter);
-  const typeEl  = createTypeTile(palette, typePos.x, typePos.y, typePos.z, typeId);
-  if (animate) { typeEl.style.opacity = '0'; }
-  canvas.appendChild(typeEl);
-  attachDragListeners(typeEl);
-  typeEl.addEventListener('mousedown', () => selectTile(typeId), { passive: true });
-  typeEl.addEventListener('touchstart', () => selectTile(typeId), { passive: true });
-  tiles.push({ id: typeId, el: typeEl, type: 'type', x: typePos.x, y: typePos.y, z: typePos.z });
-  if (animate) setTimeout(() => { typeEl.classList.add('animating'); typeEl.style.opacity = '1'; }, 12 * 60 + 20);
-
-  // 3 material chips
-  MATERIALS.forEach((mat, i) => {
-    const pos = positions[posIdx++];
-    const id  = 'tile-' + (++tileIdCounter);
-    const el  = createMaterialChip(mat, pos.x, pos.y, pos.z, id);
-    if (animate) { el.style.opacity = '0'; }
-    canvas.appendChild(el);
-    attachDragListeners(el);
-    el.addEventListener('mousedown', () => selectTile(id), { passive: true });
-    el.addEventListener('touchstart', () => selectTile(id), { passive: true });
-    tiles.push({ id, el, type: 'material', x: pos.x, y: pos.y, z: pos.z });
-    if (animate) setTimeout(() => { el.classList.add('animating'); el.style.opacity = '1'; }, (13 + i) * 60 + 20);
+    if (animate) setTimeout(() => { el.classList.add('animating'); el.style.opacity = '1'; }, i * 45 + 20);
   });
 }
 

← d3bffed mood-board-studio: use real DW products + photos instead of  ·  back to Model Arena  ·  mood-board-studio: material fidelity — replace fabric-mislab f6f19f3 →