Add MoonWatch app, widget extension, shared module, and .gitignore

Adds the real MoonWatch implementation on top of the initial Xcode
template: new SwiftUI views (MoonDiskView, StarFieldView), the
MoonWatchWidget extension, and a Shared module containing the moon
phase calculator, shadow shape, and shared asset catalog. Also adds a
Swift/Xcode/macOS .gitignore and untracks per-user xcuserdata files.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-07 21:05:16 -07:00
parent 1698e3ce15
commit 87f8c7173c
17 changed files with 1211 additions and 24 deletions

105
Shared/MoonImageDisk.swift Normal file
View File

@@ -0,0 +1,105 @@
//
// MoonImageDisk.swift
// Shared moon disk view used by both the app and widget.
//
// Renders the bundled "Moon" image masked to a circle, applies the
// geometric phase shadow (multiplied for soft Earthshine), a subtle
// spherical limb darkening, and a crisp limb stroke.
//
import SwiftUI
public struct MoonImageDisk: View {
public var normalizedPhase: Double
/// Mirror horizontally for southern hemisphere observers.
public var southernHemisphere: Bool
/// Scale the moon photo inside the circle (e.g. if the source has
/// a small black border around the disk).
public var imageScale: CGFloat
public init(
normalizedPhase: Double,
southernHemisphere: Bool = false,
imageScale: CGFloat = 1.30
) {
self.normalizedPhase = normalizedPhase
self.southernHemisphere = southernHemisphere
self.imageScale = imageScale
}
public var body: some View {
GeometryReader { geo in
let side = min(geo.size.width, geo.size.height)
ZStack {
Image("Moon", bundle: .main)
.resizable()
.interpolation(.high)
.aspectRatio(contentMode: .fill)
.frame(width: side, height: side)
.scaleEffect(imageScale)
MoonShadowShape(phaseFraction: normalizedPhase)
.fill(
RadialGradient(
colors: [
Color(red: 0.04, green: 0.04, blue: 0.10),
Color(red: 0.01, green: 0.01, blue: 0.04),
],
center: .center,
startRadius: 0,
endRadius: side * 0.6
)
)
.blur(radius: max(1.0, side * 0.018))
.blendMode(.multiply)
}
.frame(width: side, height: side)
.compositingGroup()
.clipShape(Circle())
.scaleEffect(x: southernHemisphere ? -1 : 1, y: 1)
.overlay {
Circle()
.strokeBorder(
LinearGradient(
colors: [
Color.white.opacity(0.40),
Color.white.opacity(0.05),
Color.black.opacity(0.30),
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: max(0.6, side * 0.005)
)
.frame(width: side, height: side)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.aspectRatio(1, contentMode: .fit)
}
}
#Preview("Waxing Crescent") {
ZStack {
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
MoonImageDisk(normalizedPhase: 0.12)
.frame(width: 280, height: 280)
}
}
#Preview("Waxing Gibbous") {
ZStack {
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
MoonImageDisk(normalizedPhase: 0.35)
.frame(width: 280, height: 280)
}
}
#Preview("Full") {
ZStack {
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
MoonImageDisk(normalizedPhase: 0.50)
.frame(width: 280, height: 280)
}
}