// // MoonShadowShape.swift // Shared between MoonWatch and MoonWatchWidget. // import SwiftUI /// Geometric terminator: half-circle along the dark limb stitched to a /// half-ellipse whose horizontal axis = cos(2π·t)·R. Renders correctly at /// new (full dark), quarter (straight terminator), gibbous, and full /// (no shadow). Slight `oversizeFactor` lets a parent `Circle` clip keep /// the limb sharp while a small `.blur` softens the terminator. public struct MoonShadowShape: Shape { public var phaseFraction: Double public var oversizeFactor: CGFloat public init(phaseFraction: Double, oversizeFactor: CGFloat = 1.04) { self.phaseFraction = phaseFraction self.oversizeFactor = oversizeFactor } public var animatableData: Double { get { phaseFraction } set { phaseFraction = newValue } } public func path(in rect: CGRect) -> Path { let r = min(rect.width, rect.height) / 2 let outer = r * oversizeFactor let cx = rect.midX let cy = rect.midY let raw = phaseFraction.truncatingRemainder(dividingBy: 1) let u = raw < 0 ? raw + 1 : raw let cosT = CGFloat(cos(2 * Double.pi * u)) let waning = u > 0.5 let bulge: CGFloat = waning ? -cosT * outer : cosT * outer let kappa: CGFloat = 0.5522847498 var path = Path() let top = CGPoint(x: cx, y: cy - outer) path.move(to: top) path.addArc( center: CGPoint(x: cx, y: cy), radius: outer, startAngle: .degrees(270), endAngle: .degrees(90), clockwise: !waning ) path.addCurve( to: CGPoint(x: cx + bulge, y: cy), control1: CGPoint(x: cx + bulge * kappa, y: cy + outer), control2: CGPoint(x: cx + bulge, y: cy + outer * kappa) ) path.addCurve( to: top, control1: CGPoint(x: cx + bulge, y: cy - outer * kappa), control2: CGPoint(x: cx + bulge * kappa, y: cy - outer) ) path.closeSubpath() return path } }