Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.11] — 2026-02-22

### Fixed
- Updated preview mode moon motion so left/right travel direction follows local contact bearing progression when bearing data is available.
- Wired contact bearing values into preview payload construction so direction-aware geometry has access to C1/C2/C3/C4 bearing inputs.
- Added regression tests for bearing-based travel direction selection and fallback behavior when bearing pairs are incomplete.

## [1.1.10] — 2026-02-22

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eclipse-timer/mobile",
"version": "1.1.10",
"version": "1.1.11",
"private": true,
"main": "index.js",
"scripts": {
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/navigation/RootNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,10 @@ function TimerRoute({ navigation, catalog, onOpenMenu }: TimerRouteProps) {
maxUtc: result.maxUtc,
c3Utc: result.c3Utc,
c4Utc: result.c4Utc,
c1BearingDeg: result.c1BearingDeg,
c2BearingDeg: result.c2BearingDeg,
c3BearingDeg: result.c3BearingDeg,
c4BearingDeg: result.c4BearingDeg,
};

navigation.navigate("Preview", { payload });
Expand Down
22 changes: 21 additions & 1 deletion apps/mobile/src/screens/EclipsePreviewScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { colorForContactKey } from "../utils/contactTheme";
import { fmtLocalHuman, fmtUtcHuman } from "../utils/date";
import {
calculatePreviewMoonGeometry,
determinePreviewTravelDirection,
PREVIEW_STAGE_SIZE,
PREVIEW_SUN_RADIUS,
} from "../utils/previewGeometry";
Expand Down Expand Up @@ -46,6 +47,10 @@ export type PreviewPayload = {
maxUtc?: string;
c3Utc?: string;
c4Utc?: string;
c1BearingDeg?: number;
c2BearingDeg?: number;
c3BearingDeg?: number;
c4BearingDeg?: number;
};

type EclipsePreviewScreenProps = {
Expand Down Expand Up @@ -310,8 +315,23 @@ export default function EclipsePreviewScreen({
contacts: contactProgress,
stageSize: SIM_STAGE_SIZE,
sunRadius: SUN_RADIUS,
travelDirection: determinePreviewTravelDirection({
c1BearingDeg: payload.c1BearingDeg,
c2BearingDeg: payload.c2BearingDeg,
c3BearingDeg: payload.c3BearingDeg,
c4BearingDeg: payload.c4BearingDeg,
}),
}),
[contactProgress, payload.kindAtLocation, payload.magnitude, progress],
[
contactProgress,
payload.c1BearingDeg,
payload.c2BearingDeg,
payload.c3BearingDeg,
payload.c4BearingDeg,
payload.kindAtLocation,
payload.magnitude,
progress,
],
);

const phaseLabel = useMemo(
Expand Down
43 changes: 42 additions & 1 deletion apps/mobile/src/utils/previewGeometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,49 @@ export type PreviewMoonGeometry = {
moonTravelHalfSpan: number;
};

export type PreviewDirectionBearings = {
c1BearingDeg?: number;
c2BearingDeg?: number;
c3BearingDeg?: number;
c4BearingDeg?: number;
};

function clamp01(v: number) {
return Math.max(0, Math.min(1, v));
}

function normalizeSignedDeltaDeg(fromDeg: number, toDeg: number) {
const delta = ((toDeg - fromDeg + 540) % 360) - 180;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve bearing sweep direction across >180° contacts

determinePreviewTravelDirection currently relies on normalizeSignedDeltaDeg, which forces the C1→C4 bearing delta into [-180, 180]; this flips the sign when the true contact progression spans just over 180° (for example, catalog case 2038-07-02T has increasing bearings from ~90° at C1 to ~270° at C4, while C2/C3 are also increasing, but this normalization yields a negative delta). In those cases the preview gets travelDirection = -1 and animates the moon in the opposite horizontal direction from the chronological contact progression.

Useful? React with 👍 / 👎.

return delta === -180 ? 180 : delta;
}

function resolveDirectionalBearingPair(bearings: PreviewDirectionBearings) {
const pairs: Array<[number | undefined, number | undefined]> = [
[bearings.c1BearingDeg, bearings.c4BearingDeg],
[bearings.c2BearingDeg, bearings.c3BearingDeg],
[bearings.c1BearingDeg, bearings.c3BearingDeg],
[bearings.c2BearingDeg, bearings.c4BearingDeg],
];

for (const [start, end] of pairs) {
if (typeof start !== "number" || !Number.isFinite(start)) continue;
if (typeof end !== "number" || !Number.isFinite(end)) continue;
return { start, end };
}

return null;
}

export function determinePreviewTravelDirection(
bearings: PreviewDirectionBearings | undefined,
): 1 | -1 {
if (!bearings) return 1;
const pair = resolveDirectionalBearingPair(bearings);
if (!pair) return 1;
const delta = normalizeSignedDeltaDeg(pair.start, pair.end);
return delta >= 0 ? 1 : -1;
}

export function determineMoonRadius(kindAtLocation: EclipseKindAtLocation) {
if (kindAtLocation === "annular") return 58;
if (kindAtLocation === "total") return 76;
Expand Down Expand Up @@ -109,6 +148,7 @@ export function calculatePreviewMoonGeometry(params: {
contacts?: PreviewMotionContacts;
stageSize?: number;
sunRadius?: number;
travelDirection?: 1 | -1;
}): PreviewMoonGeometry {
const stageSize = params.stageSize ?? PREVIEW_STAGE_SIZE;
const sunRadius = params.sunRadius ?? PREVIEW_SUN_RADIUS;
Expand All @@ -121,7 +161,8 @@ export function calculatePreviewMoonGeometry(params: {
);

const anchors = buildMotionAnchors(params.contacts ?? {}, sunRadius, moonRadius);
const moonOffsetX = interpolateOffsetX(params.progress, anchors);
const travelDirection = params.travelDirection ?? 1;
const moonOffsetX = interpolateOffsetX(params.progress, anchors) * travelDirection;
const moonCenterX = stageSize / 2 + moonOffsetX;
const moonCenterY = stageSize / 2 + moonClosestOffset;

Expand Down
41 changes: 40 additions & 1 deletion apps/mobile/tests/preview-geometry.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";

import { calculatePreviewMoonGeometry, PREVIEW_SUN_RADIUS } from "../src/utils/previewGeometry";
import {
calculatePreviewMoonGeometry,
determinePreviewTravelDirection,
PREVIEW_SUN_RADIUS,
} from "../src/utils/previewGeometry";

describe("preview moon geometry", () => {
it("places C1 at exact outer tangency", () => {
Expand Down Expand Up @@ -49,4 +53,39 @@ describe("preview moon geometry", () => {
expect(c3Distance).toBeCloseTo(Math.abs(PREVIEW_SUN_RADIUS - c3Geometry.moonRadius), 6);
expect(postC3Distance).toBeGreaterThan(c3Distance);
});

it("uses contact bearings to keep moon travel direction accurate", () => {
const leftToRightDirection = determinePreviewTravelDirection({
c1BearingDeg: 100,
c4BearingDeg: 140,
});
const rightToLeftDirection = determinePreviewTravelDirection({
c1BearingDeg: 140,
c4BearingDeg: 100,
});

const baseParams = {
progress: 0.25,
kindAtLocation: "total" as const,
contacts: { c1: 0, c2: 0.25, max: 0.5, c3: 0.75, c4: 1 },
};

const leftToRight = calculatePreviewMoonGeometry({
...baseParams,
travelDirection: leftToRightDirection,
});
const rightToLeft = calculatePreviewMoonGeometry({
...baseParams,
travelDirection: rightToLeftDirection,
});

expect(leftToRight.moonOffsetX).toBeLessThan(0);
expect(rightToLeft.moonOffsetX).toBeGreaterThan(0);
expect(Math.abs(leftToRight.moonOffsetX)).toBeCloseTo(Math.abs(rightToLeft.moonOffsetX), 6);
});

it("falls back to default travel direction when bearings are missing", () => {
expect(determinePreviewTravelDirection(undefined)).toBe(1);
expect(determinePreviewTravelDirection({ c2BearingDeg: 120 })).toBe(1);
});
});