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 82b363a829

82b363a829582c2296a60f5791c29b1538333683

parent: 5a2ad3b532

Unsigned

cmc <hello@cleberg.net> · 2026-10-19 05:47 UTC

Scale of the universe: zoom buttons (!47)

- Large + and − buttons either side of the slider zoom in or out by a factor of ten per tap, snapping to whole powers of ten. The slider and pinch still work.
- Discs use the page's colour (planets, moons, the Sun) instead of grey.

Layout: unified · split

App/Sources/ScaleScreen.swift +23 −7
@@ -15,6 +15,22 @@ struct ScaleScreen: View {
15 15
16 private var width: Double { pow(10, exponent) } 16 private var width: Double { pow(10, exponent) }
17 17
18 /// Zooms by a factor of ten per tap, snapping to the nearest whole power of ten first.
19 private func zoomButton(_ symbol: String, _ label: String, by step: Double) -> some View {
20 Button {
21 withAnimation(.easeInOut(duration: 0.35)) {
22 let snapped = step < 0 ? ceil(exponent) - 1 : floor(exponent) + 1
23 exponent = min(max(snapped, range.lowerBound), range.upperBound)
24 }
25 } label: {
26 Image(systemName: symbol).font(.title3).frame(width: 48, height: 48)
27 .background(ink.opacity(0.1), in: Circle())
28 }
29 .buttonStyle(.plain)
30 .accessibilityLabel(label)
31 .disabled(step < 0 ? exponent <= range.lowerBound : exponent >= range.upperBound)
32 }
33
18 /// The width to two significant figures, for the label. 34 /// The width to two significant figures, for the label.
19 private var roundedWidth: Double { 35 private var roundedWidth: Double {
20 let p = pow(10, floor(exponent) - 1) 36 let p = pow(10, floor(exponent) - 1)
@@ -41,7 +57,7 @@ struct ScaleScreen: View {
41 selected = o.page.flatMap { Library.page(id: $0) } 57 selected = o.page.flatMap { Library.page(id: $0) }
42 } label: { 58 } label: {
43 VStack(spacing: 6) { 59 VStack(spacing: 6) {
44 Circle().fill(o.page == nil ? ink.opacity(0.18) : ink.opacity(0.55)) 60 Circle().fill(o.page == nil ? ink.opacity(0.18) : (Color(hex: o.page.flatMap { Library.page(id: $0) }?.drawing?.color) ?? ink.opacity(0.55)))
45 .overlay(Circle().stroke(ink.opacity(0.5), lineWidth: 0.5)) 61 .overlay(Circle().stroke(ink.opacity(0.5), lineWidth: 0.5))
46 .frame(width: d, height: d) 62 .frame(width: d, height: d)
47 Text(o.name).font(.caption2).lineLimit(1) 63 Text(o.name).font(.caption2).lineLimit(1)
@@ -67,13 +83,13 @@ struct ScaleScreen: View {
67 exponent = min(max(pinchStart! - log10(g.magnification), range.lowerBound), range.upperBound) 83 exponent = min(max(pinchStart! - log10(g.magnification), range.lowerBound), range.upperBound)
68 } 84 }
69 .onEnded { _ in pinchStart = nil }) 85 .onEnded { _ in pinchStart = nil })
70 Slider(value: $exponent, in: range) { Text("Width shown") } minimumValueLabel: { 86 HStack(spacing: 12) {
71 Image(systemName: "plus.magnifyingglass") 87 zoomButton("plus.magnifyingglass", "Zoom in", by: -1)
72 } maximumValueLabel: { 88 Slider(value: $exponent, in: range) { Text("Width shown") }
73 Image(systemName: "minus.magnifyingglass") 89 .accessibilityValue(Library.formatSize(roundedWidth))
90 zoomButton("minus.magnifyingglass", "Zoom out", by: 1)
74 } 91 }
75 .accessibilityValue(Library.formatSize(roundedWidth)) 92 Text("Each tap of + or − is a factor of ten. You can also drag the slider or pinch. Objects without a Library page are drawn faint.")
76 Text("The slider spans 300 m to the observable universe; each tenth of it is a factor of ten. Pinch to zoom. Objects without a Library page are drawn faint.")
77 .font(.caption2).dimmed(0.5) 93 .font(.caption2).dimmed(0.5)
78 } 94 }
79 .padding(20) 95 .padding(20)