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.
This commit is contained in:
jwinterm
2026-07-13 08:49:23 -04:00
parent 2bbdffe867
commit 4c7b256c9a
2 changed files with 30 additions and 14 deletions
+5 -8
View File
@@ -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;
+25 -6
View File
@@ -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);
};