// // 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) } }