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:
105
Shared/MoonImageDisk.swift
Normal file
105
Shared/MoonImageDisk.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user