← back to Quadrille Showroom
PJ hero framing: narrow FOV (55->30) on focus + back off proportionally so the 6ft board fills the frame flat/dead-on with no perspective flare; restore FOV on unfocus. Distance solved from fillH=2*d*tan(fov/2). FPS 72, errorCount 0
1e557355b164e4c4671cf55920ff659e0c894787 · 2026-06-27 12:08:28 -0700 · Steve
Files touched
Diff
commit 1e557355b164e4c4671cf55920ff659e0c894787
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jun 27 12:08:28 2026 -0700
PJ hero framing: narrow FOV (55->30) on focus + back off proportionally so the 6ft board fills the frame flat/dead-on with no perspective flare; restore FOV on unfocus. Distance solved from fillH=2*d*tan(fov/2). FPS 72, errorCount 0
---
public/js/showroom.js | 55 +++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 45 insertions(+), 10 deletions(-)
diff --git a/public/js/showroom.js b/public/js/showroom.js
index f104996..cc7682f 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -1178,15 +1178,30 @@ function focusOnWing(pivot) {
const wingWorldPos = new THREE.Vector3();
pivot.getWorldPosition(wingWorldPos);
- // Stand ~6 ft back, DEAD-CENTRE on the board, aimed at its vertical centre so the
- // whole 6-ft design reads cleanly; the board opens to 65° (BOOK_OPEN), isolated.
- const camTarget = new THREE.Vector3(wingWorldPos.x, 0.98, wingWorldPos.z);
- const camPos = new THREE.Vector3(wingWorldPos.x, 1.02, wingWorldPos.z + 1.95);
-
- // Fly camera there, then hard-lock
+ // PJ HERO FRAMING: DEAD-CENTRE on the board, aimed at its vertical centre, but
+ // shot through a NARROW FOV (FOV_HERO=30°) from further back so perspective is
+ // compressed and the wallcovering reads as a FLAT rectangular plane — no edge
+ // bow, no leaf flare — like a Phillip Jeffries hero photograph. Distance is solved
+ // so the 6-ft board still fills the frame top-to-bottom with a hair of margin:
+ // fillH = 2 · d · tan(fov/2) → d = (fillH/2) / tan(fov/2)
+ const boardCenterY = CONFIG.wing.height / 2 + 0.04; // 0.954 m, the real board centre
+ // Fill the frame TIGHT: target ~82% of the 6-ft board so the design reads large and
+ // fills top-to-bottom like a PJ hero panel (the very ends can run past the frame edge
+ // — that's the hero look). The HUD eats the right third, so the board lives in the
+ // left two-thirds; this is the clean dead-on read REVIEW.md asked for.
+ const fillH = CONFIG.wing.height * 0.82; // board fills the frame vertically
+ const dHero = (fillH / 2) / Math.tan((FOV_HERO * Math.PI / 180) / 2);
+ // Clamp so the camera never backs through the front wall. The board sits near the
+ // back wall (z≈-2.8); the camera moves toward +z. Usable run = front-wall − board.
+ const maxBack = (CONFIG.room.depth / 2) - wingWorldPos.z - 0.4;
+ const back = Math.min(dHero, maxBack);
+ const camTarget = new THREE.Vector3(wingWorldPos.x, boardCenterY, wingWorldPos.z);
+ const camPos = new THREE.Vector3(wingWorldPos.x, boardCenterY, wingWorldPos.z + back);
+
+ // Fly camera there + narrow the FOV in lock-step, then hard-lock.
smoothCameraTo(camPos, camTarget, () => {
lockControls(camPos, camTarget);
- });
+ }, FOV_HERO);
// Lazy-load this wing's book-matched pattern immediately on focus
if (pivot.userData.pendingImage && !pivot.userData.imageLoaded) {
@@ -1252,11 +1267,13 @@ function unfocusWing(skipPanel) {
// Unlock first so smoothCameraTo can move
unlockControls();
- // Fly back to previous position
+ // Fly back to previous position AND restore the wide walk-around FOV.
if (previousCamPos && previousCamTarget) {
- smoothCameraTo(previousCamPos, previousCamTarget);
+ smoothCameraTo(previousCamPos, previousCamTarget, null, FOV_DEFAULT);
previousCamPos = null;
previousCamTarget = null;
+ } else if (camera.fov !== FOV_DEFAULT) {
+ targetFov = FOV_DEFAULT; camera.fov = FOV_DEFAULT; camera.updateProjectionMatrix();
}
document.getElementById('info-text').textContent = 'WASD to walk | Click a wing board to view pattern | M for minimap';
}
@@ -1483,6 +1500,9 @@ function animate() {
if (cameraAnim.progress >= 1) {
camera.position.copy(cameraAnim.endPos);
controls.target.copy(cameraAnim.endTarget);
+ if (cameraAnim.endFov !== undefined && Math.abs(camera.fov - cameraAnim.endFov) > 0.01) {
+ camera.fov = cameraAnim.endFov; camera.updateProjectionMatrix();
+ }
const cb = cameraAnim.onComplete;
cameraAnim = null;
if (cb) cb();
@@ -1490,6 +1510,10 @@ function animate() {
const e = 1 - Math.pow(1 - cameraAnim.progress, 3); // cubic ease-out
camera.position.lerpVectors(cameraAnim.startPos, cameraAnim.endPos, e);
controls.target.lerpVectors(cameraAnim.startTarget, cameraAnim.endTarget, e);
+ if (cameraAnim.endFov !== undefined && cameraAnim.startFov !== cameraAnim.endFov) {
+ camera.fov = cameraAnim.startFov + (cameraAnim.endFov - cameraAnim.startFov) * e;
+ camera.updateProjectionMatrix();
+ }
}
}
@@ -1796,13 +1820,24 @@ function populateVendorSidebar() {
});
}
-function smoothCameraTo(pos, target, onComplete) {
+// Narrow-FOV HERO framing. A wide FOV (55°) up close makes the open board flare in
+// perspective — the leaves splay and the top edges bow outward. Backing the camera
+// off and narrowing the FOV compresses perspective so the wallcovering reads as a
+// flat rectangular plane, like a PJ hero shot. Restored to FOV_DEFAULT on unfocus.
+const FOV_DEFAULT = CONFIG.camera.fov; // 55
+const FOV_HERO = 30; // tight telephoto-ish framing when focused
+let targetFov = FOV_DEFAULT;
+
+function smoothCameraTo(pos, target, onComplete, fov) {
// Clock-based animation — runs inside main animate() loop, not a competing rAF chain
+ if (fov !== undefined) targetFov = fov;
cameraAnim = {
startPos: camera.position.clone(),
endPos: pos.clone(),
startTarget: controls.target.clone(),
endTarget: target.clone(),
+ startFov: camera.fov,
+ endFov: targetFov,
progress: 0,
duration: 0.8, // seconds
onComplete: onComplete || null
← df96412 Floor normal+roughness maps derived locally from floor-oak.p
·
back to Quadrille Showroom
·
NOW VIEWING navigator HUD: museum media-player strip (swatch a38c66c →