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