← 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
M public/js/showroom.jsM public/showroom.html
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 →