Skip to content

Commit 63f4ae0

Browse files
authored
Fix eclipse preview moon direction vector (#5)
1 parent a7ae3b0 commit 63f4ae0

5 files changed

Lines changed: 110 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.12] — 2026-02-22
9+
10+
### Changed
11+
- Fixed eclipse preview moon trajectory to use contact-bearing motion vectors (including vertical drift), so locations like the 2024 eclipse path animate from lower-left toward upper-right when appropriate.
12+
- Preview now shows a text summary of the computed moon-path direction relative to the sun for the selected GPS point.
13+
14+
### Tests
15+
- Added regression tests for diagonal travel vector behavior and user-facing direction labeling in preview geometry utilities.
16+
817
## [1.1.11] — 2026-02-22
918

1019
### Fixed

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eclipse-timer/mobile",
3-
"version": "1.1.11",
3+
"version": "1.1.12",
44
"private": true,
55
"main": "index.js",
66
"scripts": {

apps/mobile/src/screens/EclipsePreviewScreen.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import { colorForContactKey } from "../utils/contactTheme";
1616
import { fmtLocalHuman, fmtUtcHuman } from "../utils/date";
1717
import {
1818
calculatePreviewMoonGeometry,
19-
determinePreviewTravelDirection,
19+
describePreviewTravelDirection,
20+
determinePreviewTravelVector,
2021
PREVIEW_STAGE_SIZE,
2122
PREVIEW_SUN_RADIUS,
2223
} from "../utils/previewGeometry";
@@ -306,6 +307,17 @@ export default function EclipsePreviewScreen({
306307
timelineDurationMs,
307308
]);
308309

310+
const travelVector = useMemo(
311+
() =>
312+
determinePreviewTravelVector({
313+
c1BearingDeg: payload.c1BearingDeg,
314+
c2BearingDeg: payload.c2BearingDeg,
315+
c3BearingDeg: payload.c3BearingDeg,
316+
c4BearingDeg: payload.c4BearingDeg,
317+
}),
318+
[payload.c1BearingDeg, payload.c2BearingDeg, payload.c3BearingDeg, payload.c4BearingDeg],
319+
);
320+
309321
const moonGeometry = useMemo(
310322
() =>
311323
calculatePreviewMoonGeometry({
@@ -315,23 +327,14 @@ export default function EclipsePreviewScreen({
315327
contacts: contactProgress,
316328
stageSize: SIM_STAGE_SIZE,
317329
sunRadius: SUN_RADIUS,
318-
travelDirection: determinePreviewTravelDirection({
319-
c1BearingDeg: payload.c1BearingDeg,
320-
c2BearingDeg: payload.c2BearingDeg,
321-
c3BearingDeg: payload.c3BearingDeg,
322-
c4BearingDeg: payload.c4BearingDeg,
323-
}),
330+
travelVector,
324331
}),
325-
[
326-
contactProgress,
327-
payload.c1BearingDeg,
328-
payload.c2BearingDeg,
329-
payload.c3BearingDeg,
330-
payload.c4BearingDeg,
331-
payload.kindAtLocation,
332-
payload.magnitude,
333-
progress,
334-
],
332+
[contactProgress, payload.kindAtLocation, payload.magnitude, progress, travelVector],
333+
);
334+
335+
const travelDirectionLabel = useMemo(
336+
() => describePreviewTravelDirection(travelVector),
337+
[travelVector],
335338
);
336339

337340
const phaseLabel = useMemo(
@@ -431,6 +434,7 @@ export default function EclipsePreviewScreen({
431434
? ` | Mag ${payload.magnitude.toFixed(3)}`
432435
: ""}
433436
</Text>
437+
<Text style={styles.directionText}>Moon path: {travelDirectionLabel}</Text>
434438
</View>
435439

436440
<View style={styles.simContainer}>
@@ -649,6 +653,12 @@ const styles = StyleSheet.create({
649653
fontWeight: "700",
650654
textTransform: "uppercase",
651655
},
656+
directionText: {
657+
marginTop: 2,
658+
color: "#9ea4c8",
659+
fontSize: 11,
660+
fontWeight: "600",
661+
},
652662
simContainer: {
653663
flex: 1,
654664
justifyContent: "center",

apps/mobile/src/utils/previewGeometry.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export type PreviewMoonGeometry = {
2020
moonTravelHalfSpan: number;
2121
};
2222

23+
export type PreviewTravelVector = {
24+
x: number;
25+
y: number;
26+
};
27+
2328
export type PreviewDirectionBearings = {
2429
c1BearingDeg?: number;
2530
c2BearingDeg?: number;
@@ -53,14 +58,44 @@ function resolveDirectionalBearingPair(bearings: PreviewDirectionBearings) {
5358
return null;
5459
}
5560

56-
export function determinePreviewTravelDirection(
61+
function bearingDegToUnitCirclePoint(bearingDeg: number): PreviewTravelVector {
62+
const angleRad = (bearingDeg * Math.PI) / 180;
63+
return {
64+
x: Math.sin(angleRad),
65+
y: -Math.cos(angleRad),
66+
};
67+
}
68+
69+
export function determinePreviewTravelVector(
5770
bearings: PreviewDirectionBearings | undefined,
58-
): 1 | -1 {
59-
if (!bearings) return 1;
71+
): PreviewTravelVector {
72+
if (!bearings) return { x: 1, y: 0 };
6073
const pair = resolveDirectionalBearingPair(bearings);
61-
if (!pair) return 1;
62-
const delta = normalizeSignedDeltaDeg(pair.start, pair.end);
63-
return delta >= 0 ? 1 : -1;
74+
if (!pair) return { x: 1, y: 0 };
75+
76+
const start = bearingDegToUnitCirclePoint(pair.start);
77+
const end = bearingDegToUnitCirclePoint(pair.end);
78+
const deltaX = end.x - start.x;
79+
const deltaY = end.y - start.y;
80+
const magnitude = Math.hypot(deltaX, deltaY);
81+
82+
if (!Number.isFinite(magnitude) || magnitude < 1e-6) {
83+
const fallbackDirection = normalizeSignedDeltaDeg(pair.start, pair.end) >= 0 ? 1 : -1;
84+
return { x: fallbackDirection, y: 0 };
85+
}
86+
87+
return {
88+
x: deltaX / magnitude,
89+
y: deltaY / magnitude,
90+
};
91+
}
92+
93+
export function describePreviewTravelDirection(vector: PreviewTravelVector): string {
94+
const horizontal = vector.x >= 0 ? "left to right" : "right to left";
95+
const vertical = vector.y <= -0.2 ? "bottom to top" : vector.y >= 0.2 ? "top to bottom" : "level";
96+
97+
if (vertical === "level") return horizontal;
98+
return `${vertical}, ${horizontal}`;
6499
}
65100

66101
export function determineMoonRadius(kindAtLocation: EclipseKindAtLocation) {
@@ -148,7 +183,7 @@ export function calculatePreviewMoonGeometry(params: {
148183
contacts?: PreviewMotionContacts;
149184
stageSize?: number;
150185
sunRadius?: number;
151-
travelDirection?: 1 | -1;
186+
travelVector?: PreviewTravelVector;
152187
}): PreviewMoonGeometry {
153188
const stageSize = params.stageSize ?? PREVIEW_STAGE_SIZE;
154189
const sunRadius = params.sunRadius ?? PREVIEW_SUN_RADIUS;
@@ -161,10 +196,12 @@ export function calculatePreviewMoonGeometry(params: {
161196
);
162197

163198
const anchors = buildMotionAnchors(params.contacts ?? {}, sunRadius, moonRadius);
164-
const travelDirection = params.travelDirection ?? 1;
165-
const moonOffsetX = interpolateOffsetX(params.progress, anchors) * travelDirection;
199+
const axisOffset = interpolateOffsetX(params.progress, anchors);
200+
const travelVector = params.travelVector ?? { x: 1, y: 0 };
201+
const moonOffsetX = axisOffset * travelVector.x - moonClosestOffset * travelVector.y;
202+
const moonOffsetY = axisOffset * travelVector.y + moonClosestOffset * travelVector.x;
166203
const moonCenterX = stageSize / 2 + moonOffsetX;
167-
const moonCenterY = stageSize / 2 + moonClosestOffset;
204+
const moonCenterY = stageSize / 2 + moonOffsetY;
168205

169206
return {
170207
moonRadius,

apps/mobile/tests/preview-geometry.test.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { describe, expect, it } from "vitest";
22

33
import {
44
calculatePreviewMoonGeometry,
5-
determinePreviewTravelDirection,
5+
describePreviewTravelDirection,
6+
determinePreviewTravelVector,
67
PREVIEW_SUN_RADIUS,
78
} from "../src/utils/previewGeometry";
89

@@ -54,38 +55,44 @@ describe("preview moon geometry", () => {
5455
expect(postC3Distance).toBeGreaterThan(c3Distance);
5556
});
5657

57-
it("uses contact bearings to keep moon travel direction accurate", () => {
58-
const leftToRightDirection = determinePreviewTravelDirection({
59-
c1BearingDeg: 100,
60-
c4BearingDeg: 140,
61-
});
62-
const rightToLeftDirection = determinePreviewTravelDirection({
63-
c1BearingDeg: 140,
64-
c4BearingDeg: 100,
58+
it("uses contact bearings to produce a diagonal moon travel vector", () => {
59+
const travelVector = determinePreviewTravelVector({
60+
c1BearingDeg: 246,
61+
c4BearingDeg: 66,
6562
});
6663

64+
expect(travelVector.x).toBeGreaterThan(0);
65+
expect(travelVector.y).toBeLessThan(0);
66+
6767
const baseParams = {
6868
progress: 0.25,
6969
kindAtLocation: "total" as const,
7070
contacts: { c1: 0, c2: 0.25, max: 0.5, c3: 0.75, c4: 1 },
7171
};
7272

73-
const leftToRight = calculatePreviewMoonGeometry({
73+
const earlyGeometry = calculatePreviewMoonGeometry({
7474
...baseParams,
75-
travelDirection: leftToRightDirection,
75+
travelVector,
7676
});
77-
const rightToLeft = calculatePreviewMoonGeometry({
77+
const lateGeometry = calculatePreviewMoonGeometry({
7878
...baseParams,
79-
travelDirection: rightToLeftDirection,
79+
progress: 0.75,
80+
travelVector,
8081
});
8182

82-
expect(leftToRight.moonOffsetX).toBeLessThan(0);
83-
expect(rightToLeft.moonOffsetX).toBeGreaterThan(0);
84-
expect(Math.abs(leftToRight.moonOffsetX)).toBeCloseTo(Math.abs(rightToLeft.moonOffsetX), 6);
83+
expect(lateGeometry.moonCenterX).toBeGreaterThan(earlyGeometry.moonCenterX);
84+
expect(lateGeometry.moonCenterY).toBeLessThan(earlyGeometry.moonCenterY);
85+
});
86+
87+
it("falls back to default travel vector when bearings are missing", () => {
88+
expect(determinePreviewTravelVector(undefined)).toEqual({ x: 1, y: 0 });
89+
expect(determinePreviewTravelVector({ c2BearingDeg: 120 })).toEqual({ x: 1, y: 0 });
8590
});
8691

87-
it("falls back to default travel direction when bearings are missing", () => {
88-
expect(determinePreviewTravelDirection(undefined)).toBe(1);
89-
expect(determinePreviewTravelDirection({ c2BearingDeg: 120 })).toBe(1);
92+
it("describes moon travel direction in user-facing terms", () => {
93+
expect(describePreviewTravelDirection({ x: 0.8, y: -0.4 })).toBe(
94+
"bottom to top, left to right",
95+
);
96+
expect(describePreviewTravelDirection({ x: -0.7, y: 0.1 })).toBe("right to left");
9097
});
9198
});

0 commit comments

Comments
 (0)