krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

App/Vision/MoonriseVisionApp.swift

04b2b1d914788057bcd20945b876847386eb8f65
moonrise/App/Vision/MoonriseVisionApp.swift history · blame · raw

103 lines · 4461 bytes

4 symbols in this file
  1import Library
  2import RealityKit
  3import SkyEngine
  4import SkyRenderer
  5import SwiftUI
  6
  7@main
  8struct MoonriseVisionApp: App {
  9    var body: some SwiftUI.Scene {
 10        WindowGroup { VisionHome() }
 11
 12        WindowGroup(id: "orrery") { OrreryVolume() }
 13            .windowStyle(.volumetric)
 14            .defaultSize(width: 0.8, height: 0.4, depth: 0.8, in: .meters)
 15
 16        ImmersiveSpace(id: "sky") { ImmersiveSky() }
 17    }
 18}
 19
 20/// Library globes, plus buttons for the orrery volume and the immersive sky.
 21struct VisionHome: View {
 22    @Environment(\.openWindow) private var openWindow
 23    @Environment(\.openImmersiveSpace) private var openImmersiveSpace
 24    @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
 25    @State private var immersed = false
 26
 27    private var bodies: [LibraryPage] {
 28        [LibrarySection.sun, .planets, .dwarfPlanets, .moons].flatMap { Library.pages(in: $0) }
 29    }
 30
 31    var body: some View {
 32        NavigationSplitView {
 33            List(bodies) { page in
 34                NavigationLink(page.title) { GlobeView(page: page, credit: GlobeMaps.credit[page.id]) }
 35            }
 36            .navigationTitle("Moonrise")
 37            .toolbar {
 38                ToolbarItem(placement: .bottomOrnament) {
 39                    HStack {
 40                        Button("Orrery") { openWindow(id: "orrery") }
 41                        Button(immersed ? "Leave the sky" : "Tonight's sky") {
 42                            Task {
 43                                if immersed { await dismissImmersiveSpace() } else { _ = await openImmersiveSpace(id: "sky") }
 44                                immersed.toggle()
 45                            }
 46                        }
 47                    }
 48                }
 49            }
 50        } detail: {
 51            Text("Choose a body to see it as a globe.").foregroundStyle(.secondary)
 52        }
 53    }
 54}
 55
 56/// Outside the view, where `Body` is SkyEngine's rather than the view's body type.
 57private let orreryPlanets = Body.planets
 58
 59/// The Sun and planets at today's heliocentric positions. Distances are square-rooted so Neptune fits.
 60struct OrreryVolume: View {
 61    var body: some View {
 62        RealityView { content in
 63            let sun = ModelEntity(mesh: .generateSphere(radius: 0.025), materials: [UnlitMaterial(color: .yellow)])
 64            content.add(sun)
 65            for body in orreryPlanets {
 66                guard let p = try? SkyEngine.heliocentricEcliptic(body, at: .now) else { continue }
 67                let au = (p * p).sum().squareRoot()
 68                let scale = 0.065 * au.squareRoot() / au
 69                let page = Library.page(body: body.rawValue)
 70                let colour = UIColor(Color(hex: page?.drawing?.color) ?? .white)
 71                let planet = ModelEntity(mesh: .generateSphere(radius: 0.008), materials: [SimpleMaterial(color: colour, isMetallic: false)])
 72                // Ecliptic x, y in the horizontal plane; ecliptic north up.
 73                planet.position = [Float(p.x * scale), Float(p.z * scale), Float(-p.y * scale)]
 74                content.add(planet)
 75            }
 76        }
 77    }
 78}
 79
 80/// Stars brighter than magnitude 4.5 on a 20 m sphere, placed for now from Chicago,
 81/// with the horizon as a faint disc.
 82struct ImmersiveSky: View {
 83    var body: some View {
 84        RealityView { content in
 85            let observer = Observer(latitude: 41.8781, longitude: -87.6298)
 86            let stars = Catalog.stars.filter { $0.magnitude < 4.5 }
 87            let positions = SkyEngine.horizontal(j2000: stars.map { $0.position(at: .now) }, at: .now, from: observer)
 88            let mesh = MeshResource.generateSphere(radius: 0.04)
 89            for (star, h) in zip(stars, positions) where h.altitude > -2 {
 90                let alt = Float(h.altitude * .pi / 180), az = Float(h.azimuth * .pi / 180)
 91                let r: Float = 20
 92                let s = ModelEntity(mesh: mesh, materials: [UnlitMaterial(color: .white)])
 93                s.scale = .init(repeating: max(0.3, Float(2.2 - 0.4 * star.magnitude)))
 94                // Azimuth from north (−z) towards east (+x).
 95                s.position = [r * cos(alt) * sin(az), r * sin(alt), -r * cos(alt) * cos(az)]
 96                content.add(s)
 97            }
 98            let ground = ModelEntity(mesh: .generatePlane(width: 40, depth: 40), materials: [UnlitMaterial(color: UIColor(white: 0.05, alpha: 0.9))])
 99            ground.position.y = -0.01
100            content.add(ground)
101        }
102    }
103}