Skip to content

Commit a26acbc

Browse files
vdayanandclaude
andcommitted
Swing the camera square on when the versions part
The tilt is there to show depth. Asking for two columns says the reader is done with depth, so the camera eases square on and the frame goes back to fit, where both columns are in view. Dragging writes the goal as well as the angle, so the hand always wins and the camera never pulls back from where it was put. Stacking them again leaves the camera alone: by then the angle is the reader's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9ab0556 commit a26acbc

1 file changed

Lines changed: 24 additions & 2 deletions

File tree

‎src/viewer.html‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,9 +393,14 @@
393393
const scene = new THREE.Scene();
394394
const camera = new THREE.PerspectiveCamera(44, 1, 0.1, 400);
395395

396-
const orbit = { theta: Math.PI / 2 - 0.4, phi: Math.PI / 2 - 0.15, radius: 26,
396+
const HEADON = Math.PI / 2; // square on to the plates, where a column reads flat
397+
const orbit = { theta: HEADON - 0.4, phi: HEADON - 0.15, radius: 26,
397398
target: new THREE.Vector3() };
398-
const goal = { radius: 26, target: new THREE.Vector3() };
399+
const goal = { theta: orbit.theta, phi: orbit.phi, radius: 26,
400+
target: new THREE.Vector3() };
401+
402+
// dragging can wind theta past a turn, and the camera should not unwind it
403+
const nearest = (from, to) => to + Math.round((from - to) / (2 * Math.PI)) * 2 * Math.PI;
399404

400405
/* The camera keeps a readable window rather than fitting the whole hunk, and
401406
follows the caret through it. Scrolling detaches the follow, like a log tail;
@@ -441,6 +446,8 @@
441446
} else {
442447
orbit.theta -= dx * 0.005;
443448
orbit.phi = Math.min(Math.PI - 0.12, Math.max(0.12, orbit.phi - dy * 0.005));
449+
goal.theta = orbit.theta; // the hand wins over whatever the camera was easing to
450+
goal.phi = orbit.phi;
444451
}
445452
drag = { x: e.clientX, y: e.clientY, pan: drag.pan };
446453
});
@@ -961,6 +968,7 @@
961968
aside: () => {
962969
if (!overlay) return;
963970
apart = !apart;
971+
if (apart) squareOn();
964972
syncAsideBtn();
965973
},
966974
ctx: () => {
@@ -1001,6 +1009,18 @@
10011009
});
10021010
}
10031011

1012+
/* The tilt is there to show depth. Standing the versions apart says the reader
1013+
is done with depth and wants two columns, so the camera swings square on and
1014+
the frame goes back to fit, where both columns are in view. Stacking them
1015+
again leaves the camera alone: by then the angle is the reader's own. */
1016+
function squareOn() {
1017+
goal.theta = nearest(orbit.theta, HEADON);
1018+
goal.phi = nearest(orbit.phi, HEADON);
1019+
zoom = 1;
1020+
panX = 0;
1021+
syncFitBtn();
1022+
}
1023+
10041024
/* one button for both directions: it names what clicking it gives you */
10051025
function syncAsideBtn() {
10061026
button("aside").textContent = apart ? "stacked" : "side by side";
@@ -1269,6 +1289,8 @@
12691289
goal.target.set(framing.centre - widened / 2 + panX, -viewRow * ROW, 0);
12701290
goal.radius = fit * zoom;
12711291
orbit.radius += (goal.radius - orbit.radius) * 0.08;
1292+
orbit.theta += (goal.theta - orbit.theta) * 0.08;
1293+
orbit.phi += (goal.phi - orbit.phi) * 0.08;
12721294
orbit.target.lerp(goal.target, 0.1);
12731295
camera.position.set(
12741296
orbit.target.x + orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta),

0 commit comments

Comments
 (0)