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

Commit d75d05b065

d75d05b065710e481f34d56cf3585ad807c4c106

parent: 69c34c3ded

Unsigned

cmc <hello@cleberg.net> · 2026-10-18 06:51 UTC

Contrast and larger text (!20)

- Night red brightened to 6.2:1 on black; dimmed secondary text never below 90% in night mode (5.1:1) and undimmed with Increase Contrast. Day-mode dimmed text is 4.8:1 or better.
- Canvas labels (sky, orrery, aurora map, Library drawings) use text styles; edge labels flip; time labels fit; orrery labels capped at xxLarge with the spacecraft list stacking at accessibility sizes.

Checked in the simulator at the "accessibility large" text size with Increase Contrast on. Audit: 44 issues, of which the contrast flags are text under the translucent tab bar and the system Done button, and the Dynamic Type notes also appear on system controls.

Layout: unified · split

App/Sources/Dimmed.swift added +21
@@ -0,0 +1,21 @@
1import SwiftUI
2
3extension EnvironmentValues {
4 /// True while the Sky tab draws in night mode; dimmed text stays above 90% so red text keeps AA contrast.
5 @Entry var nightMode = false
6}
7
8/// Secondary text: dimmed by `opacity`, but never below 90% in night mode and not at all with Increase Contrast.
9struct Dimmed: ViewModifier {
10 var opacity: Double
11 @Environment(\.colorSchemeContrast) private var contrast
12 @Environment(\.nightMode) private var night
13
14 func body(content: Content) -> some View {
15 content.opacity(contrast == .increased ? 1 : night ? max(opacity, 0.9) : opacity)
16 }
17}
18
19extension View {
20 func dimmed(_ opacity: Double) -> some View { modifier(Dimmed(opacity: opacity)) }
21}
App/Sources/LaunchesScreen.swift +8 −8
@@ -30,7 +30,7 @@ struct LaunchesScreen: View {
3030 .buttonStyle(.plain)
3131 }
3232 Text("Launch data: The Space Devs, Launch Library 2.")
33 .font(.caption2).opacity(0.5).padding(.vertical, 16)
33 .font(.caption2).dimmed(0.5).padding(.vertical, 16)
3434 } else if error == nil {
3535 ProgressView().tint(ink).padding(.top, 40).frame(maxWidth: .infinity)
3636 }
@@ -57,16 +57,16 @@ struct LaunchesScreen: View {
5757 private func row(_ l: Launch, index: Int) -> some View {
5858 VStack(alignment: .leading, spacing: 6) {
5959 HStack(alignment: .firstTextBaseline) {
60 Text(String(format: "%02d", index)).font(.caption2.monospacedDigit()).opacity(0.5)
60 Text(String(format: "%02d", index)).font(.caption2.monospacedDigit()).dimmed(0.5)
6161 Text(l.net.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).hour().minute()))
62 .font(.caption2).opacity(0.7)
62 .font(.caption2).dimmed(0.7)
6363 Spacer()
6464 StatusTag(status: l.status, ink: ink, accent: accent)
6565 }
6666 Text(l.rocket).font(.title2.weight(.medium))
6767 if let m = l.mission { Text(m).font(.subheadline) }
6868 HStack {
69 Text("\(l.provider) · \(l.location)").font(.caption2).opacity(0.7)
69 Text("\(l.provider) · \(l.location)").font(.caption2).dimmed(0.7)
7070 Spacer()
7171 Countdown(to: l.net).font(.footnote.weight(.medium).monospacedDigit())
7272 }
@@ -121,7 +121,7 @@ struct LaunchDetail: View {
121121 VStack(alignment: .leading, spacing: 10) {
122122 HStack {
123123 StatusTag(status: launch.status, ink: ink, accent: accent)
124 Text(launch.statusName).font(.caption).opacity(0.7)
124 Text(launch.statusName).font(.caption).dimmed(0.7)
125125 }
126126 Text(launch.rocket).font(.title.weight(.medium))
127127 if let m = launch.mission { Text(m).font(.body) }
@@ -139,9 +139,9 @@ struct LaunchDetail: View {
139139 }
140140 .font(.subheadline)
141141 followButton.padding(.top, 8)
142 if let message { Text(message).font(.caption).opacity(0.7) }
142 if let message { Text(message).font(.caption).dimmed(0.7) }
143143 if let d = launch.missionDescription {
144 Text(d).font(.subheadline).opacity(0.85).padding(.top, 8)
144 Text(d).font(.subheadline).dimmed(0.85).padding(.top, 8)
145145 }
146146 }
147147 .padding(20)
@@ -200,7 +200,7 @@ struct LaunchDetail: View {
200200
201201 private func fact(_ label: String, _ value: String) -> some View {
202202 GridRow {
203 Text(label).opacity(0.6)
203 Text(label).dimmed(0.6)
204204 Text(value)
205205 }
206206 }
App/Sources/LibraryScreen.swift +10 −10
@@ -19,7 +19,7 @@ struct LibraryScreen: View {
1919 HStack {
2020 Text(section.title).font(.body)
2121 Spacer()
22 Text("\(pages.count)").font(.footnote.monospacedDigit()).opacity(0.5)
22 Text("\(pages.count)").font(.footnote.monospacedDigit()).dimmed(0.5)
2323 }
2424 }
2525 .listRowBackground(pageBackground)
@@ -66,7 +66,7 @@ struct PageRow: View {
6666 var body: some View {
6767 VStack(alignment: .leading, spacing: 2) {
6868 Text(page.title).font(.body)
69 Text(page.subtitle).font(.caption).opacity(0.7)
69 Text(page.subtitle).font(.caption).dimmed(0.7)
7070 }
7171 .padding(.vertical, 2)
7272 }
@@ -78,9 +78,9 @@ struct LibraryPageView: View {
7878 var body: some View {
7979 ScrollView {
8080 VStack(alignment: .leading, spacing: 0) {
81 Text(page.section.title).font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).opacity(0.6)
81 Text(page.section.title).font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).dimmed(0.6)
8282 Text(page.title).font(.largeTitle.weight(.medium)).padding(.top, 4)
83 Text(page.subtitle).font(.subheadline).opacity(0.75).padding(.top, 2)
83 Text(page.subtitle).font(.subheadline).dimmed(0.75).padding(.top, 2)
8484 Rectangle().fill(accent).frame(width: 48, height: 5).padding(.top, 10).padding(.bottom, 16)
8585
8686 PageDrawing(page: page, ink: ink, accent: accent)
@@ -108,7 +108,7 @@ struct LibraryPageView: View {
108108 Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) {
109109 ForEach(page.facts, id: \.self) { f in
110110 GridRow(alignment: .firstTextBaseline) {
111 Text(f.label).font(.footnote).opacity(0.6)
111 Text(f.label).font(.footnote).dimmed(0.6)
112112 Text(f.value).font(.subheadline.monospacedDigit())
113113 }
114114 .accessibilityElement(children: .combine)
@@ -122,23 +122,23 @@ struct LibraryPageView: View {
122122 AsyncImage(url: image.url) { phase in
123123 switch phase {
124124 case .success(let img): img.resizable().scaledToFit()
125 case .failure: Color.black.frame(height: 120).overlay(Text("Image unavailable").font(.caption).opacity(0.6))
125 case .failure: Color.black.frame(height: 120).overlay(Text("Image unavailable").font(.caption).dimmed(0.6))
126126 default: Color.black.frame(height: 200).overlay(ProgressView().tint(ink))
127127 }
128128 }
129 Text(image.caption).font(.footnote).opacity(0.75)
129 Text(image.caption).font(.footnote).dimmed(0.75)
130130 Link(destination: image.page) {
131 Text("\(image.credit) · \(image.licence)").font(.caption2).opacity(0.55)
131 Text("\(image.credit) · \(image.licence)").font(.caption2).dimmed(0.55)
132132 }
133133 }
134134 }
135135
136136 private var sources: some View {
137137 VStack(alignment: .leading, spacing: 6) {
138 Text("Sources").font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).opacity(0.6)
138 Text("Sources").font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).dimmed(0.6)
139139 ForEach(Array(page.sources.enumerated()), id: \.offset) { i, s in
140140 Link(destination: s.url) {
141 Text("\(i + 1). \(s.title)").font(.caption).underline().opacity(0.8)
141 Text("\(i + 1). \(s.title)").font(.caption).underline().dimmed(0.8)
142142 }
143143 }
144144 }
App/Sources/MoonriseApp.swift +2 −1
@@ -1,3 +1,4 @@
1import SkyRenderer
12import SwiftUI
23import UIKit
34import UserNotifications
@@ -22,7 +23,7 @@ struct MoonriseApp: App {
2223 Tab("Library", systemImage: "books.vertical", value: "library") { LibraryScreen() }
2324 }
2425 // Red tab highlight while the Sky tab is in night mode.
25 .tint(tab == "sky" && nightActive ? Color(red: 0.85, green: 0.12, blue: 0.08) : nil)
26 .tint(tab == "sky" && nightActive ? SkyStyle.nightRed : nil)
2627 .preferredColorScheme(.dark)
2728 .fullScreenCover(isPresented: .constant(!onboarded)) {
2829 OnboardingScreen().preferredColorScheme(.dark)
App/Sources/PageDrawing.swift +6 −6
@@ -55,7 +55,7 @@ struct PageDrawing: View {
5555 if page.id != "earth" {
5656 let ec = CGPoint(x: size.width - er - 6, y: size.height - er - 16)
5757 context.fill(Path(ellipseIn: CGRect(x: ec.x - er, y: ec.y - er, width: 2 * er, height: 2 * er)), with: .color(Color(red: 0.35, green: 0.6, blue: 0.95)))
58 context.draw(Text("Earth").font(.system(size: 9)).foregroundStyle(ink.opacity(0.6)), at: CGPoint(x: size.width - 2, y: size.height - 6), anchor: .trailing)
58 context.draw(Text("Earth").font(.caption2).foregroundStyle(ink.opacity(0.6)), at: CGPoint(x: size.width - 2, y: size.height - 6), anchor: .trailing)
5959 }
6060 }
6161 .frame(height: 200)
@@ -121,7 +121,7 @@ struct PageDrawing: View {
121121 return VStack(spacing: 8) {
122122 Circle().fill(color).frame(width: 90, height: 90)
123123 if let sp = page.drawing?.spectralType {
124 Text("Spectral type \(sp) · B−V \(String(format: "%.2f", bv))").font(.system(size: 11)).opacity(0.6)
124 Text("Spectral type \(sp) · B−V \(String(format: "%.2f", bv))").font(.caption2).opacity(0.6)
125125 }
126126 }
127127 .frame(height: 140)
@@ -151,8 +151,8 @@ struct PageDrawing: View {
151151 context.fill(Path(ellipseIn: rect), with: .color(ink.opacity(0.3)))
152152 }
153153 context.stroke(Path(ellipseIn: CGRect(x: m.x - mr, y: m.y - mr, width: 2 * mr, height: 2 * mr)), with: .color(ink.opacity(0.5)), style: StrokeStyle(lineWidth: 0.8, dash: [3, 3]))
154 context.draw(Text("\(Int(size.rounded()))′").font(.system(size: 10)).foregroundStyle(ink.opacity(0.7)), at: CGPoint(x: c.x, y: canvas.height - 10))
155 context.draw(Text("Full Moon").font(.system(size: 10)).foregroundStyle(ink.opacity(0.7)), at: CGPoint(x: m.x, y: canvas.height - 10))
154 context.draw(Text("\(Int(size.rounded()))′").font(.caption2).foregroundStyle(ink.opacity(0.7)), at: CGPoint(x: c.x, y: canvas.height - 10))
155 context.draw(Text("Full Moon").font(.caption2).foregroundStyle(ink.opacity(0.7)), at: CGPoint(x: m.x, y: canvas.height - 10))
156156 }
157157 .frame(height: 170)
158158 .accessibilityLabel("\(page.title) apparent size compared with the full Moon")
@@ -164,11 +164,11 @@ struct PageDrawing: View {
164164 let d = page.drawing!
165165 return HStack(spacing: 10) {
166166 Text((d.status ?? "").uppercased())
167 .font(.system(size: 10, weight: .medium)).tracking(0.6)
167 .font(.caption2.weight(.medium)).tracking(0.6)
168168 .padding(.horizontal, 6).padding(.vertical, 2)
169169 .foregroundStyle(d.status == "active" ? Color.black : ink)
170170 .background(d.status == "active" ? ink : ink.opacity(0.15), in: .rect(cornerRadius: 2))
171 if let launch = d.launch { Text("Launched \(launch)").font(.system(size: 13).monospacedDigit()) }
171 if let launch = d.launch { Text("Launched \(launch)").font(.footnote.monospacedDigit()) }
172172 Spacer()
173173 }
174174 }
App/Sources/SkyScreen.swift +7 −5
@@ -23,7 +23,7 @@ struct SkyScreen: View {
2323
2424 private var night: Bool { nightMode == "on" || (nightMode == "auto" && model.sunAltitude < -12) }
2525 private var style: SkyStyle { night ? .night : SkyStyle() }
26 private var ink: Color { night ? Color(red: 0.85, green: 0.12, blue: 0.08) : Color(red: 0.95, green: 0.95, blue: 0.93) }
26 private var ink: Color { night ? SkyStyle.nightRed : Color(red: 0.95, green: 0.95, blue: 0.93) }
2727
2828 var body: some View {
2929 GeometryReader { geo in
@@ -56,6 +56,7 @@ struct SkyScreen: View {
5656 }
5757 }
5858 .foregroundStyle(ink)
59 .environment(\.nightMode, night)
5960 .sheet(isPresented: $showSettings) {
6061 SettingsScreen { model.applyLocationSettings() }
6162 .preferredColorScheme(.dark)
@@ -66,6 +67,7 @@ struct SkyScreen: View {
6667 }
6768 .sheet(isPresented: $showToday) {
6869 TodaySheet(model: model, ink: ink, background: style.ground)
70 .environment(\.nightMode, night)
6971 .presentationDetents([.medium, .large])
7072 }
7173 .onChange(of: model.offsetHours) { select(selection) }
@@ -126,11 +128,11 @@ struct SkyScreen: View {
126128 Text(model.offsetHours == 0 ? "Tonight" : "Sky at \(model.displayedDate.formatted(date: .omitted, time: .shortened))")
127129 .font(.title2.weight(.medium))
128130 Text(model.placeName)
129 .font(.caption2).opacity(0.7)
131 .font(.caption2).dimmed(0.7)
130132 #if DEBUG
131133 TimelineView(.periodic(from: .now, by: 1)) { _ in
132134 Text(stats.summary(build: model.lastBuild))
133 .font(.caption2.monospacedDigit()).opacity(0.7)
135 .font(.caption2.monospacedDigit()).dimmed(0.7)
134136 }
135137 #endif
136138 }
@@ -177,7 +179,7 @@ struct SkyScreen: View {
177179 case .body:
178180 if let d = details { bodyFacts(d) } else { ProgressView().tint(ink) }
179181 case .constellation(let id):
180 Text("Constellation · \(id)").font(.footnote).opacity(0.7)
182 Text("Constellation · \(id)").font(.footnote).dimmed(0.7)
181183 }
182184 if let page = libraryPage(for: object) {
183185 Button { libraryPage = page } label: {
@@ -223,7 +225,7 @@ struct SkyScreen: View {
223225 return Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 4) {
224226 ForEach(rows, id: \.0) { row in
225227 GridRow {
226 Text(row.0).opacity(0.7)
228 Text(row.0).dimmed(0.7)
227229 Text(row.1).monospacedDigit()
228230 }
229231 .font(.footnote)
App/Sources/SunScreen.swift +12 −12
@@ -28,8 +28,8 @@ struct SunScreen: View {
2828 }
2929 .pickerStyle(.segmented)
3030 .padding(.top, 12)
31 Text(wavelength.caption).font(.footnote).opacity(0.75).padding(.top, 8)
32 Text(SolarImage.credit).font(.caption2).opacity(0.5).padding(.top, 4)
31 Text(wavelength.caption).font(.footnote).dimmed(0.75).padding(.top, 8)
32 Text(SolarImage.credit).font(.caption2).dimmed(0.5).padding(.top, 4)
3333
3434 section("Aurora") { auroraMap }
3535 section("Solar flares, last 24 h") { flares }
@@ -38,7 +38,7 @@ struct SunScreen: View {
3838 if failed {
3939 Text("Couldn't load space weather. Pull to retry.").font(.footnote).foregroundStyle(accent).padding(.top, 12)
4040 }
41 Text("Space weather data: NOAA Space Weather Prediction Center. Coastlines: Natural Earth.").font(.caption2).opacity(0.5).padding(.vertical, 16)
41 Text("Space weather data: NOAA Space Weather Prediction Center. Coastlines: Natural Earth.").font(.caption2).dimmed(0.5).padding(.vertical, 16)
4242 }
4343 .padding(.horizontal, 20)
4444 }
@@ -64,7 +64,7 @@ struct SunScreen: View {
6464 AsyncImage(url: wavelength.url(size: 1024)) { phase in
6565 switch phase {
6666 case .success(let img): img.resizable().scaledToFit()
67 case .failure: Color.black.overlay(Text("Image unavailable").font(.footnote).opacity(0.6))
67 case .failure: Color.black.overlay(Text("Image unavailable").font(.footnote).dimmed(0.6))
6868 default: Color.black.overlay(ProgressView().tint(ink))
6969 }
7070 }
@@ -75,7 +75,7 @@ struct SunScreen: View {
7575
7676 private func section(_ title: String, @ViewBuilder content: () -> some View) -> some View {
7777 VStack(alignment: .leading, spacing: 10) {
78 Text(title).font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).opacity(0.6)
78 Text(title).font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).dimmed(0.6)
7979 content()
8080 }
8181 .padding(.top, 24)
@@ -94,9 +94,9 @@ struct SunScreen: View {
9494 .accessibilityElement()
9595 .accessibilityLabel(auroraDescription(ovation))
9696 Text("Chance of visible aurora overhead, OVATION model, for \(ovation.forecastTime.formatted(date: .omitted, time: .shortened)). Brighter green is more likely.")
97 .font(.caption2).opacity(0.6)
97 .font(.caption2).dimmed(0.6)
9898 } else {
99 Text("Loading").font(.footnote).opacity(0.6)
99 Text("Loading").font(.footnote).dimmed(0.6)
100100 }
101101 }
102102
@@ -127,7 +127,7 @@ struct SunScreen: View {
127127 .foregroundStyle(c.0 == "C" ? ink.opacity(0.25) : accent.opacity(0.6))
128128 .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [3, 3]))
129129 .annotation(position: .top, alignment: .leading) {
130 Text(c.0).font(.caption2).opacity(0.7)
130 Text(c.0).font(.caption2).dimmed(0.7)
131131 }
132132 }
133133 }
@@ -141,14 +141,14 @@ struct SunScreen: View {
141141 }
142142 .frame(height: 160)
143143 } else {
144 Text("Loading").font(.footnote).opacity(0.6)
144 Text("Loading").font(.footnote).dimmed(0.6)
145145 }
146146 }
147147
148148 @ViewBuilder
149149 private var kpChart: some View {
150150 if kp.isEmpty {
151 Text("Loading").font(.footnote).opacity(0.6)
151 Text("Loading").font(.footnote).dimmed(0.6)
152152 } else {
153153 Chart(kp) { bar in
154154 BarMark(x: .value("Start", bar.start, unit: .hour), y: .value("Kp", bar.kp), width: .ratio(0.8))
@@ -169,13 +169,13 @@ struct SunScreen: View {
169169 }
170170 .frame(height: 140)
171171 Text("Solid: observed. Faint: forecast. Red: storm level (Kp 5 or more).")
172 .font(.caption2).opacity(0.6)
172 .font(.caption2).dimmed(0.6)
173173 }
174174 }
175175
176176 private func stat(_ label: String, _ value: String) -> some View {
177177 VStack(alignment: .leading, spacing: 2) {
178 Text(label).font(.caption2).opacity(0.6)
178 Text(label).font(.caption2).dimmed(0.6)
179179 Text(value).font(.title3.weight(.medium).monospacedDigit())
180180 }
181181 }
App/Sources/SystemScreen.swift +48 −23
@@ -7,6 +7,7 @@ struct SystemScreen: View {
77 @State private var offsetDays = 0.0
88 @State private var playing = false
99 @State private var libraryPage: LibraryPage?
10 @Environment(\.dynamicTypeSize) private var typeSize
1011
1112 private let ink = Color(red: 0.95, green: 0.95, blue: 0.93)
1213 private let accent = Color(red: 0.88, green: 0.24, blue: 0.19)
@@ -25,7 +26,9 @@ struct SystemScreen: View {
2526 Text("Solar system").font(.title.weight(.medium))
2627 Rectangle().fill(accent).frame(width: 48, height: 5).padding(.top, 8).padding(.bottom, 16)
2728
29 // Labels in this dense diagram stop growing at xxLarge; the table below scales fully.
2830 OrreryView(date: date, ink: ink, accent: accent)
31 .dynamicTypeSize(...DynamicTypeSize.xxLarge)
2932 .accessibilityElement()
3033 .accessibilityLabel("Orrery for \(date.formatted(.dateTime.year().month(.wide))). Planets and spacecraft distances are listed below.")
3134
@@ -36,7 +39,7 @@ struct SystemScreen: View {
3639 .accessibilityLabel(playing ? "Pause" : "Play")
3740 Text(date.formatted(.dateTime.year().month(.abbreviated)))
3841 .font(.subheadline.weight(.medium).monospacedDigit())
39 .frame(width: 80, alignment: .leading)
42 .fixedSize()
4043 Slider(value: $offsetDays, in: range).tint(ink)
4144 Button { playing = false; offsetDays = 0 } label: {
4245 Text("Today").frame(minWidth: 44, minHeight: 44).contentShape(Rectangle())
@@ -47,37 +50,45 @@ struct SystemScreen: View {
4750 .padding(.top, 12)
4851
4952 Text("Distances beyond 1 AU are drawn on a logarithmic scale. Rings: planet orbits, then 50, 100 and 200 AU.")
50 .font(.caption2).opacity(0.6).padding(.top, 8)
53 .font(.caption2).dimmed(0.6).padding(.top, 8)
5154
5255 section("Spacecraft") {
53 Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) {
54 GridRow {
55 Text("").gridColumnAlignment(.leading)
56 Text("From Sun").opacity(0.6)
57 Text("Light time").opacity(0.6)
56 if typeSize.isAccessibilitySize {
57 VStack(alignment: .leading, spacing: 12) {
58 ForEach(Spacecraft.all) { craft in
59 VStack(alignment: .leading, spacing: 2) {
60 craftName(craft)
61 Text(craftDistance(craft)).monospacedDigit().dimmed(0.7)
62 }
63 .font(.subheadline)
64 }
5865 }
59 .font(.caption2)
60 ForEach(Spacecraft.all) { craft in
66 } else {
67 Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) {
6168 GridRow {
62 if let page = Library.page(spacecraft: craft.name) {
63 Button { libraryPage = page } label: { Text(craft.name).underline().frame(minHeight: 44, alignment: .leading).contentShape(Rectangle()) }.buttonStyle(.plain)
64 } else {
65 Text(craft.name)
66 }
67 if let d = craft.distance(at: date) {
68 Text(String(format: "%.1f AU", d)).monospacedDigit()
69 Text(lightTime(au: d)).monospacedDigit()
70 } else {
71 Text("launch \(craft.launch.prefix(4))").opacity(0.5)
72 Text("")
69 Text("").gridColumnAlignment(.leading)
70 Text("From Sun").dimmed(0.6)
71 Text("Light time").dimmed(0.6)
72 }
73 .font(.caption2)
74 ForEach(Spacecraft.all) { craft in
75 GridRow {
76 craftName(craft)
77 if let d = craft.distance(at: date) {
78 Text(String(format: "%.1f AU", d)).monospacedDigit()
79 Text(lightTime(au: d)).monospacedDigit()
80 } else {
81 Text("launch \(craft.launch.prefix(4))").dimmed(0.5)
82 Text("")
83 }
7384 }
85 .font(.subheadline)
7486 }
75 .font(.subheadline)
7687 }
7788 }
7889 }
7990 Text("Spacecraft positions: NASA/JPL Horizons. Pioneer 10 and 11 positions after contact was lost are predictions.")
80 .font(.caption2).opacity(0.5).padding(.vertical, 16)
91 .font(.caption2).dimmed(0.5).padding(.vertical, 16)
8192 }
8293 .padding(.horizontal, 20)
8394 }
@@ -95,6 +106,20 @@ struct SystemScreen: View {
95106 }
96107 }
97108
109 @ViewBuilder
110 private func craftName(_ craft: Spacecraft) -> some View {
111 if let page = Library.page(spacecraft: craft.name) {
112 Button { libraryPage = page } label: { Text(craft.name).underline().frame(minHeight: 44, alignment: .leading).contentShape(Rectangle()) }.buttonStyle(.plain)
113 } else {
114 Text(craft.name)
115 }
116 }
117
118 private func craftDistance(_ craft: Spacecraft) -> String {
119 guard let d = craft.distance(at: date) else { return "launch \(craft.launch.prefix(4))" }
120 return String(format: "%.1f AU from the Sun · light time %@", d, lightTime(au: d))
121 }
122
98123 /// One-way light time from the Sun's distance, as hours and minutes.
99124 private func lightTime(au: Double) -> String {
100125 let seconds = au * 499.004784
@@ -104,7 +129,7 @@ struct SystemScreen: View {
104129
105130 private func section(_ title: String, @ViewBuilder content: () -> some View) -> some View {
106131 VStack(alignment: .leading, spacing: 10) {
107 Text(title).font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).opacity(0.6)
132 Text(title).font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).dimmed(0.6)
108133 content()
109134 }
110135 .padding(.top, 24)
App/Sources/TodaySheet.swift +7 −7
@@ -18,7 +18,7 @@ struct TodaySheet: View {
1818 }
1919 section("Space weather") { weather }
2020 Text("Space weather data: NOAA Space Weather Prediction Center.")
21 .font(.caption2).opacity(0.5).padding(.top, 8)
21 .font(.caption2).dimmed(0.5).padding(.top, 8)
2222 }
2323 .padding(.horizontal, 20)
2424 .padding(.top, 18)
@@ -29,7 +29,7 @@ struct TodaySheet: View {
2929
3030 private func section(_ title: String, @ViewBuilder content: () -> some View) -> some View {
3131 VStack(alignment: .leading, spacing: 8) {
32 Text(title).font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).opacity(0.6)
32 Text(title).font(.caption2.weight(.medium)).textCase(.uppercase).tracking(0.8).dimmed(0.6)
3333 content()
3434 }
3535 .frame(maxWidth: .infinity, alignment: .leading)
@@ -86,7 +86,7 @@ struct TodaySheet: View {
8686 ("Updated", time(w.observedAt)),
8787 ])
8888 } else {
89 Text("Loading").font(.footnote).opacity(0.6)
89 Text("Loading").font(.footnote).dimmed(0.6)
9090 }
9191 }
9292
@@ -94,7 +94,7 @@ struct TodaySheet: View {
9494 Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) {
9595 ForEach(items, id: \.0) { item in
9696 GridRow {
97 Text(item.0).opacity(0.7)
97 Text(item.0).dimmed(0.7)
9898 Text(item.1).monospacedDigit()
9999 }
100100 .font(.subheadline)
@@ -121,9 +121,9 @@ struct TodayBar: View {
121121 scrubber
122122 Button(action: onExpand) {
123123 HStack {
124 Text(summary).font(.footnote).opacity(0.8)
124 Text(summary).font(.footnote).dimmed(0.8)
125125 Spacer()
126 Image(systemName: "chevron.up").font(.caption.weight(.medium)).opacity(0.6)
126 Image(systemName: "chevron.up").font(.caption.weight(.medium)).dimmed(0.6)
127127 }
128128 .frame(minHeight: 44)
129129 .contentShape(Rectangle())
@@ -157,7 +157,7 @@ struct TodayBar: View {
157157 HStack(spacing: 12) {
158158 Text(model.displayedDate.formatted(date: .omitted, time: .shortened))
159159 .font(.footnote.monospacedDigit())
160 .frame(width: 70, alignment: .leading)
160 .fixedSize()
161161 Slider(value: $model.offsetHours, in: -12...12, step: 0.25).tint(ink)
162162 Button { model.offsetHours = 0 } label: {
163163 Text("Now").frame(minWidth: 44, minHeight: 44).contentShape(Rectangle())
Packages/MoonriseKit/Sources/SkyRenderer/Orrery.swift +3 −3
@@ -100,7 +100,7 @@ public struct OrreryView: View {
100100 let r = CGFloat(s.radius(au))
101101 context.stroke(Path(ellipseIn: CGRect(x: c.x - r, y: c.y - r, width: 2 * r, height: 2 * r)),
102102 with: .color(ink.opacity(0.12)), style: StrokeStyle(lineWidth: 0.5, dash: [2, 4]))
103 context.draw(Text("\(Int(au)) AU").font(.system(size: 9)).foregroundStyle(ink.opacity(0.4)), at: CGPoint(x: c.x, y: c.y - r - 7))
103 context.draw(Text("\(Int(au)) AU").font(.caption2).foregroundStyle(ink.opacity(0.6)), at: CGPoint(x: c.x, y: c.y - r - 7))
104104 }
105105 for (_, au) in Self.orbits {
106106 let r = CGFloat(s.radius(au))
@@ -117,7 +117,7 @@ public struct OrreryView: View {
117117 context.stroke(line, with: .color(accent.opacity(0.7)), lineWidth: 0.8)
118118 let p = s.point(last)
119119 context.fill(Path(ellipseIn: CGRect(x: p.x - 2.5, y: p.y - 2.5, width: 5, height: 5)), with: .color(accent))
120 context.draw(Text(craft.name).font(.system(size: 9)).foregroundStyle(accent), at: CGPoint(x: p.x + 6, y: p.y), anchor: .leading)
120 context.draw(Text(craft.name).font(.caption2).foregroundStyle(accent), at: CGPoint(x: p.x + 6, y: p.y), anchor: .leading)
121121 }
122122
123123 for body in [SolarBody.mercury, .venus, .mars, .jupiter, .saturn, .uranus, .neptune] {
@@ -126,7 +126,7 @@ public struct OrreryView: View {
126126 let r: CGFloat = [.jupiter, .saturn].contains(body) ? 4 : 3
127127 context.fill(Path(ellipseIn: CGRect(x: p.x - r, y: p.y - r, width: 2 * r, height: 2 * r)), with: .color(ink))
128128 if [.jupiter, .saturn, .uranus, .neptune].contains(body) {
129 context.draw(Text(body.rawValue.capitalized).font(.system(size: 9)).foregroundStyle(ink.opacity(0.8)), at: CGPoint(x: p.x, y: p.y + 10))
129 context.draw(Text(body.rawValue.capitalized).font(.caption2).foregroundStyle(ink.opacity(0.8)), at: CGPoint(x: p.x, y: p.y + 10))
130130 }
131131 }
132132 if let earth = try? SkyEngine.heliocentricEarth(at: date) {
Packages/MoonriseKit/Sources/SkyRenderer/PolarMap.swift +1 −1
@@ -94,7 +94,7 @@ public struct AuroraMap: View {
9494
9595 if let you = p.point(lon: observer.lon, lat: observer.lat) {
9696 context.fill(Path(ellipseIn: CGRect(x: you.x - 4, y: you.y - 4, width: 8, height: 8)), with: .color(ink))
97 context.draw(Text("You").font(.system(size: 10)).foregroundStyle(ink), at: CGPoint(x: you.x, y: you.y + 12))
97 context.draw(Text("You").font(.caption2).foregroundStyle(ink), at: CGPoint(x: you.x, y: you.y + 12))
9898 }
9999 }
100100 .aspectRatio(1, contentMode: .fit)
Packages/MoonriseKit/Sources/SkyRenderer/SkyView.swift +13 −6
@@ -45,15 +45,18 @@ public struct SkyStyle: Sendable {
4545
4646 public init() {}
4747
48 /// Night-mode red: 6.2:1 against black, 5.1:1 at 90% opacity (WCAG AA needs 4.5:1).
49 public static let nightRed = Color(red: 1.0, green: 0.27, blue: 0.2)
50
4851 /// Red on black, to keep night vision.
4952 public static let night: SkyStyle = {
5053 var s = SkyStyle()
51 let red = Color(red: 0.85, green: 0.12, blue: 0.08)
54 let red = SkyStyle.nightRed
5255 s.background = .black
5356 s.ground = Color(red: 0.06, green: 0, blue: 0)
5457 s.line = red.opacity(0.35)
5558 s.label = red
56 s.muted = red.opacity(0.7)
59 s.muted = red.opacity(0.9)
5760 s.moonLight = red
5861 s.moonDark = Color(red: 0.12, green: 0, blue: 0)
5962 s.planet = red
@@ -135,11 +138,15 @@ public struct SkyView: View {
135138 } else {
136139 let r = max(2.0, 3.6 - 0.35 * b.magnitude)
137140 context.fill(Path(ellipseIn: CGRect(x: pt.x - r, y: pt.y - r, width: 2 * r, height: 2 * r)), with: .color(style.planet))
141 // Label to the right, or to the left when it would run off the edge (large text sizes).
142 let name = b.body.rawValue.capitalized
143 let width = context.resolve(Text(name).font(.caption2)).measure(in: p.size).width
144 let side: Double = pt.x + r + 18 + width > p.size.width ? -1 : 1
138145 var tick = Path()
139 tick.move(to: CGPoint(x: pt.x + r + 3, y: pt.y))
140 tick.addLine(to: CGPoint(x: pt.x + r + 15, y: pt.y))
146 tick.move(to: CGPoint(x: pt.x + side * (r + 3), y: pt.y))
147 tick.addLine(to: CGPoint(x: pt.x + side * (r + 15), y: pt.y))
141148 context.stroke(tick, with: .color(style.muted), lineWidth: 0.6)
142 label(&context, b.body.rawValue.capitalized, at: CGPoint(x: pt.x + r + 18, y: pt.y), anchor: .leading)
149 label(&context, name, at: CGPoint(x: pt.x + side * (r + 18), y: pt.y), anchor: side > 0 ? .leading : .trailing)
143150 }
144151 }
145152 }
@@ -172,7 +179,7 @@ public struct SkyView: View {
172179 }
173180
174181 func label(_ context: inout GraphicsContext, _ text: String, at point: CGPoint, anchor: UnitPoint, color: Color? = nil) {
175 let t = Text(text).font(.system(size: 11, weight: .regular)).foregroundStyle(color ?? style.label)
182 let t = Text(text).font(.caption2).foregroundStyle(color ?? style.label)
176183 context.draw(t, at: point, anchor: anchor)
177184 }
178185}