Commit a542759a71
Verified · cmc
Layout: unified · split
App/Sources/Dimmed.swift added +21
| @@ -0,0 +1,21 @@ | ||
| 1 | import SwiftUI | |
| 2 | ||
| 3 | extension 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. | |
| 9 | struct 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 | ||
| 19 | extension 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 { | ||
| 30 | 30 | .buttonStyle(.plain) |
| 31 | 31 | } |
| 32 | 32 | 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) | |
| 34 | 34 | } else if error == nil { |
| 35 | 35 | ProgressView().tint(ink).padding(.top, 40).frame(maxWidth: .infinity) |
| 36 | 36 | } |
| @@ -57,16 +57,16 @@ struct LaunchesScreen: View { | ||
| 57 | 57 | private func row(_ l: Launch, index: Int) -> some View { |
| 58 | 58 | VStack(alignment: .leading, spacing: 6) { |
| 59 | 59 | 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) | |
| 61 | 61 | Text(l.net.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).hour().minute())) |
| 62 | .font(.caption2).opacity(0.7) | |
| 62 | .font(.caption2).dimmed(0.7) | |
| 63 | 63 | Spacer() |
| 64 | 64 | StatusTag(status: l.status, ink: ink, accent: accent) |
| 65 | 65 | } |
| 66 | 66 | Text(l.rocket).font(.title2.weight(.medium)) |
| 67 | 67 | if let m = l.mission { Text(m).font(.subheadline) } |
| 68 | 68 | HStack { |
| 69 | Text("\(l.provider) · \(l.location)").font(.caption2).opacity(0.7) | |
| 69 | Text("\(l.provider) · \(l.location)").font(.caption2).dimmed(0.7) | |
| 70 | 70 | Spacer() |
| 71 | 71 | Countdown(to: l.net).font(.footnote.weight(.medium).monospacedDigit()) |
| 72 | 72 | } |
| @@ -121,7 +121,7 @@ struct LaunchDetail: View { | ||
| 121 | 121 | VStack(alignment: .leading, spacing: 10) { |
| 122 | 122 | HStack { |
| 123 | 123 | 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) | |
| 125 | 125 | } |
| 126 | 126 | Text(launch.rocket).font(.title.weight(.medium)) |
| 127 | 127 | if let m = launch.mission { Text(m).font(.body) } |
| @@ -139,9 +139,9 @@ struct LaunchDetail: View { | ||
| 139 | 139 | } |
| 140 | 140 | .font(.subheadline) |
| 141 | 141 | 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) } | |
| 143 | 143 | 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) | |
| 145 | 145 | } |
| 146 | 146 | } |
| 147 | 147 | .padding(20) |
| @@ -200,7 +200,7 @@ struct LaunchDetail: View { | ||
| 200 | 200 | |
| 201 | 201 | private func fact(_ label: String, _ value: String) -> some View { |
| 202 | 202 | GridRow { |
| 203 | Text(label).opacity(0.6) | |
| 203 | Text(label).dimmed(0.6) | |
| 204 | 204 | Text(value) |
| 205 | 205 | } |
| 206 | 206 | } |
App/Sources/LibraryScreen.swift +10 −10
| @@ -19,7 +19,7 @@ struct LibraryScreen: View { | ||
| 19 | 19 | HStack { |
| 20 | 20 | Text(section.title).font(.body) |
| 21 | 21 | Spacer() |
| 22 | Text("\(pages.count)").font(.footnote.monospacedDigit()).opacity(0.5) | |
| 22 | Text("\(pages.count)").font(.footnote.monospacedDigit()).dimmed(0.5) | |
| 23 | 23 | } |
| 24 | 24 | } |
| 25 | 25 | .listRowBackground(pageBackground) |
| @@ -66,7 +66,7 @@ struct PageRow: View { | ||
| 66 | 66 | var body: some View { |
| 67 | 67 | VStack(alignment: .leading, spacing: 2) { |
| 68 | 68 | Text(page.title).font(.body) |
| 69 | Text(page.subtitle).font(.caption).opacity(0.7) | |
| 69 | Text(page.subtitle).font(.caption).dimmed(0.7) | |
| 70 | 70 | } |
| 71 | 71 | .padding(.vertical, 2) |
| 72 | 72 | } |
| @@ -78,9 +78,9 @@ struct LibraryPageView: View { | ||
| 78 | 78 | var body: some View { |
| 79 | 79 | ScrollView { |
| 80 | 80 | 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) | |
| 82 | 82 | 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) | |
| 84 | 84 | Rectangle().fill(accent).frame(width: 48, height: 5).padding(.top, 10).padding(.bottom, 16) |
| 85 | 85 | |
| 86 | 86 | PageDrawing(page: page, ink: ink, accent: accent) |
| @@ -108,7 +108,7 @@ struct LibraryPageView: View { | ||
| 108 | 108 | Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { |
| 109 | 109 | ForEach(page.facts, id: \.self) { f in |
| 110 | 110 | GridRow(alignment: .firstTextBaseline) { |
| 111 | Text(f.label).font(.footnote).opacity(0.6) | |
| 111 | Text(f.label).font(.footnote).dimmed(0.6) | |
| 112 | 112 | Text(f.value).font(.subheadline.monospacedDigit()) |
| 113 | 113 | } |
| 114 | 114 | .accessibilityElement(children: .combine) |
| @@ -122,23 +122,23 @@ struct LibraryPageView: View { | ||
| 122 | 122 | AsyncImage(url: image.url) { phase in |
| 123 | 123 | switch phase { |
| 124 | 124 | 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)) | |
| 126 | 126 | default: Color.black.frame(height: 200).overlay(ProgressView().tint(ink)) |
| 127 | 127 | } |
| 128 | 128 | } |
| 129 | Text(image.caption).font(.footnote).opacity(0.75) | |
| 129 | Text(image.caption).font(.footnote).dimmed(0.75) | |
| 130 | 130 | 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) | |
| 132 | 132 | } |
| 133 | 133 | } |
| 134 | 134 | } |
| 135 | 135 | |
| 136 | 136 | private var sources: some View { |
| 137 | 137 | 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) | |
| 139 | 139 | ForEach(Array(page.sources.enumerated()), id: \.offset) { i, s in |
| 140 | 140 | 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) | |
| 142 | 142 | } |
| 143 | 143 | } |
| 144 | 144 | } |
App/Sources/MoonriseApp.swift +2 −1
| @@ -1,3 +1,4 @@ | ||
| 1 | import SkyRenderer | |
| 1 | 2 | import SwiftUI |
| 2 | 3 | import UIKit |
| 3 | 4 | import UserNotifications |
| @@ -22,7 +23,7 @@ struct MoonriseApp: App { | ||
| 22 | 23 | Tab("Library", systemImage: "books.vertical", value: "library") { LibraryScreen() } |
| 23 | 24 | } |
| 24 | 25 | // 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) | |
| 26 | 27 | .preferredColorScheme(.dark) |
| 27 | 28 | .fullScreenCover(isPresented: .constant(!onboarded)) { |
| 28 | 29 | OnboardingScreen().preferredColorScheme(.dark) |
App/Sources/PageDrawing.swift +6 −6
| @@ -55,7 +55,7 @@ struct PageDrawing: View { | ||
| 55 | 55 | if page.id != "earth" { |
| 56 | 56 | let ec = CGPoint(x: size.width - er - 6, y: size.height - er - 16) |
| 57 | 57 | 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) | |
| 59 | 59 | } |
| 60 | 60 | } |
| 61 | 61 | .frame(height: 200) |
| @@ -121,7 +121,7 @@ struct PageDrawing: View { | ||
| 121 | 121 | return VStack(spacing: 8) { |
| 122 | 122 | Circle().fill(color).frame(width: 90, height: 90) |
| 123 | 123 | 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) | |
| 125 | 125 | } |
| 126 | 126 | } |
| 127 | 127 | .frame(height: 140) |
| @@ -151,8 +151,8 @@ struct PageDrawing: View { | ||
| 151 | 151 | context.fill(Path(ellipseIn: rect), with: .color(ink.opacity(0.3))) |
| 152 | 152 | } |
| 153 | 153 | 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)) | |
| 156 | 156 | } |
| 157 | 157 | .frame(height: 170) |
| 158 | 158 | .accessibilityLabel("\(page.title) apparent size compared with the full Moon") |
| @@ -164,11 +164,11 @@ struct PageDrawing: View { | ||
| 164 | 164 | let d = page.drawing! |
| 165 | 165 | return HStack(spacing: 10) { |
| 166 | 166 | Text((d.status ?? "").uppercased()) |
| 167 | .font(.system(size: 10, weight: .medium)).tracking(0.6) | |
| 167 | .font(.caption2.weight(.medium)).tracking(0.6) | |
| 168 | 168 | .padding(.horizontal, 6).padding(.vertical, 2) |
| 169 | 169 | .foregroundStyle(d.status == "active" ? Color.black : ink) |
| 170 | 170 | .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()) } | |
| 172 | 172 | Spacer() |
| 173 | 173 | } |
| 174 | 174 | } |
App/Sources/SkyScreen.swift +7 −5
| @@ -23,7 +23,7 @@ struct SkyScreen: View { | ||
| 23 | 23 | |
| 24 | 24 | private var night: Bool { nightMode == "on" || (nightMode == "auto" && model.sunAltitude < -12) } |
| 25 | 25 | 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) } | |
| 27 | 27 | |
| 28 | 28 | var body: some View { |
| 29 | 29 | GeometryReader { geo in |
| @@ -56,6 +56,7 @@ struct SkyScreen: View { | ||
| 56 | 56 | } |
| 57 | 57 | } |
| 58 | 58 | .foregroundStyle(ink) |
| 59 | .environment(\.nightMode, night) | |
| 59 | 60 | .sheet(isPresented: $showSettings) { |
| 60 | 61 | SettingsScreen { model.applyLocationSettings() } |
| 61 | 62 | .preferredColorScheme(.dark) |
| @@ -66,6 +67,7 @@ struct SkyScreen: View { | ||
| 66 | 67 | } |
| 67 | 68 | .sheet(isPresented: $showToday) { |
| 68 | 69 | TodaySheet(model: model, ink: ink, background: style.ground) |
| 70 | .environment(\.nightMode, night) | |
| 69 | 71 | .presentationDetents([.medium, .large]) |
| 70 | 72 | } |
| 71 | 73 | .onChange(of: model.offsetHours) { select(selection) } |
| @@ -126,11 +128,11 @@ struct SkyScreen: View { | ||
| 126 | 128 | Text(model.offsetHours == 0 ? "Tonight" : "Sky at \(model.displayedDate.formatted(date: .omitted, time: .shortened))") |
| 127 | 129 | .font(.title2.weight(.medium)) |
| 128 | 130 | Text(model.placeName) |
| 129 | .font(.caption2).opacity(0.7) | |
| 131 | .font(.caption2).dimmed(0.7) | |
| 130 | 132 | #if DEBUG |
| 131 | 133 | TimelineView(.periodic(from: .now, by: 1)) { _ in |
| 132 | 134 | Text(stats.summary(build: model.lastBuild)) |
| 133 | .font(.caption2.monospacedDigit()).opacity(0.7) | |
| 135 | .font(.caption2.monospacedDigit()).dimmed(0.7) | |
| 134 | 136 | } |
| 135 | 137 | #endif |
| 136 | 138 | } |
| @@ -177,7 +179,7 @@ struct SkyScreen: View { | ||
| 177 | 179 | case .body: |
| 178 | 180 | if let d = details { bodyFacts(d) } else { ProgressView().tint(ink) } |
| 179 | 181 | case .constellation(let id): |
| 180 | Text("Constellation · \(id)").font(.footnote).opacity(0.7) | |
| 182 | Text("Constellation · \(id)").font(.footnote).dimmed(0.7) | |
| 181 | 183 | } |
| 182 | 184 | if let page = libraryPage(for: object) { |
| 183 | 185 | Button { libraryPage = page } label: { |
| @@ -223,7 +225,7 @@ struct SkyScreen: View { | ||
| 223 | 225 | return Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 4) { |
| 224 | 226 | ForEach(rows, id: \.0) { row in |
| 225 | 227 | GridRow { |
| 226 | Text(row.0).opacity(0.7) | |
| 228 | Text(row.0).dimmed(0.7) | |
| 227 | 229 | Text(row.1).monospacedDigit() |
| 228 | 230 | } |
| 229 | 231 | .font(.footnote) |
App/Sources/SunScreen.swift +12 −12
| @@ -28,8 +28,8 @@ struct SunScreen: View { | ||
| 28 | 28 | } |
| 29 | 29 | .pickerStyle(.segmented) |
| 30 | 30 | .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) | |
| 33 | 33 | |
| 34 | 34 | section("Aurora") { auroraMap } |
| 35 | 35 | section("Solar flares, last 24 h") { flares } |
| @@ -38,7 +38,7 @@ struct SunScreen: View { | ||
| 38 | 38 | if failed { |
| 39 | 39 | Text("Couldn't load space weather. Pull to retry.").font(.footnote).foregroundStyle(accent).padding(.top, 12) |
| 40 | 40 | } |
| 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) | |
| 42 | 42 | } |
| 43 | 43 | .padding(.horizontal, 20) |
| 44 | 44 | } |
| @@ -64,7 +64,7 @@ struct SunScreen: View { | ||
| 64 | 64 | AsyncImage(url: wavelength.url(size: 1024)) { phase in |
| 65 | 65 | switch phase { |
| 66 | 66 | 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)) | |
| 68 | 68 | default: Color.black.overlay(ProgressView().tint(ink)) |
| 69 | 69 | } |
| 70 | 70 | } |
| @@ -75,7 +75,7 @@ struct SunScreen: View { | ||
| 75 | 75 | |
| 76 | 76 | private func section(_ title: String, @ViewBuilder content: () -> some View) -> some View { |
| 77 | 77 | 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) | |
| 79 | 79 | content() |
| 80 | 80 | } |
| 81 | 81 | .padding(.top, 24) |
| @@ -94,9 +94,9 @@ struct SunScreen: View { | ||
| 94 | 94 | .accessibilityElement() |
| 95 | 95 | .accessibilityLabel(auroraDescription(ovation)) |
| 96 | 96 | 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) | |
| 98 | 98 | } else { |
| 99 | Text("Loading").font(.footnote).opacity(0.6) | |
| 99 | Text("Loading").font(.footnote).dimmed(0.6) | |
| 100 | 100 | } |
| 101 | 101 | } |
| 102 | 102 | |
| @@ -127,7 +127,7 @@ struct SunScreen: View { | ||
| 127 | 127 | .foregroundStyle(c.0 == "C" ? ink.opacity(0.25) : accent.opacity(0.6)) |
| 128 | 128 | .lineStyle(StrokeStyle(lineWidth: 0.5, dash: [3, 3])) |
| 129 | 129 | .annotation(position: .top, alignment: .leading) { |
| 130 | Text(c.0).font(.caption2).opacity(0.7) | |
| 130 | Text(c.0).font(.caption2).dimmed(0.7) | |
| 131 | 131 | } |
| 132 | 132 | } |
| 133 | 133 | } |
| @@ -141,14 +141,14 @@ struct SunScreen: View { | ||
| 141 | 141 | } |
| 142 | 142 | .frame(height: 160) |
| 143 | 143 | } else { |
| 144 | Text("Loading").font(.footnote).opacity(0.6) | |
| 144 | Text("Loading").font(.footnote).dimmed(0.6) | |
| 145 | 145 | } |
| 146 | 146 | } |
| 147 | 147 | |
| 148 | 148 | @ViewBuilder |
| 149 | 149 | private var kpChart: some View { |
| 150 | 150 | if kp.isEmpty { |
| 151 | Text("Loading").font(.footnote).opacity(0.6) | |
| 151 | Text("Loading").font(.footnote).dimmed(0.6) | |
| 152 | 152 | } else { |
| 153 | 153 | Chart(kp) { bar in |
| 154 | 154 | BarMark(x: .value("Start", bar.start, unit: .hour), y: .value("Kp", bar.kp), width: .ratio(0.8)) |
| @@ -169,13 +169,13 @@ struct SunScreen: View { | ||
| 169 | 169 | } |
| 170 | 170 | .frame(height: 140) |
| 171 | 171 | Text("Solid: observed. Faint: forecast. Red: storm level (Kp 5 or more).") |
| 172 | .font(.caption2).opacity(0.6) | |
| 172 | .font(.caption2).dimmed(0.6) | |
| 173 | 173 | } |
| 174 | 174 | } |
| 175 | 175 | |
| 176 | 176 | private func stat(_ label: String, _ value: String) -> some View { |
| 177 | 177 | VStack(alignment: .leading, spacing: 2) { |
| 178 | Text(label).font(.caption2).opacity(0.6) | |
| 178 | Text(label).font(.caption2).dimmed(0.6) | |
| 179 | 179 | Text(value).font(.title3.weight(.medium).monospacedDigit()) |
| 180 | 180 | } |
| 181 | 181 | } |
App/Sources/SystemScreen.swift +48 −23
| @@ -7,6 +7,7 @@ struct SystemScreen: View { | ||
| 7 | 7 | @State private var offsetDays = 0.0 |
| 8 | 8 | @State private var playing = false |
| 9 | 9 | @State private var libraryPage: LibraryPage? |
| 10 | @Environment(\.dynamicTypeSize) private var typeSize | |
| 10 | 11 | |
| 11 | 12 | private let ink = Color(red: 0.95, green: 0.95, blue: 0.93) |
| 12 | 13 | private let accent = Color(red: 0.88, green: 0.24, blue: 0.19) |
| @@ -25,7 +26,9 @@ struct SystemScreen: View { | ||
| 25 | 26 | Text("Solar system").font(.title.weight(.medium)) |
| 26 | 27 | Rectangle().fill(accent).frame(width: 48, height: 5).padding(.top, 8).padding(.bottom, 16) |
| 27 | 28 | |
| 29 | // Labels in this dense diagram stop growing at xxLarge; the table below scales fully. | |
| 28 | 30 | OrreryView(date: date, ink: ink, accent: accent) |
| 31 | .dynamicTypeSize(...DynamicTypeSize.xxLarge) | |
| 29 | 32 | .accessibilityElement() |
| 30 | 33 | .accessibilityLabel("Orrery for \(date.formatted(.dateTime.year().month(.wide))). Planets and spacecraft distances are listed below.") |
| 31 | 34 | |
| @@ -36,7 +39,7 @@ struct SystemScreen: View { | ||
| 36 | 39 | .accessibilityLabel(playing ? "Pause" : "Play") |
| 37 | 40 | Text(date.formatted(.dateTime.year().month(.abbreviated))) |
| 38 | 41 | .font(.subheadline.weight(.medium).monospacedDigit()) |
| 39 | .frame(width: 80, alignment: .leading) | |
| 42 | .fixedSize() | |
| 40 | 43 | Slider(value: $offsetDays, in: range).tint(ink) |
| 41 | 44 | Button { playing = false; offsetDays = 0 } label: { |
| 42 | 45 | Text("Today").frame(minWidth: 44, minHeight: 44).contentShape(Rectangle()) |
| @@ -47,37 +50,45 @@ struct SystemScreen: View { | ||
| 47 | 50 | .padding(.top, 12) |
| 48 | 51 | |
| 49 | 52 | 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) | |
| 51 | 54 | |
| 52 | 55 | 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 | } | |
| 58 | 65 | } |
| 59 | .font(.caption2) | |
| 60 | ForEach(Spacecraft.all) { craft in | |
| 66 | } else { | |
| 67 | Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) { | |
| 61 | 68 | 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 | } | |
| 73 | 84 | } |
| 85 | .font(.subheadline) | |
| 74 | 86 | } |
| 75 | .font(.subheadline) | |
| 76 | 87 | } |
| 77 | 88 | } |
| 78 | 89 | } |
| 79 | 90 | 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) | |
| 81 | 92 | } |
| 82 | 93 | .padding(.horizontal, 20) |
| 83 | 94 | } |
| @@ -95,6 +106,20 @@ struct SystemScreen: View { | ||
| 95 | 106 | } |
| 96 | 107 | } |
| 97 | 108 | |
| 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 | ||
| 98 | 123 | /// One-way light time from the Sun's distance, as hours and minutes. |
| 99 | 124 | private func lightTime(au: Double) -> String { |
| 100 | 125 | let seconds = au * 499.004784 |
| @@ -104,7 +129,7 @@ struct SystemScreen: View { | ||
| 104 | 129 | |
| 105 | 130 | private func section(_ title: String, @ViewBuilder content: () -> some View) -> some View { |
| 106 | 131 | 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) | |
| 108 | 133 | content() |
| 109 | 134 | } |
| 110 | 135 | .padding(.top, 24) |
App/Sources/TodaySheet.swift +7 −7
| @@ -18,7 +18,7 @@ struct TodaySheet: View { | ||
| 18 | 18 | } |
| 19 | 19 | section("Space weather") { weather } |
| 20 | 20 | 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) | |
| 22 | 22 | } |
| 23 | 23 | .padding(.horizontal, 20) |
| 24 | 24 | .padding(.top, 18) |
| @@ -29,7 +29,7 @@ struct TodaySheet: View { | ||
| 29 | 29 | |
| 30 | 30 | private func section(_ title: String, @ViewBuilder content: () -> some View) -> some View { |
| 31 | 31 | 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) | |
| 33 | 33 | content() |
| 34 | 34 | } |
| 35 | 35 | .frame(maxWidth: .infinity, alignment: .leading) |
| @@ -86,7 +86,7 @@ struct TodaySheet: View { | ||
| 86 | 86 | ("Updated", time(w.observedAt)), |
| 87 | 87 | ]) |
| 88 | 88 | } else { |
| 89 | Text("Loading").font(.footnote).opacity(0.6) | |
| 89 | Text("Loading").font(.footnote).dimmed(0.6) | |
| 90 | 90 | } |
| 91 | 91 | } |
| 92 | 92 | |
| @@ -94,7 +94,7 @@ struct TodaySheet: View { | ||
| 94 | 94 | Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 6) { |
| 95 | 95 | ForEach(items, id: \.0) { item in |
| 96 | 96 | GridRow { |
| 97 | Text(item.0).opacity(0.7) | |
| 97 | Text(item.0).dimmed(0.7) | |
| 98 | 98 | Text(item.1).monospacedDigit() |
| 99 | 99 | } |
| 100 | 100 | .font(.subheadline) |
| @@ -121,9 +121,9 @@ struct TodayBar: View { | ||
| 121 | 121 | scrubber |
| 122 | 122 | Button(action: onExpand) { |
| 123 | 123 | HStack { |
| 124 | Text(summary).font(.footnote).opacity(0.8) | |
| 124 | Text(summary).font(.footnote).dimmed(0.8) | |
| 125 | 125 | 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) | |
| 127 | 127 | } |
| 128 | 128 | .frame(minHeight: 44) |
| 129 | 129 | .contentShape(Rectangle()) |
| @@ -157,7 +157,7 @@ struct TodayBar: View { | ||
| 157 | 157 | HStack(spacing: 12) { |
| 158 | 158 | Text(model.displayedDate.formatted(date: .omitted, time: .shortened)) |
| 159 | 159 | .font(.footnote.monospacedDigit()) |
| 160 | .frame(width: 70, alignment: .leading) | |
| 160 | .fixedSize() | |
| 161 | 161 | Slider(value: $model.offsetHours, in: -12...12, step: 0.25).tint(ink) |
| 162 | 162 | Button { model.offsetHours = 0 } label: { |
| 163 | 163 | 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 { | ||
| 100 | 100 | let r = CGFloat(s.radius(au)) |
| 101 | 101 | context.stroke(Path(ellipseIn: CGRect(x: c.x - r, y: c.y - r, width: 2 * r, height: 2 * r)), |
| 102 | 102 | 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)) | |
| 104 | 104 | } |
| 105 | 105 | for (_, au) in Self.orbits { |
| 106 | 106 | let r = CGFloat(s.radius(au)) |
| @@ -117,7 +117,7 @@ public struct OrreryView: View { | ||
| 117 | 117 | context.stroke(line, with: .color(accent.opacity(0.7)), lineWidth: 0.8) |
| 118 | 118 | let p = s.point(last) |
| 119 | 119 | 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) | |
| 121 | 121 | } |
| 122 | 122 | |
| 123 | 123 | for body in [SolarBody.mercury, .venus, .mars, .jupiter, .saturn, .uranus, .neptune] { |
| @@ -126,7 +126,7 @@ public struct OrreryView: View { | ||
| 126 | 126 | let r: CGFloat = [.jupiter, .saturn].contains(body) ? 4 : 3 |
| 127 | 127 | context.fill(Path(ellipseIn: CGRect(x: p.x - r, y: p.y - r, width: 2 * r, height: 2 * r)), with: .color(ink)) |
| 128 | 128 | 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)) | |
| 130 | 130 | } |
| 131 | 131 | } |
| 132 | 132 | if let earth = try? SkyEngine.heliocentricEarth(at: date) { |
Packages/MoonriseKit/Sources/SkyRenderer/PolarMap.swift +1 −1
| @@ -94,7 +94,7 @@ public struct AuroraMap: View { | ||
| 94 | 94 | |
| 95 | 95 | if let you = p.point(lon: observer.lon, lat: observer.lat) { |
| 96 | 96 | 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)) | |
| 98 | 98 | } |
| 99 | 99 | } |
| 100 | 100 | .aspectRatio(1, contentMode: .fit) |
Packages/MoonriseKit/Sources/SkyRenderer/SkyView.swift +13 −6
| @@ -45,15 +45,18 @@ public struct SkyStyle: Sendable { | ||
| 45 | 45 | |
| 46 | 46 | public init() {} |
| 47 | 47 | |
| 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 | ||
| 48 | 51 | /// Red on black, to keep night vision. |
| 49 | 52 | public static let night: SkyStyle = { |
| 50 | 53 | var s = SkyStyle() |
| 51 | let red = Color(red: 0.85, green: 0.12, blue: 0.08) | |
| 54 | let red = SkyStyle.nightRed | |
| 52 | 55 | s.background = .black |
| 53 | 56 | s.ground = Color(red: 0.06, green: 0, blue: 0) |
| 54 | 57 | s.line = red.opacity(0.35) |
| 55 | 58 | s.label = red |
| 56 | s.muted = red.opacity(0.7) | |
| 59 | s.muted = red.opacity(0.9) | |
| 57 | 60 | s.moonLight = red |
| 58 | 61 | s.moonDark = Color(red: 0.12, green: 0, blue: 0) |
| 59 | 62 | s.planet = red |
| @@ -135,11 +138,15 @@ public struct SkyView: View { | ||
| 135 | 138 | } else { |
| 136 | 139 | let r = max(2.0, 3.6 - 0.35 * b.magnitude) |
| 137 | 140 | 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 | |
| 138 | 145 | 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)) | |
| 141 | 148 | 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) | |
| 143 | 150 | } |
| 144 | 151 | } |
| 145 | 152 | } |
| @@ -172,7 +179,7 @@ public struct SkyView: View { | ||
| 172 | 179 | } |
| 173 | 180 | |
| 174 | 181 | 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) | |
| 176 | 183 | context.draw(t, at: point, anchor: anchor) |
| 177 | 184 | } |
| 178 | 185 | } |