feat(federation): flat 2D-style top-down view on portrait/mobile

Portrait containers now render near top-down and near-orthographic (tilt
-1.35, persp 16) so the map reads like the previous 2D radial layout —
rings become circles with no perspective squash. Desktop keeps the 3D
depth camera. Globe point-spheres unchanged on both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-09 17:22:31 -04:00
co-authored by Claude Fable 5
parent abb4c95337
commit aea90984d6
@@ -111,7 +111,9 @@ let frame = 0
let selfDots: GlobeDot[] = []
let selfRadiusPx = 15
const PERSP = 3.2
/** Perspective strength — measure() sets it per form factor: 3.2 gives the
* desktop scene real depth; a large value is near-orthographic (flat). */
let persp = 3.2
let resizeObserver: ResizeObserver | null = null
let intro: gsap.core.Timeline | null = null
let spinTween: gsap.core.Tween | null = null
@@ -155,7 +157,7 @@ function project(x: number, y: number, z: number) {
const ct = Math.cos(cam.tilt), st = Math.sin(cam.tilt)
const y2 = y * ct - z1 * st
const z2 = y * st + z1 * ct
const s = PERSP / (PERSP + z2)
const s = persp / (persp + z2)
return {
x: width / 2 + x1 * unit * s * cam.zoom,
y: centerY + y2 * unit * s * cam.zoom,
@@ -554,10 +556,12 @@ function measure() {
height = c.clientHeight
svgRef.value?.setAttribute('viewBox', `0 0 ${width} ${height}`)
// Portrait (phone/companion): tilt the camera towards top-down so the
// orbit becomes a tall ellipse that uses the vertical space — nodes and
// labels spread out in two dimensions instead of stacking on a flat band.
cam.tilt = height > width * 1.15 ? -0.95 : -0.5
// Portrait (phone/companion): go near top-down and near-orthographic so
// the map reads like the old 2D radial view — rings become circles, no
// perspective squash, maximum label spread. Desktop keeps the 3D depth.
const portrait = height > width
cam.tilt = portrait ? -1.35 : -0.5
persp = portrait ? 16 : 3.2
// Auto-fit: sample the outermost orbit through the real camera math
// (rotation-invariant — a ring about the Y axis projects identically at any
@@ -577,7 +581,7 @@ function measure() {
for (const y of [-0.2, 0.2]) { // covers yJitter + sway/bob amplitude
const y2 = y * ct - z * st
const z2 = y * st + z * ct
const s = PERSP / (PERSP + z2)
const s = persp / (persp + z2)
maxAbsX = Math.max(maxAbsX, Math.abs(x * s))
yMin = Math.min(yMin, y2 * s)
yMax = Math.max(yMax, y2 * s)