|
393 | 393 | const scene = new THREE.Scene(); |
394 | 394 | const camera = new THREE.PerspectiveCamera(44, 1, 0.1, 400); |
395 | 395 |
|
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, |
397 | 398 | 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; |
399 | 404 |
|
400 | 405 | /* The camera keeps a readable window rather than fitting the whole hunk, and |
401 | 406 | follows the caret through it. Scrolling detaches the follow, like a log tail; |
|
441 | 446 | } else { |
442 | 447 | orbit.theta -= dx * 0.005; |
443 | 448 | 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; |
444 | 451 | } |
445 | 452 | drag = { x: e.clientX, y: e.clientY, pan: drag.pan }; |
446 | 453 | }); |
|
961 | 968 | aside: () => { |
962 | 969 | if (!overlay) return; |
963 | 970 | apart = !apart; |
| 971 | + if (apart) squareOn(); |
964 | 972 | syncAsideBtn(); |
965 | 973 | }, |
966 | 974 | ctx: () => { |
|
1001 | 1009 | }); |
1002 | 1010 | } |
1003 | 1011 |
|
| 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 | + |
1004 | 1024 | /* one button for both directions: it names what clicking it gives you */ |
1005 | 1025 | function syncAsideBtn() { |
1006 | 1026 | button("aside").textContent = apart ? "stacked" : "side by side"; |
|
1269 | 1289 | goal.target.set(framing.centre - widened / 2 + panX, -viewRow * ROW, 0); |
1270 | 1290 | goal.radius = fit * zoom; |
1271 | 1291 | 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; |
1272 | 1294 | orbit.target.lerp(goal.target, 0.1); |
1273 | 1295 | camera.position.set( |
1274 | 1296 | orbit.target.x + orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta), |
|
0 commit comments