From 4c7b256c9a9005d634c31eaabbf6b9ecc892ae61 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 13 Jul 2026 08:49:23 -0400 Subject: [PATCH] feat(3d): real player-follow camera (not fit-to-view) 3D is a paid rung and should behave like the other modes. Rework the THREE camera to FOLLOW the player instead of framing the whole level: canvas now sizes to the host (setSize updateStyle), the ortho frustum is a fixed player-centered zoom (span 8) with host aspect, and _animate re-targets the camera to the player world position every frame (keeps the iso (8,9,8) offset; glides smoothly since the player mesh lerps). SP._applyCamera no longer CSS-transforms the 3D canvas (it fills the host and THREE owns the camera). Handles host resize via _fitToHost. --- html/js/render/spGameRenderer.js | 13 +++++-------- html/js/render/threeRenderer.js | 31 +++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/html/js/render/spGameRenderer.js b/html/js/render/spGameRenderer.js index 41118f9..03cbbcb 100644 --- a/html/js/render/spGameRenderer.js +++ b/html/js/render/spGameRenderer.js @@ -102,15 +102,12 @@ var r = SP._renderer; var host = doc() && doc().getElementById('rk-game-host'); if (!r || !r.canvas || !host) return; - // The 3D renderer drives its OWN THREE camera and sizes its canvas to the level, so a - // player-follow CSS transform would zoom/crop it wrongly. Instead scale-to-FIT the whole - // canvas into the host and centre it (no player follow) — a sane, non-broken view. + // The 3D renderer fills the host itself (setSize) and its OWN THREE camera follows the + // player — so DON'T apply a CSS transform (it would double-transform the WebGL view). if (r.name === '3d') { - var cw = r.canvas.width || 1, ch = r.canvas.height || 1; - var hw3 = host.clientWidth || host.offsetWidth || 640, hh3 = host.clientHeight || host.offsetHeight || 400; - var fit = Math.min(hw3 / cw, hh3 / ch); - r.canvas.style.transformOrigin = '0 0'; - r.canvas.style.transform = 'translate(' + ((hw3 - cw * fit) / 2) + 'px,' + ((hh3 - ch * fit) / 2) + 'px) scale(' + fit + ')'; + r.canvas.style.transform = 'none'; + r.canvas.style.left = '0'; + r.canvas.style.top = '0'; return; } var scale = SP._zoom || 1.7; diff --git a/html/js/render/threeRenderer.js b/html/js/render/threeRenderer.js index fe4bf49..9131343 100644 --- a/html/js/render/threeRenderer.js +++ b/html/js/render/threeRenderer.js @@ -50,11 +50,17 @@ this._raf = requestAnimationFrame(function tick() { self._animate(); self._raf = requestAnimationFrame(tick); }); }; - ThreeRenderer.prototype._resize = function (scene) { - var w = Math.max(420, scene.cols * 32); - var h = Math.max(360, scene.rows * 28 + 120); - this.renderer.setSize(w, h, false); - var aspect = w / h, span = Math.max(scene.cols, scene.rows) * 0.78 + 2.5; + // Fill the host and frame a fixed "screenful" around the player. The camera FOLLOWS the player + // (see _animate) instead of framing the whole level, so 3D behaves like the other modes. Only + // does work when the host size actually changed (cheap to call every frame). + ThreeRenderer.prototype._fitToHost = function () { + var host = this.host; + var w = (host && (host.clientWidth || host.offsetWidth)) || 640; + var h = (host && (host.clientHeight || host.offsetHeight)) || 400; + if (w === this._cw && h === this._ch) return; + this._cw = w; this._ch = h; + this.renderer.setSize(w, h, true); // updateStyle:true → the canvas CSS-fills the host + var aspect = w / h, span = 8; // fixed zoom = a screenful around the player this.camera.left = -span * aspect; this.camera.right = span * aspect; this.camera.top = span; @@ -219,7 +225,7 @@ ThreeRenderer.prototype.render = function (scene) { if (!scene || !this.renderer) return; - this._resize(scene); + this._fitToHost(); var key = scene.cols + 'x' + scene.rows + ':' + scene.grid.map(function (r) { return r.join(''); }).join('/'); if (key !== this._sizeKey) { this._buildTiles(scene); @@ -273,6 +279,19 @@ o.rotation.y += (r - o.rotation.y) * 0.18; } } + + // Player-follow camera: keep the fixed iso offset (8,9,8) but re-target the player's world + // position each frame. The player mesh itself lerps, so the camera glides smoothly with it. + this._fitToHost(); // pick up any host resize + var pcam = null; + for (var pid in this.entities) { + var pen = this.entities[pid]; + if (pen.e && (pen.e.you || pen.e.kind === 'player')) { pcam = pen.obj; break; } + } + if (pcam) { + this.camera.position.set(pcam.position.x + 8, 9, pcam.position.z + 8); + this.camera.lookAt(pcam.position.x, 0, pcam.position.z); + } this.renderer.render(this.scene, this.camera); };