← 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
M public/games/dw-mood-board-studio.html
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 →