[object Object]

← back to Quadrille Showroom

Boards 30"×6ft (real sample wings, 8/wall + paging); fan-out opens the board's L/R panels to present 1 design; Peruse 4ft-centered, 5s dwell

5921951cadb2a459311a1fe11cf7dbb9bb299130 · 2026-06-27 00:12:12 -0700 · Steve

Files touched

Diff

commit 5921951cadb2a459311a1fe11cf7dbb9bb299130
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jun 27 00:12:12 2026 -0700

    Boards 30"×6ft (real sample wings, 8/wall + paging); fan-out opens the board's L/R panels to present 1 design; Peruse 4ft-centered, 5s dwell
---
 public/js/showroom.js | 151 +++++++++++++++++++++-----------------------------
 public/showroom.html  |   2 +-
 2 files changed, 63 insertions(+), 90 deletions(-)

diff --git a/public/js/showroom.js b/public/js/showroom.js
index 76ed264..51180b6 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -7,18 +7,14 @@
 const CONFIG = {
   room: { width: 7.5, depth: 6.0, height: 2.75 },
   wing: {
-    height: 1.83,
-    totalCount: 50,
-    wallMargin: 0.25,
+    height: 1.8288,         // 6 ft tall (real sample-board height)
+    boardWidthM: 0.762,     // 30" wide (real sample-board width)
+    totalCount: 8,          // boards per wall — 8 × 30" fits the back wall; Peruse pages the rest
+    wallMargin: 0.3,
     animSpeed: 0.06,
-    fanCascade: {
-      center: Math.PI / 2,  // 90° for clicked wing
-      ring1: Math.PI / 3,   // 60° for ±1
-      ring2: 0.7,           // ~40° for ±2
-      ring3: 0.35           // ~20° for ±3
-    }
+    panelOpen: 1.35         // ~77° leaf-hinge when "fanned out" to present one design
   },
-  camera: { startPos: { x: 0, y: 1.6, z: 1.0 }, fov: 55 }
+  camera: { startPos: { x: 0, y: 1.6, z: 2.4 }, fov: 55 }
 };
 
 // ============================================================
@@ -32,11 +28,11 @@ let controlsLocked = false, lockedCamPos = null, lockedCamTarget = null, previou
 let cameraAnim = null; // { startPos, endPos, startTarget, endTarget, progress, duration, onComplete }
 
 // Window paging across the full catalog (50 live wings at a time)
-let windowOffset = 0, windowTotal = 0, windowSize = 50;
+let windowOffset = 0, windowTotal = 0, windowSize = 8;
 let currentBrand = 'all', currentSort = 'natural', currentWallGroup = null;
 // Peruse (endless auto-tour) state
 let peruseActive = false, peruseIndex = 0, peruseTimer = null;
-const PERUSE_DWELL = 3000; // ms per wing
+const PERUSE_DWELL = 5000; // ms per wing — dwell 5s on each open design
 // Wall-cladding: render the focused pattern onto the actual room walls
 let roomWalls = [];          // [{ mesh, w, h, origMat, cladMat }]
 let wallsAutoClad = true;    // focusing a wing clads the room walls in its pattern
@@ -44,7 +40,7 @@ let wallsAutoClad = true;    // focusing a wing clads the room walls in its patt
 // like a sample book — left leaf mirrored, right leaf normal (same pattern, both sides).
 let proximityEnabled = true;
 const PROX_FT = 1.22;        // 4 feet, in meters
-const BOOK_OPEN = 0.66;      // leaf hinge angle when open (~38°)
+const BOOK_OPEN = 1.35;      // leaf hinge angle when open (~77°) — panels open out to present 1 design
 
 // SHARED materials — key perf optimization
 const MAT = {};
@@ -141,26 +137,44 @@ function init() {
 // ENVIRONMENT — PMREM-baked studio gradient for PBR reflections
 // (no external HDR needed; a vertical canvas gradient reads as a softbox ceiling)
 // ============================================================
+// Photoreal IBL — PMREM-bake a small "gallery interior" scene (warm ceiling cap,
+// 8 warm track-light spheres, dark polished floor, warm wall bands). Ported from
+// the dw-war-room ShowroomEnvironment; gives chrome/floor/board reflections real
+// directional warmth instead of a flat gradient. Pure core THREE — no HDR file.
 function buildEnvironment() {
-  const c = document.createElement('canvas'); c.width = 64; c.height = 256;
-  const x = c.getContext('2d');
-  const g = x.createLinearGradient(0, 0, 0, 256);
-  g.addColorStop(0.00, '#34363d'); // upper sky (dim, slightly lifted)
-  g.addColorStop(0.40, '#928d81'); // wall horizon
-  g.addColorStop(0.455, '#fbf6ea'); // crisp warm softbox core (ceiling light)
-  g.addColorStop(0.50, '#efe9da');
-  g.addColorStop(0.57, '#949086');
-  g.addColorStop(1.00, '#1c1c22'); // floor falloff
-  x.fillStyle = g; x.fillRect(0, 0, 64, 256);
-  const tex = new THREE.CanvasTexture(c);
-  tex.mapping = THREE.EquirectangularReflectionMapping;
   try {
     const pmrem = new THREE.PMREMGenerator(renderer);
-    const rt = pmrem.fromEquirectangular(tex);
+    if (pmrem.compileEquirectangularShader) pmrem.compileEquirectangularShader();
+    const env = new THREE.Scene();
+    const disposables = [];
+    const mesh = (geo, mat) => { disposables.push(geo, mat); const m = new THREE.Mesh(geo, mat); env.add(m); return m; };
+
+    // Warm dark interior dome
+    mesh(new THREE.SphereGeometry(50, 32, 16), new THREE.MeshBasicMaterial({ color: 0x1a1714, side: THREE.BackSide }));
+    // Warm ceiling cap (~3200K gallery light)
+    const ceil = mesh(new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, 0, Math.PI * 0.22),
+      new THREE.MeshBasicMaterial({ color: 0xf6e8d2, side: THREE.BackSide }));
+    ceil.rotation.x = Math.PI;
+    // Dark polished floor band (reflection source)
+    mesh(new THREE.SphereGeometry(40, 16, 8, 0, Math.PI * 2, Math.PI * 0.78, Math.PI * 0.22),
+      new THREE.MeshBasicMaterial({ color: 0x0d0c0a, side: THREE.BackSide }));
+    // 4 warm wall bands
+    for (let i = 0; i < 4; i++) {
+      const w = mesh(new THREE.PlaneGeometry(30, 16), new THREE.MeshBasicMaterial({ color: 0x33261d, side: THREE.DoubleSide }));
+      const a = (i / 4) * Math.PI * 2;
+      w.position.set(Math.cos(a) * 35, 5, Math.sin(a) * 35); w.lookAt(0, 5, 0);
+    }
+    // 8 bright warm track-light spots at ceiling level → crisp specular hits
+    for (let i = 0; i < 8; i++) {
+      const s = mesh(new THREE.SphereGeometry(2.2, 8, 8), new THREE.MeshBasicMaterial({ color: 0xffe9c6 }));
+      const a = (i / 8) * Math.PI * 2;
+      s.position.set(Math.cos(a) * 15, 18, Math.sin(a) * 15);
+    }
+    const rt = pmrem.fromScene(env, 0.02);
     scene.environment = rt.texture;
     pmrem.dispose();
+    disposables.forEach(d => d.dispose && d.dispose());
   } catch (e) { console.warn('env build failed', e.message); }
-  tex.dispose();
 }
 
 // Tag a texture as color data so the renderer gamma-corrects it (legacy three API)
@@ -696,9 +710,10 @@ function buildWingWall() {
   const W = CONFIG.room.width, D = CONFIG.room.depth;
   const WC = CONFIG.wing;
   const margin = WC.wallMargin;
-  const availW = W - 2 * margin;  // 7.0m usable
-  const slotW = availW / WC.totalCount;  // constant 50-slot spacing (0.14m)
-  const wingW = slotW - 0.02;            // 0.12m wing face width
+  const availW = W - 2 * margin;         // ~6.9m usable
+  const nSlots = Math.max(1, Math.min(WC.totalCount, products.length || WC.totalCount));
+  const slotW = availW / nSlots;         // pitch between boards
+  const wingW = Math.min(WC.boardWidthM, slotW - 0.06); // 30" board face (with a gap so they don't touch)
   const backZ = -D / 2 + 0.2;            // 0.2m off back wall
 
   // Server returns the window already sorted/brand-filtered — keep its order.
@@ -1069,6 +1084,7 @@ function focusOnWing(pivot) {
   previousCamTarget = controls.target.clone();
 
   focusedWing = pivot;
+  pivot.userData.panelClosed = false;  // focusing a board fans it open to present its design
 
   // Calculate world position of the wing
   const wingWorldPos = new THREE.Vector3();
@@ -1097,67 +1113,24 @@ function focusOnWing(pivot) {
   document.getElementById('info-text').textContent = `Wing: ${product.pattern_name || product.name} by ${product.vendor} | Fan open to browse adjacent patterns`;
 }
 
-// STEP 2: Fan Cascade — open center wing + 3 adjacent on each side
+// "Fan out" — open the FOCUSED board's two panels (left & right) to present its
+// single design. The leaf hinge is animated by updateBooks for the focused board;
+// this just toggles the open/closed override + button state. (No more multi-board
+// cascade — one board, opened to show one design.)
 function fanCascadeOpen(direction) {
   if (!focusedWing) return;
-  const centerIdx = focusedWing.userData.index;
-  const fc = CONFIG.wing.fanCascade;
-
-  // If fan already open on this wing in this direction, close it
-  if (fanCenterIndex === centerIdx && fanWings.length > 0 && focusedWing.userData.wingDirection === direction) {
-    closeFanCascade();
-    return;
-  }
-
-  // Close any existing fan first
-  if (fanWings.length > 0) closeFanCascade(true);
-
-  fanCenterIndex = centerIdx;
-  fanWings = [];
-  focusedWing.userData.wingDirection = direction;
-
-  // Cascade rings: [offset, angle]
-  const rings = [
-    [0, fc.center],  // 90° center
-    [1, fc.ring1],   // 60° ±1
-    [2, fc.ring2],   // 40° ±2
-    [3, fc.ring3]    // 20° ±3
-  ];
-
-  rings.forEach(([offset, angle]) => {
-    if (offset === 0) {
-      const pivot = wingBoards[centerIdx];
-      pivot.userData.targetAngle = direction * angle;
-      if (!animatingWings.includes(pivot)) animatingWings.push(pivot);
-      fanWings.push(pivot);
-    } else {
-      // Left neighbor
-      const leftIdx = centerIdx - offset;
-      if (leftIdx >= 0) {
-        const lp = wingBoards[leftIdx];
-        lp.userData.targetAngle = direction * angle;
-        if (!animatingWings.includes(lp)) animatingWings.push(lp);
-        fanWings.push(lp);
-      }
-      // Right neighbor
-      const rightIdx = centerIdx + offset;
-      if (rightIdx < wingBoards.length) {
-        const rp = wingBoards[rightIdx];
-        rp.userData.targetAngle = direction * angle;
-        if (!animatingWings.includes(rp)) animatingWings.push(rp);
-        fanWings.push(rp);
-      }
-    }
-  });
-
+  const ud = focusedWing.userData;
+  // Re-pressing the same side closes it.
+  if (!ud.panelClosed && fanWings.length && ud.wingDirection === direction) { closeFanCascade(); return; }
+  ud.panelClosed = false;
+  ud.wingDirection = direction;
+  fanWings = [focusedWing];
+  fanCenterIndex = ud.index;
   updateOpenBtnState(direction);
 }
 
 function closeFanCascade(skipBtnUpdate) {
-  fanWings.forEach(pivot => {
-    pivot.userData.targetAngle = 0;
-    if (!animatingWings.includes(pivot)) animatingWings.push(pivot);
-  });
+  if (focusedWing) focusedWing.userData.panelClosed = true;
   fanWings = [];
   fanCenterIndex = -1;
   if (!skipBtnUpdate) updateOpenBtnState(0);
@@ -1383,7 +1356,7 @@ function updateBooks(dt) {
   const k = Math.min(1, dt * 6); // ease toward target
   for (let i = 0; i < wingBoards.length; i++) {
     const p = wingBoards[i], ud = p.userData;
-    const wantOpen = (p === nearest) || (p === focusedWing);
+    const wantOpen = (p === nearest) || (p === focusedWing && !ud.panelClosed);
     const target = wantOpen ? BOOK_OPEN : 0;
     if (wantOpen && !ud.imageLoaded) { ud.imageLoaded = true; loadWingBook(p); }
     if (Math.abs(ud.bookOpen - target) > 0.0008) {
@@ -1516,9 +1489,9 @@ function peruseStep() {
   const pivot = wingBoards[peruseIndex];
   if (!pivot) { stopPeruse(); return; }
 
-  focusOnWing(pivot);  // fly ~4ft in, lock, lazy-load texture, show detail panel
-  // Open the wing like a sample book once the camera lands
-  setTimeout(() => { if (peruseActive && focusedWing === pivot) fanCascadeOpen(1); }, 700);
+  // Fly to 4 ft back, centered; the board fans its panels open (updateBooks opens
+  // the focused board) to present its single design, dwell PERUSE_DWELL, then advance.
+  focusOnWing(pivot);
 
   peruseTimer = setTimeout(async () => {
     if (!peruseActive) return;
diff --git a/public/showroom.html b/public/showroom.html
index 17707df..6f51d79 100644
--- a/public/showroom.html
+++ b/public/showroom.html
@@ -133,7 +133,7 @@
         <option value="book">Book</option>
       </select>
       <span id="density-wrap" title="Wings per wall">
-        <span>Density</span><input type="range" id="density-range" min="20" max="80" step="10" value="50">
+        <span>Boards</span><input type="range" id="density-range" min="4" max="12" step="2" value="8">
       </span>
     </div>
 

← a76a8f6 Add automated Playwright walkthrough recorder (move-around s  ·  back to Quadrille Showroom  ·  Scene luxury pass 1: limewash plaster left wall (was brick), 575ee37 →