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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ 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.38] — 2026-02-27

### Added
- Added landscape composite horizon direction markings (16-point compass labels) in the Photography Guide modal.
- Added a landscape composite toggle to show/hide markings, direction labels, and shot numbers together.

### Fixed
- Updated landscape composite rendering to hide sun/moon placements when the sampled sun is below the horizon.

### Changed
- Bumped `apps/mobile` version to `1.1.38`.

## [1.1.37] — 2026-02-26

### Changed
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.37",
"version": "1.1.38",
"private": true,
"main": "index.js",
"scripts": {
Expand Down
266 changes: 187 additions & 79 deletions apps/mobile/src/screens/PhotographyGuideScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,30 @@ const TOTALITY_MOON_COLOR = TOTALITY_SKY_COLOR;
const TOTALITY_MOON_BORDER_COLOR = "#1f2c47";
const TOTALITY_HORIZON_LINE_COLOR = "rgba(255, 174, 205, 0.75)";
const TOTALITY_HORIZON_GLOW_COLOR = "rgba(255, 136, 182, 0.42)";
const LANDSCAPE_HORIZONTAL_FOV_DEG_24MM = 74;
const COMPASS_MARKERS = [
{ label: "N", azimuthDeg: 0 },
Comment on lines +37 to +39
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

LANDSCAPE_HORIZONTAL_FOV_DEG_24MM is defined here and also in apps/mobile/src/utils/photographyGuide.ts (same value). Duplicating this constant risks the UI compass markers drifting out of sync with the layout math if one side changes. Consider exporting the FOV constant from the utils module (or a shared constants module) and reusing it here.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@codex Consider this

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

Testing

  • pnpm -C apps/mobile test tests/photography-guide.test.ts
  • pnpm -C apps/mobile typecheck
  • ⚠️ N/A (React Native screen change; browser screenshot capture is not applicable in this environment)

View task →

{ label: "NNE", azimuthDeg: 22.5 },
{ label: "NE", azimuthDeg: 45 },
{ label: "ENE", azimuthDeg: 67.5 },
{ label: "E", azimuthDeg: 90 },
{ label: "ESE", azimuthDeg: 112.5 },
{ label: "SE", azimuthDeg: 135 },
{ label: "SSE", azimuthDeg: 157.5 },
{ label: "S", azimuthDeg: 180 },
{ label: "SSW", azimuthDeg: 202.5 },
{ label: "SW", azimuthDeg: 225 },
{ label: "WSW", azimuthDeg: 247.5 },
{ label: "W", azimuthDeg: 270 },
{ label: "WNW", azimuthDeg: 292.5 },
{ label: "NW", azimuthDeg: 315 },
{ label: "NNW", azimuthDeg: 337.5 },
] as const;

function normalizeSignedDeltaDeg(fromDeg: number, toDeg: number) {
const delta = ((toDeg - fromDeg + 540) % 360) - 180;
return delta === -180 ? 180 : delta;
}
Comment on lines +57 to +60
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

normalizeSignedDeltaDeg duplicates the same function in apps/mobile/src/utils/photographyGuide.ts. To avoid subtle divergence in angle wrap behavior between layout computation and compass marker placement, consider exporting and reusing a single implementation.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@codex check this

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.


export type PhotographyGuidePayload = {
eclipseId: string;
Expand Down Expand Up @@ -89,6 +113,7 @@ export default function PhotographyGuideScreen({
const [totalPictures, setTotalPictures] = useState<PhotographyGuidePictureCount>(5);
const [isCountPickerOpen, setIsCountPickerOpen] = useState(false);
const [isLandscapeCompositeOpen, setIsLandscapeCompositeOpen] = useState(false);
const [showCompositeMarkings, setShowCompositeMarkings] = useState(true);
const [compositeStageSize, setCompositeStageSize] = useState({
width: 0,
height: 0,
Expand Down Expand Up @@ -184,6 +209,22 @@ export default function PhotographyGuideScreen({
if (typeof activeCompositeHorizonY !== "number") return undefined;
return Math.max(0, compositeStageSize.height - activeCompositeHorizonY);
}, [activeCompositeHorizonY, compositeStageSize.height]);
const horizonCompassMarkers = useMemo(() => {
if (!compositeLayout) return [];
const maxPlacement = compositeLayout.placements.find(
(placement) => placement.phaseBucket === "MAX" && typeof placement.sunAzimuthDeg === "number",
);
if (!maxPlacement || typeof maxPlacement.sunAzimuthDeg !== "number") return [];
const centerAzimuthDeg = maxPlacement.sunAzimuthDeg;

return COMPASS_MARKERS.map((marker) => {
const deltaDeg = normalizeSignedDeltaDeg(centerAzimuthDeg, marker.azimuthDeg);
const x =
compositeLayout.anchorX +
(deltaDeg / LANDSCAPE_HORIZONTAL_FOV_DEG_24MM) * compositeStageSize.width;
return { ...marker, x, inFrame: x >= 0 && x <= compositeStageSize.width };
}).filter((marker) => marker.inFrame);
}, [compositeLayout, compositeStageSize.width]);
const handleCompositeStageLayout = (event: LayoutChangeEvent) => {
const nextWidth = Math.round(event.nativeEvent.layout.width);
const nextHeight = Math.round(event.nativeEvent.layout.height);
Expand Down Expand Up @@ -363,6 +404,16 @@ export default function PhotographyGuideScreen({
/>
<View style={styles.compositeModal}>
<Text style={styles.compositeModalTitle}>Landscape composite</Text>
<Pressable
style={styles.compositeMarkingsToggleBtn}
onPress={() => setShowCompositeMarkings((current) => !current)}
accessibilityRole="button"
accessibilityLabel="Toggle composite markings"
>
<Text style={styles.compositeMarkingsToggleText}>
{showCompositeMarkings ? "Hide" : "Show"} markings, directions, and shot numbers
Comment thread
c0d3rb4b4 marked this conversation as resolved.
</Text>
</Pressable>
<View style={styles.compositeFrame} onLayout={handleCompositeStageLayout}>
<View
style={[
Expand Down Expand Up @@ -402,6 +453,26 @@ export default function PhotographyGuideScreen({
: null,
]}
/>
{showCompositeMarkings && typeof activeCompositeHorizonY === "number"
? horizonCompassMarkers.map((marker) => (
<View key={`horizon-marker-${marker.label}`} style={{ left: marker.x - 10 }}>
<View
style={[
styles.compositeDirectionTick,
{ top: activeCompositeHorizonY - 6 },
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The horizon compass marker wrapper is offset by left: marker.x - 10, but the tick has no horizontal offset within that wrapper, so the tick renders ~10px left of the intended azimuth line while the label is centered. Consider centering the tick on marker.x (e.g., by offsetting the tick inside the wrapper or changing the wrapper positioning strategy) so tick and label align.

Suggested change
{ top: activeCompositeHorizonY - 6 },
{ top: activeCompositeHorizonY - 6, left: 10 },

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@codex check this

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

  • Fixed the horizon compass tick alignment in the landscape composite overlay by adding a horizontal offset (left: 10) to the tick style, so each tick now centers on the same azimuth line as its corresponding label. apps/mobile/src/screens/PhotographyGuideScreen.tsxL456-L468
  • Committed the change on the current branch with message: Fix horizon marker tick centering in composite overlay.

Testing

  • pnpm -C apps/mobile typecheck
  • ⚠️ Screenshot not captured: this is a React Native mobile screen change and there is no runnable browser target in this environment to render PhotographyGuideScreen via the browser tool.

View task →

]}
/>
<Text
style={[
styles.compositeDirectionLabel,
{ top: activeCompositeHorizonY + 4 },
]}
>
{marker.label}
</Text>
</View>
))
: null}
{compositeLayout ? (
<>
<View
Expand All @@ -415,92 +486,98 @@ export default function PhotographyGuideScreen({
/>
{compositeLayout.placements.map((placement) => (
<View key={placement.index}>
{isTotalCompositeTheme &&
placement.phaseBucket === "MAX" &&
placement.showMoon &&
placement.moon ? (
{!placement.isAboveHorizon ? null : (
<>
{isTotalCompositeTheme &&
placement.phaseBucket === "MAX" &&
Comment thread
c0d3rb4b4 marked this conversation as resolved.
placement.showMoon &&
placement.moon ? (
<>
<View
style={[
styles.compositeCoronaGlow,
{
width: Math.max(placement.sunRadius * 9, 16),
height: Math.max(placement.sunRadius * 9, 16),
borderRadius: Math.max(placement.sunRadius * 4.5, 8),
left: placement.x - Math.max(placement.sunRadius * 4.5, 8),
top: placement.y - Math.max(placement.sunRadius * 4.5, 8),
},
]}
/>
<View
style={[
styles.compositeCoronaRing,
{
width: Math.max(placement.sunRadius * 6, 10),
height: Math.max(placement.sunRadius * 6, 10),
borderRadius: Math.max(placement.sunRadius * 3, 5),
left: placement.x - Math.max(placement.sunRadius * 3, 5),
top: placement.y - Math.max(placement.sunRadius * 3, 5),
},
]}
/>
</>
) : null}
<View
style={[
styles.compositeCoronaGlow,
styles.compositeSun,
{
width: Math.max(placement.sunRadius * 9, 16),
height: Math.max(placement.sunRadius * 9, 16),
borderRadius: Math.max(placement.sunRadius * 4.5, 8),
left: placement.x - Math.max(placement.sunRadius * 4.5, 8),
top: placement.y - Math.max(placement.sunRadius * 4.5, 8),
},
]}
/>
<View
style={[
styles.compositeCoronaRing,
{
width: Math.max(placement.sunRadius * 6, 10),
height: Math.max(placement.sunRadius * 6, 10),
borderRadius: Math.max(placement.sunRadius * 3, 5),
left: placement.x - Math.max(placement.sunRadius * 3, 5),
top: placement.y - Math.max(placement.sunRadius * 3, 5),
width: placement.sunRadius * 2,
height: placement.sunRadius * 2,
borderRadius: placement.sunRadius,
left: placement.x - placement.sunRadius,
top: placement.y - placement.sunRadius,
},
]}
/>
{placement.showMoon && placement.moon ? (
<View
style={[
styles.compositeMoon,
isTotalCompositeTheme
? {
backgroundColor: TOTALITY_MOON_COLOR,
borderColor: TOTALITY_MOON_BORDER_COLOR,
}
: null,
{
width: placement.moon.radius * 2,
height: placement.moon.radius * 2,
borderRadius: placement.moon.radius,
left: placement.moon.x - placement.moon.radius,
top: placement.moon.y - placement.moon.radius,
},
]}
/>
) : null}
{showCompositeMarkings ? (
<View
style={[
styles.compositeShotIndexTag,
{
left: placement.x - 9,
top: placement.y + placement.sunRadius + 3,
},
placement.clamped ? styles.compositeShotIndexTagClamped : null,
]}
>
<Text style={styles.compositeShotIndexText}>{placement.index}</Text>
</View>
) : null}
{showCompositeMarkings && placement.clamped ? (
<View
style={[
styles.compositeClampIndicator,
{
left: placement.x + placement.sunRadius - 4,
top: placement.y - placement.sunRadius - 4,
},
]}
/>
) : null}
</>
) : null}
<View
style={[
styles.compositeSun,
{
width: placement.sunRadius * 2,
height: placement.sunRadius * 2,
borderRadius: placement.sunRadius,
left: placement.x - placement.sunRadius,
top: placement.y - placement.sunRadius,
},
]}
/>
{placement.showMoon && placement.moon ? (
<View
style={[
styles.compositeMoon,
isTotalCompositeTheme
? {
backgroundColor: TOTALITY_MOON_COLOR,
borderColor: TOTALITY_MOON_BORDER_COLOR,
}
: null,
{
width: placement.moon.radius * 2,
height: placement.moon.radius * 2,
borderRadius: placement.moon.radius,
left: placement.moon.x - placement.moon.radius,
top: placement.moon.y - placement.moon.radius,
},
]}
/>
) : null}
<View
style={[
styles.compositeShotIndexTag,
{
left: placement.x - 9,
top: placement.y + placement.sunRadius + 3,
},
placement.clamped ? styles.compositeShotIndexTagClamped : null,
]}
>
<Text style={styles.compositeShotIndexText}>{placement.index}</Text>
</View>
{placement.clamped ? (
<View
style={[
styles.compositeClampIndicator,
{
left: placement.x + placement.sunRadius - 4,
top: placement.y - placement.sunRadius - 4,
},
]}
/>
) : null}
)}
</View>
))}
</>
Expand All @@ -510,7 +587,8 @@ export default function PhotographyGuideScreen({
24mm framing simulation with MAX anchored at frame center.
</Text>
<Text style={styles.compositeLegendText}>
Numbers are shot indices. Amber dots mark edge-clamped shots.
Numbers are shot indices. Horizon ticks show compass directions. Amber dots mark
edge-clamped shots.
</Text>
<Pressable
style={styles.compositeModalCloseBtn}
Expand Down Expand Up @@ -903,6 +981,36 @@ function createStyles(colors: ReturnType<typeof useAppTheme>["colors"]) {
borderColor: "#7a5222",
opacity: 0.9,
},
compositeDirectionTick: {
position: "absolute",
width: 1,
height: 12,
backgroundColor: "rgba(255,255,255,0.7)",
},
compositeDirectionLabel: {
position: "absolute",
width: 20,
textAlign: "center",
color: "#ffffff",
fontSize: 9,
fontWeight: "700",
textShadowColor: "rgba(0,0,0,0.4)",
textShadowRadius: 2,
textShadowOffset: { width: 0, height: 1 },
},
compositeMarkingsToggleBtn: {
borderRadius: 10,
borderWidth: 1,
borderColor: colors.inputBorder,
backgroundColor: colors.surfaceMuted,
paddingVertical: 8,
paddingHorizontal: 10,
},
compositeMarkingsToggleText: {
color: colors.textSecondary,
fontSize: 12,
fontWeight: "700",
},
compositeModal: {
width: "100%",
borderRadius: 12,
Expand Down
7 changes: 7 additions & 0 deletions apps/mobile/src/utils/photographyGuide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export type LandscapeCompositePlacement = {
sunRadius: number;
clamped: boolean;
showMoon: boolean;
sunAltitudeDeg?: number;
sunAzimuthDeg?: number;
isAboveHorizon: boolean;
moon?: {
x: number;
y: number;
Expand Down Expand Up @@ -265,6 +268,7 @@ function buildLandscapeCompositeLayoutFallback(
sunRadius,
clamped,
showMoon: row.showMoon,
isAboveHorizon: clampedY + sunRadius <= frameHeight * LANDSCAPE_HORIZON_FALLBACK_Y_RATIO,
moon,
};
});
Expand Down Expand Up @@ -532,6 +536,9 @@ export function buildLandscapeCompositeLayout(
sunRadius,
clamped,
showMoon: row.showMoon && Boolean(moon),
sunAltitudeDeg: sample.sun.altitudeDeg,
sunAzimuthDeg: sample.sun.azimuthDeg,
isAboveHorizon: sample.sun.altitudeDeg >= 0,
moon,
};
});
Expand Down
Loading