[object Object]

← back to Quadrille Showroom

showroom P1: remove angled/orbit/topdown, boot=walk-through-center, macro=2.5ft, carousel=continuous parade, compare=user-choosable two designs

bc32b4a9069910878fbc736938298c1abf40790c · 2026-06-30 19:38:07 -0700 · Steve Abrams

Files touched

Diff

commit bc32b4a9069910878fbc736938298c1abf40790c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 30 19:38:07 2026 -0700

    showroom P1: remove angled/orbit/topdown, boot=walk-through-center, macro=2.5ft, carousel=continuous parade, compare=user-choosable two designs
---
 public/js/showroom.js  |  22 +++++++
 public/js/versions.js  |   4 +-
 public/js/viewmodes.js | 165 +++++++++++++++++--------------------------------
 3 files changed, 81 insertions(+), 110 deletions(-)

diff --git a/public/js/showroom.js b/public/js/showroom.js
index 6a58d1a..116d41d 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -3816,6 +3816,28 @@ function smoothCameraTo(pos, target, onComplete, fov) {
   };
 }
 
+// Automated "walk through the CENTER" of the showroom on boot (Steve 2026-06-30).
+// A slow forward dolly down the middle of the room, on the shared cameraAnim tween
+// (eases inside the main render loop). walk.enter() parked us at the front looking
+// at the board wall; this glides us up the centre to just in front of the arc, then
+// hands back to free Explore (WASD). Cheap + interruptible (cameraAnim auto-clears
+// on complete; a click stops any tour).
+function _qhStartCenterWalk() {
+  if (cameraAnim) return;
+  const backZ = -CONFIG.room.depth / 2;
+  cameraAnim = {
+    startPos: camera.position.clone(),
+    endPos: new THREE.Vector3(0, 1.62, backZ + 2.4),   // centre of room, eye-level, near the boards
+    startTarget: controls.target.clone(),
+    endTarget: new THREE.Vector3(0, 1.4, backZ + 0.2), // looking straight down the centre at the board wall
+    startFov: camera.fov, endFov: camera.fov,
+    progress: 0,
+    duration: 7.0,        // slow, cinematic centre walk
+    onComplete: null      // leaves the user in Explore among the boards
+  };
+}
+window._qhStartCenterWalk = _qhStartCenterWalk;
+
 function updateSampleTray() {
   const tray = document.getElementById('sample-tray'), items = document.getElementById('tray-items');
   document.getElementById('tray-count').textContent = sampleTray.length;
diff --git a/public/js/versions.js b/public/js/versions.js
index 3692f1b..54ab949 100644
--- a/public/js/versions.js
+++ b/public/js/versions.js
@@ -239,7 +239,7 @@ function boot() {
         // the intended radial fan of swatches laid on the table (the fan mesh is the
         // next-pass build; this is the camera, not the pinwheel yet).
         setTheme('warm');
-        setMode('topdown');
+        setMode('gallery');   // (top-down mode retired 2026-06-30 — gallery is the closest overview)
       },
       elements: [
         { n: 1, label: 'Top-down plan over the consultation table', anchor: { named: 'nook' } },
@@ -257,7 +257,7 @@ function boot() {
         setTheme('boutique');
         setWingDeg(20);
         focusMiddle();
-        setMode('orbit');
+        setMode('carousel');   // (orbit mode retired 2026-06-30 — carousel is the closest rotating preview)
       },
       elements: [
         { n: 1, label: 'Orbit around the hero board',               anchor: { board: -1 } },
diff --git a/public/js/viewmodes.js b/public/js/viewmodes.js
index 0285042..8a42c0f 100644
--- a/public/js/viewmodes.js
+++ b/public/js/viewmodes.js
@@ -23,6 +23,7 @@ function boot() {
   const WING = QH.CONFIG.wing;
   const backZ = -ROOM.depth / 2 + 0.2;     // wing-wall plane
   const boardCY = WING.height / 2 + 0.04;  // vertical centre of a 6-ft board
+  const MACRO_DIST = 0.762;                // macro camera distance = 2.5 ft (Steve 2026-06-30)
 
   // ---- state ----
   let currentMode = null;     // active view-mode key
@@ -33,6 +34,8 @@ function boot() {
   let _carouselWG = null;     // cached wall group (avoid full scene.traverse per frame)
   let _carouselShadowAccum = 0; // throttle carousel shadow rebakes
   let macroPhase = 0;         // macro slow drift phase
+  let compareB = -1;          // compare: user-chosen SECOND board index (-1 = auto next)
+  let compareKeyHandler = null; // compare: keydown listener to cycle the 2nd design
   let compareSetup = false;   // compare mode: two boards staged
   let savedFocusIndex = 0;    // remember which board to return to
 
@@ -73,6 +76,24 @@ function boot() {
     }
   }
 
+  // Compare-mode render: show boards i + j open, frame the pair, name both in the banner.
+  function renderCompare(i, j) {
+    QH.revertWalls();
+    const boards = QH.wingBoards;
+    boards.forEach((b, k) => { b.visible = (k === i || k === j); if (b.userData.wallDrop) b.userData.wallDrop.visible = false; });
+    [i, j].forEach(k => {
+      const ud = boards[k] && boards[k].userData; if (!ud) return;
+      ud.panelClosed = false;
+      if (ud.leafL) ud.leafL.rotation.y = 0.10;
+      if (ud.leafR) ud.leafR.rotation.y = -0.10;
+    });
+    const a = boardWorld(i), b = boardWorld(j);
+    const midX = (a.x + b.x) / 2;
+    park(new THREE.Vector3(midX, boardCY, backZ + 2.9), new THREE.Vector3(midX, boardCY, backZ), 44);
+    const nameOf = (k) => { const p = boards[k] && boards[k].userData && boards[k].userData.product; return p ? (p.pattern_name || p.name || ('Board ' + (k + 1))) : ('Board ' + (k + 1)); };
+    setBanner('Comparing “' + nameOf(i) + '”  vs  “' + nameOf(j) + '”   ·   ◀ ▶ change the second design');
+  }
+
   const MODES = {
     hero: {
       label: 'Hero', hint: 'Flat, dead-on — the gallery hero panel.', guided: true,
@@ -90,57 +111,6 @@ function boot() {
       }
     },
 
-    angled: {
-      label: 'Angled', hint: 'Board opens to a ~65° V — the open-book look.', guided: true,
-      enter() {
-        ensureFocused();
-        setWingDeg(60);
-        // Step a touch closer + lower so the V reads dimensionally.
-        const c = boardWorld(curIdx());
-        const pos = new THREE.Vector3(c.x + 0.35, c.y - 0.05, c.z + 1.7);
-        const tgt = new THREE.Vector3(c.x, c.y, c.z);
-        park(pos, tgt, 38);
-      },
-      exit() { setWingDeg(8); }
-    },
-
-    orbit: {
-      label: 'Orbit', hint: 'Drag to orbit around the focused board.', guided: true,
-      enter() {
-        // Open the focused board to present its design, but keep neighbours visible so
-        // the orbit reads as moving *around* a board in the room (richer than a lone slab).
-        const idx = curIdx();
-        QH.restoreBoards();
-        if (!QH.focusedWing || QH.focusedWing.userData.index !== idx) { if (QH.wingBoards[idx]) QH.focusOnWing(QH.wingBoards[idx]); }
-        QH.restoreBoards();           // un-isolate again (focusOnWing isolated)
-        if (QH.focusedWing) QH.focusedWing.userData.panelClosed = false;
-        setWingDeg(20);
-        QH.revertWalls();
-        freeCam();
-        // Turn orbit controls ON for this mode regardless of Explore.
-        const ctl = QH.controls;
-        ctl.enableRotate = true; ctl.enableZoom = true; ctl.enablePan = false;
-        const c = boardWorld(curIdx());
-        ctl.target.copy(c);
-        ctl.minDistance = 1.2; ctl.maxDistance = 5.5;
-        QH.camera.position.set(c.x, c.y + 0.1, c.z + 2.6);
-        QH.setTargetFov(40); QH.camera.fov = 40; QH.camera.updateProjectionMatrix();
-        orbitAuto = true; orbitSpin = 0;
-      },
-      frame(dt) {
-        if (!orbitAuto) return;              // user drag cancels auto-spin
-        const ctl = QH.controls, cam = QH.camera, c = ctl.target;
-        orbitSpin += dt * 0.32;
-        const r = 2.6, h = c.y + 0.12;
-        cam.position.set(c.x + Math.sin(orbitSpin) * r, h, c.z + Math.cos(orbitSpin) * r);
-        cam.lookAt(c);
-      },
-      exit() {
-        orbitAuto = false;
-        QH.controls.minDistance = 1.0; QH.controls.maxDistance = 8.0;
-      }
-    },
-
     gallery: {
       label: 'Gallery', hint: 'Pull back to see the whole wall of boards.', guided: false,
       enter() {
@@ -156,29 +126,6 @@ function boot() {
       }
     },
 
-    topdown: {
-      label: 'Top-Down', hint: 'High overhead plan view of the showroom.', guided: false,
-      enter() {
-        QH.restoreBoards();
-        if (QH.focusedWing) QH.unfocusWing(true);
-        QH.revertWalls();
-        setWingDeg(0);
-        // The room has a real ceiling, so a camera ABOVE it would just see the ceiling.
-        // Instead hide the ceiling for this mode and shoot from straight above the room
-        // centre, tilted slightly toward the board wall — a true overhead plan that
-        // reads the floor, furniture footprint, and the rail of boards.
-        setCeilingVisible(false);
-        // Camera tucked just under where the ceiling was, at the FRONT of the room,
-        // looking down-and-in across the whole floor to the board wall — a high
-        // establishing/plan angle that actually reads inside a closed box (a straight
-        // top-down would only see wall tops). The floor + furniture + rail all show.
-        const pos = new THREE.Vector3(0, ROOM.height - 0.15, ROOM.depth / 2 - 0.3);
-        const tgt = new THREE.Vector3(0, 0.0, backZ + 0.5);
-        park(pos, tgt, 70);
-      },
-      exit() { setCeilingVisible(true); }
-    },
-
     walk: {
       label: 'Walk-Through', hint: 'First-person — W A S D to walk the room.', guided: false,
       enter() {
@@ -224,7 +171,11 @@ function boot() {
         if (!_carouselWG) _carouselWG = findWallGroup();
         const wg = _carouselWG;
         if (wg) {
-          carouselT += dt * 0.22; wg.rotation.y = Math.sin(carouselT) * 0.5;
+          // CONTINUOUS parade (Steve 2026-06-30) — boards rotate steadily past like a
+          // real turntable, instead of the old Math.sin() rock (which just wobbled the
+          // arc back and forth ±0.5 rad and never actually paraded a new board to front).
+          wg.rotation.y += dt * 0.18;
+          if (wg.rotation.y > Math.PI * 2) wg.rotation.y -= Math.PI * 2;
           _carouselShadowAccum += dt;
           if (_carouselShadowAccum > 0.16) { _carouselShadowAccum = 0; if (QH.requestShadowUpdate) QH.requestShadowUpdate(1); }
         }
@@ -238,11 +189,12 @@ function boot() {
         ensureFocused();
         setWingDeg(2);
         const c = boardWorld(curIdx());
-        // Very close, tight FOV — read the weave/print. Slight off-centre so the
-        // grazing light catches the surface.
-        const pos = new THREE.Vector3(c.x - 0.12, c.y + 0.15, c.z + 0.62);
+        // Zoom in to exactly 2.5 FEET from the board (Steve 2026-06-30) — 0.762 m —
+        // tight FOV to read the weave/print. Slight off-centre so grazing light catches
+        // the surface. MACRO_DIST is reused by the parallax drift so it stays at 2.5 ft.
+        const pos = new THREE.Vector3(c.x - 0.12, c.y + 0.15, c.z + MACRO_DIST);
         const tgt = new THREE.Vector3(c.x - 0.05, c.y + 0.1, c.z);
-        park(pos, tgt, 26);
+        park(pos, tgt, 30);
         macroPhase = 0;
       },
       frame(dt) {
@@ -259,36 +211,27 @@ function boot() {
     },
 
     compare: {
-      label: 'Compare', hint: 'Two boards side-by-side for A/B.', guided: false,
+      label: 'Compare', hint: 'Two designs side-by-side — ◀ ▶ change the second one.', guided: false,
       enter() {
-        // Stage two adjacent boards open + visible; frame both.
-        QH.revertWalls();
+        // A = the current board; B = a user-CHOOSABLE second design (Steve 2026-06-30).
+        // Previously compare force-picked i+1 with no way to change it — confusing. Now
+        // ◀ ▶ (or [ ]) cycle the second design; the banner names both so you know what
+        // you're looking at.
+        const len = QH.wingBoards.length;
         const i = curIdx();
-        const j = (i + 1) % QH.wingBoards.length;
-        // Show only these two.
-        QH.wingBoards.forEach((b, k) => { b.visible = (k === i || k === j); if (b.userData.wallDrop) b.userData.wallDrop.visible = false; });
-        // Open both leaves a touch so each presents its design.
-        [i, j].forEach(k => {
-          const ud = QH.wingBoards[k].userData;
-          ud.panelClosed = false;
-          // mark as a "compare-open" so the showroom's updateBooks won't fight it:
-          // we set the leaf rotation directly here each enter; small static splay.
-          if (ud.leafL) ud.leafL.rotation.y = 0.10;
-          if (ud.leafR) ud.leafR.rotation.y = -0.10;
-        });
-        QH.focusOnWing && (QH._noop = 0);
-        // Frame the pair: camera centred between the two boards, pulled back.
-        const a = boardWorld(i), b = boardWorld(j);
-        const midX = (a.x + b.x) / 2;
-        const pos = new THREE.Vector3(midX, boardCY, backZ + 2.9);
-        const tgt = new THREE.Vector3(midX, boardCY, backZ);
-        park(pos, tgt, 44);
+        if (compareB < 0 || compareB === i || compareB >= len) compareB = (i + 1) % len;
+        renderCompare(i, compareB);
+        compareKeyHandler = (e) => {
+          if (!compareSetup) return;
+          if (e.code === 'ArrowRight' || e.key === ']') { do { compareB = (compareB + 1) % len; } while (compareB === i); renderCompare(i, compareB); e.preventDefault(); }
+          else if (e.code === 'ArrowLeft' || e.key === '[') { do { compareB = (compareB - 1 + len) % len; } while (compareB === i); renderCompare(i, compareB); e.preventDefault(); }
+        };
+        window.addEventListener('keydown', compareKeyHandler);
         compareSetup = true;
-        // Update the title to say what's being compared.
-        setBanner('Comparing two designs side-by-side — Next steps both forward.');
       },
       exit() {
         compareSetup = false;
+        if (compareKeyHandler) { window.removeEventListener('keydown', compareKeyHandler); compareKeyHandler = null; }
         QH.restoreBoards();
         clearBanner();
       }
@@ -312,7 +255,7 @@ function boot() {
     }
   };
 
-  const MODE_ORDER = ['hero','angled','orbit','gallery','topdown','walk','carousel','macro','compare','room'];
+  const MODE_ORDER = ['walk','hero','gallery','carousel','macro','compare','room'];
 
   // Helper to drive the showroom's open-board angle (the Wing° slider value).
   function setWingDeg(deg) {
@@ -506,11 +449,17 @@ function boot() {
   // settled — except never auto-restore the free-camera modes on boot (they'd land
   // on an empty room for a returning senior). Restore only the board-presentation
   // modes; otherwise fall back to Hero.
+  // Boot = WALK-THROUGH the CENTER of the showroom (Steve 2026-06-30): first-person
+  // walk down the middle of the room past the boards, with an automated slow dolly
+  // (Peruse) so it moves on its own until the user takes WASD control. A returning
+  // user's saved board-presentation mode still restores; free-cam modes never do.
   const savedMode = localStorage.getItem('qh_viewmode');
-  const safeBootModes = { hero: 1, angled: 1, macro: 1, room: 1 };
-  setMode('hero', true);   // boot = Hero, guided focus already set the middle board
-  if (savedMode && MODES[savedMode] && safeBootModes[savedMode] && savedMode !== 'hero') {
-    setTimeout(() => setMode(savedMode, true), 1200);
+  const safeBootModes = { walk: 1, hero: 1, macro: 1, room: 1 };
+  const bootMode = (savedMode && MODES[savedMode] && safeBootModes[savedMode]) ? savedMode : 'walk';
+  setMode(bootMode, true);
+  if (bootMode === 'walk') {
+    // Kick the automated center walk-through a beat after the scene settles.
+    setTimeout(() => { if (currentMode === 'walk' && window._qhStartCenterWalk) window._qhStartCenterWalk(); }, 1400);
   }
 
   // Expose for the test harness + for showroom guided buttons to re-assert the view.

← 2b90e02 spec: 5 room types for showroom room-preview  ·  back to Quadrille Showroom  ·  auto-save: 2026-06-30T20:43:13 (9 files) — public/js/showroo 02a9c21 →