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:
92
MoonWatch/StarFieldView.swift
Normal file
92
MoonWatch/StarFieldView.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user