improve fox hunt compass

This commit is contained in:
Garth Vander Houwen 2026-04-20 13:51:06 -07:00
parent 704c08803c
commit 3a2a4833e1

View file

@ -36,21 +36,21 @@ struct FoxhuntCompassView: View {
let size = min(geometry.size.width, geometry.size.height)
let dialRadius = size * 0.44
VStack(spacing: 2) {
VStack(spacing: 0) {
// Node short name circle
WatchCircleText(
text: node.shortName.isEmpty ? "?" : node.shortName,
color: WatchCircleText.color(for: node.num),
circleSize: 32
circleSize: 26
)
ZStack {
// Fixed heading indicator at top
ZStack {
// Fixed heading indicator at top of ring
Image(systemName: "triangle.fill")
.font(.system(size: 8, weight: .bold))
.foregroundStyle(.primary)
.foregroundStyle(.white.opacity(0.9))
.rotationEffect(.degrees(180))
.offset(y: -(dialRadius + 12))
.offset(y: -(dialRadius + 6))
// Rotating compass group
ZStack {
@ -86,10 +86,19 @@ struct FoxhuntCompassView: View {
// Bearing arrow to target
if let bearing = bearingToNode() {
Image(systemName: "arrowtriangle.up.fill")
.font(.system(size: 12, weight: .bold))
// Directional cone showing general heading direction
DirectionCone(
bearing: bearing,
heading: locationManager.heading,
radius: dialRadius + 10,
color: distanceColor
)
Image(systemName: "location.north.fill")
.font(.system(size: 22, weight: .bold))
.foregroundStyle(distanceColor)
.offset(y: -(dialRadius + 8))
.shadow(color: distanceColor.opacity(0.8), radius: 6)
.offset(y: -(dialRadius + 20))
.rotationEffect(.degrees(bearing))
.onChange(of: locationManager.heading) {
checkAlignment(bearing: bearing, heading: locationManager.heading)
@ -98,7 +107,7 @@ struct FoxhuntCompassView: View {
}
.rotationEffect(.degrees(-locationManager.heading))
}
.frame(width: dialRadius * 2 + 30, height: dialRadius * 2 + 30)
.frame(width: dialRadius * 2 + 48, height: dialRadius * 2 + 48)
// Distance at bottom
if let dist = distanceToNode() {
@ -164,13 +173,13 @@ struct FoxhuntCompassView: View {
return node.distance(from: userLoc)
}
/// Colour that shifts from blue (far) yellow (mid) red (close).
/// Colour that shifts from red (far) yellow (mid) green (close).
private var distanceColor: Color {
guard let dist = distanceToNode() else { return .blue }
guard let dist = distanceToNode() else { return .red }
let ratio = min(dist / Self.maxDistanceMetres, 1.0)
if ratio > 0.66 { return .blue }
if ratio > 0.66 { return .red }
if ratio > 0.33 { return .yellow }
return .red
return .green
}
private func checkAlignment(bearing: Double, heading: Double) {
@ -211,6 +220,62 @@ struct FoxhuntCompassView: View {
}
}
// MARK: - Direction Cone (Backtrack-style scope)
/// A translucent cone drawn from the centre of the compass toward the
/// bearing, giving a visual "scope" so the user can see when they are
/// heading in roughly the right direction.
private struct DirectionCone: View {
let bearing: Double
let heading: Double
let radius: CGFloat
let color: Color
/// Half-width of the cone in degrees.
private let coneHalfAngle: Double = 20
var body: some View {
let onTarget = isOnTarget
ConeShape(halfAngle: coneHalfAngle, radius: radius)
.fill(
RadialGradient(
colors: [
color.opacity(onTarget ? 0.55 : 0.3),
color.opacity(onTarget ? 0.25 : 0.08)
],
center: .center,
startRadius: 0,
endRadius: radius
)
)
.rotationEffect(.degrees(bearing))
}
private var isOnTarget: Bool {
let rawDiff = abs(heading - bearing).truncatingRemainder(dividingBy: 360)
let diff = min(rawDiff, 360 - rawDiff)
return diff <= coneHalfAngle
}
}
private struct ConeShape: Shape {
let halfAngle: Double
let radius: CGFloat
func path(in rect: CGRect) -> Path {
let center = CGPoint(x: rect.midX, y: rect.midY)
let startAngle = Angle(degrees: -90 - halfAngle)
let endAngle = Angle(degrees: -90 + halfAngle)
var path = Path()
path.move(to: center)
path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
path.closeSubpath()
return path
}
}
// MARK: - Watch-sized compass sub-views
private struct WatchTickMark: View {