Skip to content

Commit 211e75f

Browse files
authored
chore(release): bump mobile version to 1.1.10 (#3)
1 parent 4dcba2b commit 211e75f

5 files changed

Lines changed: 242 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ 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.10] — 2026-02-22
9+
10+
### Fixed
11+
- Improved eclipse preview moon-path geometry so contact phases align with expected tangency behavior: C1 starts at outer tangency, C2 reaches inner tangency, MAX is centered, and C3 remains at inner tangency before sun reappears.
12+
- Added regression tests for preview geometry calculations to keep C1/C2/MAX/C3 positioning behavior verifiable.
13+
814
## [1.1.9] — 2026-02-21
915

1016
### Added

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.9",
3+
"version": "1.1.10",
44
"private": true,
55
"main": "index.js",
66
"scripts": {

apps/mobile/src/screens/EclipsePreviewScreen.tsx

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import BurgerButton from "../components/BurgerButton";
1414
import type { ContactKey } from "../utils/contacts";
1515
import { colorForContactKey } from "../utils/contactTheme";
1616
import { fmtLocalHuman, fmtUtcHuman } from "../utils/date";
17+
import {
18+
calculatePreviewMoonGeometry,
19+
PREVIEW_STAGE_SIZE,
20+
PREVIEW_SUN_RADIUS,
21+
} from "../utils/previewGeometry";
1722

1823
type PreviewContactKey = ContactKey;
1924

@@ -52,8 +57,8 @@ type EclipsePreviewScreenProps = {
5257
const DEFAULT_WINDOW_MS = 2 * 60 * 60 * 1000;
5358
const MIN_WINDOW_MS = 5 * 60 * 1000;
5459
const PLAYBACK_SPEED = 480;
55-
const SIM_STAGE_SIZE = 300;
56-
const SUN_RADIUS = 72;
60+
const SIM_STAGE_SIZE = PREVIEW_STAGE_SIZE;
61+
const SUN_RADIUS = PREVIEW_SUN_RADIUS;
5762
const MARKER_LABEL_HALF_WIDTH_PX = 18;
5863
const MARKER_LABEL_MIN_GAP_PX = 40;
5964
const MARKER_LABEL_ROW_LIMIT = 1;
@@ -104,31 +109,6 @@ function buildTimelineEvents(payload: PreviewPayload): TimelineEvent[] {
104109
.sort((a, b) => a.t - b.t);
105110
}
106111

107-
function determineMoonRadius(kindAtLocation: EclipseKindAtLocation) {
108-
if (kindAtLocation === "annular") return 58;
109-
if (kindAtLocation === "total") return 76;
110-
if (kindAtLocation === "partial") return 68;
111-
return 66;
112-
}
113-
114-
function determineApproachOffset(
115-
kindAtLocation: EclipseKindAtLocation,
116-
magnitude: number | undefined,
117-
moonRadius: number,
118-
) {
119-
if (kindAtLocation === "none") {
120-
return SUN_RADIUS + moonRadius + 14;
121-
}
122-
123-
if (kindAtLocation === "partial") {
124-
const safeMag =
125-
typeof magnitude === "number" && Number.isFinite(magnitude) ? clamp01(magnitude) : 0.6;
126-
return (1 - safeMag) * (SUN_RADIUS + moonRadius - 6);
127-
}
128-
129-
return 0;
130-
}
131-
132112
function phaseLabelForTime(nowMs: number, events: TimelineEvent[]) {
133113
if (!events.length) return "No contact times available for this location";
134114

@@ -297,18 +277,42 @@ export default function EclipsePreviewScreen({
297277
return positionedMarkers;
298278
}, [progressTrackWidth, timelineBounds.startMs, timelineDurationMs, timelineEvents]);
299279

300-
const moonRadius = useMemo(
301-
() => determineMoonRadius(payload.kindAtLocation),
302-
[payload.kindAtLocation],
303-
);
304-
const moonClosestOffset = useMemo(
305-
() => determineApproachOffset(payload.kindAtLocation, payload.magnitude, moonRadius),
306-
[moonRadius, payload.kindAtLocation, payload.magnitude],
307-
);
280+
const contactProgress = useMemo(() => {
281+
const toProgress = (iso?: string) => {
282+
const t = parseUtcMs(iso);
283+
if (typeof t !== "number") return undefined;
284+
return clamp01((t - timelineBounds.startMs) / timelineDurationMs);
285+
};
308286

309-
const moonTravelHalfSpan = SUN_RADIUS + moonRadius + 26;
310-
const moonCenterX = SIM_STAGE_SIZE / 2 - moonTravelHalfSpan + progress * moonTravelHalfSpan * 2;
311-
const moonCenterY = SIM_STAGE_SIZE / 2 + moonClosestOffset;
287+
return {
288+
c1: toProgress(payload.c1Utc),
289+
c2: toProgress(payload.c2Utc),
290+
max: toProgress(payload.maxUtc),
291+
c3: toProgress(payload.c3Utc),
292+
c4: toProgress(payload.c4Utc),
293+
};
294+
}, [
295+
payload.c1Utc,
296+
payload.c2Utc,
297+
payload.c3Utc,
298+
payload.c4Utc,
299+
payload.maxUtc,
300+
timelineBounds.startMs,
301+
timelineDurationMs,
302+
]);
303+
304+
const moonGeometry = useMemo(
305+
() =>
306+
calculatePreviewMoonGeometry({
307+
progress,
308+
kindAtLocation: payload.kindAtLocation,
309+
magnitude: payload.magnitude,
310+
contacts: contactProgress,
311+
stageSize: SIM_STAGE_SIZE,
312+
sunRadius: SUN_RADIUS,
313+
}),
314+
[contactProgress, payload.kindAtLocation, payload.magnitude, progress],
315+
);
312316

313317
const phaseLabel = useMemo(
314318
() => phaseLabelForTime(currentMs, timelineEvents),
@@ -417,11 +421,11 @@ export default function EclipsePreviewScreen({
417421
style={[
418422
styles.moonDisk,
419423
{
420-
width: moonRadius * 2,
421-
height: moonRadius * 2,
422-
borderRadius: moonRadius,
423-
left: moonCenterX - moonRadius,
424-
top: moonCenterY - moonRadius,
424+
width: moonGeometry.moonRadius * 2,
425+
height: moonGeometry.moonRadius * 2,
426+
borderRadius: moonGeometry.moonRadius,
427+
left: moonGeometry.moonCenterX - moonGeometry.moonRadius,
428+
top: moonGeometry.moonCenterY - moonGeometry.moonRadius,
425429
},
426430
]}
427431
/>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import type { EclipseKindAtLocation } from "@eclipse-timer/shared";
2+
3+
export const PREVIEW_SUN_RADIUS = 72;
4+
export const PREVIEW_STAGE_SIZE = 300;
5+
6+
export type PreviewMotionContacts = {
7+
c1?: number;
8+
c2?: number;
9+
max?: number;
10+
c3?: number;
11+
c4?: number;
12+
};
13+
14+
export type PreviewMoonGeometry = {
15+
moonRadius: number;
16+
moonClosestOffset: number;
17+
moonCenterX: number;
18+
moonCenterY: number;
19+
moonOffsetX: number;
20+
moonTravelHalfSpan: number;
21+
};
22+
23+
function clamp01(v: number) {
24+
return Math.max(0, Math.min(1, v));
25+
}
26+
27+
export function determineMoonRadius(kindAtLocation: EclipseKindAtLocation) {
28+
if (kindAtLocation === "annular") return 58;
29+
if (kindAtLocation === "total") return 76;
30+
if (kindAtLocation === "partial") return 68;
31+
return 66;
32+
}
33+
34+
export function determineApproachOffset(
35+
kindAtLocation: EclipseKindAtLocation,
36+
magnitude: number | undefined,
37+
moonRadius: number,
38+
sunRadius = PREVIEW_SUN_RADIUS,
39+
) {
40+
if (kindAtLocation === "none") {
41+
return sunRadius + moonRadius + 14;
42+
}
43+
44+
if (kindAtLocation === "partial") {
45+
const safeMag =
46+
typeof magnitude === "number" && Number.isFinite(magnitude) ? clamp01(magnitude) : 0.6;
47+
return (1 - safeMag) * (sunRadius + moonRadius - 6);
48+
}
49+
50+
return 0;
51+
}
52+
53+
function buildMotionAnchors(
54+
contacts: PreviewMotionContacts,
55+
sunRadius: number,
56+
moonRadius: number,
57+
): Array<{ progress: number; offsetX: number }> {
58+
const externalTouchOffset = sunRadius + moonRadius;
59+
const internalTouchOffset = Math.abs(sunRadius - moonRadius);
60+
61+
const anchors: Array<{ progress: number; offsetX: number }> = [
62+
{ progress: 0, offsetX: -externalTouchOffset },
63+
{ progress: 1, offsetX: externalTouchOffset },
64+
];
65+
66+
const maybePush = (progress: number | undefined, offsetX: number) => {
67+
if (typeof progress !== "number" || !Number.isFinite(progress)) return;
68+
anchors.push({ progress: clamp01(progress), offsetX });
69+
};
70+
71+
maybePush(contacts.c1, -externalTouchOffset);
72+
maybePush(contacts.c2, -internalTouchOffset);
73+
maybePush(contacts.max, 0);
74+
maybePush(contacts.c3, internalTouchOffset);
75+
maybePush(contacts.c4, externalTouchOffset);
76+
77+
return anchors.sort((a, b) => a.progress - b.progress);
78+
}
79+
80+
function interpolateOffsetX(
81+
progress: number,
82+
anchors: Array<{ progress: number; offsetX: number }>,
83+
) {
84+
const clampedProgress = clamp01(progress);
85+
const first = anchors[0];
86+
const last = anchors[anchors.length - 1];
87+
if (!first || !last) return 0;
88+
if (clampedProgress <= first.progress) return first.offsetX;
89+
if (clampedProgress >= last.progress) return last.offsetX;
90+
91+
for (let idx = 1; idx < anchors.length; idx += 1) {
92+
const prev = anchors[idx - 1];
93+
const next = anchors[idx];
94+
if (!prev || !next) continue;
95+
if (clampedProgress > next.progress) continue;
96+
const span = next.progress - prev.progress;
97+
if (span <= 0) return next.offsetX;
98+
const segmentProgress = (clampedProgress - prev.progress) / span;
99+
return prev.offsetX + (next.offsetX - prev.offsetX) * segmentProgress;
100+
}
101+
102+
return last.offsetX;
103+
}
104+
105+
export function calculatePreviewMoonGeometry(params: {
106+
progress: number;
107+
kindAtLocation: EclipseKindAtLocation;
108+
magnitude?: number;
109+
contacts?: PreviewMotionContacts;
110+
stageSize?: number;
111+
sunRadius?: number;
112+
}): PreviewMoonGeometry {
113+
const stageSize = params.stageSize ?? PREVIEW_STAGE_SIZE;
114+
const sunRadius = params.sunRadius ?? PREVIEW_SUN_RADIUS;
115+
const moonRadius = determineMoonRadius(params.kindAtLocation);
116+
const moonClosestOffset = determineApproachOffset(
117+
params.kindAtLocation,
118+
params.magnitude,
119+
moonRadius,
120+
sunRadius,
121+
);
122+
123+
const anchors = buildMotionAnchors(params.contacts ?? {}, sunRadius, moonRadius);
124+
const moonOffsetX = interpolateOffsetX(params.progress, anchors);
125+
const moonCenterX = stageSize / 2 + moonOffsetX;
126+
const moonCenterY = stageSize / 2 + moonClosestOffset;
127+
128+
return {
129+
moonRadius,
130+
moonClosestOffset,
131+
moonCenterX,
132+
moonCenterY,
133+
moonOffsetX,
134+
moonTravelHalfSpan: sunRadius + moonRadius,
135+
};
136+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { calculatePreviewMoonGeometry, PREVIEW_SUN_RADIUS } from "../src/utils/previewGeometry";
4+
5+
describe("preview moon geometry", () => {
6+
it("places C1 at exact outer tangency", () => {
7+
const geometry = calculatePreviewMoonGeometry({
8+
progress: 0,
9+
kindAtLocation: "total",
10+
contacts: { c1: 0, c2: 0.25, max: 0.5, c3: 0.75, c4: 1 },
11+
});
12+
13+
const centerDistance = Math.abs(geometry.moonCenterX - 150);
14+
expect(centerDistance).toBeCloseTo(PREVIEW_SUN_RADIUS + geometry.moonRadius, 6);
15+
});
16+
17+
it("places C2 at exact inner tangency and max at center", () => {
18+
const c2Geometry = calculatePreviewMoonGeometry({
19+
progress: 0.25,
20+
kindAtLocation: "total",
21+
contacts: { c1: 0, c2: 0.25, max: 0.5, c3: 0.75, c4: 1 },
22+
});
23+
const maxGeometry = calculatePreviewMoonGeometry({
24+
progress: 0.5,
25+
kindAtLocation: "total",
26+
contacts: { c1: 0, c2: 0.25, max: 0.5, c3: 0.75, c4: 1 },
27+
});
28+
29+
const c2Distance = Math.abs(c2Geometry.moonCenterX - 150);
30+
expect(c2Distance).toBeCloseTo(Math.abs(PREVIEW_SUN_RADIUS - c2Geometry.moonRadius), 6);
31+
expect(maxGeometry.moonCenterX).toBeCloseTo(150, 6);
32+
});
33+
34+
it("keeps C3 as inner tangency and only exposes sun after C3", () => {
35+
const c3Geometry = calculatePreviewMoonGeometry({
36+
progress: 0.75,
37+
kindAtLocation: "total",
38+
contacts: { c1: 0, c2: 0.25, max: 0.5, c3: 0.75, c4: 1 },
39+
});
40+
const postC3Geometry = calculatePreviewMoonGeometry({
41+
progress: 0.8,
42+
kindAtLocation: "total",
43+
contacts: { c1: 0, c2: 0.25, max: 0.5, c3: 0.75, c4: 1 },
44+
});
45+
46+
const c3Distance = Math.abs(c3Geometry.moonCenterX - 150);
47+
const postC3Distance = Math.abs(postC3Geometry.moonCenterX - 150);
48+
49+
expect(c3Distance).toBeCloseTo(Math.abs(PREVIEW_SUN_RADIUS - c3Geometry.moonRadius), 6);
50+
expect(postC3Distance).toBeGreaterThan(c3Distance);
51+
});
52+
});

0 commit comments

Comments
 (0)