Commit 94af22ba11
Verified · cmc
Layout: unified · split
Packages/MoonriseKit/Sources/SkyRenderer/SkyView.swift +30 −5
| @@ -67,14 +67,32 @@ public struct SkyStyle: Sendable { | |||
| 67 | return s | 67 | return s |
| 68 | }() | 68 | }() |
| 69 | 69 | ||
| 70 | /// Star tint from B−V colour index, kept pale. | 70 | /// Star colour for a tint from 0 (blue-white) to 1 (orange), kept pale. |
| 71 | func starColor(_ bv: Double) -> Color { | 71 | func starColor(tint t: Double) -> Color { |
| 72 | if let monochromeStars { return monochromeStars } | 72 | if let monochromeStars { return monochromeStars } |
| 73 | let t = min(max((bv + 0.3) / 2.0, 0), 1) | ||
| 74 | return Color(red: 0.80 + 0.20 * t, green: 0.86 - 0.06 * t, blue: 1.0 - 0.32 * t) | 73 | return Color(red: 0.80 + 0.20 * t, green: 0.86 - 0.06 * t, blue: 1.0 - 0.32 * t) |
| 75 | } | 74 | } |
| 76 | } | 75 | } |
| 77 | 76 | ||
| 77 | /// Stars are filled one path per bin: half a magnitude by four tints from B−V. | ||
| 78 | struct StarBin { | ||
| 79 | static let tints = 4 | ||
| 80 | /// Magnitudes −2 to 6.5. | ||
| 81 | static let count = 18 * tints | ||
| 82 | var index: Int | ||
| 83 | |||
| 84 | init(index: Int) { self.index = index } | ||
| 85 | |||
| 86 | init(magnitude: Double, colorIndex: Double) { | ||
| 87 | let m = min(max(Int(((magnitude + 2) * 2).rounded()), 0), 17) | ||
| 88 | let t = min(max(Int(((colorIndex + 0.3) / 2 * Double(Self.tints - 1)).rounded()), 0), Self.tints - 1) | ||
| 89 | index = m * Self.tints + t | ||
| 90 | } | ||
| 91 | |||
| 92 | var magnitude: Double { Double(index / Self.tints) / 2 - 2 } | ||
| 93 | var tint: Double { Double(index % Self.tints) / Double(Self.tints - 1) } | ||
| 94 | } | ||
| 95 | |||
| 78 | /// Something to draw attention to, e.g. the current stop of a guided tour. | 96 | /// Something to draw attention to, e.g. the current stop of a guided tour. |
| 79 | public enum SkyHighlight: Sendable, Equatable { | 97 | public enum SkyHighlight: Sendable, Equatable { |
| 80 | /// IAU abbreviation. | 98 | /// IAU abbreviation. |
| @@ -193,12 +211,19 @@ public struct SkyView: View { | |||
| 193 | } | 211 | } |
| 194 | } | 212 | } |
| 195 | 213 | ||
| 214 | /// Each star at its own radius; colour and opacity per `StarBin`, faintest bins first. | ||
| 196 | func drawStars(_ context: inout GraphicsContext, _ p: Projection) { | 215 | func drawStars(_ context: inout GraphicsContext, _ p: Projection) { |
| 216 | var bins = [Path](repeating: Path(), count: StarBin.count) | ||
| 197 | for s in scene.stars where s.position.altitude > 0 { | 217 | for s in scene.stars where s.position.altitude > 0 { |
| 198 | guard let pt = p.point(s.position) else { continue } | 218 | guard let pt = p.point(s.position) else { continue } |
| 199 | let r = max(0.3, 2.6 - 0.42 * s.star.magnitude) | 219 | let r = max(0.3, 2.6 - 0.42 * s.star.magnitude) |
| 200 | let opacity = min(1, max(0.25, 1.15 - 0.13 * s.star.magnitude)) | 220 | bins[StarBin(magnitude: s.star.magnitude, colorIndex: s.star.colorIndex).index] |
| 201 | context.fill(Path(ellipseIn: CGRect(x: pt.x - r, y: pt.y - r, width: 2 * r, height: 2 * r)), with: .color(style.starColor(s.star.colorIndex).opacity(opacity))) | 221 | .addEllipse(in: CGRect(x: pt.x - r, y: pt.y - r, width: 2 * r, height: 2 * r)) |
| 222 | } | ||
| 223 | for i in bins.indices.reversed() where !bins[i].isEmpty { | ||
| 224 | let bin = StarBin(index: i) | ||
| 225 | let opacity = min(1, max(0.25, 1.15 - 0.13 * bin.magnitude)) | ||
| 226 | context.fill(bins[i], with: .color(style.starColor(tint: bin.tint).opacity(opacity))) | ||
| 202 | } | 227 | } |
| 203 | } | 228 | } |
| 204 | 229 | ||
Packages/MoonriseKit/Tests/SkyRendererTests/SkyRenderTests.swift +12
| @@ -56,6 +56,18 @@ let evening = Date(timeIntervalSince1970: 1_792_202_400) // 2026-10-17T02:00:00Z | |||
| 56 | #expect(scene.object(at: a, projection: p) == .constellation("Aql")) | 56 | #expect(scene.object(at: a, projection: p) == .constellation("Aql")) |
| 57 | } | 57 | } |
| 58 | 58 | ||
| 59 | @Test func starBinsStayCloseToEachStar() { | ||
| 60 | var used = Set<Int>() | ||
| 61 | for star in Catalog.stars where star.magnitude <= 6.5 { | ||
| 62 | let bin = StarBin(magnitude: star.magnitude, colorIndex: star.colorIndex) | ||
| 63 | #expect(abs(bin.magnitude - star.magnitude) <= 0.25, "HR \(star.hr)") | ||
| 64 | let tint = min(max((star.colorIndex + 0.3) / 2, 0), 1) | ||
| 65 | #expect(abs(bin.tint - tint) <= 1.0 / 6 + 1e-9, "HR \(star.hr)") | ||
| 66 | used.insert(bin.index) | ||
| 67 | } | ||
| 68 | #expect(used.allSatisfy { (0..<StarBin.count).contains($0) }) | ||
| 69 | } | ||
| 70 | |||
| 59 | @MainActor | 71 | @MainActor |
| 60 | @Test func nightStyleRenders() throws { | 72 | @Test func nightStyleRenders() throws { |
| 61 | let scene = try SkyScene(date: evening, observer: chicago) | 73 | let scene = try SkyScene(date: evening, observer: chicago) |