diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.ts index 3dcf282..84b3cfc 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -44,6 +44,7 @@ import { galacticNormal, PolarGridPlane, TetherField } from './grid-plane'; import { MilkyWayRenderer } from './milky-way-renderer'; import { closestApproachAu, + NEIGHBOUR_RING_FRACTION, SUN_RADIUS_AU, systemFrameRadiusAu, systemFramingDistanceAu, @@ -218,11 +219,6 @@ const ROUTE_RANGE_CEILING_PC = MAX_JUMP_RANGE_PC; /** How many neighbouring stars are named from inside a system. */ const NEIGHBOUR_COUNT = 4; -/** - * How far out from the centre of the view a neighbour's name sits, as a fraction of the frame's - * half-height. Clear of the scale rail at the top and the dock at the bottom. - */ -const NEIGHBOUR_RING_NDC = 0.74; /** * How far in front of the camera a neighbour's name is planted, in AU. Any depth projects to * the same place on the ring, but not to the same stability: unprojecting at the middle of the @@ -1523,7 +1519,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { const canvas = this.canvasRef().nativeElement; const placed = ringPlacement( angle, - NEIGHBOUR_RING_NDC, + NEIGHBOUR_RING_FRACTION, { width: canvas.clientWidth, height: canvas.clientHeight }, this.reserved, ); @@ -2256,7 +2252,8 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { // Framed against the perspective camera whichever is active: the framing distance is what // the orthographic frustum is then sized from, so both projections show the same extent. const framingCamera = this.engine.getPerspectiveCamera(); - const viewport = { fovDegrees: framingCamera.fov, aspect: framingCamera.aspect }; + const canvas = this.canvasRef().nativeElement; + const viewport = { fovDegrees: framingCamera.fov, aspect: framingCamera.aspect, shorterSidePx: Math.min(canvas.clientWidth, canvas.clientHeight) }; const framingDistance = systemFramingDistanceAu( this.systemRenderer.gridOuterRadiusAu, viewport, diff --git a/src/app/features/galaxy-system/system-framing.spec.ts b/src/app/features/galaxy-system/system-framing.spec.ts index ee65dba..194c12f 100644 --- a/src/app/features/galaxy-system/system-framing.spec.ts +++ b/src/app/features/galaxy-system/system-framing.spec.ts @@ -63,6 +63,21 @@ describe('systemFramingDistanceAu', () => { expect(systemFramingDistanceAu(0, undefined, 0.00465)).toBe(systemFramingDistanceAu(0)); }); + it("keeps a giant's disc clear of its neighbours' names on a phone, which hang a fixed 78 px in from their ring", () => { + // Betelgeuse at 390x844 and at 1600x1000. Kept to half the half-side, its disc was 98 px on the + // phone, where the names come within 66 px of the centre. + const betelgeuseAu = 2.72; + for (const [width, height] of [[390, 844], [1600, 1000]]) { + const viewport = { fovDegrees: 50, aspect: width / height, shorterSidePx: Math.min(width, height) }; + const distance = systemFramingDistanceAu(0, viewport, betelgeuseAu); + const tight = Math.tan((25 * Math.PI) / 180) * Math.min(1, viewport.aspect); + const halfSidePx = viewport.shorterSidePx / 2; + const discPx = (Math.tan(Math.asin(betelgeuseAu / distance)) / tight) * halfSidePx; + expect(discPx).toBeLessThan(0.74 * halfSidePx - 78); + expect(discPx).toBeGreaterThan(0.2 * halfSidePx); + } + }); + it("holds a giant's disc inside the ring its neighbours are named on, and the camera clear of its closest approach", () => { // The ring is at 0.74 of the tighter half-extent; the disc is kept to half of it, in either window. const betelgeuseAu = 2.72; diff --git a/src/app/features/galaxy-system/system-framing.ts b/src/app/features/galaxy-system/system-framing.ts index 5718542..a26aa21 100644 --- a/src/app/features/galaxy-system/system-framing.ts +++ b/src/app/features/galaxy-system/system-framing.ts @@ -33,6 +33,8 @@ const FRAME_MARGIN = 0.12; export interface SystemViewport { fovDegrees: number; aspect: number; + /** The canvas's shorter side in CSS pixels, which a giant's disc is kept clear of its neighbours' names on. */ + shorterSidePx?: number; } export const DEFAULT_SYSTEM_VIEWPORT: SystemViewport = { fovDegrees: 50, aspect: 1 }; @@ -61,12 +63,35 @@ const MAX_FRAMING_DISTANCE_AU = 200; /** * How much of the view's tighter half-extent a giant's disc may take on arrival: inside the ring - * the system view names the star's neighbours on, at 0.74 of it, and clear of the names hung - * inward from it, whose nearest corners come within 292 px of the centre on a 1 000 px view. - * Framed to fill the frame instead, Betelgeuse settled at the three-radius closest approach with - * a disc of 379 px, past the 370 px ring, and its neighbours' names on it. + * the system view names the star's neighbours on, and clear of the names hung inward from it, + * whose nearest corners come within 292 px of the centre on a 1 000 px view. Framed to fill the + * frame instead, Betelgeuse settled at the three-radius closest approach with a disc of 379 px, + * past the 370 px ring, and its neighbours' names on it. */ const STAR_FRAME_FRACTION = 0.5; +/** + * The ring the system view names a star's neighbours on, as a fraction of the frame's shorter + * half-side (see `ringPlacement`). Clear of the scale rail at the top and the dock at the bottom. + */ +export const NEIGHBOUR_RING_FRACTION = 0.74; +/** + * How far a neighbour's name reaches in from that ring, in pixels whatever the window: its nearest + * corner measured 74 px in on a 390 px phone and 78 px on a 1 000 px view, and a margin on that. + * The fraction above left the names 0.24 of the half-side, 47 px on a phone, and at 390x844 + * Betelgeuse's 98 px disc had HD 39374's name 70 px from its centre. + */ +const NAME_REACH_PX = 90; +/** However small the window, the disc still takes this much of it. */ +const MIN_STAR_FRAME_FRACTION = 0.1; + +/** The fraction of the tighter half-extent a giant's disc may take in this viewport. */ +function starFrameFraction(viewport: SystemViewport): number { + if (!viewport.shorterSidePx) { + return STAR_FRAME_FRACTION; + } + const clear = NEIGHBOUR_RING_FRACTION - NAME_REACH_PX / (viewport.shorterSidePx / 2); + return Math.min(STAR_FRAME_FRACTION, Math.max(MIN_STAR_FRAME_FRACTION, clear)); +} /** Framing for a star with no known planets, where there is nothing to fit. */ const EMPTY_SYSTEM_FRAMING_DISTANCE_AU = 3; @@ -127,7 +152,7 @@ export function systemFramingDistanceAu(framedRadiusAu: number, viewport: System // A giant drawn at its own radius can be wider than the system around it — Betelgeuse's 584 // solar radii are 2.7 AU — or than the empty framing. Its disc is a sphere's, whose silhouette // from d subtends asin(R / d): the distance that makes it the fraction above of the view. - const star = starRadiusAu * Math.sqrt(1 + 1 / (STAR_FRAME_FRACTION * tightHalfExtent(viewport)) ** 2); + const star = starRadiusAu * Math.sqrt(1 + 1 / (starFrameFraction(viewport) * tightHalfExtent(viewport)) ** 2); if (!Number.isFinite(framedRadiusAu) || framedRadiusAu <= 0) { return Math.max(EMPTY_SYSTEM_FRAMING_DISTANCE_AU, star); }