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>
106 lines
3.5 KiB
Swift
106 lines
3.5 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
}
|