← back to Quadrille Showroom
Default view = walk the avatar + single centered mirrored book + minimal caption bar
fa8ee901fbb1e551f9f2162c0728f35b74d1000c · 2026-06-29 15:14:24 -0700 · Steve
- Walking is now the default: exploreMode ON + persisted; arrows = up/down walk along
facing dir, left/right turn the body. Avatar rig + camera move together, clamped to room.
- Spawn dead-centre, stationary, facing the wing -- no boot auto-fly (enterFixedCentre /
enterGuidedDefault are no-ops in book mode).
- SKU display is now a quiet fixed #wing-caption strip (Pattern . Colorway . SKU . Vendor),
never pops/animates; the old wing-detail popup card is suppressed in book mode.
- The view is ONE open sample book centred on -Z with two leaves hinged at the spine;
left leaf is a horizontal MIRROR of the right (repeat.x flipped). The 50-board arc is
hidden. [ ] / N-P page the catalog.
- Consultation table+chairs nudged so the furnished 20x20 room reads in the scene.
Files touched
M public/js/showroom.jsM public/showroom.html
Diff
commit fa8ee901fbb1e551f9f2162c0728f35b74d1000c
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jun 29 15:14:24 2026 -0700
Default view = walk the avatar + single centered mirrored book + minimal caption bar
- Walking is now the default: exploreMode ON + persisted; arrows = up/down walk along
facing dir, left/right turn the body. Avatar rig + camera move together, clamped to room.
- Spawn dead-centre, stationary, facing the wing -- no boot auto-fly (enterFixedCentre /
enterGuidedDefault are no-ops in book mode).
- SKU display is now a quiet fixed #wing-caption strip (Pattern . Colorway . SKU . Vendor),
never pops/animates; the old wing-detail popup card is suppressed in book mode.
- The view is ONE open sample book centred on -Z with two leaves hinged at the spine;
left leaf is a horizontal MIRROR of the right (repeat.x flipped). The 50-board arc is
hidden. [ ] / N-P page the catalog.
- Consultation table+chairs nudged so the furnished 20x20 room reads in the scene.
---
public/js/showroom.js | 299 ++++++++++++++++++++++++++++++++++++++++++++++----
public/showroom.html | 22 ++++
2 files changed, 301 insertions(+), 20 deletions(-)
diff --git a/public/js/showroom.js b/public/js/showroom.js
index 50c8c3f..d646d0f 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -135,9 +135,22 @@ const WALK_SPEED = 2.5; // meters per second
// Previous/Next/Auto-Tour buttons drive everything, NO keyboard + NO 3D walking
// required. Free-walk (WASD/orbit) is demoted behind an 'Explore' toggle that is
// OFF by default. exploreMode gates all walking/orbit/proximity input.
-let exploreMode = (localStorage.getItem('qh_explore') === '1');
+// Steve takeover (2026-06-29): WALKING is now the DEFAULT. The avatar spawns dead-centre
+// facing the wing and the user "takes over the body" — ↑/↓ walk along the facing
+// direction, ←/→ turn the body. exploreMode therefore defaults ON (and is persisted ON
+// the first time) so boot is the first-person walk experience, not the guided arc browser.
+let exploreMode = (localStorage.getItem('qh_explore') !== '0');
+if (localStorage.getItem('qh_explore') === null) localStorage.setItem('qh_explore', '1');
let guidedReady = false; // set true once the middle board has been auto-focused on load
+// CENTER BOOK (Steve 2026-06-29): the DEFAULT view is ONE open sample book hinged at the
+// spine, centred on the spawn's forward axis (-Z), with the LEFT leaf a horizontal MIRROR
+// of the right leaf — the same pattern reads symmetrically on both sides. The 50-board arc
+// is NOT built in this view; [ ] (or N/P) page through the catalog to change the design.
+const BOOK_MODE = true;
+let centerBook = null; // { group, leafL, leafR, faceL, faceR } — the single open book
+let bookIndex = 0; // which catalog design is shown (within the current window)
+
// ============================================================
// INIT
// ============================================================
@@ -221,6 +234,9 @@ function init() {
window.addEventListener('resize', onResize, false);
window.addEventListener('keydown', e => {
keysDown[e.code] = true;
+ // CENTER BOOK paging — [ / ] (and N / P) change which catalog design the open book shows.
+ if (BOOK_MODE && (e.code === 'BracketRight' || e.code === 'KeyN')) { e.preventDefault(); pageCenterBook(1); return; }
+ if (BOOK_MODE && (e.code === 'BracketLeft' || e.code === 'KeyP')) { e.preventDefault(); pageCenterBook(-1); return; }
if (e.code === 'KeyM') {
minimapVisible = !minimapVisible;
const mc = document.getElementById('minimap');
@@ -930,7 +946,10 @@ function buildFurniture() {
// walking, and the 72° clamp still reaches it. Relocated proportionally for the 20'×20'
// room (Chunk A′): old (1.25,-0.55) × 6.10/4.27 ≈ (1.78,-0.78) — same yaw angle, farther out.
// ============================================================
- const nookX = 1.78, nookZ = -0.78;
+ // Pulled a touch left + forward (was 1.78,-0.78) so the table + bouclé chairs read as
+ // part of the furnished 20'×20' scene — a gentle right-turn from the spawn lands on them,
+ // and they stay clear of the centred open book (x=0, z≈-2.4). Steve 2026-06-29.
+ const nookX = 1.55, nookZ = -1.1;
buildConsultationNook(nookX, nookZ);
// (Removed: boxy TV + bezel + slideshow screen — the wallcoverings are the only
@@ -1389,6 +1408,17 @@ async function loadProducts() {
updateLoadStatus('Building wing wall...', 80);
buildWingWall();
updateWindowLabel();
+
+ // CENTER BOOK default (Steve 2026-06-29): hide the 50-board arc and present ONE open
+ // mirrored sample book centred on the spawn's forward axis. The avatar spawns dead-centre,
+ // stationary, facing it — NO auto-fly. [ ] / N-P page the catalog.
+ if (BOOK_MODE) {
+ if (currentWallGroup) currentWallGroup.visible = false; // no arc in the book view
+ buildCenterBook();
+ setCenterBook(Math.floor((products.slice(0, windowSize).length) / 2));
+ document.body.classList.add('book-mode');
+ }
+
updateLoadStatus('Ready!', 100);
setTimeout(() => {
document.getElementById('loading-screen').classList.add('fade-out');
@@ -1970,6 +2000,159 @@ function loadWingBook(pivot, onDone) {
img.src = src;
}
+// ============================================================
+// CENTER BOOK — single open mirrored sample book (Steve 2026-06-29 default view)
+// ------------------------------------------------------------
+// ONE board centred on the spawn's forward axis (-Z), opened like a sample book with
+// TWO leaves hinged at the spine. The RIGHT leaf shows the design normally; the LEFT leaf
+// shows a horizontal MIRROR of it (repeat.x negative) so the pattern reads symmetrically
+// meeting at the centre spine. Paged with [ ] / N-P. No arc, no auto-fly.
+// ============================================================
+const BOOK_SPINE_Z = -2.4; // spine sits ~8ft ahead of the centred spawn
+const BOOK_LEAF_SPLAY = 0.30; // radians each leaf opens off flat (gentle V, like an open book on a stand)
+
+function buildCenterBook() {
+ if (centerBook) return centerBook;
+ const WC = CONFIG.wing;
+ const H = WC.height;
+ const leafW = WC.boardWidthM; // each leaf is a full 30" face → ~60" open spread
+ const yC = H / 2 + 0.04;
+ const THICK = 0.014;
+
+ const group = new THREE.Group();
+ group.position.set(0, 0, BOOK_SPINE_Z); // hinge/spine on the room centre-line, ahead of the viewer
+ group.rotation.y = Math.PI; // face the leaves back toward the +Z spawn (viewer at z=0)
+
+ const faceGeo = new THREE.PlaneGeometry(leafW, H);
+
+ // RIGHT leaf — hinged at the spine (local x=0), extends to +x, splayed toward the viewer.
+ const leafR = new THREE.Group();
+ leafR.rotation.y = -BOOK_LEAF_SPLAY;
+ const faceR = new THREE.Mesh(faceGeo, MAT.wall.clone ? new THREE.MeshStandardMaterial({ color: 0xece4d2, roughness: 0.7 }) : MAT.wall);
+ faceR.position.set(leafW / 2, yC, 0);
+ faceR.receiveShadow = true; faceR.castShadow = true;
+ faceR.userData = { isWingFace: true, parentPivot: centerBookPivotProxy(), leaf: 'R' };
+ leafR.add(faceR);
+
+ // LEFT leaf — mirror twin: hinged at the spine, extends to -x, splayed toward the viewer.
+ const leafL = new THREE.Group();
+ leafL.rotation.y = BOOK_LEAF_SPLAY;
+ const faceL = new THREE.Mesh(faceGeo.clone(), new THREE.MeshStandardMaterial({ color: 0xece4d2, roughness: 0.7 }));
+ faceL.position.set(-leafW / 2, yC, 0);
+ faceL.receiveShadow = true; faceL.castShadow = true;
+ faceL.userData = { isWingFace: true, parentPivot: null, leaf: 'L' };
+ leafL.add(faceL);
+
+ // Slim dark-bronze spine post + a thin frame around the open spread.
+ const spine = new THREE.Mesh(new THREE.BoxGeometry(0.02, H + 0.04, 0.05), MAT.frame || MAT.dark);
+ spine.position.set(0, yC, 0.0); spine.castShadow = true;
+ group.add(spine);
+
+ group.add(leafL);
+ group.add(leafR);
+ scene.add(group);
+
+ centerBook = { group, leafL, leafR, faceL, faceR, leafW, H, yC };
+ if (window._requestShadowUpdate) window._requestShadowUpdate(8);
+ return centerBook;
+}
+
+// Lightweight pivot proxy so loadWingBook (which reads ud.face / ud.product / ud.boardWidth
+// off a parentPivot) can paint the RIGHT leaf via the existing loader path.
+let _bookPivotProxy = null;
+function centerBookPivotProxy() {
+ if (!_bookPivotProxy) _bookPivotProxy = { userData: {} };
+ return _bookPivotProxy;
+}
+
+// Load the catalog design at bookIndex onto both leaves: the right leaf normal, the left
+// leaf a horizontal mirror (repeat.x flipped) so the two meet symmetrically at the spine.
+function setCenterBook(index) {
+ if (!centerBook || !products.length) return;
+ const list = products.slice(0, windowSize);
+ if (!list.length) return;
+ bookIndex = ((index % list.length) + list.length) % list.length;
+ const raw = list[bookIndex];
+ const product = normalizeProduct(raw);
+ centerBook.product = product;
+
+ const imageUrl = product.image || raw.tile || raw.image;
+ updateWingCaption(product); // minimal fixed caption bar (Steve's pick)
+
+ if (!imageUrl) return;
+ const src = imageUrl.charAt(0) === '/' ? imageUrl : ('/api/proxy/image?url=' + encodeURIComponent(imageUrl));
+ const maxAniso = (renderer.capabilities && renderer.capabilities.getMaxAnisotropy) ? renderer.capabilities.getMaxAnisotropy() : 1;
+ const wingW = centerBook.leafW;
+
+ const paint = (source, fdImg) => {
+ const isFD = product.isSeamlessTile ? true : (fdImg ? detectFullDesign(fdImg).isFullDesign : true);
+ const { rx, ry } = isFD ? computeRepeat(wingW, product, source) : { rx: 1, ry: 1 };
+ const mkTex = (mirror) => {
+ const t = (typeof ImageBitmap !== 'undefined' && source instanceof ImageBitmap)
+ ? new THREE.CanvasTexture(source) : new THREE.Texture(source);
+ t.image = source;
+ t.minFilter = THREE.LinearMipmapLinearFilter; t.magFilter = THREE.LinearFilter; t.generateMipmaps = true;
+ t.anisotropy = maxAniso; srgb(t);
+ if (isFD) {
+ t.wrapS = t.wrapT = THREE.RepeatWrapping;
+ // RIGHT leaf: repeat.x = +rx. LEFT leaf: repeat.x = -rx flips the map horizontally
+ // (offset.x = rx keeps it positioned), so the left leaf is a true mirror of the right.
+ t.repeat.set(mirror ? -Math.max(1, rx) : Math.max(1, rx), ry);
+ if (mirror) t.offset.x = Math.max(1, rx);
+ } else {
+ t.wrapS = t.wrapT = THREE.ClampToEdgeWrapping;
+ t.repeat.set(mirror ? -1 : 1, 1);
+ if (mirror) t.offset.x = 1;
+ }
+ t.needsUpdate = true;
+ return t;
+ };
+ centerBook.faceR.material = new THREE.MeshStandardMaterial({ map: mkTex(false), roughness: 0.6, metalness: 0.0, envMapIntensity: 0.18 });
+ centerBook.faceL.material = new THREE.MeshStandardMaterial({ map: mkTex(true), roughness: 0.6, metalness: 0.0, envMapIntensity: 0.18 });
+ if (window._requestShadowUpdate) window._requestShadowUpdate(6);
+ };
+
+ if (typeof createImageBitmap === 'function' && src.charAt(0) === '/') {
+ let fdImg = null;
+ if (!product.isSeamlessTile) { fdImg = new Image(); fdImg.src = src; }
+ fetch(src).then(r => r.blob()).then(b => createImageBitmap(b))
+ .then(bmp => paint(bmp, (fdImg && fdImg.complete) ? fdImg : null))
+ .catch(() => { const im = new Image(); im.onload = () => paint(im, im); im.src = src; });
+ return;
+ }
+ const im = new Image(); im.onload = () => paint(im, im); im.src = src;
+}
+
+// Page the centered book through the catalog ([ ] / N-P). delta = +1 next, -1 previous.
+function pageCenterBook(delta) {
+ if (!centerBook) return;
+ setCenterBook(bookIndex + delta);
+}
+
+// Normalize a raw product row into the display shape used by the book + caption.
+// (Mirrors the per-board normalization in buildWingWall, kept small + shared.)
+function normalizeProduct(p) {
+ let widthInches = 27;
+ if (typeof p.width === 'string') { const m = p.width.match(/([\d.]+)/); if (m) widthInches = parseFloat(m[1]); }
+ else if (typeof p.width === 'number' && p.width) widthInches = p.width;
+ let repeatInches = null;
+ if (typeof p.repeat === 'string') { const m = p.repeat.match(/([\d.]+)/); if (m) repeatInches = parseFloat(m[1]); }
+ else if (typeof p.repeat === 'number' && p.repeat) repeatInches = p.repeat;
+ return {
+ pattern_name: p.pattern_name || p.name || 'Pattern',
+ color: p.color_name || p.color || '',
+ sku: p.sku || p.mfr_sku || '',
+ vendor: p.vendor || 'Quadrille House',
+ width_inches: widthInches, repeat_inches: repeatInches,
+ material: p.material || 'Wallcovering',
+ name: p.pattern_name || p.name || 'Pattern',
+ image: p.tile || p.image || null,
+ isSeamlessTile: !!p.tile,
+ collection: p.collection || '',
+ isFullDesign: null
+ };
+}
+
// ============================================================
// CLOSED-SLIVER REAL THUMBNAILS (Image #6 gap closer)
// ------------------------------------------------------------
@@ -2253,6 +2436,8 @@ function restoreBoards() {
// Next / Back — flip to the next/previous wingboard (pages window edges, endless).
async function gotoBoardOffset(delta) {
+ // CENTER BOOK default: Next/Prev page the open book through the catalog.
+ if (BOOK_MODE) { pageCenterBook(delta > 0 ? 1 : -1); return; }
if (!wingBoards.length) return;
let idx = (focusedWing ? focusedWing.userData.index : 0) + delta;
if (idx >= wingBoards.length) { if (await advanceWindow(1)) { restoreBoards(); focusOnWing(wingBoards[0]); reframeView(); return; } idx = wingBoards.length - 1; }
@@ -2366,6 +2551,24 @@ function setDetailCollapsed(c) {
if (btn) { btn.innerHTML = c ? '▸' : '▾'; btn.title = c ? 'Show details' : 'Hide details'; }
}
+// MINIMAL FIXED CAPTION BAR (Steve's pick 2026-06-29) — a small static strip pinned to
+// the bottom of the screen showing `PatternName · Colorway · SKU · Vendor` for the
+// currently-faced / centred design. It NEVER pops, slides, or animates — it just updates
+// its text. Replaces the popping wing-detail card.
+function updateWingCaption(product) {
+ const bar = document.getElementById('wing-caption');
+ if (!bar) return;
+ window._activeProduct = product;
+ if (!product) { bar.textContent = ''; return; }
+ const { pattern, colorway } = splitPatternColor(product.pattern_name || product.name);
+ const color = product.color || colorway || '';
+ const parts = [pattern || 'Pattern'];
+ if (color) parts.push(color);
+ if (product.sku) parts.push(product.sku);
+ if (product.vendor) parts.push(product.vendor);
+ bar.textContent = parts.join(' · ');
+}
+
function showWingDetail(product) {
window._activeProduct = product;
const panel = document.getElementById('wing-detail');
@@ -2559,9 +2762,41 @@ function applySpinPose() {
controls.target.set(cp.x + dir.x * TARGET_DIST, cp.y + dir.y * TARGET_DIST, cp.z + dir.z * TARGET_DIST);
}
+// Half the room (minus a margin) the avatar may roam — keeps the body inside the walls.
+const WALK_HALF = (CONFIG.room.width / 2) - 0.5;
+
function updateWASD(dt) {
- if (controlsLocked || cameraAnim) return; // no spin when focused on a wing or animating
+ if (controlsLocked || cameraAnim) return; // no input when focused on a wing or animating
+ // ── EXPLORE / TAKEOVER (default): you ARE the avatar. ↑/↓ = walk forward/back along the
+ // facing direction; ←/→ = TURN the body (yaw). The camera AND avatarRig move together.
+ if (exploreMode) {
+ let moveIn = 0, turnIn = 0;
+ if (keysDown['ArrowUp'] || keysDown['KeyW']) moveIn += 1; // walk forward
+ if (keysDown['ArrowDown'] || keysDown['KeyS']) moveIn -= 1; // walk back
+ if (keysDown['ArrowLeft'] || keysDown['KeyA']) turnIn -= 1; // turn body left
+ if (keysDown['ArrowRight'] || keysDown['KeyD']) turnIn += 1; // turn body right
+
+ // Turn the body first so a held forward walks along the NEW facing this frame.
+ if (turnIn !== 0) { spinYaw += turnIn * SPIN_YAW_SPEED * dt; applySpinPose(); }
+
+ if (moveIn !== 0 && lockedSpinEye) {
+ // Forward = the look direction projected onto the floor (XZ). yaw=0 faces -Z.
+ const fx = Math.sin(spinYaw), fz = -Math.cos(spinYaw);
+ const step = moveIn * WALK_SPEED * dt;
+ let nx = lockedSpinEye.x + fx * step;
+ let nz = lockedSpinEye.z + fz * step;
+ nx = Math.max(-WALK_HALF, Math.min(WALK_HALF, nx));
+ nz = Math.max(-WALK_HALF, Math.min(WALK_HALF, nz));
+ lockedSpinEye.x = nx; lockedSpinEye.z = nz;
+ camera.position.x = nx; camera.position.z = nz;
+ applySpinPose(); // re-aim the look-target from the new eye position
+ }
+ return;
+ }
+
+ // ── GUIDED (Explore off): the legacy fixed-centre yaw-spin. ←/→ spin the look,
+ // ↑/↓ pitch it; the eye never moves.
let yawIn = 0, pitchIn = 0;
if (keysDown['ArrowLeft'] || keysDown['KeyA']) yawIn -= 1;
if (keysDown['ArrowRight'] || keysDown['KeyD']) yawIn += 1;
@@ -2705,19 +2940,14 @@ function animate() {
camera.position.copy(lockedCamPos);
controls.target.copy(lockedCamTarget);
} else if (!cameraAnim) {
- // FIXED-CENTRE YAW-SPIN: the camera never moves position. updateWASD rotates only the
- // clamped look-target around the fixed eye point, then we aim the camera at it directly.
- // We deliberately do NOT call controls.update() in guided mode — OrbitControls would
- // re-derive the camera position around the moving target and break the fixed centre.
- if (exploreMode) {
- updateWASD(dt);
- controls.update();
- } else {
- const fixedEye = lockedSpinEye || camera.position.clone();
- updateWASD(dt);
- camera.position.copy(fixedEye); // hard-pin the eye every frame
- camera.lookAt(controls.target);
- }
+ // FIRST-PERSON: the arrow keys drive everything. updateWASD walks the eye (explore)
+ // or spins the look-target (guided); either way we then aim the camera directly at the
+ // resolved look-target. We deliberately do NOT call controls.update() — OrbitControls
+ // would re-derive the camera position around the target and fight the walk/spin pose.
+ updateWASD(dt);
+ const fixedEye = lockedSpinEye || camera.position;
+ camera.position.copy(fixedEye); // hard-pin the eye to the walked/centred point
+ camera.lookAt(controls.target);
}
// Chunk K — keep Steve's first-person body yawed with the look + pinned under the eye
@@ -2972,6 +3202,8 @@ function syncGuidedTourBtn() {
// On load (and when leaving Explore), fly to the MIDDLE board, open it, and present
// its design — so the big Previous/Next/Auto-Tour buttons immediately have a hero.
function enterGuidedDefault() {
+ // CENTER BOOK is the default presentation — there is no resting-open arc board to set.
+ if (BOOK_MODE) { guidedReady = true; return; }
if (!wingBoards.length) return;
// BOOT RACE GUARD: if the user clicked a board while the collection was still loading, that
// queued click has already been replayed into a real selection — do NOT overwrite their
@@ -3006,6 +3238,9 @@ function enterGuidedDefault() {
// the resting-open middle board. This is the home pose for the build — used at boot and
// when V1 is (re)applied. NO camera lock, NO detail panel, NOTHING "selected".
function enterFixedCentre() {
+ // CENTER BOOK is the default presentation; the arc fixed-centre rest pose does not apply.
+ // (V1's boot apply still calls this — keep it a no-op so it can't disturb the book/eye.)
+ if (BOOK_MODE) return;
// BOOT RACE GUARD: during the boot window, if the user has a queued/just-consumed boot click,
// the version engine's boot-time V1 apply (versions.js → enterFixedCentre) must NOT wipe
// their selection. Their click is the authoritative final boot action (replayed at +2600ms).
@@ -3022,10 +3257,17 @@ function enterFixedCentre() {
previousCamPos = null; previousCamTarget = null;
const wd = document.getElementById('wing-detail'); if (wd) wd.classList.add('hidden');
hideGuidedTitle();
- // Pin the eye back at room centre and face the bank.
- if (lockedSpinEye) camera.position.copy(lockedSpinEye);
- else { camera.position.set(CONFIG.camera.startPos.x, CONFIG.camera.startPos.y, CONFIG.camera.startPos.z); lockedSpinEye = camera.position.clone(); }
- spinYaw = 0; spinPitch = -0.08;
+ // TAKEOVER: in explore/walk mode the eye belongs to the user — never snatch it back to
+ // centre on a V1 (re)apply, just make sure it exists. In guided mode, pin the eye at
+ // room centre facing the bank (the legacy fixed-centre rest pose).
+ if (!lockedSpinEye) {
+ camera.position.set(CONFIG.camera.startPos.x, CONFIG.camera.startPos.y, CONFIG.camera.startPos.z);
+ lockedSpinEye = camera.position.clone();
+ }
+ if (!exploreMode) {
+ camera.position.copy(lockedSpinEye);
+ spinYaw = 0; spinPitch = -0.08;
+ }
applySpinPose();
if (camera.fov !== FOV_DEFAULT) { targetFov = FOV_DEFAULT; camera.fov = FOV_DEFAULT; camera.updateProjectionMatrix(); }
camera.lookAt(controls.target);
@@ -3526,6 +3768,23 @@ window._qh = {
get restingOpenWing(){ return restingOpenWing; },
get peruseActive(){ return peruseActive; },
get exploreMode(){ return exploreMode; },
+ // CENTER BOOK (Steve 2026-06-29) verification surface — proves the single mirrored book
+ // is present, which catalog design it shows, and lets a test page through it.
+ get bookMode() { return BOOK_MODE; },
+ get bookPresent(){ return !!centerBook; },
+ get bookIndex() { return bookIndex; },
+ bookState() {
+ if (!centerBook) return null;
+ const fl = centerBook.faceL, fr = centerBook.faceR;
+ return {
+ present: true, index: bookIndex,
+ product: centerBook.product ? { pattern: centerBook.product.pattern_name, sku: centerBook.product.sku, vendor: centerBook.product.vendor } : null,
+ leftMirrored: !!(fl && fl.material && fl.material.map && fl.material.map.repeat.x < 0),
+ rightNormal: !!(fr && fr.material && fr.material.map && fr.material.map.repeat.x > 0),
+ spineZ: centerBook.group.position.z
+ };
+ },
+ pageCenterBook,
CONFIG,
// Camera + focus
smoothCameraTo,
diff --git a/public/showroom.html b/public/showroom.html
index ce2b3f5..6fb9c5b 100644
--- a/public/showroom.html
+++ b/public/showroom.html
@@ -178,6 +178,24 @@
linear-gradient(180deg, rgba(28,28,34,0.10) 0%, rgba(255,255,255,0) 26%, rgba(255,255,255,0) 80%, rgba(28,28,34,0.07) 100%),
radial-gradient(120% 90% at 50% 42%, rgba(255,255,255,0) 58%, rgba(20,18,22,0.10) 86%, rgba(16,14,18,0.20) 100%);
}
+
+ /* MINIMAL FIXED CAPTION BAR (Steve's pick) — a quiet static strip at the bottom centre.
+ NO transition / NO animation: it only ever changes its text. */
+ #wing-caption {
+ position:fixed; left:50%; bottom:18px; transform:translateX(-50%); z-index:24;
+ max-width:90vw; padding:7px 18px; border-radius:6px;
+ background:rgba(14,14,20,0.66); border:1px solid rgba(201,169,110,0.28);
+ color:#e9e1cf; font-family:Georgia,"Times New Roman",serif; font-size:15px;
+ letter-spacing:.04em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
+ pointer-events:none; backdrop-filter:blur(6px);
+ }
+ #wing-caption:empty { display:none; }
+ /* In book mode the caption bar IS the SKU surface — keep the popping card + guided
+ surfaces out of the way so the view stays the quiet open mirrored book + caption. */
+ body.book-mode #wing-detail,
+ body.book-mode #now-viewing,
+ body.book-mode #guided-title,
+ body.book-mode #guided-instruction { display:none !important; }
</style>
</head>
<body class="guided-mode">
@@ -310,6 +328,10 @@
<span id="fps-counter">60 FPS</span>
</div>
+ <!-- MINIMAL FIXED CAPTION BAR (Steve's pick) — quiet static strip for the centred design.
+ Pattern · Colorway · SKU · Vendor. Never pops, slides, or animates; text only. -->
+ <div id="wing-caption" aria-live="polite"></div>
+
<!-- GUIDED MODE — large readable pattern title (top-centre) -->
<div id="guided-title">
<div class="gt-name" id="gt-name"> </div>
← 04948ab Phase 5.5 /contrarian final pass: prove the AVATAR half + fi
·
back to Quadrille Showroom
·
Fix center-book leaf orientation: leaves face the viewer (dr f1439ea →