App/Sources/StoryScreen.swift
70 lines · 2863 bytes
2 symbols in this file
1import Library
2import SkyRenderer
3import SwiftUI
4
5/// A mission's chapters on the orrery. Moving between chapters runs the clock so the craft travels.
6struct StoryScreen: View {
7 var story: MissionStory
8 @State private var chapter = 0
9 @State private var date: Date
10 @State private var travel: Task<Void, Never>?
11
12 private let ink = Color(red: 0.95, green: 0.95, blue: 0.93)
13 private let accent = Color(red: 0.88, green: 0.24, blue: 0.19)
14
15 init(story: MissionStory) {
16 self.story = story
17 _date = State(initialValue: story.chapters[0].day)
18 }
19
20 var body: some View {
21 let c = story.chapters[chapter]
22 ScrollView {
23 VStack(alignment: .leading, spacing: 12) {
24 OrreryView(date: date, ink: ink, accent: accent, highlight: story.spacecraft)
25 .dynamicTypeSize(...DynamicTypeSize.xxLarge)
26 .accessibilityHidden(true)
27 Text(date.formatted(date: .long, time: .omitted)).font(.subheadline.monospacedDigit()).dimmed(0.7)
28 Text(c.title).font(.title2.weight(.medium))
29 Text(c.text).font(.body).fixedSize(horizontal: false, vertical: true)
30 HStack {
31 Button { go(to: chapter - 1) } label: { Label("Back", systemImage: "chevron.left") }
32 .disabled(chapter == 0)
33 Spacer()
34 Text("\(chapter + 1) of \(story.chapters.count)").font(.footnote).dimmed(0.6)
35 Spacer()
36 Button { go(to: chapter + 1) } label: { Label("Next", systemImage: "chevron.right").labelStyle(.titleAndIcon) }
37 .disabled(chapter == story.chapters.count - 1)
38 }
39 .font(.subheadline.weight(.medium))
40 .padding(.top, 8)
41 Text("Positions: NASA/JPL Horizons.").font(.caption2).dimmed(0.5).padding(.top, 8)
42 }
43 .padding(20)
44 .readableWidth()
45 }
46 .foregroundStyle(ink)
47 .tint(accent)
48 .background(Color.black.ignoresSafeArea())
49 .navigationTitle(story.spacecraft)
50 .navigationBarTitleDisplayMode(.inline)
51 .onDisappear { travel?.cancel() }
52 }
53
54 /// Moves to chapter `i`, running the date there over about 1.5 s.
55 private func go(to i: Int) {
56 let from = date, to = story.chapters[i].day
57 chapter = i
58 travel?.cancel()
59 travel = Task {
60 let frames = 45
61 for f in 1...frames {
62 try? await Task.sleep(for: .milliseconds(33))
63 if Task.isCancelled { return }
64 let t = Double(f) / Double(frames)
65 let eased = t * t * (3 - 2 * t)
66 date = from.addingTimeInterval(to.timeIntervalSince(from) * eased)
67 }
68 }
69 }
70}