[object Object]

← back to Model Arena

mood-board-studio: cycle-2 Cody-gate fixes — honest export + resize + dead CSS

e76e1ab9ba321c2f9ac9f3a9b38ac74e3d9c0e52 · 2026-07-30 16:12:30 -0700 · Steve

Adversarial red-team caught real holes; all verified & fixed:
- Export now AWAITS all product photos loading before drawing+sampling, so the
  'COLOR STORY · SAMPLED FROM SELECTED WALLCOVERINGS' strip is truthful even on
  immediate export (was: unloaded tiles fell to procedural SVG → strip sampled
  fake colors). Verified: immediate export samples real photo colors.
- Empty-board export: strip + caption now skipped when no products (was: a lone
  fallback-brown strip + caption on a cleared board looked broken). Verified.
- Resize handler no longer repopulates the board (was: silently rotated in a
  different 12 products AND discarded the user's arrangement); only re-measures chrome.
- Sidebar 'Palette' -> 'Style Concept Palette' (honest: it's the style concept,
  distinct from the export's sampled real colors).
- Dead code: removed ~2.3KB dead CSS (.color-chip/.type-tile/.material-chip/etc.),
  the orphan MATERIAL-CHIP-CONFIGS header, and corrected the stale plan-doc comment.

Verified: 0 console errors/warnings, immediate + empty export both clean/untainted.

Files touched

Diff

commit e76e1ab9ba321c2f9ac9f3a9b38ac74e3d9c0e52
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 30 16:12:30 2026 -0700

    mood-board-studio: cycle-2 Cody-gate fixes — honest export + resize + dead CSS
    
    Adversarial red-team caught real holes; all verified & fixed:
    - Export now AWAITS all product photos loading before drawing+sampling, so the
      'COLOR STORY · SAMPLED FROM SELECTED WALLCOVERINGS' strip is truthful even on
      immediate export (was: unloaded tiles fell to procedural SVG → strip sampled
      fake colors). Verified: immediate export samples real photo colors.
    - Empty-board export: strip + caption now skipped when no products (was: a lone
      fallback-brown strip + caption on a cleared board looked broken). Verified.
    - Resize handler no longer repopulates the board (was: silently rotated in a
      different 12 products AND discarded the user's arrangement); only re-measures chrome.
    - Sidebar 'Palette' -> 'Style Concept Palette' (honest: it's the style concept,
      distinct from the export's sampled real colors).
    - Dead code: removed ~2.3KB dead CSS (.color-chip/.type-tile/.material-chip/etc.),
      the orphan MATERIAL-CHIP-CONFIGS header, and corrected the stale plan-doc comment.
    
    Verified: 0 console errors/warnings, immediate + empty export both clean/untainted.
---
 public/games/dw-mood-board-studio.html | 164 +++++++--------------------------
 1 file changed, 33 insertions(+), 131 deletions(-)

diff --git a/public/games/dw-mood-board-studio.html b/public/games/dw-mood-board-studio.html
index 993ce61..ff99c0d 100644
--- a/public/games/dw-mood-board-studio.html
+++ b/public/games/dw-mood-board-studio.html
@@ -50,11 +50,8 @@ UX FLOW
         - Linen: natural woven mesh pattern, oat/flax
         - Raffia: diagonal bast fiber weave, warm straw
         - Chinoiserie: hand-painted branch/blossom on pale background
-    • 6 COLOR CHIPS — rounded rectangular chips, each labeled:
-        - Oatmeal #e8dccb, Celadon #a8c4b0, Terracotta #c1623f,
-          Ink #1c2b3a, Brass #c9a961, Sage #7a9472
-    • 1 TYPE PAIRING TILE — shows a serif headline + sans body combination
-    • 3 MATERIAL CHIPS — small rounded badges: Brass / Marble / White Oak
+    • (Earlier versions also had color chips, a type-pairing tile, and material
+      chips; the board is now real-products-only — those were removed.)
     Each tile: draggable to anywhere on canvas, z-index promoted on pickup,
     hover lift (translateY -4px, shadow deepen), snaps to 10px grid on drop.
 
@@ -443,111 +440,6 @@ header {
   background: #1a1712;
 }
 
-/* ============================================================
-   COLOR CHIP TILES
-   ============================================================ */
-.color-chip {
-  width: 80px;
-  height: 110px;
-  border-radius: 4px;
-  overflow: hidden;
-  display: flex;
-  flex-direction: column;
-}
-.chip-swatch {
-  flex: 1;
-}
-.chip-info {
-  padding: 6px 7px 7px;
-  background: #fff;
-  display: flex;
-  flex-direction: column;
-  gap: 2px;
-}
-.chip-name {
-  font-family: var(--sans);
-  font-size: 8px;
-  letter-spacing: .06em;
-  text-transform: uppercase;
-  font-weight: 700;
-  color: var(--ink);
-  line-height: 1.2;
-}
-.chip-hex {
-  font-family: var(--sans);
-  font-size: 7px;
-  color: #888;
-  letter-spacing: .04em;
-}
-
-/* ============================================================
-   TYPE PAIRING TILE
-   ============================================================ */
-.type-tile {
-  width: 220px;
-  height: 130px;
-  background: #fff;
-  padding: 16px 18px 14px;
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-  border-left: 3px solid var(--accent);
-}
-.type-tile-serif {
-  font-family: var(--serif);
-  font-size: 22px;
-  font-weight: 600;
-  color: var(--ink);
-  line-height: 1.1;
-  letter-spacing: .02em;
-}
-.type-tile-sans {
-  font-family: var(--sans);
-  font-size: 9.5px;
-  color: #666;
-  letter-spacing: .06em;
-  line-height: 1.5;
-}
-.type-tile-label {
-  position: absolute;
-  top: 7px; right: 8px;
-  font-size: 7px;
-  letter-spacing: .08em;
-  text-transform: uppercase;
-  color: #bbb;
-}
-
-/* ============================================================
-   MATERIAL CHIP TILES
-   ============================================================ */
-.material-chip {
-  width: 100px;
-  height: 56px;
-  border-radius: 4px;
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  padding: 0 12px;
-  overflow: hidden;
-}
-.material-chip .mat-icon {
-  width: 28px;
-  height: 28px;
-  border-radius: 50%;
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.material-chip .mat-label {
-  font-family: var(--sans);
-  font-size: 9px;
-  letter-spacing: .09em;
-  text-transform: uppercase;
-  font-weight: 700;
-  line-height: 1.2;
-}
-
 /* ============================================================
    SIDEBAR
    ============================================================ */
@@ -830,7 +722,7 @@ header {
     <div class="sidebar-section">
       <div class="sidebar-heading">Active Style</div>
       <div class="style-thumb" id="styleThumb"></div>
-      <div class="sidebar-heading" style="margin-bottom:8px;">Palette</div>
+      <div class="sidebar-heading" style="margin-bottom:8px;">Style Concept Palette</div>
       <div class="palette-strip" id="paletteSidebarStrip"></div>
       <div class="palette-names" id="paletteSidebarNames"></div>
     </div>
@@ -1210,9 +1102,6 @@ function buildSwatchSVG(type, palette) {
   }
 }
 
-/* ============================================================
-   MATERIAL CHIP CONFIGS
-   ============================================================ */
 /* ============================================================
    LAYOUT COMPOSER — determines tile positions
    ============================================================ */
@@ -1662,6 +1551,15 @@ async function openExport() {
 
   await new Promise(r => setTimeout(r, 50));
 
+  // Wait for every product photo to finish loading BEFORE we draw + sample the
+  // color story — otherwise unloaded tiles fall through to the procedural SVG
+  // fallback and the "sampled from real wallcoverings" strip would be a lie.
+  await Promise.allSettled(tiles.flatMap(t => {
+    const im = t.el.querySelector('.swatch-img');
+    if (!im || im.complete) return [];
+    return [new Promise(res => { im.onload = im.onerror = res; })];
+  }));
+
   const exportCanvas = document.getElementById('exportCanvas');
   const ctx   = exportCanvas.getContext('2d', { willReadFrequently: true });
   const W     = 1080;
@@ -1756,24 +1654,30 @@ async function openExport() {
   ctx.fillRect(0, barY - 40, W, barH + 40);
 
   // --- Color-story strip: real colors sampled from the actual wallcoverings ---
+  // Only drawn when the board actually has products — an empty board gets no
+  // (misleading) strip or caption.
   const stripY  = H - 108;
   const stripH  = 10;
   const stripW  = W - 120;
   const stripX  = 60;
-  const storyColors = _storyColors.length ? _storyColors : ['#7a6a4a'];
-  const colW    = stripW / storyColors.length;
-  storyColors.forEach((c, i) => {
-    ctx.fillStyle = c;
-    ctx.fillRect(stripX + i * colW, stripY, colW, stripH);
-  });
+  const hasStory = _storyColors.length > 0;
+  if (hasStory) {
+    const colW = stripW / _storyColors.length;
+    _storyColors.forEach((c, i) => {
+      ctx.fillStyle = c;
+      ctx.fillRect(stripX + i * colW, stripY, colW, stripH);
+    });
+  }
 
   // Caption — truthful about where the palette came from (no abstract color names)
-  ctx.save();
-  ctx.font = `500 15px -apple-system,Helvetica,Arial,sans-serif`;
-  ctx.fillStyle = 'rgba(232,227,218,0.50)';
-  ctx.textAlign = 'center';
-  ctx.fillText('COLOR STORY · SAMPLED FROM SELECTED WALLCOVERINGS', W/2, stripY + stripH + 24);
-  ctx.restore();
+  if (hasStory) {
+    ctx.save();
+    ctx.font = `500 15px -apple-system,Helvetica,Arial,sans-serif`;
+    ctx.fillStyle = 'rgba(232,227,218,0.50)';
+    ctx.textAlign = 'center';
+    ctx.fillText('COLOR STORY · SAMPLED FROM SELECTED WALLCOVERINGS', W/2, stripY + stripH + 24);
+    ctx.restore();
+  }
 
   // --- Brass separator line ---
   ctx.strokeStyle = 'rgba(201,169,97,0.50)';
@@ -1996,14 +1900,12 @@ window.addEventListener('load', () => {
   populateBoard('grandmillennial', true);
 });
 
-// Handle resize
+// Handle resize — only re-measure chrome; do NOT repopulate the board (that would
+// discard the user's arrangement AND rotate in a different set of products).
 let resizeTimer;
 window.addEventListener('resize', () => {
   clearTimeout(resizeTimer);
-  resizeTimer = setTimeout(() => {
-    updateChromeHeight();
-    populateBoard(currentPreset, false);
-  }, 300);
+  resizeTimer = setTimeout(updateChromeHeight, 300);
 });
 </script>
 </body>

← 493c6dc mood-board-studio: cycle-2 polish — honest export, no stale  ·  back to Model Arena  ·  mood-board-studio: swap 3 non-swatch product images for flat 7b0d542 →