From af597d68476c7f9d68c3d0e6591fab35d0a12430 Mon Sep 17 00:00:00 2001 From: Lalit Sharma Date: Fri, 27 Feb 2026 07:48:41 +0000 Subject: [PATCH 1/2] Add horizon direction markings and composite annotation toggle --- .../src/screens/PhotographyGuideScreen.tsx | 266 ++++++++++++------ apps/mobile/src/utils/photographyGuide.ts | 7 + apps/mobile/tests/photography-guide.test.ts | 33 +++ 3 files changed, 227 insertions(+), 79 deletions(-) diff --git a/apps/mobile/src/screens/PhotographyGuideScreen.tsx b/apps/mobile/src/screens/PhotographyGuideScreen.tsx index d62ca0b..85a613e 100644 --- a/apps/mobile/src/screens/PhotographyGuideScreen.tsx +++ b/apps/mobile/src/screens/PhotographyGuideScreen.tsx @@ -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 }, + { 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; +} export type PhotographyGuidePayload = { eclipseId: string; @@ -89,6 +113,7 @@ export default function PhotographyGuideScreen({ const [totalPictures, setTotalPictures] = useState(5); const [isCountPickerOpen, setIsCountPickerOpen] = useState(false); const [isLandscapeCompositeOpen, setIsLandscapeCompositeOpen] = useState(false); + const [showCompositeMarkings, setShowCompositeMarkings] = useState(true); const [compositeStageSize, setCompositeStageSize] = useState({ width: 0, height: 0, @@ -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); @@ -363,6 +404,16 @@ export default function PhotographyGuideScreen({ /> Landscape composite + setShowCompositeMarkings((current) => !current)} + accessibilityRole="button" + accessibilityLabel="Toggle composite markings" + > + + {showCompositeMarkings ? "Hide" : "Show"} markings, directions, and shot numbers + + + {showCompositeMarkings && typeof activeCompositeHorizonY === "number" + ? horizonCompassMarkers.map((marker) => ( + + + + {marker.label} + + + )) + : null} {compositeLayout ? ( <> {compositeLayout.placements.map((placement) => ( - {isTotalCompositeTheme && - placement.phaseBucket === "MAX" && - placement.showMoon && - placement.moon ? ( + {!placement.isAboveHorizon ? null : ( <> + {isTotalCompositeTheme && + placement.phaseBucket === "MAX" && + placement.showMoon && + placement.moon ? ( + <> + + + + ) : null} - + {placement.showMoon && placement.moon ? ( + + ) : null} + {showCompositeMarkings ? ( + + {placement.index} + + ) : null} + {showCompositeMarkings && placement.clamped ? ( + + ) : null} - ) : null} - - {placement.showMoon && placement.moon ? ( - - ) : null} - - {placement.index} - - {placement.clamped ? ( - - ) : null} + )} ))} @@ -510,7 +587,8 @@ export default function PhotographyGuideScreen({ 24mm framing simulation with MAX anchored at frame center. - 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. ["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, diff --git a/apps/mobile/src/utils/photographyGuide.ts b/apps/mobile/src/utils/photographyGuide.ts index 9ec1e58..ebfe9db 100644 --- a/apps/mobile/src/utils/photographyGuide.ts +++ b/apps/mobile/src/utils/photographyGuide.ts @@ -71,6 +71,9 @@ export type LandscapeCompositePlacement = { sunRadius: number; clamped: boolean; showMoon: boolean; + sunAltitudeDeg?: number; + sunAzimuthDeg?: number; + isAboveHorizon: boolean; moon?: { x: number; y: number; @@ -265,6 +268,7 @@ function buildLandscapeCompositeLayoutFallback( sunRadius, clamped, showMoon: row.showMoon, + isAboveHorizon: clampedY + sunRadius <= frameHeight * LANDSCAPE_HORIZON_FALLBACK_Y_RATIO, moon, }; }); @@ -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, }; }); diff --git a/apps/mobile/tests/photography-guide.test.ts b/apps/mobile/tests/photography-guide.test.ts index aeac30d..3afddf9 100644 --- a/apps/mobile/tests/photography-guide.test.ts +++ b/apps/mobile/tests/photography-guide.test.ts @@ -267,4 +267,37 @@ describe("photography guide schedule", () => { expect(centerDistance).toBeLessThan(placement.sunRadius * 2.5); } }); + + it("flags above/below horizon and exposes azimuth/altitude in observer-aware layouts", () => { + const scheduleResult = buildPhotographyGuideSchedule({ + visible: true, + totalPictures: 5, + kindAtLocation: "partial", + c1Utc: "2027-08-02T01:00:00.000Z", + maxUtc: "2027-08-02T02:00:00.000Z", + c4Utc: "2027-08-02T03:00:00.000Z", + }); + expect(scheduleResult.ok).toBe(true); + if (!scheduleResult.ok) return; + + const layout = buildLandscapeCompositeLayout({ + schedule: scheduleResult.schedule, + kindAtLocation: "partial", + maxUtc: "2027-08-02T02:00:00.000Z", + frameWidth: 360, + frameHeight: 216, + observer: { + latDeg: 36.13173, + lonDeg: -5.34095, + }, + travelVector: { x: 1, y: 0 }, + }); + + expect(layout.placements.some((placement) => placement.isAboveHorizon)).toBe(false); + for (const placement of layout.placements) { + expect(typeof placement.sunAzimuthDeg).toBe("number"); + expect(typeof placement.sunAltitudeDeg).toBe("number"); + expect(placement.isAboveHorizon).toBe(false); + } + }); }); From 2bae5a534e1921c4e8a63407e88816a918824448 Mon Sep 17 00:00:00 2001 From: Lalit Sharma Date: Fri, 27 Feb 2026 07:53:25 +0000 Subject: [PATCH 2/2] chore: bump mobile version to 1.1.38 and update changelog --- CHANGELOG.md | 12 ++++++++++++ apps/mobile/package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c14b4..c6180cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c14bb52..dd2009e 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@eclipse-timer/mobile", - "version": "1.1.37", + "version": "1.1.38", "private": true, "main": "index.js", "scripts": {