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>
98 lines
3.1 KiB
Swift
98 lines
3.1 KiB
Swift
//
|
|
// MoonDiskView.swift
|
|
// MoonWatch
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
private struct MoonSouthernHemisphereKey: EnvironmentKey {
|
|
static let defaultValue = false
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
/// When true, waxing/waning orientation matches southern-sky convention (mirrored).
|
|
var moonSouthernHemisphere: Bool {
|
|
get { self[MoonSouthernHemisphereKey.self] }
|
|
set { self[MoonSouthernHemisphereKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
/// In-app moon view: photographic disk + atmospheric halo, drop shadows,
|
|
/// and limb stroke. The image+shadow rendering lives in `MoonImageDisk`
|
|
/// (in `Shared/`) so the widget can reuse it.
|
|
struct MoonDiskView: View {
|
|
/// Synodic phase in `[0, 1)`; 0 = new, 0.5 = full.
|
|
var normalizedPhase: Double
|
|
@Environment(\.moonSouthernHemisphere) private var southernHemisphere
|
|
|
|
var body: some View {
|
|
GeometryReader { geo in
|
|
let side = min(geo.size.width, geo.size.height)
|
|
|
|
ZStack {
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [
|
|
Color(red: 0.98, green: 0.94, blue: 0.78).opacity(0.32),
|
|
Color(red: 0.55, green: 0.50, blue: 0.85).opacity(0.12),
|
|
Color.clear,
|
|
],
|
|
center: .center,
|
|
startRadius: side * 0.32,
|
|
endRadius: side * 0.95
|
|
)
|
|
)
|
|
.frame(width: side * 1.9, height: side * 1.9)
|
|
.blendMode(.screen)
|
|
.blur(radius: side * 0.05)
|
|
|
|
MoonImageDisk(
|
|
normalizedPhase: normalizedPhase,
|
|
southernHemisphere: southernHemisphere
|
|
)
|
|
.frame(width: side, height: side)
|
|
}
|
|
.shadow(color: Color(red: 0.98, green: 0.94, blue: 0.78).opacity(0.30),
|
|
radius: side * 0.10)
|
|
.shadow(color: Color(red: 0.45, green: 0.55, blue: 0.95).opacity(0.30),
|
|
radius: side * 0.22)
|
|
.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()
|
|
MoonDiskView(normalizedPhase: 0.12)
|
|
.frame(width: 280, height: 280)
|
|
}
|
|
}
|
|
|
|
#Preview("First Quarter") {
|
|
ZStack {
|
|
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
|
|
MoonDiskView(normalizedPhase: 0.25)
|
|
.frame(width: 280, height: 280)
|
|
}
|
|
}
|
|
|
|
#Preview("Full") {
|
|
ZStack {
|
|
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
|
|
MoonDiskView(normalizedPhase: 0.50)
|
|
.frame(width: 280, height: 280)
|
|
}
|
|
}
|
|
|
|
#Preview("Waning Gibbous") {
|
|
ZStack {
|
|
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
|
|
MoonDiskView(normalizedPhase: 0.62)
|
|
.frame(width: 280, height: 280)
|
|
}
|
|
}
|