Skip to content

Commit fde93b5

Browse files
authored
Add landscape composite horizon directions and annotation toggle (#22)
* Add horizon direction markings and composite annotation toggle * chore: bump mobile version to 1.1.38 and update changelog
1 parent 8dbad2f commit fde93b5

5 files changed

Lines changed: 240 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ 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.38] — 2026-02-27
9+
10+
### Added
11+
- Added landscape composite horizon direction markings (16-point compass labels) in the Photography Guide modal.
12+
- Added a landscape composite toggle to show/hide markings, direction labels, and shot numbers together.
13+
14+
### Fixed
15+
- Updated landscape composite rendering to hide sun/moon placements when the sampled sun is below the horizon.
16+
17+
### Changed
18+
- Bumped `apps/mobile` version to `1.1.38`.
19+
820
## [1.1.37] — 2026-02-26
921

1022
### Changed

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

apps/mobile/src/screens/PhotographyGuideScreen.tsx

Lines changed: 187 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,30 @@ const TOTALITY_MOON_COLOR = TOTALITY_SKY_COLOR;
3434
const TOTALITY_MOON_BORDER_COLOR = "#1f2c47";
3535
const TOTALITY_HORIZON_LINE_COLOR = "rgba(255, 174, 205, 0.75)";
3636
const TOTALITY_HORIZON_GLOW_COLOR = "rgba(255, 136, 182, 0.42)";
37+
const LANDSCAPE_HORIZONTAL_FOV_DEG_24MM = 74;
38+
const COMPASS_MARKERS = [
39+
{ label: "N", azimuthDeg: 0 },
40+
{ label: "NNE", azimuthDeg: 22.5 },
41+
{ label: "NE", azimuthDeg: 45 },
42+
{ label: "ENE", azimuthDeg: 67.5 },
43+
{ label: "E", azimuthDeg: 90 },
44+
{ label: "ESE", azimuthDeg: 112.5 },
45+
{ label: "SE", azimuthDeg: 135 },
46+
{ label: "SSE", azimuthDeg: 157.5 },
47+
{ label: "S", azimuthDeg: 180 },
48+
{ label: "SSW", azimuthDeg: 202.5 },
49+
{ label: "SW", azimuthDeg: 225 },
50+
{ label: "WSW", azimuthDeg: 247.5 },
51+
{ label: "W", azimuthDeg: 270 },
52+
{ label: "WNW", azimuthDeg: 292.5 },
53+
{ label: "NW", azimuthDeg: 315 },
54+
{ label: "NNW", azimuthDeg: 337.5 },
55+
] as const;
56+
57+
function normalizeSignedDeltaDeg(fromDeg: number, toDeg: number) {
58+
const delta = ((toDeg - fromDeg + 540) % 360) - 180;
59+
return delta === -180 ? 180 : delta;
60+
}
3761

3862
export type PhotographyGuidePayload = {
3963
eclipseId: string;
@@ -89,6 +113,7 @@ export default function PhotographyGuideScreen({
89113
const [totalPictures, setTotalPictures] = useState<PhotographyGuidePictureCount>(5);
90114
const [isCountPickerOpen, setIsCountPickerOpen] = useState(false);
91115
const [isLandscapeCompositeOpen, setIsLandscapeCompositeOpen] = useState(false);
116+
const [showCompositeMarkings, setShowCompositeMarkings] = useState(true);
92117
const [compositeStageSize, setCompositeStageSize] = useState({
93118
width: 0,
94119
height: 0,
@@ -184,6 +209,22 @@ export default function PhotographyGuideScreen({
184209
if (typeof activeCompositeHorizonY !== "number") return undefined;
185210
return Math.max(0, compositeStageSize.height - activeCompositeHorizonY);
186211
}, [activeCompositeHorizonY, compositeStageSize.height]);
212+
const horizonCompassMarkers = useMemo(() => {
213+
if (!compositeLayout) return [];
214+
const maxPlacement = compositeLayout.placements.find(
215+
(placement) => placement.phaseBucket === "MAX" && typeof placement.sunAzimuthDeg === "number",
216+
);
217+
if (!maxPlacement || typeof maxPlacement.sunAzimuthDeg !== "number") return [];
218+
const centerAzimuthDeg = maxPlacement.sunAzimuthDeg;
219+
220+
return COMPASS_MARKERS.map((marker) => {
221+
const deltaDeg = normalizeSignedDeltaDeg(centerAzimuthDeg, marker.azimuthDeg);
222+
const x =
223+
compositeLayout.anchorX +
224+
(deltaDeg / LANDSCAPE_HORIZONTAL_FOV_DEG_24MM) * compositeStageSize.width;
225+
return { ...marker, x, inFrame: x >= 0 && x <= compositeStageSize.width };
226+
}).filter((marker) => marker.inFrame);
227+
}, [compositeLayout, compositeStageSize.width]);
187228
const handleCompositeStageLayout = (event: LayoutChangeEvent) => {
188229
const nextWidth = Math.round(event.nativeEvent.layout.width);
189230
const nextHeight = Math.round(event.nativeEvent.layout.height);
@@ -363,6 +404,16 @@ export default function PhotographyGuideScreen({
363404
/>
364405
<View style={styles.compositeModal}>
365406
<Text style={styles.compositeModalTitle}>Landscape composite</Text>
407+
<Pressable
408+
style={styles.compositeMarkingsToggleBtn}
409+
onPress={() => setShowCompositeMarkings((current) => !current)}
410+
accessibilityRole="button"
411+
accessibilityLabel="Toggle composite markings"
412+
>
413+
<Text style={styles.compositeMarkingsToggleText}>
414+
{showCompositeMarkings ? "Hide" : "Show"} markings, directions, and shot numbers
415+
</Text>
416+
</Pressable>
366417
<View style={styles.compositeFrame} onLayout={handleCompositeStageLayout}>
367418
<View
368419
style={[
@@ -402,6 +453,26 @@ export default function PhotographyGuideScreen({
402453
: null,
403454
]}
404455
/>
456+
{showCompositeMarkings && typeof activeCompositeHorizonY === "number"
457+
? horizonCompassMarkers.map((marker) => (
458+
<View key={`horizon-marker-${marker.label}`} style={{ left: marker.x - 10 }}>
459+
<View
460+
style={[
461+
styles.compositeDirectionTick,
462+
{ top: activeCompositeHorizonY - 6 },
463+
]}
464+
/>
465+
<Text
466+
style={[
467+
styles.compositeDirectionLabel,
468+
{ top: activeCompositeHorizonY + 4 },
469+
]}
470+
>
471+
{marker.label}
472+
</Text>
473+
</View>
474+
))
475+
: null}
405476
{compositeLayout ? (
406477
<>
407478
<View
@@ -415,92 +486,98 @@ export default function PhotographyGuideScreen({
415486
/>
416487
{compositeLayout.placements.map((placement) => (
417488
<View key={placement.index}>
418-
{isTotalCompositeTheme &&
419-
placement.phaseBucket === "MAX" &&
420-
placement.showMoon &&
421-
placement.moon ? (
489+
{!placement.isAboveHorizon ? null : (
422490
<>
491+
{isTotalCompositeTheme &&
492+
placement.phaseBucket === "MAX" &&
493+
placement.showMoon &&
494+
placement.moon ? (
495+
<>
496+
<View
497+
style={[
498+
styles.compositeCoronaGlow,
499+
{
500+
width: Math.max(placement.sunRadius * 9, 16),
501+
height: Math.max(placement.sunRadius * 9, 16),
502+
borderRadius: Math.max(placement.sunRadius * 4.5, 8),
503+
left: placement.x - Math.max(placement.sunRadius * 4.5, 8),
504+
top: placement.y - Math.max(placement.sunRadius * 4.5, 8),
505+
},
506+
]}
507+
/>
508+
<View
509+
style={[
510+
styles.compositeCoronaRing,
511+
{
512+
width: Math.max(placement.sunRadius * 6, 10),
513+
height: Math.max(placement.sunRadius * 6, 10),
514+
borderRadius: Math.max(placement.sunRadius * 3, 5),
515+
left: placement.x - Math.max(placement.sunRadius * 3, 5),
516+
top: placement.y - Math.max(placement.sunRadius * 3, 5),
517+
},
518+
]}
519+
/>
520+
</>
521+
) : null}
423522
<View
424523
style={[
425-
styles.compositeCoronaGlow,
524+
styles.compositeSun,
426525
{
427-
width: Math.max(placement.sunRadius * 9, 16),
428-
height: Math.max(placement.sunRadius * 9, 16),
429-
borderRadius: Math.max(placement.sunRadius * 4.5, 8),
430-
left: placement.x - Math.max(placement.sunRadius * 4.5, 8),
431-
top: placement.y - Math.max(placement.sunRadius * 4.5, 8),
432-
},
433-
]}
434-
/>
435-
<View
436-
style={[
437-
styles.compositeCoronaRing,
438-
{
439-
width: Math.max(placement.sunRadius * 6, 10),
440-
height: Math.max(placement.sunRadius * 6, 10),
441-
borderRadius: Math.max(placement.sunRadius * 3, 5),
442-
left: placement.x - Math.max(placement.sunRadius * 3, 5),
443-
top: placement.y - Math.max(placement.sunRadius * 3, 5),
526+
width: placement.sunRadius * 2,
527+
height: placement.sunRadius * 2,
528+
borderRadius: placement.sunRadius,
529+
left: placement.x - placement.sunRadius,
530+
top: placement.y - placement.sunRadius,
444531
},
445532
]}
446533
/>
534+
{placement.showMoon && placement.moon ? (
535+
<View
536+
style={[
537+
styles.compositeMoon,
538+
isTotalCompositeTheme
539+
? {
540+
backgroundColor: TOTALITY_MOON_COLOR,
541+
borderColor: TOTALITY_MOON_BORDER_COLOR,
542+
}
543+
: null,
544+
{
545+
width: placement.moon.radius * 2,
546+
height: placement.moon.radius * 2,
547+
borderRadius: placement.moon.radius,
548+
left: placement.moon.x - placement.moon.radius,
549+
top: placement.moon.y - placement.moon.radius,
550+
},
551+
]}
552+
/>
553+
) : null}
554+
{showCompositeMarkings ? (
555+
<View
556+
style={[
557+
styles.compositeShotIndexTag,
558+
{
559+
left: placement.x - 9,
560+
top: placement.y + placement.sunRadius + 3,
561+
},
562+
placement.clamped ? styles.compositeShotIndexTagClamped : null,
563+
]}
564+
>
565+
<Text style={styles.compositeShotIndexText}>{placement.index}</Text>
566+
</View>
567+
) : null}
568+
{showCompositeMarkings && placement.clamped ? (
569+
<View
570+
style={[
571+
styles.compositeClampIndicator,
572+
{
573+
left: placement.x + placement.sunRadius - 4,
574+
top: placement.y - placement.sunRadius - 4,
575+
},
576+
]}
577+
/>
578+
) : null}
447579
</>
448-
) : null}
449-
<View
450-
style={[
451-
styles.compositeSun,
452-
{
453-
width: placement.sunRadius * 2,
454-
height: placement.sunRadius * 2,
455-
borderRadius: placement.sunRadius,
456-
left: placement.x - placement.sunRadius,
457-
top: placement.y - placement.sunRadius,
458-
},
459-
]}
460-
/>
461-
{placement.showMoon && placement.moon ? (
462-
<View
463-
style={[
464-
styles.compositeMoon,
465-
isTotalCompositeTheme
466-
? {
467-
backgroundColor: TOTALITY_MOON_COLOR,
468-
borderColor: TOTALITY_MOON_BORDER_COLOR,
469-
}
470-
: null,
471-
{
472-
width: placement.moon.radius * 2,
473-
height: placement.moon.radius * 2,
474-
borderRadius: placement.moon.radius,
475-
left: placement.moon.x - placement.moon.radius,
476-
top: placement.moon.y - placement.moon.radius,
477-
},
478-
]}
479-
/>
480-
) : null}
481-
<View
482-
style={[
483-
styles.compositeShotIndexTag,
484-
{
485-
left: placement.x - 9,
486-
top: placement.y + placement.sunRadius + 3,
487-
},
488-
placement.clamped ? styles.compositeShotIndexTagClamped : null,
489-
]}
490-
>
491-
<Text style={styles.compositeShotIndexText}>{placement.index}</Text>
492-
</View>
493-
{placement.clamped ? (
494-
<View
495-
style={[
496-
styles.compositeClampIndicator,
497-
{
498-
left: placement.x + placement.sunRadius - 4,
499-
top: placement.y - placement.sunRadius - 4,
500-
},
501-
]}
502-
/>
503-
) : null}
580+
)}
504581
</View>
505582
))}
506583
</>
@@ -510,7 +587,8 @@ export default function PhotographyGuideScreen({
510587
24mm framing simulation with MAX anchored at frame center.
511588
</Text>
512589
<Text style={styles.compositeLegendText}>
513-
Numbers are shot indices. Amber dots mark edge-clamped shots.
590+
Numbers are shot indices. Horizon ticks show compass directions. Amber dots mark
591+
edge-clamped shots.
514592
</Text>
515593
<Pressable
516594
style={styles.compositeModalCloseBtn}
@@ -903,6 +981,36 @@ function createStyles(colors: ReturnType<typeof useAppTheme>["colors"]) {
903981
borderColor: "#7a5222",
904982
opacity: 0.9,
905983
},
984+
compositeDirectionTick: {
985+
position: "absolute",
986+
width: 1,
987+
height: 12,
988+
backgroundColor: "rgba(255,255,255,0.7)",
989+
},
990+
compositeDirectionLabel: {
991+
position: "absolute",
992+
width: 20,
993+
textAlign: "center",
994+
color: "#ffffff",
995+
fontSize: 9,
996+
fontWeight: "700",
997+
textShadowColor: "rgba(0,0,0,0.4)",
998+
textShadowRadius: 2,
999+
textShadowOffset: { width: 0, height: 1 },
1000+
},
1001+
compositeMarkingsToggleBtn: {
1002+
borderRadius: 10,
1003+
borderWidth: 1,
1004+
borderColor: colors.inputBorder,
1005+
backgroundColor: colors.surfaceMuted,
1006+
paddingVertical: 8,
1007+
paddingHorizontal: 10,
1008+
},
1009+
compositeMarkingsToggleText: {
1010+
color: colors.textSecondary,
1011+
fontSize: 12,
1012+
fontWeight: "700",
1013+
},
9061014
compositeModal: {
9071015
width: "100%",
9081016
borderRadius: 12,

apps/mobile/src/utils/photographyGuide.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export type LandscapeCompositePlacement = {
7171
sunRadius: number;
7272
clamped: boolean;
7373
showMoon: boolean;
74+
sunAltitudeDeg?: number;
75+
sunAzimuthDeg?: number;
76+
isAboveHorizon: boolean;
7477
moon?: {
7578
x: number;
7679
y: number;
@@ -265,6 +268,7 @@ function buildLandscapeCompositeLayoutFallback(
265268
sunRadius,
266269
clamped,
267270
showMoon: row.showMoon,
271+
isAboveHorizon: clampedY + sunRadius <= frameHeight * LANDSCAPE_HORIZON_FALLBACK_Y_RATIO,
268272
moon,
269273
};
270274
});
@@ -532,6 +536,9 @@ export function buildLandscapeCompositeLayout(
532536
sunRadius,
533537
clamped,
534538
showMoon: row.showMoon && Boolean(moon),
539+
sunAltitudeDeg: sample.sun.altitudeDeg,
540+
sunAzimuthDeg: sample.sun.azimuthDeg,
541+
isAboveHorizon: sample.sun.altitudeDeg >= 0,
535542
moon,
536543
};
537544
});

0 commit comments

Comments
 (0)