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>
93 lines
3.3 KiB
Swift
93 lines
3.3 KiB
Swift
//
|
|
// StarFieldView.swift
|
|
// MoonWatch
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct StarFieldView: View {
|
|
var starCount: Int = 160
|
|
|
|
@State private var stars: [Star] = []
|
|
|
|
private struct Star: Identifiable {
|
|
let id = UUID()
|
|
let position: CGPoint // 0...1 normalized
|
|
let radius: CGFloat
|
|
let baseOpacity: Double
|
|
let twinklePhase: Double
|
|
let twinkleSpeed: Double
|
|
let warm: Bool
|
|
}
|
|
|
|
var body: some View {
|
|
GeometryReader { geo in
|
|
TimelineView(.animation(minimumInterval: 1.0 / 12.0)) { ctx in
|
|
Canvas { context, size in
|
|
let t = ctx.date.timeIntervalSinceReferenceDate
|
|
for star in stars {
|
|
let twinkle = (sin(t * star.twinkleSpeed + star.twinklePhase) + 1) / 2
|
|
let alpha = star.baseOpacity * (0.35 + 0.65 * twinkle)
|
|
let center = CGPoint(
|
|
x: star.position.x * size.width,
|
|
y: star.position.y * size.height
|
|
)
|
|
let rect = CGRect(
|
|
x: center.x - star.radius,
|
|
y: center.y - star.radius,
|
|
width: star.radius * 2,
|
|
height: star.radius * 2
|
|
)
|
|
let color: Color = star.warm
|
|
? Color(red: 1.0, green: 0.92, blue: 0.78).opacity(alpha)
|
|
: Color(red: 0.92, green: 0.95, blue: 1.0).opacity(alpha)
|
|
context.fill(Path(ellipseIn: rect), with: .color(color))
|
|
if star.radius > 1.1 {
|
|
// Soft halo for the larger stars.
|
|
let halo = rect.insetBy(dx: -star.radius * 1.6, dy: -star.radius * 1.6)
|
|
context.fill(
|
|
Path(ellipseIn: halo),
|
|
with: .color(color.opacity(0.20))
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
if stars.isEmpty {
|
|
stars = Self.generate(count: starCount)
|
|
}
|
|
}
|
|
}
|
|
.ignoresSafeArea()
|
|
.allowsHitTesting(false)
|
|
}
|
|
|
|
private static func generate(count: Int) -> [Star] {
|
|
var rng = SystemRandomNumberGenerator()
|
|
return (0..<count).map { _ in
|
|
let big = Double.random(in: 0...1, using: &rng) > 0.88
|
|
return Star(
|
|
position: CGPoint(
|
|
x: CGFloat.random(in: 0...1, using: &rng),
|
|
y: CGFloat.random(in: 0...1, using: &rng)
|
|
),
|
|
radius: big
|
|
? CGFloat.random(in: 1.4...2.2, using: &rng)
|
|
: CGFloat.random(in: 0.4...1.1, using: &rng),
|
|
baseOpacity: Double.random(in: 0.35...0.95, using: &rng),
|
|
twinklePhase: Double.random(in: 0...(2 * .pi), using: &rng),
|
|
twinkleSpeed: Double.random(in: 0.5...1.7, using: &rng),
|
|
warm: Double.random(in: 0...1, using: &rng) > 0.75
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ZStack {
|
|
Color(red: 0.03, green: 0.04, blue: 0.10).ignoresSafeArea()
|
|
StarFieldView()
|
|
}
|
|
}
|